fix(favorites): refresh installed apps across origins

This commit is contained in:
2026-05-18 23:53:44 +08:00
parent 58789ecd1f
commit 3a8baf606c
4 changed files with 156 additions and 3 deletions
+91 -1
View File
@@ -322,6 +322,7 @@ import type {
ReviewTags, ReviewTags,
FavoriteFolder, FavoriteFolder,
FavoriteItem, FavoriteItem,
InstalledAppInfo,
ResolvedFavoriteItem, ResolvedFavoriteItem,
} from "./global/typedefinition"; } from "./global/typedefinition";
import type { Ref } from "vue"; import type { Ref } from "vue";
@@ -1134,6 +1135,95 @@ const refreshInstalledApps = async () => {
} }
}; };
const mapInstalledAppToCatalogApp = (
app: InstalledAppInfo,
origin: "spark" | "apm",
): App | null => {
let appInfo = apps.value.find(
(catalogApp) =>
catalogApp.pkgname === app.pkgname && catalogApp.origin === origin,
);
if (origin === "spark" && !appInfo) {
return null;
}
if (appInfo) {
appInfo.flags = app.flags;
appInfo.arch = app.arch;
appInfo.currentStatus = "installed";
appInfo.isDependency = app.isDependency;
return appInfo;
}
return {
name: app.name || app.pkgname,
pkgname: app.pkgname,
version: app.version,
category: "unknown",
tags: "",
more: "",
filename: "",
torrent_address: "",
author: "",
contributor: "",
website: "",
update: "",
size: "",
img_urls: [],
icons: app.icon || "",
origin: app.origin || (app.arch?.includes("apm") ? "apm" : "spark"),
currentStatus: "installed",
arch: app.arch,
flags: app.flags,
isDependency: app.isDependency,
};
};
const refreshFavoriteInstalledApps = async (): Promise<void> => {
const origins: Array<"spark" | "apm"> = [];
if (isOriginEnabled(storeFilter.value, "spark") && sparkAvailable.value) {
origins.push("spark");
}
if (isOriginEnabled(storeFilter.value, "apm") && apmAvailable.value) {
origins.push("apm");
}
const refreshedApps: App[] = [];
await Promise.all(
origins.map(async (origin) => {
const pkgnameList =
origin === "spark"
? apps.value
.filter((app) => app.origin === "spark")
.map((app) => app.pkgname)
: undefined;
const result = await window.ipcRenderer.invoke("list-installed", {
origin,
pkgnameList,
});
if (!result?.success) return;
for (const app of result.apps as InstalledAppInfo[]) {
const appInfo = mapInstalledAppToCatalogApp(app, origin);
if (appInfo) refreshedApps.push(appInfo);
}
}),
);
const refreshedKeys = new Set(
refreshedApps.map((app) => `${app.origin}:${app.pkgname}`),
);
installedApps.value = [
...installedApps.value.filter(
(app) =>
!origins.includes(app.origin) &&
!refreshedKeys.has(`${app.origin}:${app.pkgname}`),
),
...refreshedApps,
];
};
const requestUninstall = (app: App) => { const requestUninstall = (app: App) => {
uninstallTargetApp.value = app; uninstallTargetApp.value = app;
showUninstallModal.value = true; showUninstallModal.value = true;
@@ -1274,7 +1364,7 @@ const refreshFavorites = async (): Promise<void> => {
favoriteLoading.value = true; favoriteLoading.value = true;
favoriteError.value = ""; favoriteError.value = "";
try { try {
await Promise.all([refreshInstalledApps(), loadFavoriteFolders()]); await Promise.all([refreshFavoriteInstalledApps(), loadFavoriteFolders()]);
await loadActiveFavoriteItems(); await loadActiveFavoriteItems();
} catch (error: unknown) { } catch (error: unknown) {
favoriteError.value = (error as Error)?.message || "读取收藏夹失败"; favoriteError.value = (error as Error)?.message || "读取收藏夹失败";
@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from "@testing-library/vue"; import { fireEvent, render, screen, waitFor } from "@testing-library/vue";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import App from "@/App.vue"; import App from "@/App.vue";
@@ -197,6 +197,10 @@ describe("App account placeholders", () => {
}); });
render(App); render(App);
await waitFor(() => {
expect(screen.getByText("2")).toBeTruthy();
});
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ })); await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("我的收藏")); await fireEvent.click(screen.getByText("我的收藏"));
@@ -209,4 +213,50 @@ describe("App account placeholders", () => {
pkgnameList: undefined, pkgnameList: undefined,
}); });
}); });
it("refreshes Spark installed state for favorites in both mode", async () => {
invoke.mockImplementation(async (channel: string, payload?: unknown) => {
if (channel === "get-store-filter") return "both";
if (channel === "check-spark-available") return true;
if (channel === "check-apm-available") return true;
if (channel === "get-app-version") return "5.0.0";
if (channel === "list-installed") {
const request = payload as { origin?: string };
if (request.origin === "spark") {
return {
success: true,
apps: [
{
pkgname: "wps",
name: "WPS",
version: "1.0.0",
arch: "amd64",
flags: "installed",
origin: "spark",
},
],
};
}
return { success: true, apps: [] };
}
return [];
});
render(App);
await waitFor(() => {
expect(screen.getByText("2")).toBeTruthy();
});
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("我的收藏"));
expect(
await screen.findByRole("heading", { name: "我的收藏" }),
).toBeTruthy();
expect(await screen.findByText("已安装")).toBeTruthy();
expect(invoke).toHaveBeenCalledWith("list-installed", {
origin: "spark",
pkgnameList: ["wps"],
});
});
}); });
@@ -112,4 +112,15 @@ describe("favoriteAvailability", () => {
)[0]; )[0];
expect(resolved.status).toBe("installed"); expect(resolved.status).toBe("installed");
}); });
it("marks installed favorites from all-category catalog matches", () => {
const resolved = resolveFavoriteItems(
[favorite],
[app("spark")],
[app("spark", { category: "all", currentStatus: "installed" })],
{ spark: true, apm: true },
"both",
)[0];
expect(resolved.status).toBe("installed");
});
}); });
+3 -1
View File
@@ -19,7 +19,9 @@ const appMatchesFavorite = (app: App, item: FavoriteItem): boolean =>
const installedAppMatchesFavorite = (app: App, item: FavoriteItem): boolean => const installedAppMatchesFavorite = (app: App, item: FavoriteItem): boolean =>
app.pkgname === item.pkgname && app.pkgname === item.pkgname &&
(app.category === item.category || app.category === "unknown"); (app.category === item.category ||
app.category === "all" ||
app.category === "unknown");
const appMatchesClientArch = (app: App, clientArch: string): boolean => { const appMatchesClientArch = (app: App, clientArch: string): boolean => {
if (!app.arch) return true; if (!app.arch) return true;