mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 01:10:16 +08:00
fix(update-center): cascade local and remote icon fallbacks
Keep update list icons from dropping straight to placeholders by retrying the remote store icon after local load failures. Align the update-center IPC and renderer types with the split local/remote icon contract.
This commit is contained in:
@@ -181,21 +181,31 @@ export const buildRemoteFallbackIconUrl = ({
|
|||||||
return `${baseUrl}/${storeArch}/${category}/${pkgname}/icon.png`;
|
return `${baseUrl}/${storeArch}/${category}/${pkgname}/icon.png`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resolveUpdateItemIcon = (item: UpdateCenterItem): string => {
|
export const resolveUpdateItemIcons = (
|
||||||
|
item: UpdateCenterItem,
|
||||||
|
): Pick<UpdateCenterItem, "localIcon" | "remoteIcon"> => {
|
||||||
const localIcon =
|
const localIcon =
|
||||||
item.source === "aptss"
|
item.source === "aptss"
|
||||||
? resolveDesktopIcon(item.pkgname)
|
? resolveDesktopIcon(item.pkgname)
|
||||||
: resolveApmIcon(item.pkgname);
|
: resolveApmIcon(item.pkgname);
|
||||||
if (localIcon) {
|
const remoteIcon = buildRemoteFallbackIconUrl({
|
||||||
return localIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
buildRemoteFallbackIconUrl({
|
|
||||||
pkgname: item.pkgname,
|
pkgname: item.pkgname,
|
||||||
source: item.source,
|
source: item.source,
|
||||||
arch: item.arch,
|
arch: item.arch,
|
||||||
category: item.category,
|
category: item.category,
|
||||||
}) || ""
|
});
|
||||||
);
|
|
||||||
|
if (localIcon && remoteIcon) {
|
||||||
|
return { localIcon, remoteIcon };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localIcon) {
|
||||||
|
return { localIcon };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteIcon) {
|
||||||
|
return { remoteIcon };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
parseAptssUpgradableOutput,
|
parseAptssUpgradableOutput,
|
||||||
parsePrintUrisOutput,
|
parsePrintUrisOutput,
|
||||||
} from "./query";
|
} from "./query";
|
||||||
import { resolveUpdateItemIcon } from "./icons";
|
import { resolveUpdateItemIcons } from "./icons";
|
||||||
import {
|
import {
|
||||||
createUpdateCenterService,
|
createUpdateCenterService,
|
||||||
type UpdateCenterIgnorePayload,
|
type UpdateCenterIgnorePayload,
|
||||||
@@ -249,9 +249,9 @@ const enrichItemCategories = async (
|
|||||||
|
|
||||||
const enrichItemIcons = (items: UpdateCenterItem[]): UpdateCenterItem[] => {
|
const enrichItemIcons = (items: UpdateCenterItem[]): UpdateCenterItem[] => {
|
||||||
return items.map((item) => {
|
return items.map((item) => {
|
||||||
const icon = resolveUpdateItemIcon(item);
|
const icons = resolveUpdateItemIcons(item);
|
||||||
|
|
||||||
return icon ? { ...item, icon } : item;
|
return Object.keys(icons).length > 0 ? { ...item, ...icons } : item;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export interface UpdateCenterServiceItem {
|
|||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
newVersion: string;
|
newVersion: string;
|
||||||
source: UpdateSource;
|
source: UpdateSource;
|
||||||
icon?: string;
|
localIcon?: string;
|
||||||
|
remoteIcon?: string;
|
||||||
ignored?: boolean;
|
ignored?: boolean;
|
||||||
downloadUrl?: string;
|
downloadUrl?: string;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
@@ -41,7 +42,8 @@ export interface UpdateCenterServiceTask {
|
|||||||
taskKey: string;
|
taskKey: string;
|
||||||
packageName: string;
|
packageName: string;
|
||||||
source: UpdateSource;
|
source: UpdateSource;
|
||||||
icon?: string;
|
localIcon?: string;
|
||||||
|
remoteIcon?: string;
|
||||||
status: UpdateCenterQueueSnapshot["tasks"][number]["status"];
|
status: UpdateCenterQueueSnapshot["tasks"][number]["status"];
|
||||||
progress: number;
|
progress: number;
|
||||||
logs: UpdateCenterQueueSnapshot["tasks"][number]["logs"];
|
logs: UpdateCenterQueueSnapshot["tasks"][number]["logs"];
|
||||||
@@ -98,7 +100,8 @@ const toState = (
|
|||||||
currentVersion: item.currentVersion,
|
currentVersion: item.currentVersion,
|
||||||
newVersion: item.nextVersion,
|
newVersion: item.nextVersion,
|
||||||
source: item.source,
|
source: item.source,
|
||||||
icon: item.icon,
|
localIcon: item.localIcon,
|
||||||
|
remoteIcon: item.remoteIcon,
|
||||||
ignored: item.ignored,
|
ignored: item.ignored,
|
||||||
downloadUrl: item.downloadUrl,
|
downloadUrl: item.downloadUrl,
|
||||||
fileName: item.fileName,
|
fileName: item.fileName,
|
||||||
@@ -113,7 +116,8 @@ const toState = (
|
|||||||
taskKey: getTaskKey(task.item),
|
taskKey: getTaskKey(task.item),
|
||||||
packageName: task.pkgname,
|
packageName: task.pkgname,
|
||||||
source: task.item.source,
|
source: task.item.source,
|
||||||
icon: task.item.icon,
|
localIcon: task.item.localIcon,
|
||||||
|
remoteIcon: task.item.remoteIcon,
|
||||||
status: task.status,
|
status: task.status,
|
||||||
progress: task.progress,
|
progress: task.progress,
|
||||||
logs: task.logs.map((log) => ({ ...log })),
|
logs: task.logs.map((log) => ({ ...log })),
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export interface UpdateCenterItem {
|
|||||||
nextVersion: string;
|
nextVersion: string;
|
||||||
arch?: string;
|
arch?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
icon?: string;
|
localIcon?: string;
|
||||||
|
remoteIcon?: string;
|
||||||
ignored?: boolean;
|
ignored?: boolean;
|
||||||
downloadUrl?: string;
|
downloadUrl?: string;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ type UpdateCenterSnapshot = {
|
|||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
newVersion: string;
|
newVersion: string;
|
||||||
source: "aptss" | "apm";
|
source: "aptss" | "apm";
|
||||||
|
localIcon?: string;
|
||||||
|
remoteIcon?: string;
|
||||||
ignored?: boolean;
|
ignored?: boolean;
|
||||||
}>;
|
}>;
|
||||||
tasks: Array<{
|
tasks: Array<{
|
||||||
taskKey: string;
|
taskKey: string;
|
||||||
packageName: string;
|
packageName: string;
|
||||||
source: "aptss" | "apm";
|
source: "aptss" | "apm";
|
||||||
|
localIcon?: string;
|
||||||
|
remoteIcon?: string;
|
||||||
status:
|
status:
|
||||||
| "queued"
|
| "queued"
|
||||||
| "downloading"
|
| "downloading"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { fireEvent, render, screen } from "@testing-library/vue";
|
import { fireEvent, render, screen } from "@testing-library/vue";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { defineComponent, nextTick, reactive, ref } from "vue";
|
||||||
|
|
||||||
import UpdateCenterItem from "@/components/update-center/UpdateCenterItem.vue";
|
import UpdateCenterItem from "@/components/update-center/UpdateCenterItem.vue";
|
||||||
import type {
|
import type {
|
||||||
@@ -33,10 +34,13 @@ const createTask = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("UpdateCenterItem", () => {
|
describe("UpdateCenterItem", () => {
|
||||||
it("renders an icon image when item.icon exists", () => {
|
it("renders localIcon first when both icon sources exist", () => {
|
||||||
render(UpdateCenterItem, {
|
render(UpdateCenterItem, {
|
||||||
props: {
|
props: {
|
||||||
item: createItem({ icon: "/usr/share/pixmaps/spark-weather.png" }),
|
item: createItem({
|
||||||
|
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||||
|
remoteIcon: "https://example.com/spark-weather.png",
|
||||||
|
}),
|
||||||
task: createTask(),
|
task: createTask(),
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
@@ -50,10 +54,13 @@ describe("UpdateCenterItem", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to a placeholder icon when the image fails", async () => {
|
it("falls back to remoteIcon when localIcon fails", async () => {
|
||||||
render(UpdateCenterItem, {
|
render(UpdateCenterItem, {
|
||||||
props: {
|
props: {
|
||||||
item: createItem({ icon: "https://example.com/spark-weather.png" }),
|
item: createItem({
|
||||||
|
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||||
|
remoteIcon: "https://example.com/spark-weather.png",
|
||||||
|
}),
|
||||||
task: createTask(),
|
task: createTask(),
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
@@ -63,16 +70,42 @@ describe("UpdateCenterItem", () => {
|
|||||||
|
|
||||||
await fireEvent.error(icon);
|
await fireEvent.error(icon);
|
||||||
|
|
||||||
|
expect(icon).toHaveAttribute(
|
||||||
|
"src",
|
||||||
|
"https://example.com/spark-weather.png",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the placeholder after localIcon and remoteIcon both fail", async () => {
|
||||||
|
render(UpdateCenterItem, {
|
||||||
|
props: {
|
||||||
|
item: createItem({
|
||||||
|
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||||
|
remoteIcon: "https://example.com/spark-weather.png",
|
||||||
|
}),
|
||||||
|
task: createTask(),
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||||
|
|
||||||
|
await fireEvent.error(icon);
|
||||||
|
await fireEvent.error(icon);
|
||||||
|
|
||||||
expect(icon.getAttribute("src")).toContain("data:image/svg+xml");
|
expect(icon.getAttribute("src")).toContain("data:image/svg+xml");
|
||||||
expect(icon.getAttribute("src")).not.toContain(
|
expect(icon.getAttribute("src")).not.toContain(
|
||||||
"https://example.com/spark-weather.png",
|
"https://example.com/spark-weather.png",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows a new item icon again after a previous icon failure", async () => {
|
it("restarts from localIcon when a new item is rendered", async () => {
|
||||||
const { rerender } = render(UpdateCenterItem, {
|
const { rerender } = render(UpdateCenterItem, {
|
||||||
props: {
|
props: {
|
||||||
item: createItem({ icon: "https://example.com/spark-weather.png" }),
|
item: createItem({
|
||||||
|
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||||
|
remoteIcon: "https://example.com/spark-weather.png",
|
||||||
|
}),
|
||||||
task: createTask(),
|
task: createTask(),
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
@@ -82,12 +115,16 @@ describe("UpdateCenterItem", () => {
|
|||||||
|
|
||||||
await fireEvent.error(firstIcon);
|
await fireEvent.error(firstIcon);
|
||||||
|
|
||||||
expect(firstIcon.getAttribute("src")).toContain("data:image/svg+xml");
|
expect(firstIcon).toHaveAttribute(
|
||||||
|
"src",
|
||||||
|
"https://example.com/spark-weather.png",
|
||||||
|
);
|
||||||
|
|
||||||
await rerender({
|
await rerender({
|
||||||
item: createItem({
|
item: createItem({
|
||||||
displayName: "Spark Clock",
|
displayName: "Spark Clock",
|
||||||
icon: "/usr/share/pixmaps/spark-clock.png",
|
localIcon: "/usr/share/pixmaps/spark-clock.png",
|
||||||
|
remoteIcon: "https://example.com/spark-clock.png",
|
||||||
}),
|
}),
|
||||||
task: createTask(),
|
task: createTask(),
|
||||||
selected: false,
|
selected: false,
|
||||||
@@ -101,11 +138,16 @@ describe("UpdateCenterItem", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("retries the same icon string when a fresh item object is rendered", async () => {
|
it("restarts from localIcon when icon sources change on the same item object", async () => {
|
||||||
const brokenIcon = "https://example.com/spark-weather.png";
|
const item = reactive(
|
||||||
const { rerender } = render(UpdateCenterItem, {
|
createItem({
|
||||||
|
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||||
|
remoteIcon: "https://example.com/spark-weather.png",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
render(UpdateCenterItem, {
|
||||||
props: {
|
props: {
|
||||||
item: createItem({ icon: brokenIcon }),
|
item,
|
||||||
task: createTask(),
|
task: createTask(),
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
@@ -113,22 +155,63 @@ describe("UpdateCenterItem", () => {
|
|||||||
|
|
||||||
const firstIcon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
const firstIcon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||||
|
|
||||||
|
await fireEvent.error(firstIcon);
|
||||||
await fireEvent.error(firstIcon);
|
await fireEvent.error(firstIcon);
|
||||||
|
|
||||||
expect(firstIcon.getAttribute("src")).toContain("data:image/svg+xml");
|
expect(firstIcon.getAttribute("src")).toContain("data:image/svg+xml");
|
||||||
|
|
||||||
await rerender({
|
item.localIcon = "/usr/share/pixmaps/spark-weather-refreshed.png";
|
||||||
item: createItem({
|
item.remoteIcon = "https://example.com/spark-weather-refreshed.png";
|
||||||
currentVersion: "1.1.0",
|
|
||||||
newVersion: "2.1.0",
|
await nextTick();
|
||||||
icon: brokenIcon,
|
|
||||||
}),
|
|
||||||
task: createTask({ progress: 75 }),
|
|
||||||
selected: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const retriedIcon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
const retriedIcon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||||
|
|
||||||
expect(retriedIcon).toHaveAttribute("src", brokenIcon);
|
expect(retriedIcon).toHaveAttribute(
|
||||||
|
"src",
|
||||||
|
"file:///usr/share/pixmaps/spark-weather-refreshed.png",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restarts from localIcon for a fresh item object with the same icon sources", async () => {
|
||||||
|
const item = ref(
|
||||||
|
createItem({
|
||||||
|
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||||
|
remoteIcon: "https://example.com/spark-weather.png",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const Wrapper = defineComponent({
|
||||||
|
components: { UpdateCenterItem },
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
item,
|
||||||
|
task: createTask(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template:
|
||||||
|
'<UpdateCenterItem :item="item" :task="task" :selected="false" />',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(Wrapper);
|
||||||
|
|
||||||
|
const firstIcon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||||
|
|
||||||
|
await fireEvent.error(firstIcon);
|
||||||
|
await fireEvent.error(firstIcon);
|
||||||
|
|
||||||
|
expect(firstIcon.getAttribute("src")).toContain("data:image/svg+xml");
|
||||||
|
|
||||||
|
item.value = createItem({
|
||||||
|
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||||
|
remoteIcon: "https://example.com/spark-weather.png",
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const retriedIcon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||||
|
|
||||||
|
expect(retriedIcon).toHaveAttribute(
|
||||||
|
"src",
|
||||||
|
"file:///usr/share/pixmaps/spark-weather.png",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -87,12 +87,12 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("update-center icons", () => {
|
describe("update-center icons", () => {
|
||||||
it("prefers local desktop icon paths for aptss items", async () => {
|
it("returns both localIcon and remoteIcon when an aptss desktop icon resolves", async () => {
|
||||||
const pkgname = "spark-weather";
|
const pkgname = "spark-weather";
|
||||||
const applicationsDirectory = "/usr/share/applications";
|
const applicationsDirectory = "/usr/share/applications";
|
||||||
const desktopPath = `${applicationsDirectory}/weather-launcher.desktop`;
|
const desktopPath = `${applicationsDirectory}/weather-launcher.desktop`;
|
||||||
const iconPath = `/usr/share/pixmaps/${pkgname}.png`;
|
const iconPath = `/usr/share/pixmaps/${pkgname}.png`;
|
||||||
const { resolveUpdateItemIcon } = await loadIconsModule({
|
const { resolveUpdateItemIcons } = await loadIconsModule({
|
||||||
directories: {
|
directories: {
|
||||||
[applicationsDirectory]: ["weather-launcher.desktop"],
|
[applicationsDirectory]: ["weather-launcher.desktop"],
|
||||||
},
|
},
|
||||||
@@ -106,13 +106,19 @@ describe("update-center icons", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
resolveUpdateItemIcon({
|
resolveUpdateItemIcons({
|
||||||
pkgname,
|
pkgname,
|
||||||
source: "aptss",
|
source: "aptss",
|
||||||
currentVersion: "1.0.0",
|
currentVersion: "1.0.0",
|
||||||
nextVersion: "2.0.0",
|
nextVersion: "2.0.0",
|
||||||
|
category: "tools",
|
||||||
|
arch: "amd64",
|
||||||
}),
|
}),
|
||||||
).toBe(iconPath);
|
).toEqual({
|
||||||
|
localIcon: iconPath,
|
||||||
|
remoteIcon:
|
||||||
|
"https://erotica.spark-app.store/amd64-store/tools/spark-weather/icon.png",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves APM icon names from entries/icons when desktop icon is not absolute", async () => {
|
it("resolves APM icon names from entries/icons when desktop icon is not absolute", async () => {
|
||||||
@@ -120,7 +126,7 @@ describe("update-center icons", () => {
|
|||||||
const desktopDirectory = `/var/lib/apm/apm/files/ace-env/var/lib/apm/${pkgname}/entries/applications`;
|
const desktopDirectory = `/var/lib/apm/apm/files/ace-env/var/lib/apm/${pkgname}/entries/applications`;
|
||||||
const desktopPath = `${desktopDirectory}/${pkgname}.desktop`;
|
const desktopPath = `${desktopDirectory}/${pkgname}.desktop`;
|
||||||
const iconPath = `/var/lib/apm/apm/files/ace-env/var/lib/apm/${pkgname}/entries/icons/hicolor/48x48/apps/${pkgname}.png`;
|
const iconPath = `/var/lib/apm/apm/files/ace-env/var/lib/apm/${pkgname}/entries/icons/hicolor/48x48/apps/${pkgname}.png`;
|
||||||
const { resolveUpdateItemIcon } = await loadIconsModule({
|
const { resolveUpdateItemIcons } = await loadIconsModule({
|
||||||
directories: {
|
directories: {
|
||||||
[desktopDirectory]: [`${pkgname}.desktop`],
|
[desktopDirectory]: [`${pkgname}.desktop`],
|
||||||
},
|
},
|
||||||
@@ -131,13 +137,13 @@ describe("update-center icons", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
resolveUpdateItemIcon({
|
resolveUpdateItemIcons({
|
||||||
pkgname,
|
pkgname,
|
||||||
source: "apm",
|
source: "apm",
|
||||||
currentVersion: "1.0.0",
|
currentVersion: "1.0.0",
|
||||||
nextVersion: "2.0.0",
|
nextVersion: "2.0.0",
|
||||||
}),
|
}),
|
||||||
).toBe(iconPath);
|
).toEqual({ localIcon: iconPath });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("checks later APM desktop entries when the first one has no usable icon", async () => {
|
it("checks later APM desktop entries when the first one has no usable icon", async () => {
|
||||||
@@ -197,11 +203,11 @@ describe("update-center icons", () => {
|
|||||||
expect(resolveApmIcon(pkgname)).toBe(sharedIconPath);
|
expect(resolveApmIcon(pkgname)).toBe(sharedIconPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds a remote fallback URL when category and arch are available", async () => {
|
it("returns only remoteIcon when no local icon resolves", async () => {
|
||||||
const { resolveUpdateItemIcon } = await loadIconsModule({});
|
const { resolveUpdateItemIcons } = await loadIconsModule({});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
resolveUpdateItemIcon({
|
resolveUpdateItemIcons({
|
||||||
pkgname: "spark-clock",
|
pkgname: "spark-clock",
|
||||||
source: "apm",
|
source: "apm",
|
||||||
currentVersion: "1.0.0",
|
currentVersion: "1.0.0",
|
||||||
@@ -209,22 +215,51 @@ describe("update-center icons", () => {
|
|||||||
category: "utility",
|
category: "utility",
|
||||||
arch: "amd64",
|
arch: "amd64",
|
||||||
}),
|
}),
|
||||||
).toBe(
|
).toEqual({
|
||||||
|
remoteIcon:
|
||||||
"https://erotica.spark-app.store/amd64-apm/utility/spark-clock/icon.png",
|
"https://erotica.spark-app.store/amd64-apm/utility/spark-clock/icon.png",
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty string when neither local nor remote icon can be determined", async () => {
|
it("returns only localIcon when a remote fallback URL cannot be built", async () => {
|
||||||
const { resolveUpdateItemIcon } = await loadIconsModule({});
|
const pkgname = "spark-reader";
|
||||||
|
const applicationsDirectory = "/usr/share/applications";
|
||||||
|
const desktopPath = `${applicationsDirectory}/reader-launcher.desktop`;
|
||||||
|
const iconPath = `/usr/share/pixmaps/${pkgname}.png`;
|
||||||
|
const { resolveUpdateItemIcons } = await loadIconsModule({
|
||||||
|
directories: {
|
||||||
|
[applicationsDirectory]: ["reader-launcher.desktop"],
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
[desktopPath]: `[Desktop Entry]\nName=Spark Reader\nIcon=${iconPath}\n`,
|
||||||
|
[iconPath]: "png",
|
||||||
|
},
|
||||||
|
packageFiles: {
|
||||||
|
[pkgname]: [desktopPath],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
resolveUpdateItemIcon({
|
resolveUpdateItemIcons({
|
||||||
|
pkgname,
|
||||||
|
source: "aptss",
|
||||||
|
currentVersion: "1.0.0",
|
||||||
|
nextVersion: "2.0.0",
|
||||||
|
}),
|
||||||
|
).toEqual({ localIcon: iconPath });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty object when neither local nor remote icons are available", async () => {
|
||||||
|
const { resolveUpdateItemIcons } = await loadIconsModule({});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveUpdateItemIcons({
|
||||||
pkgname: "spark-empty",
|
pkgname: "spark-empty",
|
||||||
source: "aptss",
|
source: "aptss",
|
||||||
currentVersion: "1.0.0",
|
currentVersion: "1.0.0",
|
||||||
nextVersion: "2.0.0",
|
nextVersion: "2.0.0",
|
||||||
}),
|
}),
|
||||||
).toBe("");
|
).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores unrelated desktop files while still resolving owned non-exact filenames", async () => {
|
it("ignores unrelated desktop files while still resolving owned non-exact filenames", async () => {
|
||||||
|
|||||||
@@ -175,7 +175,8 @@ describe("update-center load items", () => {
|
|||||||
nextVersion: "3.0.0",
|
nextVersion: "3.0.0",
|
||||||
arch: "amd64",
|
arch: "amd64",
|
||||||
category: "tools",
|
category: "tools",
|
||||||
icon: "https://erotica.spark-app.store/amd64-apm/tools/spark-weather/icon.png",
|
remoteIcon:
|
||||||
|
"https://erotica.spark-app.store/amd64-apm/tools/spark-weather/icon.png",
|
||||||
downloadUrl: "https://example.invalid/spark-weather_3.0.0_amd64.deb",
|
downloadUrl: "https://example.invalid/spark-weather_3.0.0_amd64.deb",
|
||||||
fileName: "spark-weather_3.0.0_amd64.deb",
|
fileName: "spark-weather_3.0.0_amd64.deb",
|
||||||
size: 123456,
|
size: 123456,
|
||||||
@@ -235,7 +236,8 @@ describe("update-center load items", () => {
|
|||||||
nextVersion: "2.0.0",
|
nextVersion: "2.0.0",
|
||||||
arch: "amd64",
|
arch: "amd64",
|
||||||
category: "office",
|
category: "office",
|
||||||
icon: "https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
remoteIcon:
|
||||||
|
"https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
expect(result.warnings).toEqual([
|
expect(result.warnings).toEqual([
|
||||||
@@ -308,7 +310,8 @@ describe("update-center load items", () => {
|
|||||||
nextVersion: "2.0.0",
|
nextVersion: "2.0.0",
|
||||||
arch: "amd64",
|
arch: "amd64",
|
||||||
category: "office",
|
category: "office",
|
||||||
icon: "https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
remoteIcon:
|
||||||
|
"https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -362,7 +365,8 @@ describe("update-center load items", () => {
|
|||||||
nextVersion: "2.0.0",
|
nextVersion: "2.0.0",
|
||||||
arch: "amd64",
|
arch: "amd64",
|
||||||
category: "office",
|
category: "office",
|
||||||
icon: "https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
remoteIcon:
|
||||||
|
"https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
expect(result.warnings).toEqual([
|
expect(result.warnings).toEqual([
|
||||||
|
|||||||
@@ -217,9 +217,16 @@ describe("update-center/ipc", () => {
|
|||||||
expect(snapshots.at(-1)?.items[0]).not.toHaveProperty("nextVersion");
|
expect(snapshots.at(-1)?.items[0]).not.toHaveProperty("nextVersion");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("service task snapshots keep item icons for queued work", async () => {
|
it("service task snapshots keep localIcon and remoteIcon for queued work", async () => {
|
||||||
|
let releaseTask: (() => void) | undefined;
|
||||||
const service = createUpdateCenterService({
|
const service = createUpdateCenterService({
|
||||||
loadItems: async () => [{ ...createItem(), icon: "/icons/weather.png" }],
|
loadItems: async () => [
|
||||||
|
{
|
||||||
|
...createItem(),
|
||||||
|
localIcon: "/icons/weather.png",
|
||||||
|
remoteIcon: "https://example.com/weather.png",
|
||||||
|
},
|
||||||
|
],
|
||||||
createTaskRunner: (queue: UpdateCenterQueue) => ({
|
createTaskRunner: (queue: UpdateCenterQueue) => ({
|
||||||
cancelActiveTask: vi.fn(),
|
cancelActiveTask: vi.fn(),
|
||||||
runNextTask: async () => {
|
runNextTask: async () => {
|
||||||
@@ -228,6 +235,9 @@ describe("update-center/ipc", () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
releaseTask = resolve;
|
||||||
|
});
|
||||||
queue.markActiveTask(task.id, "installing");
|
queue.markActiveTask(task.id, "installing");
|
||||||
queue.finishTask(task.id, "completed");
|
queue.finishTask(task.id, "completed");
|
||||||
return task;
|
return task;
|
||||||
@@ -236,15 +246,20 @@ describe("update-center/ipc", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await service.refresh();
|
await service.refresh();
|
||||||
await service.start(["aptss:spark-weather"]);
|
const startPromise = service.start(["aptss:spark-weather"]);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
expect(service.getState().tasks).toMatchObject([
|
expect(service.getState().tasks).toMatchObject([
|
||||||
{
|
{
|
||||||
taskKey: "aptss:spark-weather",
|
taskKey: "aptss:spark-weather",
|
||||||
icon: "/icons/weather.png",
|
localIcon: "/icons/weather.png",
|
||||||
status: "completed",
|
remoteIcon: "https://example.com/weather.png",
|
||||||
|
status: "queued",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
releaseTask?.();
|
||||||
|
await startPromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("concurrent start calls still serialize through one processing pipeline", async () => {
|
it("concurrent start calls still serialize through one processing pipeline", async () => {
|
||||||
|
|||||||
@@ -92,17 +92,13 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const PLACEHOLDER_ICON =
|
const PLACEHOLDER_ICON =
|
||||||
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"%3E%3Crect width="48" height="48" rx="12" fill="%23e2e8f0"/%3E%3Cpath d="M17 31h14v2H17zm3-12h8a2 2 0 0 1 2 2v8H18v-8a2 2 0 0 1 2-2" fill="%2394a3b8"/%3E%3C/svg%3E';
|
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"%3E%3Crect width="48" height="48" rx="12" fill="%23e2e8f0"/%3E%3Cpath d="M17 31h14v2H17zm3-12h8a2 2 0 0 1 2 2v8H18v-8a2 2 0 0 1 2-2" fill="%2394a3b8"/%3E%3C/svg%3E';
|
||||||
const failedIcon = ref<string | null>(null);
|
const iconIndex = ref(0);
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: "toggle-selection"): void;
|
(e: "toggle-selection"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const normalizeIconSrc = (icon?: string): string => {
|
const normalizeIconSrc = (icon: string): string => {
|
||||||
if (!icon || failedIcon.value === icon) {
|
|
||||||
return PLACEHOLDER_ICON;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^[a-z]+:\/\//i.test(icon)) {
|
if (/^[a-z]+:\/\//i.test(icon)) {
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
@@ -110,18 +106,30 @@ const normalizeIconSrc = (icon?: string): string => {
|
|||||||
return icon.startsWith("/") ? `file://${icon}` : icon;
|
return icon.startsWith("/") ? `file://${icon}` : icon;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const iconCandidates = computed(() => {
|
||||||
|
return [props.item.localIcon, props.item.remoteIcon].filter(
|
||||||
|
(icon): icon is string => Boolean(icon),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const handleIconError = () => {
|
const handleIconError = () => {
|
||||||
failedIcon.value = props.item.icon ?? null;
|
if (iconIndex.value < iconCandidates.value.length) {
|
||||||
|
iconIndex.value += 1;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.item,
|
[() => props.item, () => props.item.localIcon, () => props.item.remoteIcon],
|
||||||
() => {
|
() => {
|
||||||
failedIcon.value = null;
|
iconIndex.value = 0;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const iconSrc = computed(() => normalizeIconSrc(props.item.icon));
|
const iconSrc = computed(() => {
|
||||||
|
const icon = iconCandidates.value[iconIndex.value];
|
||||||
|
|
||||||
|
return icon ? normalizeIconSrc(icon) : PLACEHOLDER_ICON;
|
||||||
|
});
|
||||||
|
|
||||||
const sourceLabel = computed(() => {
|
const sourceLabel = computed(() => {
|
||||||
return props.item.source === "apm" ? "APM" : "传统deb";
|
return props.item.source === "apm" ? "APM" : "传统deb";
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ export interface UpdateCenterItem {
|
|||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
newVersion: string;
|
newVersion: string;
|
||||||
source: UpdateSource;
|
source: UpdateSource;
|
||||||
icon?: string;
|
localIcon?: string;
|
||||||
|
remoteIcon?: string;
|
||||||
ignored?: boolean;
|
ignored?: boolean;
|
||||||
downloadUrl?: string;
|
downloadUrl?: string;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
@@ -156,7 +157,8 @@ export interface UpdateCenterTaskState {
|
|||||||
taskKey: string;
|
taskKey: string;
|
||||||
packageName: string;
|
packageName: string;
|
||||||
source: UpdateSource;
|
source: UpdateSource;
|
||||||
icon?: string;
|
localIcon?: string;
|
||||||
|
remoteIcon?: string;
|
||||||
status: UpdateCenterTaskStatus;
|
status: UpdateCenterTaskStatus;
|
||||||
progress: number;
|
progress: number;
|
||||||
logs: Array<{ time: number; message: string }>;
|
logs: Array<{ time: number; message: string }>;
|
||||||
|
|||||||
Reference in New Issue
Block a user