!408 refactor(已安装应用): 合并 APM/Spark 分页为列表标签并新增统计

Merge pull request !408 from xiyidaiwa/Erotica
This commit is contained in:
xiyidaiwa
2026-07-29 06:47:41 +00:00
committed by shenmo7192
parent c1b89cab1a
commit 77b4244f1d
8 changed files with 816 additions and 321 deletions
+55 -26
View File
@@ -997,8 +997,16 @@ ipcMain.handle(
}> = [];
if (origin === "spark") {
// 如果提供了包名列表只检查这些包的安装状态(优化版)
if (pkgnameList && pkgnameList.length > 0) {
// 显式传入了包名列表(可能是空数组):只检查这些包的安装状态(优化版)
if (Array.isArray(pkgnameList)) {
if (pkgnameList.length === 0) {
// 商店目录尚未加载或该来源没有任何可枚举的包时,
// 直接返回空列表,避免退化为“全量扫描整个系统”后再被渲染端全部跳过,
// 否则会误报“已安装应用为空”。
logger.info("Spark 包名列表为空,跳过已安装检查");
return { success: true, apps: [] };
}
logger.info(
`使用优化模式检查 ${pkgnameList.length} 个 Spark 包的安装状态`,
);
@@ -1040,7 +1048,7 @@ ipcMain.handle(
return { success: true, apps: installedApps };
}
// 回退到全量扫描模式(未提供包名列表时
// 回退到全量扫描模式(仅当调用方未传入 pkgnameList 时,例如旧版直接调用
logger.info("使用全量扫描模式获取所有 Spark 已安装包");
const { code, stdout } = await runCommandCapture("dpkg-query", [
"-W",
@@ -1274,31 +1282,52 @@ ipcMain.handle("uninstall-installed", async (_event, payload: any) => {
};
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ipcMain.handle("launch-app", async (_event, payload: any) => {
const pkgname = typeof payload === "string" ? payload : payload.pkgname;
const origin = typeof payload === "string" ? "spark" : payload.origin;
interface LaunchAppPayload {
pkgname: string;
origin?: "spark" | "apm";
}
if (!pkgname) {
logger.warn("No pkgname provided for launch-app");
}
// 合法包名字符(Debian 包名规范 + Spark 应用包名常见字符),
// 用于拦截包含特殊字符的非法输入,避免命令注入。
const PKGNAME_PATTERN = /^[a-zA-Z0-9._+-]+$/;
let execCommand = "/opt/spark-store/extras/app-launcher";
let execParams = ["start", pkgname];
ipcMain.handle(
"launch-app",
async (
_event,
payload: LaunchAppPayload,
): Promise<{ success: boolean; message?: string }> => {
const pkgname = typeof payload === "string" ? payload : payload.pkgname;
const origin = typeof payload === "string" ? "spark" : payload.origin;
if (origin === "apm") {
execCommand = "apm";
execParams = ["launch", pkgname];
}
if (
!pkgname ||
typeof pkgname !== "string" ||
!PKGNAME_PATTERN.test(pkgname)
) {
logger.warn(`Invalid pkgname provided for launch-app: ${pkgname}`);
return { success: false, message: "Invalid package name" };
}
logger.info(
`Launching app: ${pkgname} with command: ${execCommand} ${execParams.join(" ")}`,
);
let execCommand = "/opt/spark-store/extras/app-launcher";
let execParams = ["start", pkgname];
spawn(execCommand, execParams, {
shell: false,
env: process.env,
detached: true,
stdio: "ignore",
}).unref();
});
if (origin === "apm") {
execCommand = "apm";
execParams = ["launch", pkgname];
}
logger.info(
`Launching app: ${pkgname} with command: ${execCommand} ${execParams.join(" ")}`,
);
spawn(execCommand, execParams, {
shell: false,
env: process.env,
detached: true,
stdio: "ignore",
}).unref();
return { success: true };
},
);
+125 -3
View File
@@ -7,6 +7,7 @@ import {
shell,
Tray,
nativeTheme,
screen,
session,
} from "electron";
import { fileURLToPath } from "node:url";
@@ -136,7 +137,9 @@ logger.info("User Agent: " + getUserAgent());
/** 根据启动参数 --no-apm / --no-spark 决定只展示的来源 */
function getStoreFilterFromArgv(): "spark" | "apm" | "both" {
if (process.arch === "loong64") {
// Currently loong64 only have spark support
// Currently loong64 only have spark support,
// 但用户显式传入 --no-spark 时应允许回退到 apm
if (process.argv.includes("--no-spark")) return "apm";
return "spark";
} else {
const argv = process.argv;
@@ -153,6 +156,12 @@ ipcMain.handle("get-store-filter", (): "spark" | "apm" | "both" =>
getStoreFilterFromArgv(),
);
// 渲染端在窗口尺寸变化时(包括无边框窗口鼠标拉边角)经此保存当前窗口尺寸
ipcMain.handle("save-window-bounds", (): boolean => {
if (win && !win.isDestroyed()) scheduleSaveBounds(win);
return true;
});
ipcMain.handle("get-app-version", (): string => getAppVersion());
ipcMain.handle("get-system-info", (): { distro: string } => getSystemInfo());
@@ -277,11 +286,103 @@ const showAndFocusMainWindow = (): void => {
win.focus();
};
// 窗口尺寸持久化:保存/恢复上一次调整后的窗口大小,避免每次打开都使用默认尺寸
const DEFAULT_WINDOW_SIZE = { width: 1366, height: 768 };
const MIN_WINDOW_SIZE = { width: 800, height: 500 };
interface WindowState {
width?: number;
height?: number;
x?: number;
y?: number;
maximized?: boolean;
}
function getWindowStatePath(): string {
// 延迟到调用时再取 userData,避免在 app ready 之前调用 app.getPath 出错
return path.join(app.getPath("userData"), "window-state.json");
}
// 校验保存的窗口位置是否至少部分落在某个显示器可见区域内,避免窗口跑到屏幕外
function isVisible(bounds: WindowState): boolean {
if (
bounds.width === undefined ||
bounds.height === undefined ||
bounds.x === undefined ||
bounds.y === undefined
) {
return false;
}
const displays = screen.getAllDisplays();
return displays.some((display) => {
const { x, y, width, height } = display.workArea;
const horizontally =
bounds.x < x + width && bounds.x + bounds.width > x;
const vertically =
bounds.y < y + height && bounds.y + bounds.height > y;
return horizontally && vertically;
});
}
function loadWindowState(): WindowState {
try {
const file = getWindowStatePath();
if (fs.existsSync(file)) {
const parsed = JSON.parse(
fs.readFileSync(file, "utf-8"),
) as WindowState;
if (
parsed.width !== undefined &&
parsed.height !== undefined &&
parsed.width >= MIN_WINDOW_SIZE.width &&
parsed.height >= MIN_WINDOW_SIZE.height &&
isVisible(parsed)
) {
return parsed;
}
logger.warn({ parsed }, "已保存的窗口状态无效,使用默认尺寸");
}
} catch (err) {
logger.warn({ err }, "读取窗口状态失败,使用默认尺寸");
}
return {};
}
function saveWindowState(state: WindowState): void {
try {
fs.writeFileSync(getWindowStatePath(), JSON.stringify(state));
logger.info({ state }, "已保存窗口状态");
} catch (err) {
logger.warn({ err }, "保存窗口状态失败");
}
}
let saveBoundsTimer: NodeJS.Timeout | null = null;
function scheduleSaveBounds(winInstance: BrowserWindow): void {
if (saveBoundsTimer) clearTimeout(saveBoundsTimer);
saveBoundsTimer = setTimeout(() => {
if (winInstance.isDestroyed()) return;
const { width, height, x, y } = winInstance.getBounds();
saveWindowState({
width,
height,
x,
y,
maximized: winInstance.isMaximized(),
});
}, 400);
}
async function createWindow() {
const saved = loadWindowState();
const mainWindow = new BrowserWindow({
title: "星火应用商店",
width: 1366,
height: 768,
width: saved.width ?? DEFAULT_WINDOW_SIZE.width,
height: saved.height ?? DEFAULT_WINDOW_SIZE.height,
x: saved.x,
y: saved.y,
minWidth: MIN_WINDOW_SIZE.width,
minHeight: MIN_WINDOW_SIZE.height,
frame: false,
autoHideMenuBar: true,
icon: path.join(process.env.VITE_PUBLIC, "favicon.ico"),
@@ -297,6 +398,18 @@ async function createWindow() {
});
win = mainWindow;
// 恢复上一次的最大化状态
if (saved.maximized) {
mainWindow.maximize();
}
logger.info({ saved }, "已恢复窗口状态");
// 窗口大小/位置/最大化变化后防抖保存,下次启动时恢复
// 位置/最大化变化由主进程事件保存;尺寸变化由渲染端 DOM resize 经 IPC 兜底保存
mainWindow.on("moved", () => scheduleSaveBounds(mainWindow));
mainWindow.on("maximize", () => scheduleSaveBounds(mainWindow));
mainWindow.on("unmaximize", () => scheduleSaveBounds(mainWindow));
if (VITE_DEV_SERVER_URL) {
// #298
mainWindow.loadURL(VITE_DEV_SERVER_URL);
@@ -330,6 +443,15 @@ async function createWindow() {
mainWindow.on("close", (event) => {
if (allowAppExit) {
// 真正退出前同步保存最终窗口尺寸(防抖可能尚未触发)
const { width, height, x, y } = mainWindow.getBounds();
saveWindowState({
width,
height,
x,
y,
maximized: mainWindow.isMaximized(),
});
return;
}