refactor: update tray icon handling and add APM installation checks

- Removed the deprecated `--no-spark` argument from launch configuration.
- Enhanced tray icon management by introducing a new function to resolve icon paths based on application packaging status.
- Implemented APM installation checks in the install manager, prompting users to install APM if not available, with appropriate dialog messages and handling for installation success or failure.
- Added a new shell script for executing the Spark Store with environment checks for container and architecture compatibility.
This commit is contained in:
2026-03-15 14:20:28 +08:00
parent 7e18ba7981
commit 6e725e25c8
4 changed files with 181 additions and 55 deletions

View File

@@ -1,4 +1,4 @@
import { ipcMain, WebContents } from "electron"; import { BrowserWindow, dialog, ipcMain, WebContents } from "electron";
import { spawn, ChildProcess, exec } from "node:child_process"; import { spawn, ChildProcess, exec } from "node:child_process";
import { promisify } from "node:util"; import { promisify } from "node:util";
import fs from "node:fs"; import fs from "node:fs";
@@ -101,6 +101,33 @@ const parseInstalledList = (output: string) => {
return apps; return apps;
}; };
/** 检测本机是否已安装 apm 命令 */
const checkApmAvailable = async (): Promise<boolean> => {
const { code, stdout } = await runCommandCapture("which", ["apm"]);
const found = code === 0 && stdout.trim().length > 0;
if (!found) logger.info("未检测到 apm 命令");
return found;
};
/** 提权执行 shell-caller aptss install apm 安装 APM安装后需用户重启电脑 */
const runInstallApm = async (superUserCmd: string): Promise<boolean> => {
const execCommand = superUserCmd || SHELL_CALLER_PATH;
const execParams = superUserCmd
? [SHELL_CALLER_PATH, "aptss", "install", "apm"]
: [SHELL_CALLER_PATH, "aptss", "install", "apm"];
logger.info(`执行安装 APM: ${execCommand} ${execParams.join(" ")}`);
const { code, stdout, stderr } = await runCommandCapture(
execCommand,
execParams,
);
if (code !== 0) {
logger.error({ code, stdout, stderr }, "安装 APM 失败");
return false;
}
logger.info("安装 APM 完成");
return true;
};
const parseUpgradableList = (output: string) => { const parseUpgradableList = (output: string) => {
const apps: Array<{ const apps: Array<{
pkgname: string; pkgname: string;
@@ -175,6 +202,63 @@ ipcMain.on("queue-install", async (event, download_json) => {
const execParams = []; const execParams = [];
const downloadDir = `/tmp/spark-store/download/${pkgname}`; const downloadDir = `/tmp/spark-store/download/${pkgname}`;
// APM 应用:若本机没有 apm 命令,弹窗提示并可选提权安装 APM安装后需重启电脑
if (origin === "apm") {
const hasApm = await checkApmAvailable();
if (!hasApm) {
const win = BrowserWindow.fromWebContents(webContents);
const { response } = await dialog.showMessageBox(win ?? undefined, {
type: "question",
title: "需要安装 APM",
message: "此应用需要使用 APM 安装。",
detail:
"APM是星火应用商店的容器包管理器安装APM后方可安装此应用是否确认安装",
buttons: ["确认", "取消"],
defaultId: 0,
cancelId: 1,
});
if (response !== 0) {
webContents.send("install-complete", {
id,
success: false,
time: Date.now(),
exitCode: -1,
message: JSON.stringify({
message: "用户取消安装 APM无法继续安装此应用",
stdout: "",
stderr: "",
}),
});
return;
}
const installApmOk = await runInstallApm(superUserCmd);
if (!installApmOk) {
webContents.send("install-complete", {
id,
success: false,
time: Date.now(),
exitCode: -1,
message: JSON.stringify({
message: "安装 APM 失败,请检查网络或权限后重试",
stdout: "",
stderr: "",
}),
});
return;
} else {
// 安装APM成功提示用户已安装成功需要重启后方可展示应用
await dialog.showMessageBox(win ?? undefined, {
type: "info",
title: "APM 安装成功",
message: "恭喜您APM 已成功安装",
detail: "APM 应用需重启后方可展示和使用,若完成安装后无法在应用列表中展示,请重启电脑后继续。",
buttons: ["确定"],
defaultId: 0,
});
}
}
}
if (origin === "spark") { if (origin === "spark") {
// Spark Store logic // Spark Store logic
if (upgradeOnly) { if (upgradeOnly) {
@@ -586,31 +670,6 @@ ipcMain.handle("list-upgradable", async () => {
return { success: true, apps }; return { success: true, apps };
}); });
ipcMain.handle("list-installed", async () => {
const superUserCmd = await checkSuperUserCommand();
const execCommand =
superUserCmd.length > 0 ? superUserCmd : SHELL_CALLER_PATH;
const execParams =
superUserCmd.length > 0
? [SHELL_CALLER_PATH, "aptss", "list", "--installed"]
: ["aptss", "list", "--installed"];
const { code, stdout, stderr } = await runCommandCapture(
execCommand,
execParams,
);
if (code !== 0) {
logger.error(`list-installed failed: ${stderr || stdout}`);
return {
success: false,
message: stderr || stdout || `list-installed failed with code ${code}`,
apps: [],
};
}
const apps = parseInstalledList(stdout);
return { success: true, apps };
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
ipcMain.handle("uninstall-installed", async (_event, payload: any) => { ipcMain.handle("uninstall-installed", async (_event, payload: any) => {

View File

@@ -3,6 +3,7 @@ import {
BrowserWindow, BrowserWindow,
ipcMain, ipcMain,
Menu, Menu,
nativeImage,
shell, shell,
Tray, Tray,
nativeTheme, nativeTheme,
@@ -234,36 +235,45 @@ app.on("will-quit", () => {
logger.info("Done, exiting"); logger.info("Done, exiting");
}); });
// 设置托盘 // 设置托盘:系统中应用名称为 spark-store图标优先 spark-store其次 spark-store.svg再次替代图标
// 获取图标路径 const ICONS_DIR = app.isPackaged
function getIconPath() { ? path.join(process.resourcesPath, "icons")
let iconPath = ""; : path.join(__dirname, "../..", "icons");
const iconName =
process.platform === "win32" ? "amber-pm-logo.ico" : "amber-pm-logo.png"; // 图标文件名linux下需要png格式不然会不显示
// 判断是否在打包模式
if (app.isPackaged) {
// 打包模式
iconPath = path.join(process.resourcesPath, "icons", iconName); // 路径根据自身情况调整
} else {
// 开发模式
const projectRoot = path.join(__dirname, "../.."); // __dirname 指向 dist-electron/main但资源在项目根目录所以..指向上一级
iconPath = path.join(projectRoot, "icons", iconName);
}
// 检查文件是否存在 function resolveIconPath(filename: string): string {
if (fs.existsSync(iconPath)) { return path.join(ICONS_DIR, filename);
logger.info("图标文件存在:" + iconPath);
return iconPath;
} else {
logger.error("图标文件不存在:" + iconPath);
// 返回一个默认图标路径或null
return null;
}
} }
let tray = null; /** 按优先级返回托盘图标路径spark-store(.png|.ico) → amber-pm-logo.png。托盘不支持 SVG故不尝试 spark-store.svg */
function getTrayIconPath(): string | null {
const ext = process.platform === "win32" ? ".ico" : ".png";
const candidates = [
`spark-store${ext}`
];
for (const name of candidates) {
const iconPath = resolveIconPath(name);
if (fs.existsSync(iconPath)) {
logger.info("托盘图标使用: " + iconPath);
return iconPath;
}
}
logger.warn("未找到托盘图标,将使用替代图标。查找目录: " + ICONS_DIR);
return null;
}
/** 16x16 透明 PNG用作托盘无图标时的替代 */
const FALLBACK_TRAY_PNG =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHklEQVQ4T2NkYGD4z0ABYBwNwMAwGoChNQAAAABJRU5ErkJggg==";
function getTrayImage(): string | ReturnType<typeof nativeImage.createFromDataURL> {
const iconPath = getTrayIconPath();
if (iconPath) return iconPath;
return nativeImage.createFromDataURL(FALLBACK_TRAY_PNG);
}
let tray: Tray | null = null;
app.whenReady().then(() => { app.whenReady().then(() => {
tray = new Tray(getIconPath()); tray = new Tray(getTrayImage());
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ {
label: "显示主界面", label: "显示主界面",

View File

@@ -55,10 +55,39 @@ case "$command_type" in
echo "操作已取消" echo "操作已取消"
exit 0 exit 0
fi fi
elif [[ "$2" == "install" ]]; then
packages="${@:3}"
# 确认框通用参数
title="确认安装"
text="正在准备安装: $packages\n\n若这是您下达的安装指令请选择确认继续安装"
# 优先尝试 garma其次 zenity
if command -v garma &> /dev/null; then
garma --question --title="$title" --text="$text" \
--ok-label="确认安装" --cancel-label="取消" --width=400
confirmed=$?
elif command -v zenity &> /dev/null; then
zenity --question --title="$title" --text="$text" \
--ok-label="确认安装" --cancel-label="取消" --width=400
confirmed=$?
else else
# 非 remove 命令,直接执行 aptss echo "错误:未找到 garma 或 zenity无法显示确认对话框。安装操作已拒绝。"
/usr/bin/aptss "${@:2}" 2>&1 exit 1
fi
# 根据确认结果执行
if [[ $confirmed -eq 0 ]]; then
/usr/bin/aptss "${@:2}" -y 2>&1
exit_code=$? exit_code=$?
else
echo "操作已取消"
exit 0
fi
else
# 非 remove 命令,拒绝执行
echo "拒绝执行 aptss 白名单外的指令"
exit 1
fi fi
;; ;;

28
extras/spark-store Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/bash
# 基础参数,始终添加 --no-sandbox
ARGS="--no-sandbox"
# 检查是否在容器中运行
# 方法1: 检查 root 路径
ROOT_PATH=$(readlink -f /proc/self/root)
if [ "$ROOT_PATH" != "/" ]; then
echo "检测到容器环境 (root path: $ROOT_PATH)"
ARGS="$ARGS --no-apm"
fi
# 方法2: 检查 IS_ACE_ENV 环境变量
if [ "$IS_ACE_ENV" = "1" ]; then
echo "检测到 ACE 容器环境"
ARGS="$ARGS --no-apm"
fi
# 检查是否为 arm64 且为 wayland session
ARCH=$(uname -m)
if [ "$ARCH" = "aarch64" ] && [ "$XDG_SESSION_TYPE" = "wayland" ]; then
echo "检测到 arm64 架构和 Wayland 会话"
ARGS="$ARGS --disable-gpu"
fi
# 执行程序
/opt/spark-store/bin/spark-store $ARGS "$@"