Files
16gagent/.benchmark/petcare/electron/electron/main.ts
T
2026-06-06 10:40:48 +08:00

239 lines
6.4 KiB
TypeScript

import { app, BrowserWindow, Menu, nativeImage, ipcMain } from "electron";
import { join } from "path";
import { existsSync } from "fs";
import { setupTray } from "./tray";
import { setupIPC } from "./ipc";
import { setupUpdater } from "./updater";
import { checkSingleInstance, getWebUrl, getApiPath, isDev } from "./utils";
// ─── Constants ───────────────────────────────────
const WEB_PORT = 3000;
const API_PORT = 3001;
const APP_NAME = "petcare";
// ─── Single Instance Lock ────────────────────────
if (!checkSingleInstance()) {
app.quit();
process.exit(0);
}
// ─── State ───────────────────────────────────────
let mainWindow: BrowserWindow | null = null;
let apiProcess: ReturnType<typeof import("child_process").spawn> | null = null;
// ─── Create Window ───────────────────────────────
function createWindow(): BrowserWindow {
const preloadPath = join(__dirname, "preload.js");
const win = new BrowserWindow({
title: APP_NAME,
width: 1400,
height: 900,
minWidth: 800,
minHeight: 600,
show: false,
icon: getIconPath(),
webPreferences: {
preload: preloadPath,
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
},
titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default",
trafficLightPosition: { x: 16, y: 16 },
});
// ── Load content ──
const url = getWebUrl(isDev(), WEB_PORT);
if (url.startsWith("http")) {
win.loadURL(url);
} else {
win.loadFile(url);
}
// ── Show when ready ──
win.once("ready-to-show", () => {
win.show();
if (isDev()) {
win.webContents.openDevTools({ mode: "detach" });
}
});
// ── External links → default browser ──
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith("http")) {
require("electron").shell.openExternal(url);
}
return { action: "deny" };
});
// ── Prevent navigation away ──
win.webContents.on("will-navigate", (event, url) => {
const allowed = isDev()
? url.startsWith("http://localhost:3000")
: url.startsWith("file://");
if (!allowed) event.preventDefault();
});
return win;
}
// ─── Icon Path ───────────────────────────────────
function getIconPath(): string {
const candidates = [
join(__dirname, "..", "assets", "icon.png"),
join(__dirname, "..", "assets", "icon.icns"),
join(__dirname, "..", "assets", "icon.ico"),
];
for (const p of candidates) {
if (existsSync(p)) return p;
}
return "";
}
// ─── API Server (Production) ─────────────────────
function startApiServer(): void {
if (isDev()) return;
const apiPath = getApiPath();
if (!apiPath || !existsSync(apiPath)) {
console.warn("[main] API server not found at", apiPath);
return;
}
const { spawn } = require("child_process");
apiProcess = spawn("node", [apiPath], {
env: {
...process.env,
NODE_ENV: "production",
PORT: String(API_PORT),
HOST: "127.0.0.1",
},
stdio: ["ignore", "pipe", "pipe"],
});
apiProcess!.stdout?.on("data", (data: Buffer) => {
console.log("[api]", data.toString().trim());
});
apiProcess!.stderr?.on("data", (data: Buffer) => {
console.error("[api]", data.toString().trim());
});
apiProcess!.on("exit", (code: number) => {
console.warn("[main] API server exited with code", code);
apiProcess = null;
});
}
function stopApiServer(): void {
if (apiProcess) {
apiProcess.kill("SIGTERM");
apiProcess = null;
}
}
// ─── Menu ────────────────────────────────────────
function buildMenu(): void {
const isMac = process.platform === "darwin";
const template: Electron.MenuItemConstructorOptions[] = [
...(isMac
? [{
label: APP_NAME,
submenu: [
{ role: "about" as const },
{ type: "separator" as const },
{ role: "services" as const },
{ type: "separator" as const },
{ role: "hide" as const },
{ role: "hideOthers" as const },
{ role: "unhide" as const },
{ type: "separator" as const },
{ role: "quit" as const },
],
}]
: []),
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{ role: "selectAll" },
],
},
{
label: "View",
submenu: [
{ role: "reload" },
{ role: "forceReload" },
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn" },
{ role: "zoomOut" },
{ type: "separator" },
{ role: "togglefullscreen" },
],
},
{
label: "Window",
submenu: [
{ role: "minimize" },
{ role: "zoom" },
...(isMac
? [
{ type: "separator" as const },
{ role: "front" as const },
]
: [{ role: "close" as const }]),
],
},
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
}
// ─── App Lifecycle ───────────────────────────────
app.whenReady().then(() => {
startApiServer();
mainWindow = createWindow();
buildMenu();
if (mainWindow) {
setupTray(mainWindow, APP_NAME);
setupIPC(mainWindow);
}
setupUpdater(APP_NAME);
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
mainWindow = createWindow();
if (mainWindow) {
setupTray(mainWindow, APP_NAME);
setupIPC(mainWindow);
}
}
});
});
app.on("window-all-closed", () => {
stopApiServer();
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("before-quit", () => {
stopApiServer();
});
app.on("gpu-process-crashed" as any, () => {
console.warn("[main] GPU process crashed — continuing");
});
process.on("exit", () => {
stopApiServer();
});