refactor: migrate PDF export to desktop runtime abstraction
- Extract desktop-specific logic into src/services/desktop.ts - Add runtime detection for Electron vs Tauri environments - Simplify NoteEditor by delegating PDF export to desktop service - Update printExport.ts to remove unused storage helpers - Add Electron main process files in electron/ directory - Update vite config and types for Electron integration - Update .gitignore and package.json for Electron workflow
This commit is contained in:
159
electron/main.cjs
Normal file
159
electron/main.cjs
Normal file
@@ -0,0 +1,159 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
8
electron/preload.cjs
Normal file
8
electron/preload.cjs
Normal file
@@ -0,0 +1,8 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronDesktop', {
|
||||
showMessage: (options) => ipcRenderer.invoke('desktop:show-message', options),
|
||||
exportPdf: (payload) => ipcRenderer.invoke('desktop:export-pdf', payload),
|
||||
httpRequest: (payload) => ipcRenderer.invoke('desktop:http-request', payload),
|
||||
getRuntime: () => 'electron',
|
||||
});
|
||||
Reference in New Issue
Block a user