const { app, BrowserWindow, dialog, ipcMain } = require('electron'); const path = require('path'); const fs = require('fs'); let mainWindow = null; let currentFilePath = null; let pendingFilePath = null; let fileWatcher = null; function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, minWidth: 800, minHeight: 600, icon: path.join(__dirname, 'assets', 'icon.ico'), webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false }, titleBarStyle: 'default', show: false }); mainWindow.webContents.on('did-finish-load', () => { if (pendingFilePath) { openFile(pendingFilePath); pendingFilePath = null; } }); mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html')); mainWindow.once('ready-to-show', () => { mainWindow.show(); }); mainWindow.setMenu(null); // Unsaved changes warning on close (with timeout fallback) mainWindow.on('close', (e) => { e.preventDefault(); try { mainWindow.webContents.send('window:confirmClose'); } catch (err) { // Renderer is gone, force close mainWindow.removeAllListeners('close'); mainWindow.close(); return; } // Safety: if renderer doesn't respond within 5s, force close setTimeout(() => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.removeAllListeners('close'); mainWindow.close(); } }, 5000); }); mainWindow.on('closed', () => { stopWatching(); mainWindow = null; }); } // File watcher - detect external modifications function startWatching(filePath) { stopWatching(); try { fileWatcher = fs.watch(filePath, (eventType) => { if (eventType === 'change' && mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('file:externallyModified', filePath); } }); } catch (err) { // Silently ignore watch errors } } function stopWatching() { if (fileWatcher) { fileWatcher.close(); fileWatcher = null; } } async function showOpenDialog() { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openFile'], filters: [ { name: 'Markdown 文件', extensions: ['md', 'markdown', 'txt'] }, { name: '所有文件', extensions: ['*'] } ] }); if (!result.canceled && result.filePaths.length > 0) { return result.filePaths[0]; } return null; } function openFile(filePath) { try { const content = fs.readFileSync(filePath, 'utf-8'); currentFilePath = filePath; startWatching(filePath); mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`); mainWindow.webContents.send('file:opened', { filePath, content }); } catch (err) { dialog.showErrorBox('错误', `无法读取文件: ${err.message}`); } } // IPC Handlers ipcMain.handle('dialog:openFile', async () => { try { const filePath = await showOpenDialog(); if (filePath) { const content = fs.readFileSync(filePath, 'utf-8'); currentFilePath = filePath; startWatching(filePath); mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`); return { filePath, content }; } return null; } catch (err) { return { error: err.message }; } }); ipcMain.handle('file:read', async (event, filePath) => { try { const content = fs.readFileSync(filePath, 'utf-8'); return { success: true, content }; } catch (err) { return { success: false, error: err.message }; } }); ipcMain.handle('file:save', async (event, { filePath, content }) => { try { if (filePath) { stopWatching(); fs.writeFileSync(filePath, content, 'utf-8'); currentFilePath = filePath; startWatching(filePath); mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`); return { success: true, filePath }; } else { const result = await dialog.showSaveDialog(mainWindow, { filters: [ { name: 'Markdown 文件', extensions: ['md'] }, { name: '文本文件', extensions: ['txt'] } ] }); if (!result.canceled) { stopWatching(); fs.writeFileSync(result.filePath, content, 'utf-8'); currentFilePath = result.filePath; startWatching(result.filePath); mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`); return { success: true, filePath: result.filePath }; } return { success: false, canceled: true }; } } catch (err) { return { success: false, error: err.message }; } }); ipcMain.handle('file:saveAs', async (event, { content }) => { try { const result = await dialog.showSaveDialog(mainWindow, { filters: [ { name: 'Markdown 文件', extensions: ['md'] }, { name: '文本文件', extensions: ['txt'] } ] }); if (!result.canceled) { stopWatching(); fs.writeFileSync(result.filePath, content, 'utf-8'); currentFilePath = result.filePath; startWatching(result.filePath); mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`); return { success: true, filePath: result.filePath }; } return { success: false, canceled: true }; } catch (err) { return { success: false, error: err.message }; } }); ipcMain.handle('file:getCurrentPath', () => { return currentFilePath; }); ipcMain.handle('file:stats', async (event, filePath) => { try { const stats = fs.statSync(filePath); return { success: true, size: stats.size, mtime: stats.mtime.toISOString() }; } catch (err) { return { success: false, error: err.message }; } }); ipcMain.handle('file:reload', async () => { if (!currentFilePath) return { success: false, error: '没有打开的文件' }; try { const content = fs.readFileSync(currentFilePath, 'utf-8'); return { success: true, content, filePath: currentFilePath }; } catch (err) { return { success: false, error: err.message }; } }); ipcMain.handle('window:forceClose', () => { stopWatching(); mainWindow.removeAllListeners('close'); mainWindow.close(); }); // Handle file open from command line or file association function getCommandLineFile() { const args = process.argv.slice(1); if (args.length > 0 && !args[0].startsWith('--')) { const filePath = args[0]; if (fs.existsSync(filePath)) { return filePath; } } return null; } // App lifecycle app.whenReady().then(() => { app.on('open-file', (event, filePath) => { event.preventDefault(); if (mainWindow && mainWindow.webContents.isLoading()) { pendingFilePath = filePath; } else if (mainWindow) { openFile(filePath); } else { pendingFilePath = filePath; } }); createWindow(); const fileToOpen = getCommandLineFile(); if (fileToOpen) { pendingFilePath = fileToOpen; } }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } });