diff --git a/electron/main/backend/install-manager.ts b/electron/main/backend/install-manager.ts index 77ed2f86..e0e5d46e 100644 --- a/electron/main/backend/install-manager.ts +++ b/electron/main/backend/install-manager.ts @@ -5,7 +5,7 @@ import fs from "node:fs"; import path from "node:path"; import pino from "pino"; -import { ChannelPayload, InstalledAppInfo } from "../../typedefinition"; +import { ChannelPayload } from "../../typedefinition"; import axios from "axios"; const logger = pino({ name: "install-manager" }); @@ -79,28 +79,6 @@ const runCommandCapture = async (execCommand: string, execParams: string[]) => { ); }; -const parseInstalledList = (output: string) => { - const apps: Array = []; - const lines = output.split("\n"); - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) continue; - if (trimmed.startsWith("Listing")) continue; - if (trimmed.startsWith("[INFO]")) continue; - - const match = trimmed.match(/^(\S+)\/\S+,\S+\s+(\S+)\s+(\S+)\s+\[(.+)\]$/); - if (!match) continue; - apps.push({ - pkgname: match[1], - version: match[2], - arch: match[3], - flags: match[4], - raw: trimmed, - }); - } - return apps; -}; - /** 检测本机是否已安装 apm 命令 */ const checkApmAvailable = async (): Promise => { const { code, stdout } = await runCommandCapture("which", ["apm"]); @@ -251,7 +229,8 @@ ipcMain.on("queue-install", async (event, download_json) => { type: "info", title: "APM 安装成功", message: "恭喜您,APM 已成功安装", - detail: "APM 应用需重启后方可展示和使用,若完成安装后无法在应用列表中展示,请重启电脑后继续。", + detail: + "APM 应用需重启后方可展示和使用,若完成安装后无法在应用列表中展示,请重启电脑后继续。", buttons: ["确定"], defaultId: 0, }); @@ -422,10 +401,15 @@ async function processNextInQueue() { const timeoutChecker = setInterval(() => { const now = Date.now(); // 只在进度为0时检查超时 - if (lastProgress === 0 && now - lastProgressTime > zeroProgressTimeout) { + if ( + lastProgress === 0 && + now - lastProgressTime > zeroProgressTimeout + ) { clearInterval(timeoutChecker); child.kill(); - reject(new Error(`下载卡在0%超过 ${zeroProgressTimeout / 1000} 秒`)); + reject( + new Error(`下载卡在0%超过 ${zeroProgressTimeout / 1000} 秒`), + ); } }, progressCheckInterval); @@ -466,7 +450,7 @@ async function processNextInQueue() { } sendLog(`下载失败,准备重试 (${retryCount}/${maxRetries})`); // 等待2秒后重试 - await new Promise(r => setTimeout(r, 2000)); + await new Promise((r) => setTimeout(r, 2000)); } } } @@ -573,8 +557,11 @@ ipcMain.handle("check-installed", async (_event, payload: any) => { "--installed", ]); if (code === 0) { - // eslint-disable-next-line no-control-regex - const cleanStdout = stdout.replace(/\x1b\[[0-9;]*m/g, ""); + 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(); @@ -625,7 +612,6 @@ ipcMain.handle("check-installed", async (_event, payload: any) => { if (isInstalled) return true; } - return isInstalled; }); @@ -691,9 +677,133 @@ ipcMain.on("remove-installed", async (_event, payload) => { }); }); +ipcMain.handle("list-installed", async () => { + const apmBasePath = "/var/lib/apm/apm/files/ace-env/var/lib/apm"; + + try { + if (!fs.existsSync(apmBasePath)) { + logger.warn(`APM base path not found: ${apmBasePath}`); + return { + success: false, + message: "APM base path not found", + apps: [], + }; + } + + const packages = fs.readdirSync(apmBasePath, { withFileTypes: true }); + const installedApps: Array<{ + pkgname: string; + name: string; + version: string; + arch: string; + flags: string; + origin: "spark" | "apm"; + icon?: string; + isDependency: boolean; + }> = []; + + for (const pkg of packages) { + if (!pkg.isDirectory()) continue; + + const pkgname = pkg.name; + const pkgPath = path.join(apmBasePath, pkgname); + + const { code, stdout } = await runCommandCapture("apm", [ + "list", + pkgname, + ]); + if (code !== 0) { + logger.warn(`Failed to list package ${pkgname}: ${stdout}`); + continue; + } + + 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; + + const match = trimmed.match( + /^(\S+)\/\S+,\S+\s+(\S+)\s+(\S+)\s+\[(.+)\]$/, + ); + if (!match) continue; + + const [, listedPkgname, version, arch, flags] = match; + if (listedPkgname !== pkgname) continue; + + let appName = pkgname; + let icon = ""; + const entriesPath = path.join(pkgPath, "entries", "applications"); + const hasEntries = fs.existsSync(entriesPath); + + if (hasEntries) { + const desktopFiles = fs.readdirSync(entriesPath); + for (const file of desktopFiles) { + if (file.endsWith(".desktop")) { + const desktopPath = path.join(entriesPath, file); + 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(); + break; + } + } + } + + 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); + }); + + 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(SHELL_CALLER_PATH, [ - "aptss", + const { code, stdout, stderr } = await runCommandCapture("apm", [ "list", "--upgradable", ]); @@ -710,6 +820,9 @@ ipcMain.handle("list-upgradable", async () => { return { success: true, apps }; }); +ipcMain.handle("check-apm-available", async () => { + return await checkApmAvailable(); +}); // eslint-disable-next-line @typescript-eslint/no-explicit-any ipcMain.handle("uninstall-installed", async (_event, payload: any) => { diff --git a/electron/main/backend/telemetry.ts b/electron/main/backend/telemetry.ts index f901f4e5..587e6f9e 100644 --- a/electron/main/backend/telemetry.ts +++ b/electron/main/backend/telemetry.ts @@ -136,4 +136,4 @@ export function sendTelemetryOnce(storeVersion: string): void { .catch((err) => { logger.warn({ err }, "Telemetry request failed"); }); -} \ No newline at end of file +} diff --git a/electron/main/index.ts b/electron/main/index.ts index ab771229..39722ead 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -101,10 +101,8 @@ function getStoreFilterFromArgv(): "spark" | "apm" | "both" { return "both"; } - ipcMain.handle("get-store-filter", (): "spark" | "apm" | "both" => getStoreFilterFromArgv(), - ); async function createWindow() { @@ -220,7 +218,7 @@ app.whenReady().then(() => { }); createWindow(); handleCommandLine(process.argv); - // 启动后执行一次遥测(仅 Linux,不阻塞) + // 启动后执行一次遥测(仅 Linux,不阻塞) sendTelemetryOnce(getAppVersion()); }); @@ -281,7 +279,6 @@ function getTrayIconPath(): string | null { const FALLBACK_TRAY_PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHklEQVQ4T2NkYGD4z0ABYBwNwMAwGoChNQAAAABJRU5ErkJggg=="; - function getTrayImage(): | string | ReturnType { diff --git a/package-lock.json b/package-lock.json index 6ddd42b5..63934464 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "spark-store", - "version": "4.9.9", + "version": "4.9.9alpha4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "spark-store", - "version": "4.9.9", + "version": "4.9.9alpha4", "license": "GPL-3.0", "dependencies": { "@tailwindcss/vite": "^4.1.18", diff --git a/src/App.vue b/src/App.vue index f7cf0438..94cefdaa 100644 --- a/src/App.vue +++ b/src/App.vue @@ -20,27 +20,32 @@ :active-category="activeCategory" :category-counts="categoryCounts" :theme-mode="themeMode" + :apm-available="apmAvailable" @toggle-theme="toggleTheme" @select-category="selectCategory" @close="isSidebarOpen = false" - @open-about="openAboutModal" + @list="handleList" + @update="handleUpdate" /> -
- - + +
- + @@ -248,6 +250,7 @@ const updateError = ref(""); const showUninstallModal = ref(false); const uninstallTargetApp: Ref = ref(null); const showAboutModal = ref(false); +const apmAvailable = ref(false); /** 启动参数 --no-apm => 仅 Spark;--no-spark => 仅 APM;由主进程 IPC 提供 */ const storeFilter = ref<"spark" | "apm" | "both">("both"); @@ -403,16 +406,16 @@ const openDetail = async (app: App | Record) => { if (fullApp.isMerged && (fullApp.sparkApp || fullApp.apmApp)) { const [sparkInstalled, apmInstalled] = await Promise.all([ fullApp.sparkApp - ? window.ipcRenderer.invoke("check-installed", { + ? (window.ipcRenderer.invoke("check-installed", { pkgname: fullApp.sparkApp.pkgname, origin: "spark", - }) as Promise + }) as Promise) : Promise.resolve(false), fullApp.apmApp - ? window.ipcRenderer.invoke("check-installed", { + ? (window.ipcRenderer.invoke("check-installed", { pkgname: fullApp.apmApp.pkgname, origin: "apm", - }) as Promise + }) as Promise) : Promise.resolve(false), ]); if (sparkInstalled && !apmInstalled) { @@ -425,9 +428,9 @@ const openDetail = async (app: App | Record) => { const displayAppForScreenshots = fullApp.viewingOrigin !== undefined && fullApp.isMerged - ? (fullApp.viewingOrigin === "spark" + ? ((fullApp.viewingOrigin === "spark" ? fullApp.sparkApp - : fullApp.apmApp) ?? fullApp + : fullApp.apmApp) ?? fullApp) : fullApp; currentApp.value = fullApp; @@ -762,6 +765,7 @@ const refreshInstalledApps = async () => { appInfo.flags = app.flags; appInfo.arch = app.arch; appInfo.currentStatus = "installed"; + appInfo.isDependency = app.isDependency; } else { // 如果在当前应用列表中找不到该应用,创建一个最小的 App 对象 appInfo = { @@ -779,11 +783,12 @@ const refreshInstalledApps = async () => { update: "", size: "", img_urls: [], - icons: "", + icons: app.icon || "", origin: app.origin || (app.arch?.includes("apm") ? "apm" : "spark"), currentStatus: "installed", arch: app.arch, flags: app.flags, + isDependency: app.isDependency, }; } installedApps.value.push(appInfo); @@ -1061,6 +1066,12 @@ onMounted(async () => { // 从主进程获取启动参数(--no-apm / --no-spark),再加载数据 storeFilter.value = await window.ipcRenderer.invoke("get-store-filter"); + + // 检查 apm 是否可用 + if (storeFilter.value !== "spark") { + apmAvailable.value = await window.ipcRenderer.invoke("check-apm-available"); + } + await loadCategories(); // 分类目录加载后,并行加载主页数据和所有应用列表 diff --git a/src/components/AboutModal.vue b/src/components/AboutModal.vue index 677ebb0c..ca6a207b 100644 --- a/src/components/AboutModal.vue +++ b/src/components/AboutModal.vue @@ -33,7 +33,9 @@ class="h-24 w-24 rounded-3xl bg-white p-4 shadow-lg ring-1 ring-slate-900/5 dark:bg-slate-800" /> -

+

星火应用商店

@@ -42,16 +44,23 @@

- 版本号 + 版本号 {{ version }}
-

- 星火应用商店是专为 Linux 设计的应用商店,提供丰富的应用资源和便捷的安装体验。 +

+ 星火应用商店是专为 Linux + 设计的应用商店,提供丰富的应用资源和便捷的安装体验。

-
+
import { computed, useAttrs, ref, watch } from "vue"; import axios from "axios"; -import { - useInstallFeedback, - downloads, -} from "../global/downloadStatus"; +import { useInstallFeedback, downloads } from "../global/downloadStatus"; import { APM_STORE_BASE_URL } from "../global/storeConfig"; import type { App } from "../global/typedefinition"; @@ -289,7 +284,7 @@ const appPkgname = computed(() => props.app?.pkgname); const isIconLoaded = ref(false); -const viewingOrigin = ref<"spark" | "apm">("apm"); +const viewingOrigin = ref<"spark" | "apm">("spark"); watch( () => props.app, @@ -297,9 +292,9 @@ watch( isIconLoaded.value = false; if (newApp) { if (newApp.isMerged) { - // 若父组件已根据安装状态设置了优先展示的版本,则使用;否则默认 APM + // 若父组件已根据安装状态设置了优先展示的版本,则使用;否则默认 Spark viewingOrigin.value = - newApp.viewingOrigin ?? (newApp.apmApp ? "apm" : "spark"); + newApp.viewingOrigin ?? (newApp.sparkApp ? "spark" : "apm"); } else { viewingOrigin.value = newApp.origin; } @@ -348,7 +343,9 @@ const installBtnText = computed(() => { return "已安装"; } if (isOtherVersionInstalled.value) { - return viewingOrigin.value === "spark" ? "已安装 APM 版" : "已安装 Spark 版"; + return viewingOrigin.value === "spark" + ? "已安装 APM 版" + : "已安装 Spark 版"; } if (installFeedback.value) { const status = activeDownload.value?.status; diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 77d47795..29793aa7 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -1,36 +1,45 @@