Files
nextcloud-notes-desktop-app/electron/main.cjs
drelich 00e1f47511 Migrate to Electron and implement native PDF export
Major changes:
- Migrate from Tauri to Electron as primary desktop runtime
- Implement native print dialog for PDF export via Electron webview
- Add desktop runtime abstraction layer (supports both Electron and Tauri)
- Implement task list rendering in preview mode
- Add favorite notes sorting to display starred notes at top
- Add attachment upload functionality with file picker
- Improve sync reliability and Unicode filename support
- Add category color sync across devices via WebDAV
- Update documentation for Electron workflow

Technical improvements:
- Add Electron main process and preload bridge
- Create desktop service layer for runtime-agnostic operations
- Implement runtimeFetch for proxying network requests through Electron
- Add PrintView component for native print rendering
- Extract print/PDF utilities to shared module
- Update build configuration for Electron integration
2026-04-06 10:16:18 +02:00

165 lines
4.3 KiB
JavaScript

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 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();
}
}
});