| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- import { app, BrowserWindow, dialog, ipcMain } from 'electron';
- import { createServer, IncomingMessage, ServerResponse } from 'http';
- import next from 'next';
- import path from 'path';
- import fs from 'fs';
- import net from 'net';
- // ─── Types ───────────────────────────────────────────────────────────────────
- interface AppConfig {
- recentProjects: RecentProject[];
- lastActiveProjectPath?: string;
- }
- interface RecentProject {
- name: string;
- path: string;
- lastOpenedAt: string;
- novelType: string;
- }
- // ─── Globals ─────────────────────────────────────────────────────────────────
- const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
- // 开发模式下直接连接已运行的 Next.js dev server,不自己启动
- let nextApp: ReturnType<typeof next> | null = null;
- let handle: ((req: IncomingMessage, res: ServerResponse) => void) | null = null;
- if (!isDev) {
- nextApp = next({ dev: false, dir: path.join(__dirname, '..') });
- handle = nextApp.getRequestHandler();
- }
- let httpServer: ReturnType<typeof createServer> | null = null;
- let mainWindow: BrowserWindow | null = null;
- // ─── Port Discovery ───────────────────────────────────────────────────────────
- function findAvailablePort(startPort: number): Promise<number> {
- return new Promise((resolve, reject) => {
- const server = net.createServer();
- server.listen(startPort, '127.0.0.1', () => {
- const address = server.address();
- const port = typeof address === 'object' && address ? address.port : startPort;
- server.close(() => resolve(port));
- });
- server.on('error', () => {
- // Port in use, try next one
- findAvailablePort(startPort + 1).then(resolve).catch(reject);
- });
- });
- }
- // ─── App Config (userData) ────────────────────────────────────────────────────
- function getAppConfigPath(): string {
- return path.join(app.getPath('userData'), 'app-config.json');
- }
- function readAppConfig(): AppConfig {
- const configPath = getAppConfigPath();
- try {
- if (fs.existsSync(configPath)) {
- const raw = fs.readFileSync(configPath, 'utf-8');
- return JSON.parse(raw) as AppConfig;
- }
- } catch {
- // Return default config if file is missing or malformed
- }
- return { recentProjects: [] };
- }
- function writeAppConfig(config: AppConfig): void {
- const configPath = getAppConfigPath();
- const dir = path.dirname(configPath);
- if (!fs.existsSync(dir)) {
- fs.mkdirSync(dir, { recursive: true });
- }
- // Atomic write: write to temp file then rename
- const tmpPath = configPath + '.tmp';
- fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), 'utf-8');
- fs.renameSync(tmpPath, configPath);
- }
- // ─── IPC Handlers ─────────────────────────────────────────────────────────────
- // Select a folder using the native OS dialog
- ipcMain.handle('select-folder', async () => {
- const result = await dialog.showOpenDialog({
- properties: ['openDirectory'],
- });
- return result.canceled ? null : result.filePaths[0];
- });
- // Read global app config from userData
- ipcMain.handle('get-app-config', () => {
- return readAppConfig();
- });
- // Write global app config to userData
- ipcMain.handle('save-app-config', (_event, config: AppConfig) => {
- writeAppConfig(config);
- return { success: true };
- });
- // ─── App Startup ──────────────────────────────────────────────────────────────
- async function startApp(): Promise<void> {
- let appPort = 3000;
- if (isDev) {
- // 开发模式:直接连接已运行的 Next.js dev server (localhost:3000)
- appPort = 3000;
- } else {
- // 生产模式:自己启动内嵌 Next.js 服务器
- await nextApp!.prepare();
- appPort = await findAvailablePort(3000);
- httpServer = createServer((req: IncomingMessage, res: ServerResponse) => {
- handle!(req, res);
- });
- await new Promise<void>((resolve) => {
- httpServer!.listen(appPort, '127.0.0.1', () => {
- resolve();
- });
- });
- }
- mainWindow = new BrowserWindow({
- width: 1400,
- height: 900,
- webPreferences: {
- contextIsolation: true,
- nodeIntegration: false,
- preload: path.join(__dirname, 'preload.js'),
- },
- });
- mainWindow.loadURL(`http://localhost:${appPort}/features/novel-writer`);
- mainWindow.on('closed', () => {
- mainWindow = null;
- });
- }
- // ─── Lifecycle ────────────────────────────────────────────────────────────────
- app.whenReady().then(startApp).catch((err) => {
- console.error('Failed to start app:', err);
- app.quit();
- });
- app.on('window-all-closed', () => {
- if (process.platform !== 'darwin') {
- app.quit();
- }
- });
- app.on('activate', () => {
- if (mainWindow === null) {
- startApp().catch((err) => {
- console.error('Failed to restart app:', err);
- });
- }
- });
- app.on('before-quit', () => {
- if (httpServer) {
- httpServer.close();
- httpServer = null;
- }
- });
|