update:apm管理改为应用管理

This commit is contained in:
2026-03-31 20:25:44 +08:00
parent 57410370b7
commit fdb5f4a51c
5 changed files with 539 additions and 439 deletions

View File

@@ -791,139 +791,177 @@ ipcMain.on("remove-installed", async (_event, payload) => {
});
});
ipcMain.handle("list-installed", async () => {
const apmBasePath = "/var/lib/apm/apm/files/ace-env/var/lib/apm";
ipcMain.handle(
"list-installed",
async (_event, origin: "apm" | "spark" = "apm") => {
const apmBasePath = "/var/lib/apm/apm/files/ace-env/var/lib/apm";
try {
// 使用 apm list --installed 获取所有已安装应用
const { code, stdout } = await runCommandCapture("apm", [
"list",
"--installed",
]);
try {
const installedApps: Array<{
pkgname: string;
name: string;
version: string;
arch: string;
flags: string;
origin: "spark" | "apm";
icon?: string;
isDependency: boolean;
}> = [];
if (code !== 0) {
logger.warn(`Failed to list installed packages: ${stdout}`);
if (origin === "spark") {
const { code, stdout } = await runCommandCapture("dpkg-query", [
"-W",
"-f=${Package} ${Version} ${Architecture}\\n",
]);
if (code !== 0) {
logger.warn(`Failed to list installed packages: ${stdout}`);
return {
success: false,
message: "Failed to list installed packages",
apps: [],
};
}
const lines = stdout.split("\n");
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
const parts = trimmed.split(" ");
if (parts.length >= 3) {
installedApps.push({
pkgname: parts[0],
name: parts[0],
version: parts[1],
arch: parts[2],
flags: "[installed]",
origin: "spark",
isDependency: false,
});
}
}
return { success: true, apps: installedApps };
}
// 使用 apm list --installed 获取所有已安装应用
const { code, stdout } = await runCommandCapture("apm", [
"list",
"--installed",
]);
if (code !== 0) {
logger.warn(`Failed to list installed packages: ${stdout}`);
return {
success: false,
message: "Failed to list installed packages",
apps: [],
};
}
const cleanStdout = stdout.replace(
// eslint-disable-next-line no-control-regex
/\x1b\[[0-9;]*m/g,
"",
);
const lines = cleanStdout.split("\n");
for (const line of lines) {
const trimmed = line.trim();
if (
!trimmed ||
trimmed.startsWith("Listing") ||
trimmed.startsWith("[INFO]") ||
trimmed.startsWith("警告")
)
continue;
// 解析格式: pkgname/repo,section version arch [flags] 或 pkgname/repo version arch [flags]
// 注意: repo后面可能有逗号和section也可能没有
const match = trimmed.match(
/^(\S+)\/\S+(?:,\S+)?\s+(\S+)\s+(\S+)\s+\[(.+)\]$/,
);
if (!match) {
logger.debug(`Failed to parse line: ${trimmed}`);
continue;
}
const [, pkgname, version, arch, flags] = match;
// 从桌面文件获取应用名称和图标
let appName = pkgname;
let icon = "";
const pkgPath = path.join(apmBasePath, pkgname);
const entriesPath = path.join(pkgPath, "entries", "applications");
const hasEntries = fs.existsSync(entriesPath);
if (hasEntries) {
try {
const desktopFiles = fs.readdirSync(entriesPath);
logger.debug(
`Found desktop files for ${pkgname}: ${desktopFiles.join(", ")}`,
);
for (const file of desktopFiles) {
if (file.endsWith(".desktop")) {
const desktopPath = path.join(entriesPath, file);
logger.debug(`Reading desktop file: ${desktopPath}`);
const content = fs.readFileSync(desktopPath, "utf-8");
const nameMatch = content.match(/^Name=(.+)$/m);
const iconMatch = content.match(/^Icon=(.+)$/m);
if (nameMatch) appName = nameMatch[1].trim();
if (iconMatch) icon = iconMatch[1].trim();
logger.debug(
`Parsed desktop file for ${pkgname}: name=${appName}, icon=${icon}`,
);
break;
}
}
} catch (e) {
logger.warn(`Failed to read desktop file for ${pkgname}: ${e}`);
}
} else {
logger.debug(`No entries path for ${pkgname}: ${entriesPath}`);
}
installedApps.push({
pkgname,
name: appName,
version,
arch,
flags,
origin: "apm",
icon: icon || undefined,
isDependency: !hasEntries,
});
}
installedApps.sort((a, b) => {
const getOrder = (app: { pkgname: string; isDependency: boolean }) => {
if (app.isDependency) return 2;
if (app.pkgname.startsWith("amber-pm")) return 1;
return 0;
};
const aOrder = getOrder(a);
const bOrder = getOrder(b);
if (aOrder !== bOrder) return aOrder - bOrder;
return a.pkgname.localeCompare(b.pkgname);
});
logger.info(`Found ${installedApps.length} installed APM apps`);
return { success: true, apps: installedApps };
} catch (error) {
logger.error(
`list-installed failed: ${error instanceof Error ? error.message : String(error)}`,
);
return {
success: false,
message: "Failed to list installed packages",
message: error instanceof Error ? error.message : String(error),
apps: [],
};
}
const installedApps: Array<{
pkgname: string;
name: string;
version: string;
arch: string;
flags: string;
origin: "spark" | "apm";
icon?: string;
isDependency: boolean;
}> = [];
const cleanStdout = stdout.replace(
// eslint-disable-next-line no-control-regex
/\x1b\[[0-9;]*m/g,
"",
);
const lines = cleanStdout.split("\n");
for (const line of lines) {
const trimmed = line.trim();
if (
!trimmed ||
trimmed.startsWith("Listing") ||
trimmed.startsWith("[INFO]") ||
trimmed.startsWith("警告")
)
continue;
// 解析格式: pkgname/repo,section version arch [flags] 或 pkgname/repo version arch [flags]
// 注意: repo后面可能有逗号和section也可能没有
const match = trimmed.match(
/^(\S+)\/\S+(?:,\S+)?\s+(\S+)\s+(\S+)\s+\[(.+)\]$/,
);
if (!match) {
logger.debug(`Failed to parse line: ${trimmed}`);
continue;
}
const [, pkgname, version, arch, flags] = match;
// 从桌面文件获取应用名称和图标
let appName = pkgname;
let icon = "";
const pkgPath = path.join(apmBasePath, pkgname);
const entriesPath = path.join(pkgPath, "entries", "applications");
const hasEntries = fs.existsSync(entriesPath);
if (hasEntries) {
try {
const desktopFiles = fs.readdirSync(entriesPath);
logger.debug(
`Found desktop files for ${pkgname}: ${desktopFiles.join(", ")}`,
);
for (const file of desktopFiles) {
if (file.endsWith(".desktop")) {
const desktopPath = path.join(entriesPath, file);
logger.debug(`Reading desktop file: ${desktopPath}`);
const content = fs.readFileSync(desktopPath, "utf-8");
const nameMatch = content.match(/^Name=(.+)$/m);
const iconMatch = content.match(/^Icon=(.+)$/m);
if (nameMatch) appName = nameMatch[1].trim();
if (iconMatch) icon = iconMatch[1].trim();
logger.debug(
`Parsed desktop file for ${pkgname}: name=${appName}, icon=${icon}`,
);
break;
}
}
} catch (e) {
logger.warn(`Failed to read desktop file for ${pkgname}: ${e}`);
}
} else {
logger.debug(`No entries path for ${pkgname}: ${entriesPath}`);
}
installedApps.push({
pkgname,
name: appName,
version,
arch,
flags,
origin: "apm",
icon: icon || undefined,
isDependency: !hasEntries,
});
}
installedApps.sort((a, b) => {
const getOrder = (app: { pkgname: string; isDependency: boolean }) => {
if (app.isDependency) return 2;
if (app.pkgname.startsWith("amber-pm")) return 1;
return 0;
};
const aOrder = getOrder(a);
const bOrder = getOrder(b);
if (aOrder !== bOrder) return aOrder - bOrder;
return a.pkgname.localeCompare(b.pkgname);
});
logger.info(`Found ${installedApps.length} installed APM apps`);
return { success: true, apps: installedApps };
} catch (error) {
logger.error(
`list-installed failed: ${error instanceof Error ? error.message : String(error)}`,
);
return {
success: false,
message: error instanceof Error ? error.message : String(error),
apps: [],
};
}
});
},
);
ipcMain.handle("list-upgradable", async () => {
const { code, stdout, stderr } = await runCommandCapture("apm", [

628
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -116,9 +116,11 @@
:apps="installedApps"
:loading="installedLoading"
:error="installedError"
:active-origin="activeInstalledOrigin"
@close="closeInstalledModal"
@refresh="refreshInstalledApps"
@uninstall="uninstallInstalledApp"
@switch-origin="handleSwitchOrigin"
/>
<UpdateAppsModal
@@ -240,6 +242,7 @@ const loading = ref(true);
const showDownloadDetailModal = ref(false);
const currentDownload: Ref<DownloadItem | null> = ref(null);
const showInstalledModal = ref(false);
const activeInstalledOrigin = ref<"apm" | "spark">("apm");
const installedApps = ref<App[]>([]);
const installedLoading = ref(false);
const installedError = ref("");
@@ -881,11 +884,17 @@ const closeInstalledModal = () => {
showInstalledModal.value = false;
};
const handleSwitchOrigin = (origin: "apm" | "spark") => {
activeInstalledOrigin.value = origin;
refreshInstalledApps();
};
const refreshInstalledApps = async () => {
installedLoading.value = true;
installedError.value = "";
try {
const result = await window.ipcRenderer.invoke("list-installed");
const origin = activeInstalledOrigin.value;
const result = await window.ipcRenderer.invoke("list-installed", origin);
if (!result?.success) {
installedApps.value = [];
installedError.value = result?.message || "读取已安装应用失败";
@@ -894,7 +903,16 @@ const refreshInstalledApps = async () => {
installedApps.value = [];
for (const app of result.apps) {
let appInfo = apps.value.find((a) => a.pkgname === app.pkgname);
// Find matching remote app to enrich data. We look exactly for that origin.
let appInfo = apps.value.find(
(a) => a.pkgname === app.pkgname && a.origin === origin,
);
if (origin === "spark" && !appInfo) {
// Only show Spark packages that exist in the App Store catalogue
continue;
}
if (appInfo) {
appInfo.flags = app.flags;
appInfo.arch = app.arch;
@@ -1298,7 +1316,9 @@ onMounted(async () => {
} else {
// 如果找不到应用,回退到搜索模式
searchQuery.value = data.pkgname;
logger.warn(`Deep link: app ${data.pkgname} not found, fallback to search`);
logger.warn(
`Deep link: app ${data.pkgname} not found, fallback to search`,
);
}
};

View File

@@ -95,7 +95,7 @@
@click="$emit('list')"
>
<i class="fas fa-download"></i>
<span>APM 应用管理</span>
<span>应用管理</span>
</button>
<button
type="button"

View File

@@ -23,10 +23,38 @@
已安装应用
</p>
<p class="text-sm text-slate-500 dark:text-slate-400">
来自本机 APM 安装列表
管理本机安装的应用程序
</p>
</div>
<div class="flex items-center gap-3">
<div
class="flex items-center rounded-2xl border border-slate-200/70 p-1 dark:border-slate-800/70"
>
<button
type="button"
class="rounded-xl px-4 py-1.5 text-sm font-semibold transition"
:class="
activeOrigin === 'apm'
? 'bg-brand/10 text-brand dark:bg-brand/15'
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'
"
@click="$emit('switch-origin', 'apm')"
>
APM 软件
</button>
<button
type="button"
class="rounded-xl px-4 py-1.5 text-sm font-semibold transition"
:class="
activeOrigin === 'spark'
? 'bg-brand/10 text-brand dark:bg-brand/15'
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'
"
@click="$emit('switch-origin', 'spark')"
>
Spark 软件
</button>
</div>
<button
type="button"
class="inline-flex items-center gap-2 rounded-2xl border border-slate-200/70 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 disabled:opacity-40 dark:border-slate-700 dark:text-slate-200"
@@ -137,11 +165,13 @@ defineProps<{
apps: App[];
loading: boolean;
error: string;
activeOrigin: "apm" | "spark";
}>();
defineEmits<{
(e: "close"): void;
(e: "refresh"): void;
(e: "uninstall", app: App): void;
(e: "switch-origin", origin: "apm" | "spark"): void;
}>();
</script>