main.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import { app, BrowserWindow, dialog, ipcMain } from 'electron';
  2. import { createServer, IncomingMessage, ServerResponse } from 'http';
  3. import next from 'next';
  4. import path from 'path';
  5. import fs from 'fs';
  6. import net from 'net';
  7. // ─── Types ───────────────────────────────────────────────────────────────────
  8. interface AppConfig {
  9. recentProjects: RecentProject[];
  10. lastActiveProjectPath?: string;
  11. }
  12. interface RecentProject {
  13. name: string;
  14. path: string;
  15. lastOpenedAt: string;
  16. novelType: string;
  17. }
  18. // ─── Globals ─────────────────────────────────────────────────────────────────
  19. const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
  20. // 开发模式下直接连接已运行的 Next.js dev server,不自己启动
  21. let nextApp: ReturnType<typeof next> | null = null;
  22. let handle: ((req: IncomingMessage, res: ServerResponse) => void) | null = null;
  23. if (!isDev) {
  24. nextApp = next({ dev: false, dir: path.join(__dirname, '..') });
  25. handle = nextApp.getRequestHandler();
  26. }
  27. let httpServer: ReturnType<typeof createServer> | null = null;
  28. let mainWindow: BrowserWindow | null = null;
  29. // ─── Port Discovery ───────────────────────────────────────────────────────────
  30. function findAvailablePort(startPort: number): Promise<number> {
  31. return new Promise((resolve, reject) => {
  32. const server = net.createServer();
  33. server.listen(startPort, '127.0.0.1', () => {
  34. const address = server.address();
  35. const port = typeof address === 'object' && address ? address.port : startPort;
  36. server.close(() => resolve(port));
  37. });
  38. server.on('error', () => {
  39. // Port in use, try next one
  40. findAvailablePort(startPort + 1).then(resolve).catch(reject);
  41. });
  42. });
  43. }
  44. // ─── App Config (userData) ────────────────────────────────────────────────────
  45. function getAppConfigPath(): string {
  46. return path.join(app.getPath('userData'), 'app-config.json');
  47. }
  48. function readAppConfig(): AppConfig {
  49. const configPath = getAppConfigPath();
  50. try {
  51. if (fs.existsSync(configPath)) {
  52. const raw = fs.readFileSync(configPath, 'utf-8');
  53. return JSON.parse(raw) as AppConfig;
  54. }
  55. } catch {
  56. // Return default config if file is missing or malformed
  57. }
  58. return { recentProjects: [] };
  59. }
  60. function writeAppConfig(config: AppConfig): void {
  61. const configPath = getAppConfigPath();
  62. const dir = path.dirname(configPath);
  63. if (!fs.existsSync(dir)) {
  64. fs.mkdirSync(dir, { recursive: true });
  65. }
  66. // Atomic write: write to temp file then rename
  67. const tmpPath = configPath + '.tmp';
  68. fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), 'utf-8');
  69. fs.renameSync(tmpPath, configPath);
  70. }
  71. // ─── IPC Handlers ─────────────────────────────────────────────────────────────
  72. // Select a folder using the native OS dialog
  73. ipcMain.handle('select-folder', async () => {
  74. const result = await dialog.showOpenDialog({
  75. properties: ['openDirectory'],
  76. });
  77. return result.canceled ? null : result.filePaths[0];
  78. });
  79. // Read global app config from userData
  80. ipcMain.handle('get-app-config', () => {
  81. return readAppConfig();
  82. });
  83. // Write global app config to userData
  84. ipcMain.handle('save-app-config', (_event, config: AppConfig) => {
  85. writeAppConfig(config);
  86. return { success: true };
  87. });
  88. // ─── App Startup ──────────────────────────────────────────────────────────────
  89. async function startApp(): Promise<void> {
  90. let appPort = 3000;
  91. if (isDev) {
  92. // 开发模式:直接连接已运行的 Next.js dev server (localhost:3000)
  93. appPort = 3000;
  94. } else {
  95. // 生产模式:自己启动内嵌 Next.js 服务器
  96. await nextApp!.prepare();
  97. appPort = await findAvailablePort(3000);
  98. httpServer = createServer((req: IncomingMessage, res: ServerResponse) => {
  99. handle!(req, res);
  100. });
  101. await new Promise<void>((resolve) => {
  102. httpServer!.listen(appPort, '127.0.0.1', () => {
  103. resolve();
  104. });
  105. });
  106. }
  107. mainWindow = new BrowserWindow({
  108. width: 1400,
  109. height: 900,
  110. webPreferences: {
  111. contextIsolation: true,
  112. nodeIntegration: false,
  113. preload: path.join(__dirname, 'preload.js'),
  114. },
  115. });
  116. mainWindow.loadURL(`http://localhost:${appPort}/features/novel-writer`);
  117. mainWindow.on('closed', () => {
  118. mainWindow = null;
  119. });
  120. }
  121. // ─── Lifecycle ────────────────────────────────────────────────────────────────
  122. app.whenReady().then(startApp).catch((err) => {
  123. console.error('Failed to start app:', err);
  124. app.quit();
  125. });
  126. app.on('window-all-closed', () => {
  127. if (process.platform !== 'darwin') {
  128. app.quit();
  129. }
  130. });
  131. app.on('activate', () => {
  132. if (mainWindow === null) {
  133. startApp().catch((err) => {
  134. console.error('Failed to restart app:', err);
  135. });
  136. }
  137. });
  138. app.on('before-quit', () => {
  139. if (httpServer) {
  140. httpServer.close();
  141. httpServer = null;
  142. }
  143. });