feat(update-center): add update list icons

This commit is contained in:
2026-04-10 21:15:43 +08:00
parent 1d51f38e64
commit c16ba5536f
14 changed files with 1921 additions and 8 deletions

View File

@@ -0,0 +1,201 @@
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import type { UpdateCenterItem } from "./types";
const APM_BASE_PATH = "/var/lib/apm/apm/files/ace-env/var/lib/apm";
const REMOTE_ICON_BASE_URL = "https://erotica.spark-app.store";
const trimTrailingSlashes = (value: string): string =>
value.replace(/\/+$/, "");
const readDesktopIcon = (desktopPath: string): string => {
if (!fs.existsSync(desktopPath)) {
return "";
}
const content = fs.readFileSync(desktopPath, "utf-8");
const iconMatch = content.match(/^Icon=(.+)$/m);
return iconMatch?.[1]?.trim() ?? "";
};
const listPackageFiles = (pkgname: string): Set<string> => {
const result = spawnSync("dpkg", ["-L", pkgname]);
if (result.error || result.status !== 0) {
return new Set();
}
return new Set(
result.stdout
.toString()
.trim()
.split("\n")
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0),
);
};
const findDesktopIconInDirectories = (
directories: string[],
pkgname: string,
): string => {
const packageFiles = listPackageFiles(pkgname);
for (const directory of directories) {
if (!fs.existsSync(directory)) {
continue;
}
for (const entry of fs.readdirSync(directory)) {
if (!entry.endsWith(".desktop")) {
continue;
}
const desktopPath = path.join(directory, entry);
if (
!desktopPath.startsWith(`/opt/apps/${pkgname}/`) &&
!packageFiles.has(desktopPath)
) {
continue;
}
const desktopIcon = readDesktopIcon(desktopPath);
if (!desktopIcon) {
continue;
}
const resolvedIcon = resolveIconName(desktopIcon, [
`/usr/share/pixmaps/${desktopIcon}.png`,
`/usr/share/icons/hicolor/48x48/apps/${desktopIcon}.png`,
`/usr/share/icons/hicolor/scalable/apps/${desktopIcon}.svg`,
`/opt/apps/${pkgname}/entries/icons/hicolor/48x48/apps/${desktopIcon}.png`,
]);
if (resolvedIcon) {
return resolvedIcon;
}
}
}
return "";
};
const resolveIconName = (iconName: string, candidates: string[]): string => {
if (path.isAbsolute(iconName)) {
return fs.existsSync(iconName) ? iconName : "";
}
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return "";
};
export const resolveDesktopIcon = (pkgname: string): string => {
return findDesktopIconInDirectories(
["/usr/share/applications", `/opt/apps/${pkgname}/entries/applications`],
pkgname,
);
};
export const resolveApmIcon = (pkgname: string): string => {
const apmRoots = [APM_BASE_PATH, "/opt/apps"];
for (const apmRoot of apmRoots) {
const desktopDirectory = path.join(
apmRoot,
pkgname,
"entries",
"applications",
);
if (!fs.existsSync(desktopDirectory)) {
continue;
}
for (const desktopFile of fs.readdirSync(desktopDirectory)) {
if (!desktopFile.endsWith(".desktop")) {
continue;
}
const desktopIcon = readDesktopIcon(
path.join(desktopDirectory, desktopFile),
);
if (!desktopIcon) {
continue;
}
const resolvedIcon = resolveIconName(desktopIcon, [
path.join(
apmRoot,
pkgname,
"entries",
"icons",
"hicolor",
"48x48",
"apps",
`${desktopIcon}.png`,
),
path.join(
apmRoot,
pkgname,
"entries",
"icons",
"hicolor",
"scalable",
"apps",
`${desktopIcon}.svg`,
),
`/usr/share/pixmaps/${desktopIcon}.png`,
`/usr/share/icons/hicolor/48x48/apps/${desktopIcon}.png`,
`/usr/share/icons/hicolor/scalable/apps/${desktopIcon}.svg`,
]);
if (resolvedIcon) {
return resolvedIcon;
}
}
}
return "";
};
export const buildRemoteFallbackIconUrl = ({
pkgname,
source,
arch,
category,
}: Pick<
UpdateCenterItem,
"pkgname" | "source" | "arch" | "category"
>): string => {
const baseUrl = trimTrailingSlashes(REMOTE_ICON_BASE_URL);
if (!baseUrl || !arch || !category) {
return "";
}
const storeArch = arch.includes("-")
? arch
: `${arch}-${source === "aptss" ? "store" : "apm"}`;
return `${baseUrl}/${storeArch}/${category}/${pkgname}/icon.png`;
};
export const resolveUpdateItemIcon = (item: UpdateCenterItem): string => {
const localIcon =
item.source === "aptss"
? resolveDesktopIcon(item.pkgname)
: resolveApmIcon(item.pkgname);
if (localIcon) {
return localIcon;
}
return (
buildRemoteFallbackIconUrl({
pkgname: item.pkgname,
source: item.source,
arch: item.arch,
category: item.category,
}) || ""
);
};