const fs = require('node:fs/promises'); const path = require('node:path'); const { app, BrowserWindow, dialog, ipcMain, net } = 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 != null ? Buffer.from(payload.bodyText, 'utf8') : null; return await new Promise((resolve, reject) => { const request = net.request({ url: payload.url, method: payload.method || 'GET', session: BrowserWindow.getAllWindows()[0]?.webContents.session, }); for (const [name, value] of Object.entries(payload.headers || {})) { request.setHeader(name, value); } request.on('response', (response) => { const chunks = []; response.on('data', (chunk) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }); response.on('end', () => { const headers = {}; for (const [name, value] of Object.entries(response.headers)) { headers[name] = Array.isArray(value) ? value.join(', ') : String(value ?? ''); } const buffer = Buffer.concat(chunks); resolve({ ok: response.statusCode >= 200 && response.statusCode < 300, status: response.statusCode, statusText: response.statusMessage || '', headers, bodyBase64: buffer.toString('base64'), }); }); response.on('error', reject); }); request.on('error', reject); if (body && body.length > 0) { request.write(body); } request.end(); }); }); 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 tempHtmlPath = path.join( app.getPath('temp'), `nextcloud-notes-export-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.html` ); const pdfWindow = new BrowserWindow({ width: 960, height: 1100, show: false, parent: ownerWindow ?? undefined, webPreferences: { sandbox: false, contextIsolation: true, nodeIntegration: false, spellcheck: false, }, }); try { await fs.writeFile(tempHtmlPath, payload.documentHtml, 'utf8'); await pdfWindow.loadFile(tempHtmlPath); 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 { await fs.unlink(tempHtmlPath).catch(() => undefined); if (!pdfWindow.isDestroyed()) { pdfWindow.destroy(); } } });