import { app, BrowserWindow, shell, ipcMain, dialog, nativeTheme, globalShortcut, clipboard } from 'electron'; import path from 'path'; import { fileURLToPath, pathToFileURL } from 'url'; import fs from 'fs'; import log from 'electron-log'; import { generatePdf } from './services/pdfGenerator'; import { selectHtmlFile } from './services/htmlImport'; import { electronState } from './state'; log.initialize(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); process.env.NOTEBOOK_ROOT = path.join(app.getPath('documents'), 'XCDesktop'); if (!fs.existsSync(process.env.NOTEBOOK_ROOT)) { try { fs.mkdirSync(process.env.NOTEBOOK_ROOT, { recursive: true }); } catch (err) { log.error('Failed to create notebook directory:', err); } } electronState.setDevelopment(!app.isPackaged); let lastClipboardText = ''; function startClipboardWatcher() { lastClipboardText = clipboard.readText(); setInterval(() => { try { const currentText = clipboard.readText(); if (currentText && currentText !== lastClipboardText) { lastClipboardText = currentText; log.info('Clipboard changed, syncing to remote'); const win = electronState.getMainWindow(); if (win) { win.webContents.send('remote-clipboard-auto-sync', currentText); } } } catch (e) { // ignore } }, 1000); } async function createWindow() { const initialSymbolColor = nativeTheme.shouldUseDarkColors ? '#ffffff' : '#000000'; const mainWindow = new BrowserWindow({ width: 1280, height: 800, minWidth: 1600, minHeight: 900, autoHideMenuBar: true, titleBarStyle: 'hidden', titleBarOverlay: { color: '#00000000', symbolColor: initialSymbolColor, height: 32, }, webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: false, webviewTag: true, preload: path.join(__dirname, 'preload.cjs'), }, }); electronState.setMainWindow(mainWindow); mainWindow.setMenu(null); mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('http:') || url.startsWith('https:')) { shell.openExternal(url); return { action: 'deny' }; } return { action: 'allow' }; }); if (electronState.isDevelopment()) { log.info('Loading development URL...'); try { await mainWindow.loadURL('http://localhost:5173'); } catch (e) { log.error('Failed to load dev URL. Make sure npm run electron:dev is used.', e); } mainWindow.webContents.openDevTools(); } else { log.info(`Loading production URL with port ${electronState.getServerPort()}...`); await mainWindow.loadURL(`http://localhost:${electronState.getServerPort()}`); } } ipcMain.handle('export-pdf', async (event, title, htmlContent) => { const win = BrowserWindow.fromWebContents(event.sender); if (!win) return { success: false, error: 'No window found' }; try { const { filePath } = await dialog.showSaveDialog(win, { title: '导出 PDF', defaultPath: `${title}.pdf`, filters: [{ name: 'PDF Files', extensions: ['pdf'] }] }); if (!filePath) return { success: false, canceled: true }; if (!htmlContent) { throw new Error('No HTML content provided for PDF export'); } const pdfData = await generatePdf(htmlContent); fs.writeFileSync(filePath, pdfData); return { success: true, filePath }; } catch (error: any) { log.error('Export PDF failed:', error); return { success: false, error: error.message }; } }); ipcMain.handle('select-html-file', async (event) => { const win = BrowserWindow.fromWebContents(event.sender); return selectHtmlFile(win); }); ipcMain.handle('update-titlebar-buttons', async (event, symbolColor: string) => { const win = BrowserWindow.fromWebContents(event.sender); if (win) { win.setTitleBarOverlay({ symbolColor }); return { success: true }; } return { success: false }; }); ipcMain.handle('clipboard-read-text', async () => { try { const text = clipboard.readText(); return { success: true, text }; } catch (error: any) { log.error('Clipboard read failed:', error); return { success: false, error: error.message }; } }); ipcMain.handle('clipboard-write-text', async (event, text: string) => { try { clipboard.writeText(text); return { success: true }; } catch (error: any) { log.error('Clipboard write failed:', error); return { success: false, error: error.message }; } }); ipcMain.handle('remote-fetch-drives', async (_event, serverHost: string, port: number, password?: string) => { try { let url = `http://${serverHost}:${port}/api/files/drives`; if (password) { url += `?password=${encodeURIComponent(password)}`; } const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch drives: ${response.statusText}`); } const data = await response.json(); const items = data.items || []; return { success: true, data: items.map((item: { name: string; isDirectory: boolean; size: number }) => ({ name: item.name, path: item.name, type: item.isDirectory ? 'dir' : 'file', size: item.size, modified: '', })) }; } catch (error: any) { log.error('Remote fetch drives failed:', error); return { success: false, error: error.message }; } }); ipcMain.handle('remote-fetch-files', async (_event, serverHost: string, port: number, filePath: string, password?: string) => { try { let url = `http://${serverHost}:${port}/api/files/browse?path=${encodeURIComponent(filePath)}&allowSystem=true`; if (password) { url += `&password=${encodeURIComponent(password)}`; } const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch files: ${response.statusText}`); } const data = await response.json(); const items = data.items || []; return { success: true, data: items.map((item: { name: string; isDirectory: boolean; size: number; modified: Date }) => ({ name: item.name, path: data.currentPath ? `${data.currentPath}/${item.name}` : item.name, type: item.isDirectory ? 'dir' : 'file', size: item.size, modified: item.modified?.toString(), })) }; } catch (error: any) { log.error('Remote fetch files failed:', error); return { success: false, error: error.message }; } }); ipcMain.handle('remote-upload-file', async (_event, serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => { try { const win = electronState.getMainWindow(); if (!win) { throw new Error('No window found'); } const fullPath = path.resolve(filePath); if (!fs.existsSync(fullPath)) { throw new Error('File not found'); } const fileBuffer = fs.readFileSync(fullPath); const fileName = path.basename(fullPath); let url = `http://${serverHost}:${port}/api/files/upload`; if (password) { url += `?password=${encodeURIComponent(password)}`; } const formData = new FormData(); const blob = new Blob([fileBuffer]); formData.append('file', blob, fileName); if (remotePath) { formData.append('path', remotePath); } const response = await fetch(url, { method: 'POST', body: formData, }); if (!response.ok) { throw new Error(`Upload failed: ${response.statusText}`); } return { success: true }; } catch (error: any) { log.error('Remote upload failed:', error); return { success: false, error: error.message }; } }); ipcMain.handle('remote-download-file', async (_event, id: string, serverHost: string, port: number, fileName: string, remotePath: string, localPath: string, password?: string) => { try { log.info('Remote download params:', { id, serverHost, port, fileName, remotePath, localPath, password }); const win = electronState.getMainWindow(); if (!win) { throw new Error('No window found'); } const fullRemotePath = remotePath ? `${remotePath}\\${fileName}` : fileName; let url = `http://${serverHost}:${port}/api/files/${encodeURIComponent(fullRemotePath)}`; if (password) { url += `?password=${encodeURIComponent(password)}`; } const response = await fetch(url); if (!response.ok) { throw new Error(`Download failed: ${response.statusText}`); } const contentLength = response.headers.get('Content-Length'); if (!contentLength) { throw new Error('Server did not return Content-Length'); } const totalSize = parseInt(contentLength, 10); const targetDir = localPath || 'C:\\'; const targetPath = path.join(targetDir, fileName); if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); } const fileStream = fs.createWriteStream(targetPath); const reader = response.body?.getReader(); if (!reader) { throw new Error('Failed to get response body reader'); } let downloadedSize = 0; const CHUNK_SIZE = 64 * 1024; while (true) { const { done, value } = await reader.read(); if (done) { break; } if (value) { downloadedSize += value.length; fileStream.write(value); const progress = Math.round((downloadedSize / totalSize) * 100); win.webContents.send('download-progress', { id, progress }); } } fileStream.end(); return { success: true, filePath: targetPath }; } catch (error: any) { log.error('Remote download failed:', error); return { success: false, error: error.message }; } }); async function startServer() { if (electronState.isDevelopment()) { log.info('In dev mode, assuming external servers are running.'); return; } const serverPath = path.join(__dirname, '../dist-api/server.js'); const serverUrl = pathToFileURL(serverPath).href; log.info(`Starting internal server from: ${serverPath}`); try { const serverModule = await import(serverUrl); if (serverModule.startServer) { const port = await serverModule.startServer(); electronState.setServerPort(port); log.info(`Internal server started successfully on port ${port}`); } else { log.warn('startServer function not found in server module, using default port 3001'); } } catch (e) { log.error('Failed to start internal server:', e); } } app.whenReady().then(async () => { await startServer(); await createWindow(); startClipboardWatcher(); globalShortcut.register('CommandOrControl+Shift+C', () => { log.info('Global shortcut: sync clipboard to remote'); const win = electronState.getMainWindow(); if (win) { win.webContents.send('remote-clipboard-sync-to-remote'); } }); globalShortcut.register('CommandOrControl+Shift+V', () => { log.info('Global shortcut: sync clipboard from remote'); const win = electronState.getMainWindow(); if (win) { win.webContents.send('remote-clipboard-sync-from-remote'); } }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('window-all-closed', () => { globalShortcut.unregisterAll(); if (process.platform !== 'darwin') { app.quit(); } });