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:
2026-04-11 11:41:01 +08:00
parent c16ba5536f
commit 180b88b5c0
11 changed files with 245 additions and 79 deletions

View File

@@ -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 {};
}; };

View File

@@ -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;
}); });
}; };

View File

@@ -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 })),

View File

@@ -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;

View File

@@ -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"

View File

@@ -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",
);
}); });
}); });

View File

@@ -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 () => {

View File

@@ -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([

View File

@@ -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 () => {

View File

@@ -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";

View File

@@ -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 }>;