const fs = require('node:fs/promises'); const path = require('node:path'); const { app, BrowserWindow, dialog, ipcMain } = require('electron'); const rendererUrl = process.env.ELECTRON_RENDERER_URL; const isDev = Boolean(rendererUrl); let mainWindow = null; const waitForPrintDocument = () => ` new Promise((resolve) => { const pendingImages = Array.from(document.images).filter((image) => !image.complete); const waitForImages = Promise.all( pendingImages.map( (image) => new Promise((done) => { image.addEventListener('load', () => done(), { once: true }); image.addEventListener('error', () => done(), { once: true }); }) ) ); const waitForFonts = document.fonts ? document.fonts.ready.catch(() => undefined) : Promise.resolve(); Promise.all([waitForImages, waitForFonts]).then(() => { requestAnimationFrame(() => requestAnimationFrame(() => resolve())); }); }); `; function createMainWindow() { mainWindow = new BrowserWindow({ width: 1300, height: 800, minWidth: 800, minHeight: 600, show: false, webPreferences: { preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, sandbox: false, }, }); mainWindow.once('ready-to-show', () => { mainWindow.show(); }); if (isDev) { void mainWindow.loadURL(rendererUrl); mainWindow.webContents.openDevTools({ mode: 'detach' }); } else { void mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); } } app.whenReady().then(() => { createMainWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createMainWindow(); } }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); ipcMain.handle('desktop:show-message', async (event, options) => { const ownerWindow = BrowserWindow.fromWebContents(event.sender) ?? mainWindow; const typeMap = { info: 'info', warning: 'warning', error: 'error', }; await dialog.showMessageBox(ownerWindow, { type: typeMap[options.kind] || 'info', title: options.title || app.name, message: options.message, buttons: ['OK'], defaultId: 0, }); }); ipcMain.handle('desktop:http-request', async (_event, payload) => { const body = payload.bodyBase64 != null ? Buffer.from(payload.bodyBase64, 'base64') : payload.bodyText; const response = await fetch(payload.url, { method: payload.method || 'GET', headers: payload.headers, body, }); const buffer = Buffer.from(await response.arrayBuffer()); return { ok: response.ok, status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()), bodyBase64: buffer.toString('base64'), }; }); ipcMain.handle('desktop:export-pdf', async (event, payload) => { const ownerWindow = BrowserWindow.fromWebContents(event.sender) ?? mainWindow; const saveResult = await dialog.showSaveDialog(ownerWindow, { title: 'Export PDF', defaultPath: payload.fileName, filters: [{ name: 'PDF Document', extensions: ['pdf'] }], }); if (saveResult.canceled || !saveResult.filePath) { return { canceled: true }; } const pdfWindow = new BrowserWindow({ width: 960, height: 1100, show: false, parent: ownerWindow ?? undefined, webPreferences: { sandbox: false, contextIsolation: true, nodeIntegration: false, spellcheck: false, }, }); try { await pdfWindow.loadURL( `data:text/html;charset=utf-8,${encodeURIComponent(payload.documentHtml)}` ); await pdfWindow.webContents.executeJavaScript(waitForPrintDocument(), true); const pdfData = await pdfWindow.webContents.printToPDF({ printBackground: true, preferCSSPageSize: true, }); await fs.writeFile(saveResult.filePath, pdfData); return { canceled: false, filePath: saveResult.filePath, }; } finally { if (!pdfWindow.isDestroyed()) { pdfWindow.destroy(); } } });