Files
spark-store/electron/main/index.ts
shenmo 6e725e25c8 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.
2026-03-15 14:20:28 +08:00

322 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
app,
BrowserWindow,
ipcMain,
Menu,
nativeImage,
shell,
Tray,
nativeTheme,
session,
} from "electron";
import { fileURLToPath } from "node:url";
import path from "node:path";
import os from "node:os";
import fs from "node:fs";
import pino from "pino";
import { handleCommandLine } from "./deeplink.js";
import { isLoaded } from "../global.js";
import { tasks } from "./backend/install-manager.js";
// Assure single instance application
if (!app.requestSingleInstanceLock()) {
app.exit(0);
}
import "./backend/install-manager.js";
import "./handle-url-scheme.js";
const logger = pino({ name: "index.ts" });
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// The built directory structure
//
// ├─┬ dist-electron
// │ ├─┬ main
// │ │ └── index.js > Electron-Main
// │ └─┬ preload
// │ └── index.mjs > Preload-Scripts
// ├─┬ dist
// │ └── index.html > Electron-Renderer
//
process.env.APP_ROOT = path.join(__dirname, "../..");
export const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron");
export const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist");
export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
? path.join(process.env.APP_ROOT, "public")
: RENDERER_DIST;
// Disable GPU Acceleration for Windows 7
if (os.release().startsWith("6.1")) app.disableHardwareAcceleration();
// Set application name for Windows 10+ notifications
if (process.platform === "win32") app.setAppUserModelId(app.getName());
if (!app.requestSingleInstanceLock()) {
app.quit();
process.exit(0);
}
let win: BrowserWindow | null = null;
const preload = path.join(__dirname, "../preload/index.mjs");
const indexHtml = path.join(RENDERER_DIST, "index.html");
// Use app.getVersion() when the app is packaged.
const getUserAgent = (): string => {
const version =
app && app.isPackaged
? app.getVersion()
: process.env.npm_package_version || "dev";
return `Spark-Store/${version}`;
};
logger.info("User Agent: " + getUserAgent());
/** 根据启动参数 --no-apm / --no-spark 决定只展示的来源 */
function getStoreFilterFromArgv(): "spark" | "apm" | "both" {
const argv = process.argv;
const noApm = argv.includes("--no-apm");
const noSpark = argv.includes("--no-spark");
if (noApm && noSpark) return "both";
if (noApm) return "spark";
if (noSpark) return "apm";
return "both";
}
ipcMain.handle(
"get-store-filter",
(): "spark" | "apm" | "both" => getStoreFilterFromArgv(),
);
async function createWindow() {
win = new BrowserWindow({
title: "星火应用商店",
width: 1366,
height: 768,
autoHideMenuBar: true,
icon: path.join(process.env.VITE_PUBLIC, "favicon.ico"),
webPreferences: {
preload,
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
// nodeIntegration: true,
// Consider using contextBridge.exposeInMainWorld
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
// contextIsolation: false,
},
});
if (VITE_DEV_SERVER_URL) {
// #298
win.loadURL(VITE_DEV_SERVER_URL);
// Open devTool if the app is not packaged
win.webContents.openDevTools({ mode: "detach" });
} else {
win.loadFile(indexHtml);
}
// Test actively push message to the Electron-Renderer
win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString());
logger.info("Renderer process is ready.");
});
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith("https:")) shell.openExternal(url);
return { action: "deny" };
});
// win.webContents.on('will-navigate', (event, url) => { }) #344
win.on("close", (event) => {
// 截获 close 默认行为
event.preventDefault();
// 点击关闭时触发close事件我们按照之前的思路在关闭时隐藏窗口隐藏任务栏窗口
if (tasks.size > 0) {
win.hide();
win.setSkipTaskbar(true);
} else {
// 如果没有下载任务,才允许关闭窗口
win.destroy();
}
});
}
ipcMain.on("renderer-ready", (event, args) => {
logger.info(
"Received renderer-ready event with args: " + JSON.stringify(args),
);
isLoaded.value = args.status;
logger.info(`isLoaded set to: ${isLoaded.value}`);
});
ipcMain.on("set-theme-source", (event, theme: "system" | "light" | "dark") => {
nativeTheme.themeSource = theme;
});
// 启动系统更新工具(使用 pkexec 提升权限)
ipcMain.handle("run-update-tool", async () => {
try {
const { spawn } = await import("node:child_process");
const pkexecPath = "/usr/bin/pkexec";
const args = ["spark-update-tool"];
const child = spawn(pkexecPath, args, {
detached: true,
stdio: "ignore",
});
// 让子进程在后台运行且不影响主进程退出
child.unref();
logger.info("Launched pkexec spark-update-tool");
return { success: true };
} catch (err) {
logger.error({ err }, "Failed to launch spark-update-tool");
return { success: false, message: (err as Error)?.message || String(err) };
}
});
// 启动安装设置脚本(可能需要提升权限)
ipcMain.handle("open-install-settings", async () => {
try {
const { spawn } = await import("node:child_process");
const scriptPath =
"/opt/durapps/spark-store/bin/update-upgrade/ss-update-controler.sh";
const child = spawn("/opt/spark-store/extras/host-spawn", [scriptPath], {
detached: true,
stdio: "ignore",
});
child.unref();
logger.info(`Launched ${scriptPath}`);
return { success: true };
} catch (err) {
logger.error({ err }, "Failed to launch install settings script");
return { success: false, message: (err as Error)?.message || String(err) };
}
});
app.whenReady().then(() => {
// Set User-Agent for client
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
details.requestHeaders["User-Agent"] = getUserAgent();
callback({ cancel: false, requestHeaders: details.requestHeaders });
});
createWindow();
handleCommandLine(process.argv);
});
app.on("window-all-closed", () => {
win = null;
if (process.platform !== "darwin") app.quit();
});
app.on("second-instance", () => {
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore();
win.focus();
}
});
app.on("activate", () => {
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length) {
allWindows[0].focus();
} else {
createWindow();
}
});
app.on("will-quit", () => {
// Clean up temp dir
logger.info("Cleaning up temp dir");
fs.rmSync("/tmp/spark-store/", { recursive: true, force: true });
logger.info("Done, exiting");
});
// 设置托盘:系统中应用名称为 spark-store图标优先 spark-store其次 spark-store.svg再次替代图标
const ICONS_DIR = app.isPackaged
? path.join(process.resourcesPath, "icons")
: path.join(__dirname, "../..", "icons");
function resolveIconPath(filename: string): string {
return path.join(ICONS_DIR, filename);
}
/** 按优先级返回托盘图标路径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(() => {
tray = new Tray(getTrayImage());
const contextMenu = Menu.buildFromTemplate([
{
label: "显示主界面",
click: () => {
win.show();
},
},
{
label: "退出程序",
click: () => {
win.destroy();
},
},
]);
tray.setToolTip("星火应用商店");
tray.setContextMenu(contextMenu);
// 双击触发
tray.on("click", () => {
// 双击通知区图标实现应用的显示或隐藏
if (win.isVisible()) {
win.hide();
win.setSkipTaskbar(true);
} else {
win.show();
win.setSkipTaskbar(false);
}
});
});
// New window example arg: new windows url
// ipcMain.handle('open-win', (_, arg) => {
// const childWindow = new BrowserWindow({
// webPreferences: {
// preload,
// nodeIntegration: true,
// contextIsolation: false,
// },
// })
// if (VITE_DEV_SERVER_URL) {
// childWindow.loadURL(`${VITE_DEV_SERVER_URL}#${arg}`)
// } else {
// childWindow.loadFile(indexHtml, { hash: arg })
// }
// })