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:
@@ -1,15 +1,17 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { marked } from 'marked';
|
||||
import { message } from '@tauri-apps/plugin-dialog';
|
||||
import { emitTo, listen } from '@tauri-apps/api/event';
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import { Note } from '../types';
|
||||
import { NextcloudAPI } from '../api/nextcloud';
|
||||
import { FloatingToolbar } from './FloatingToolbar';
|
||||
import { InsertToolbar } from './InsertToolbar';
|
||||
import {
|
||||
exportPdfDocument,
|
||||
getDesktopRuntime,
|
||||
showDesktopMessage,
|
||||
} from '../services/desktop';
|
||||
import {
|
||||
getNoteTitleFromContent,
|
||||
PRINT_EXPORT_QUERY_PARAM,
|
||||
PrintExportPayload,
|
||||
sanitizeFileName,
|
||||
} from '../printExport';
|
||||
|
||||
@@ -51,6 +53,8 @@ export function NoteEditor({ note, onUpdateNote, onToggleFavorite, onUnsavedChan
|
||||
const previousNoteContentRef = useRef<string>('');
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const desktopRuntime = getDesktopRuntime();
|
||||
const exportActionLabel = desktopRuntime === 'electron' ? 'Export PDF' : 'Print Note';
|
||||
|
||||
useEffect(() => {
|
||||
onUnsavedChanges?.(hasUnsavedChanges);
|
||||
@@ -222,13 +226,6 @@ export function NoteEditor({ note, onUpdateNote, onToggleFavorite, onUnsavedChan
|
||||
if (!note) return;
|
||||
|
||||
setIsExportingPDF(true);
|
||||
const exportState: {
|
||||
printWindow: WebviewWindow | null;
|
||||
unlistenReady: (() => void) | null;
|
||||
} = {
|
||||
printWindow: null,
|
||||
unlistenReady: null,
|
||||
};
|
||||
|
||||
try {
|
||||
let contentForPrint = localContent;
|
||||
@@ -264,68 +261,22 @@ export function NoteEditor({ note, onUpdateNote, onToggleFavorite, onUnsavedChan
|
||||
|
||||
const title = getNoteTitleFromContent(localContent);
|
||||
const fileName = `${sanitizeFileName(title)}.pdf`;
|
||||
const jobId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const windowLabel = `print-export-${jobId}`;
|
||||
const html = marked.parse(contentForPrint || '', { async: false }) as string;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let settled = false;
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
exportState.unlistenReady?.();
|
||||
reject(new Error('Print view initialization timed out.'));
|
||||
}, 10000);
|
||||
const noteHtml = marked.parse(contentForPrint || '', { async: false }) as string;
|
||||
const payload: PrintExportPayload = {
|
||||
fileName,
|
||||
title,
|
||||
html: noteHtml,
|
||||
previewFont,
|
||||
previewFontSize,
|
||||
};
|
||||
|
||||
listen<{ jobId: string }>('print-export-ready', (event) => {
|
||||
if (settled || event.payload.jobId !== jobId) return;
|
||||
|
||||
settled = true;
|
||||
window.clearTimeout(timeoutId);
|
||||
exportState.unlistenReady?.();
|
||||
|
||||
void emitTo(windowLabel, 'print-export-payload', {
|
||||
fileName,
|
||||
title,
|
||||
html,
|
||||
previewFont,
|
||||
previewFontSize,
|
||||
})
|
||||
.then(() => resolve())
|
||||
.catch(reject);
|
||||
})
|
||||
.then((unlisten) => {
|
||||
exportState.unlistenReady = unlisten;
|
||||
exportState.printWindow = new WebviewWindow(windowLabel, {
|
||||
url: `/?${PRINT_EXPORT_QUERY_PARAM}=${encodeURIComponent(jobId)}`,
|
||||
title: 'Opening Print Dialog',
|
||||
width: 960,
|
||||
height: 1100,
|
||||
center: true,
|
||||
visible: true,
|
||||
focus: true,
|
||||
skipTaskbar: true,
|
||||
resizable: false,
|
||||
parent: 'main',
|
||||
});
|
||||
|
||||
void exportState.printWindow.once('tauri://error', (event) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
window.clearTimeout(timeoutId);
|
||||
exportState.unlistenReady?.();
|
||||
reject(new Error(String(event.payload ?? 'Failed to create print window.')));
|
||||
});
|
||||
})
|
||||
.catch(reject);
|
||||
await exportPdfDocument({
|
||||
...payload,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('PDF export failed:', error);
|
||||
const pendingPrintWindow = exportState.printWindow;
|
||||
if (pendingPrintWindow) {
|
||||
void pendingPrintWindow.close().catch(() => undefined);
|
||||
}
|
||||
try {
|
||||
await message('Failed to open the print-to-PDF view. Please try again.', {
|
||||
await showDesktopMessage('Failed to export the PDF. Please try again.', {
|
||||
title: 'Export Failed',
|
||||
kind: 'error',
|
||||
});
|
||||
@@ -333,10 +284,6 @@ export function NoteEditor({ note, onUpdateNote, onToggleFavorite, onUnsavedChan
|
||||
console.error('Could not show error dialog');
|
||||
}
|
||||
} finally {
|
||||
const disposeReadyListener = exportState.unlistenReady;
|
||||
if (disposeReadyListener) {
|
||||
disposeReadyListener();
|
||||
}
|
||||
setIsExportingPDF(false);
|
||||
}
|
||||
};
|
||||
@@ -401,13 +348,13 @@ export function NoteEditor({ note, onUpdateNote, onToggleFavorite, onUnsavedChan
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
|
||||
await message(`Attachment uploaded successfully!`, {
|
||||
await showDesktopMessage('Attachment uploaded successfully!', {
|
||||
title: 'Upload Complete',
|
||||
kind: 'info',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error);
|
||||
await message(`Failed to upload attachment: ${error}`, {
|
||||
await showDesktopMessage(`Failed to upload attachment: ${error}`, {
|
||||
title: 'Upload Failed',
|
||||
kind: 'error',
|
||||
});
|
||||
@@ -719,13 +666,20 @@ export function NoteEditor({ note, onUpdateNote, onToggleFavorite, onUnsavedChan
|
||||
? 'text-blue-500 cursor-wait'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title={isExportingPDF ? "Opening print dialog..." : "Print Note"}
|
||||
title={isExportingPDF ? `${exportActionLabel} in progress...` : exportActionLabel}
|
||||
>
|
||||
{isExportingPDF ? (
|
||||
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
) : desktopRuntime === 'electron' ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 16V4" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12l4 4 4-4" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 20h14" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 20v-2a1 1 0 011-1h8a1 1 0 011 1v2" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 9V4a1 1 0 011-1h10a1 1 0 011 1v5" />
|
||||
|
||||
Reference in New Issue
Block a user