Files
Toonflow-app/scripts/main.ts
2026-04-02 23:54:56 +08:00

328 lines
10 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, 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");
declare const __APP_VERSION__: string | undefined;
/**
* 将 extraResources 中的 data 目录复制到用户数据目录(跳过已存在的文件,保留用户修改)
*/
function getVersionFromFile(filePath: string): string | null {
try {
if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath, "utf8").trim();
}
} catch {}
return null;
}
function writeVersionToFile(filePath: string, version: string): void {
fs.writeFileSync(filePath, version, { encoding: "utf8" });
}
function copyDirForce(src: string, dest: string): void {
if (!fs.existsSync(src)) return;
if (fs.existsSync(dest)) {
fs.rmSync(dest, { recursive: true, force: true });
}
copyDirRecursive(src, dest);
}
function initializeData(): void {
const srcDir = path.join(process.resourcesPath, "data");
const destDir = path.join(app.getPath("userData"), "data");
const versionFile = path.join(destDir, "version.txt");
const currentVersion = typeof __APP_VERSION__ !== "undefined" ? __APP_VERSION__ : "0.0.0";
const userVersion = getVersionFromFile(versionFile);
// 首次安装或无version.txt直接全量拷贝
if (!fs.existsSync(destDir) || !userVersion) {
copyDirRecursive(srcDir, destDir);
writeVersionToFile(versionFile, currentVersion);
return;
}
// 版本号不同则覆盖 serve 和 web 目录
if (userVersion !== currentVersion) {
copyDirForce(path.join(srcDir, "serve"), path.join(destDir, "serve"));
copyDirForce(path.join(srcDir, "web"), path.join(destDir, "web"));
writeVersionToFile(versionFile, currentVersion);
}
}
function copyDirRecursive(src: string, dest: string): void {
if (!fs.existsSync(src)) return;
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
// 跳过 oss 文件夹和 db2.sqlite 文件
if (entry.isDirectory() && entry.name === "oss") continue;
if (!entry.isDirectory() && entry.name === "db2.sqlite") continue;
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDirRecursive(srcPath, destPath);
} else if (!fs.existsSync(destPath)) {
fs.copyFileSync(srcPath, destPath);
}
}
}
//获取全部依赖路径,优先从 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> = {
getport: () => ({ port: port }),
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();
});