mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 01:10:16 +08:00
update:apm管理改为应用管理
This commit is contained in:
@@ -791,139 +791,177 @@ ipcMain.on("remove-installed", async (_event, payload) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("list-installed", async () => {
|
ipcMain.handle(
|
||||||
const apmBasePath = "/var/lib/apm/apm/files/ace-env/var/lib/apm";
|
"list-installed",
|
||||||
|
async (_event, origin: "apm" | "spark" = "apm") => {
|
||||||
|
const apmBasePath = "/var/lib/apm/apm/files/ace-env/var/lib/apm";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用 apm list --installed 获取所有已安装应用
|
const installedApps: Array<{
|
||||||
const { code, stdout } = await runCommandCapture("apm", [
|
pkgname: string;
|
||||||
"list",
|
name: string;
|
||||||
"--installed",
|
version: string;
|
||||||
]);
|
arch: string;
|
||||||
|
flags: string;
|
||||||
|
origin: "spark" | "apm";
|
||||||
|
icon?: string;
|
||||||
|
isDependency: boolean;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
if (code !== 0) {
|
if (origin === "spark") {
|
||||||
logger.warn(`Failed to list installed packages: ${stdout}`);
|
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 {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Failed to list installed packages",
|
message: error instanceof Error ? error.message : String(error),
|
||||||
apps: [],
|
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 () => {
|
ipcMain.handle("list-upgradable", async () => {
|
||||||
const { code, stdout, stderr } = await runCommandCapture("apm", [
|
const { code, stdout, stderr } = await runCommandCapture("apm", [
|
||||||
|
|||||||
628
package-lock.json
generated
628
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
src/App.vue
26
src/App.vue
@@ -116,9 +116,11 @@
|
|||||||
:apps="installedApps"
|
:apps="installedApps"
|
||||||
:loading="installedLoading"
|
:loading="installedLoading"
|
||||||
:error="installedError"
|
:error="installedError"
|
||||||
|
:active-origin="activeInstalledOrigin"
|
||||||
@close="closeInstalledModal"
|
@close="closeInstalledModal"
|
||||||
@refresh="refreshInstalledApps"
|
@refresh="refreshInstalledApps"
|
||||||
@uninstall="uninstallInstalledApp"
|
@uninstall="uninstallInstalledApp"
|
||||||
|
@switch-origin="handleSwitchOrigin"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UpdateAppsModal
|
<UpdateAppsModal
|
||||||
@@ -240,6 +242,7 @@ const loading = ref(true);
|
|||||||
const showDownloadDetailModal = ref(false);
|
const showDownloadDetailModal = ref(false);
|
||||||
const currentDownload: Ref<DownloadItem | null> = ref(null);
|
const currentDownload: Ref<DownloadItem | null> = ref(null);
|
||||||
const showInstalledModal = ref(false);
|
const showInstalledModal = ref(false);
|
||||||
|
const activeInstalledOrigin = ref<"apm" | "spark">("apm");
|
||||||
const installedApps = ref<App[]>([]);
|
const installedApps = ref<App[]>([]);
|
||||||
const installedLoading = ref(false);
|
const installedLoading = ref(false);
|
||||||
const installedError = ref("");
|
const installedError = ref("");
|
||||||
@@ -881,11 +884,17 @@ const closeInstalledModal = () => {
|
|||||||
showInstalledModal.value = false;
|
showInstalledModal.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSwitchOrigin = (origin: "apm" | "spark") => {
|
||||||
|
activeInstalledOrigin.value = origin;
|
||||||
|
refreshInstalledApps();
|
||||||
|
};
|
||||||
|
|
||||||
const refreshInstalledApps = async () => {
|
const refreshInstalledApps = async () => {
|
||||||
installedLoading.value = true;
|
installedLoading.value = true;
|
||||||
installedError.value = "";
|
installedError.value = "";
|
||||||
try {
|
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) {
|
if (!result?.success) {
|
||||||
installedApps.value = [];
|
installedApps.value = [];
|
||||||
installedError.value = result?.message || "读取已安装应用失败";
|
installedError.value = result?.message || "读取已安装应用失败";
|
||||||
@@ -894,7 +903,16 @@ const refreshInstalledApps = async () => {
|
|||||||
|
|
||||||
installedApps.value = [];
|
installedApps.value = [];
|
||||||
for (const app of result.apps) {
|
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) {
|
if (appInfo) {
|
||||||
appInfo.flags = app.flags;
|
appInfo.flags = app.flags;
|
||||||
appInfo.arch = app.arch;
|
appInfo.arch = app.arch;
|
||||||
@@ -1298,7 +1316,9 @@ onMounted(async () => {
|
|||||||
} else {
|
} else {
|
||||||
// 如果找不到应用,回退到搜索模式
|
// 如果找不到应用,回退到搜索模式
|
||||||
searchQuery.value = data.pkgname;
|
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`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
@click="$emit('list')"
|
@click="$emit('list')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-download"></i>
|
<i class="fas fa-download"></i>
|
||||||
<span>APM 应用管理</span>
|
<span>应用管理</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -23,10 +23,38 @@
|
|||||||
已安装应用
|
已安装应用
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
来自本机 APM 安装列表
|
管理本机安装的应用程序
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<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
|
<button
|
||||||
type="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"
|
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[];
|
apps: App[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
|
activeOrigin: "apm" | "spark";
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: "close"): void;
|
(e: "close"): void;
|
||||||
(e: "refresh"): void;
|
(e: "refresh"): void;
|
||||||
(e: "uninstall", app: App): void;
|
(e: "uninstall", app: App): void;
|
||||||
|
(e: "switch-origin", origin: "apm" | "spark"): void;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user