- Replace Node.js fetch with Electron net.request for better session handling - Extract WebDAV path building into reusable private methods with proper URL encoding - Add helper methods for category path encoding and attachment path construction - Fix note move operations to use remote category/filename from saved snapshots - Add ensureCategoryDirectoryExists to handle nested category creation - Only move/rename attachment folders when note has any
197 lines
5.2 KiB
JavaScript
197 lines
5.2 KiB
JavaScript
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();
|
|
}
|
|
}
|
|
});
|