Files
Toonflow-app/scripts/main.ts
ACT丶流星雨 851fb6253d no message
2026-04-11 02:43:51 +08:00

328 lines
9.9 KiB
TypeScript
Raw Permalink 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, protocol } from "electron";
import path from "path";
import fs from "fs";
import Module from "module";
// 加速 Electron 启动:跳过 GPU 信息收集,减少初始化耗时
app.commandLine.appendSwitch("disable-gpu-shader-disk-cache");
app.commandLine.appendSwitch("disable-features", "CalculateNativeWinOcclusion");
const TARGET_ENTRIES = new Set(["assets", "models", "serve", "skills", "web", "vendor"]);
function copyDir(src: string, dest: string): void {
if (!fs.existsSync(src)) return;
fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
const s = path.join(src, entry.name);
const d = path.join(dest, entry.name);
entry.isDirectory() ? copyDir(s, d) : fs.existsSync(d) || fs.copyFileSync(s, d);
}
}
declare const __APP_VERSION__: string;
function compareVersions(a: string, b: string): number {
const pa = a
.split(".")
.map((n) => Number.parseInt(n, 10))
.filter((n) => Number.isFinite(n));
const pb = b
.split(".")
.map((n) => Number.parseInt(n, 10))
.filter((n) => Number.isFinite(n));
const len = Math.max(pa.length, pb.length);
for (let i = 0; i < len; i++) {
const va = pa[i] ?? 0;
const vb = pb[i] ?? 0;
if (va > vb) return 1;
if (va < vb) return -1;
}
return 0;
}
function initializeData(): void {
const srcDir = path.join(process.resourcesPath, "data");
const destDir = path.join(app.getPath("userData"), "data");
const versionFilePath = path.join(destDir, "version.txt");
let shouldForceReplace = false;
if (!fs.existsSync(versionFilePath)) {
shouldForceReplace = true;
} else {
const localVersion = fs.readFileSync(versionFilePath, "utf-8").trim();
if (compareVersions(localVersion, __APP_VERSION__) < 0) {
shouldForceReplace = true;
}
}
for (const dir of TARGET_ENTRIES) {
const targetDir = path.join(destDir, dir);
if (shouldForceReplace) {
fs.rmSync(targetDir, { recursive: true, force: true });
copyDir(path.join(srcDir, dir), targetDir);
continue;
}
if (!fs.existsSync(targetDir)) {
copyDir(path.join(srcDir, dir), targetDir);
}
}
if (shouldForceReplace) {
fs.mkdirSync(destDir, { recursive: true });
fs.writeFileSync(versionFilePath, `${__APP_VERSION__}\n`, "utf-8");
}
}
//获取全部依赖路径,优先从 unpacked 加载原生模块,其他模块从 asar 加载
function getNodeModulesPaths(): string[] {
const paths: string[] = [];
if (app.isPackaged) {
// external 依赖(原生模块)在 unpacked 目录
const unpackedNodeModules = path.join(process.resourcesPath, "app.asar.unpacked", "node_modules");
if (fs.existsSync(unpackedNodeModules)) {
paths.push(unpackedNodeModules);
}
// 普通依赖在 asar 内
const asarNodeModules = path.join(process.resourcesPath, "app.asar", "node_modules");
paths.push(asarNodeModules);
} else {
paths.push(path.join(process.cwd(), "node_modules"));
}
return paths;
}
//动态加载
function requireWithCustomPaths(modulePath: string): any {
const appNodeModulesPaths = getNodeModulesPaths();
// 保存原始方法
const originalNodeModulePaths = (Module as any)._nodeModulePaths;
// 临时修改模块路径解析
(Module as any)._nodeModulePaths = function (from: string): string[] {
const paths = originalNodeModulePaths.call(this, from);
// 将主程序的 node_modules 添加到前面
for (let i = appNodeModulesPaths.length - 1; i >= 0; i--) {
const p = appNodeModulesPaths[i];
if (!paths.includes(p)) {
paths.unshift(p);
}
}
return paths;
};
try {
// 清除缓存确保加载最新
delete require.cache[require.resolve(modulePath)];
return require(modulePath);
} finally {
// 恢复原始方法
(Module as any)._nodeModulePaths = originalNodeModulePaths;
}
}
let mainWindow: BrowserWindow | null = null;
let loadingWindow: BrowserWindow | null = null;
const loadingHtml = `data:text/html;charset=utf-8,${encodeURIComponent(`<!DOCTYPE html>
<html><head><meta charset="utf-8"><style>
*{margin:0;padding:0;box-sizing:border-box}
body{height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;
background:#fff;color:#333;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
user-select:none;-webkit-app-region:drag}
.spinner{width:48px;height:48px;border:4px solid rgba(0,0,0,.1);
border-top-color:#000;border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
p{margin-top:20px;font-size:14px;opacity:.6}
</style></head><body><div class="spinner"></div><p>正在启动服务…</p></body></html>`)}`;
function showLoading(): void {
loadingWindow = new BrowserWindow({
width: 1000,
height: 700,
minWidth: 800,
minHeight: 500,
frame: false,
resizable: false,
maximizable: false,
minimizable: false,
show: true,
backgroundColor: "#ffffff",
autoHideMenuBar: true,
titleBarStyle: "hidden",
titleBarOverlay: {
color: "#ffffff",
symbolColor: "#333333",
height: 36,
},
});
loadingWindow.setMenuBarVisibility(false);
loadingWindow.removeMenu();
loadingWindow.on("closed", () => {
loadingWindow = null;
});
void loadingWindow.loadURL(loadingHtml);
}
function closeLoading(): void {
if (loadingWindow && !loadingWindow.isDestroyed()) {
loadingWindow.close();
loadingWindow = null;
}
}
function createMainWindow(): Promise<void> {
return new Promise((resolve) => {
const win = new BrowserWindow({
width: 1000,
height: 700,
minWidth: 800,
minHeight: 500,
frame: false,
show: false,
autoHideMenuBar: true,
resizable: true,
thickFrame: true,
});
mainWindow = win;
win.setMenuBarVisibility(false);
win.removeMenu();
win.on("closed", () => {
mainWindow = null;
});
win.once("ready-to-show", () => {
closeLoading();
win.show();
resolve();
});
const isDev = process.env.NODE_ENV === "dev" || !app.isPackaged;
if (process.env.VITE_DEV) {
void win.loadURL("http://localhost:50188");
} else {
const htmlPath = isDev
? path.join(process.cwd(), "data", "web", "index.html")
: path.join(app.getPath("userData"), "data", "web", "index.html");
void win.loadFile(htmlPath);
}
});
}
let closeServeFn: (() => Promise<void>) | undefined;
protocol.registerSchemesAsPrivileged([
{
scheme: "toonflow",
privileges: {
secure: true,
supportFetchAPI: true,
corsEnabled: true,
},
},
]);
app.whenReady().then(async () => {
// 立即显示加载窗口data URL + backgroundColor瞬间可见
showLoading();
try {
let servePath: string;
if (app.isPackaged) {
// 生产环境:让出主线程一次,确保 loading 窗口渲染后再做耗时文件拷贝
await new Promise((r) => setTimeout(r, 0));
initializeData();
servePath = path.join(app.getPath("userData"), "data", "serve", "app.js");
} else {
// 开发环境直接加载源码tsx 通过 -r tsx 注册了 require 钩子)
servePath = path.join(process.cwd(), "src", "app.ts");
}
// 使用自定义路径加载模块
const mod = requireWithCustomPaths(servePath);
closeServeFn = mod.closeServe;
const port = await mod.default(true);
process.env.PORT = port;
await new Promise<void>((resolve, reject) => {
setTimeout(() => {
resolve();
}, 2000);
});
// 注册协议处理器
protocol.handle("toonflow", (request) => {
const url = new URL(request.url);
const pathname = url.hostname.toLowerCase();
const handlers: Record<string, () => object> = {
getappurl: () => ({ url: process.env.URL ?? `http://localhost:${port}/api` }),
windowminimize: () => {
mainWindow?.minimize();
return { ok: true };
},
windowmaximize: () => {
if (mainWindow?.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow?.maximize();
}
return { ok: true };
},
windowclose: () => {
app.exit(0);
return { ok: true };
},
apprestart: () => {
// 延迟执行,让响应先返回给前端
setTimeout(() => {
app.relaunch();
app.exit(0);
}, 500);
return { ok: true, message: "应用即将重启" };
},
windowismaximized: () => ({
maximized: mainWindow?.isMaximized() ?? false,
}),
opendevtool: () => {
mainWindow?.webContents.openDevTools();
return { ok: true };
},
openurlwithbrowser: () => {
const search = url.searchParams;
const targetUrl = search.get("url");
if (targetUrl) {
const { shell } = require("electron");
shell.openExternal(targetUrl);
return { ok: true };
} else {
return { ok: false, error: "缺少url参数" };
}
},
};
const handler = handlers[pathname];
const responseData = handler ? handler() : { error: "未知接口" };
return new Response(JSON.stringify(responseData), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store",
},
});
});
// 服务启动成功,创建主窗口(主窗口 ready-to-show 时自动关闭loading
await createMainWindow();
} catch (err) {
console.error("[服务启动失败]:", err);
await createMainWindow();
}
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
}
});
app.on("before-quit", async (event) => {
if (closeServeFn) await closeServeFn();
});