Merge dev: PDF export improvements and offline fonts (v0.1.4)
This commit is contained in:
@@ -226,15 +226,120 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
|
|||||||
setTitleManuallyEdited(!titleMatchesFirstLine);
|
setTitleManuallyEdited(!titleMatchesFirstLine);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadFontAsBase64 = async (fontPath: string): Promise<string> => {
|
||||||
|
const response = await fetch(fontPath);
|
||||||
|
const blob = await response.blob();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const base64 = reader.result as string;
|
||||||
|
// Remove data URL prefix to get just the base64 string
|
||||||
|
resolve(base64.split(',')[1]);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleExportPDF = async () => {
|
const handleExportPDF = async () => {
|
||||||
if (!note) return;
|
if (!note) return;
|
||||||
|
|
||||||
setIsExportingPDF(true);
|
setIsExportingPDF(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Create PDF
|
||||||
|
const pdf = new jsPDF({
|
||||||
|
orientation: 'portrait',
|
||||||
|
unit: 'mm',
|
||||||
|
format: 'a4',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load and add custom fonts based on preview font selection
|
||||||
|
const fontMap: { [key: string]: { regular: string; italic: string; name: string } } = {
|
||||||
|
'Merriweather': {
|
||||||
|
regular: '/fonts/Merriweather-VariableFont_opsz,wdth,wght.ttf',
|
||||||
|
italic: '/fonts/Merriweather-Italic-VariableFont_opsz,wdth,wght.ttf',
|
||||||
|
name: 'Merriweather'
|
||||||
|
},
|
||||||
|
'Crimson Pro': {
|
||||||
|
regular: '/fonts/CrimsonPro-VariableFont_wght.ttf',
|
||||||
|
italic: '/fonts/CrimsonPro-Italic-VariableFont_wght.ttf',
|
||||||
|
name: 'CrimsonPro'
|
||||||
|
},
|
||||||
|
'Roboto Serif': {
|
||||||
|
regular: '/fonts/RobotoSerif-VariableFont_GRAD,opsz,wdth,wght.ttf',
|
||||||
|
italic: '/fonts/RobotoSerif-Italic-VariableFont_GRAD,opsz,wdth,wght.ttf',
|
||||||
|
name: 'RobotoSerif'
|
||||||
|
},
|
||||||
|
'Average': {
|
||||||
|
regular: '/fonts/Average-Regular.ttf',
|
||||||
|
italic: '/fonts/Average-Regular.ttf', // No italic variant
|
||||||
|
name: 'Average'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedFont = fontMap[previewFont];
|
||||||
|
if (selectedFont) {
|
||||||
|
try {
|
||||||
|
const regularBase64 = await loadFontAsBase64(selectedFont.regular);
|
||||||
|
pdf.addFileToVFS(`${selectedFont.name}-normal.ttf`, regularBase64);
|
||||||
|
pdf.addFont(`${selectedFont.name}-normal.ttf`, selectedFont.name, 'normal');
|
||||||
|
|
||||||
|
const italicBase64 = await loadFontAsBase64(selectedFont.italic);
|
||||||
|
pdf.addFileToVFS(`${selectedFont.name}-italic.ttf`, italicBase64);
|
||||||
|
pdf.addFont(`${selectedFont.name}-italic.ttf`, selectedFont.name, 'italic');
|
||||||
|
|
||||||
|
// Set the custom font as default
|
||||||
|
pdf.setFont(selectedFont.name, 'normal');
|
||||||
|
} catch (fontError) {
|
||||||
|
console.error('Failed to load custom font, using default:', fontError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Source Code Pro for code blocks
|
||||||
|
try {
|
||||||
|
const codeFont = await loadFontAsBase64('/fonts/SourceCodePro-VariableFont_wght.ttf');
|
||||||
|
pdf.addFileToVFS('SourceCodePro-normal.ttf', codeFont);
|
||||||
|
pdf.addFont('SourceCodePro-normal.ttf', 'SourceCodePro', 'normal');
|
||||||
|
} catch (codeFontError) {
|
||||||
|
console.error('Failed to load code font:', codeFontError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process images to embed them as data URLs
|
||||||
|
let contentForPDF = localContent;
|
||||||
|
if (api) {
|
||||||
|
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
||||||
|
const matches = [...localContent.matchAll(imageRegex)];
|
||||||
|
|
||||||
|
for (const match of matches) {
|
||||||
|
const [fullMatch, alt, imagePath] = match;
|
||||||
|
|
||||||
|
// Skip external URLs
|
||||||
|
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
const cacheKey = `${note.id}:${imagePath}`;
|
||||||
|
if (imageCache.has(cacheKey)) {
|
||||||
|
const dataUrl = imageCache.get(cacheKey)!;
|
||||||
|
contentForPDF = contentForPDF.replace(fullMatch, ``);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dataUrl = await api.fetchAttachment(note.id, imagePath, note.category);
|
||||||
|
imageCache.set(cacheKey, dataUrl);
|
||||||
|
contentForPDF = contentForPDF.replace(fullMatch, ``);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to fetch attachment for PDF: ${imagePath}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.style.fontFamily = `"${previewFont}", Georgia, serif`;
|
container.style.fontFamily = `"${previewFont}", Georgia, serif`;
|
||||||
container.style.fontSize = '12px';
|
container.style.fontSize = `${previewFontSize}px`;
|
||||||
container.style.lineHeight = '1.6';
|
container.style.lineHeight = '1.6';
|
||||||
container.style.color = '#000000';
|
container.style.color = '#000000';
|
||||||
|
|
||||||
@@ -250,37 +355,39 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
|
|||||||
container.appendChild(titleElement);
|
container.appendChild(titleElement);
|
||||||
|
|
||||||
const contentElement = document.createElement('div');
|
const contentElement = document.createElement('div');
|
||||||
const html = marked.parse(localContent || '', { async: false }) as string;
|
const html = marked.parse(contentForPDF || '', { async: false }) as string;
|
||||||
contentElement.innerHTML = html;
|
contentElement.innerHTML = html;
|
||||||
contentElement.style.fontSize = '12px';
|
contentElement.style.fontSize = `${previewFontSize}px`;
|
||||||
contentElement.style.lineHeight = '1.6';
|
contentElement.style.lineHeight = '1.6';
|
||||||
contentElement.style.color = '#000000';
|
contentElement.style.color = '#000000';
|
||||||
container.appendChild(contentElement);
|
container.appendChild(contentElement);
|
||||||
|
|
||||||
// Apply monospace font to code elements
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
code, pre { font-family: "Source Code Pro", ui-monospace, monospace !important; }
|
body, p, h1, h2, h3, div { font-family: "${previewFont}", Georgia, serif !important; }
|
||||||
|
code, pre, pre * { font-family: "Source Code Pro", "Courier New", monospace !important; }
|
||||||
pre { background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; }
|
pre { background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; }
|
||||||
code { background: #f0f0f0; padding: 2px 4px; border-radius: 2px; }
|
code { padding: 0; }
|
||||||
|
h1 { font-size: 2em; font-weight: bold; margin-top: 0.67em; margin-bottom: 0.67em; }
|
||||||
|
h2 { font-size: 1.5em; font-weight: bold; margin-top: 0.83em; margin-bottom: 0.83em; }
|
||||||
|
h3 { font-size: 1.17em; font-weight: bold; margin-top: 1em; margin-bottom: 1em; }
|
||||||
|
p { margin: 0.5em 0; }
|
||||||
|
ul, ol { margin: 0.5em 0; padding-left: 2em; list-style-position: outside; font-family: "${previewFont}", Georgia, serif !important; }
|
||||||
|
ul { list-style-type: disc; }
|
||||||
|
ol { list-style-type: decimal; }
|
||||||
|
li { margin: 0.25em 0; display: list-item; font-family: "${previewFont}", Georgia, serif !important; }
|
||||||
|
em { font-style: italic; vertical-align: baseline; }
|
||||||
|
strong { font-weight: bold; vertical-align: baseline; line-height: inherit; }
|
||||||
|
img { max-width: 100%; height: auto; display: block; margin: 1em 0; }
|
||||||
`;
|
`;
|
||||||
container.appendChild(style);
|
container.appendChild(style);
|
||||||
|
|
||||||
// Create PDF using jsPDF's html() method (like dompdf)
|
// Use jsPDF's html() method with custom font set
|
||||||
const pdf = new jsPDF({
|
|
||||||
orientation: 'portrait',
|
|
||||||
unit: 'mm',
|
|
||||||
format: 'a4',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use jsPDF's html() method which handles pagination automatically
|
|
||||||
await pdf.html(container, {
|
await pdf.html(container, {
|
||||||
callback: async (doc) => {
|
callback: async (doc) => {
|
||||||
// Save the PDF
|
|
||||||
const fileName = `${localTitle || 'note'}.pdf`;
|
const fileName = `${localTitle || 'note'}.pdf`;
|
||||||
doc.save(fileName);
|
doc.save(fileName);
|
||||||
|
|
||||||
// Show success message using Tauri dialog
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await message(`PDF exported successfully!\n\nFile: ${fileName}\nLocation: Downloads folder`, {
|
await message(`PDF exported successfully!\n\nFile: ${fileName}\nLocation: Downloads folder`, {
|
||||||
@@ -293,10 +400,10 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
|
|||||||
setIsExportingPDF(false);
|
setIsExportingPDF(false);
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
margin: [20, 20, 20, 20], // top, right, bottom, left margins in mm
|
margin: [20, 20, 20, 20],
|
||||||
autoPaging: 'text', // Enable automatic page breaks
|
autoPaging: 'text',
|
||||||
width: 170, // Content width in mm (A4 width 210mm - 40mm margins)
|
width: 170,
|
||||||
windowWidth: 650, // Rendering width in pixels (matches content width ratio)
|
windowWidth: 650,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('PDF export failed:', error);
|
console.error('PDF export failed:', error);
|
||||||
@@ -752,7 +859,7 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`prose prose-slate dark:prose-invert p-8 ${isFocusMode ? '' : 'max-w-none'} [&_code]:font-mono [&_pre]:font-mono [&_img]:max-w-full [&_img]:rounded-lg [&_img]:shadow-md`}
|
className={`prose prose-slate dark:prose-invert p-8 ${isFocusMode ? '' : 'max-w-none'} [&_code]:font-mono [&_pre]:font-mono [&_code]:py-0 [&_code]:px-1 [&_code]:align-baseline [&_code]:leading-none [&_img]:max-w-full [&_img]:rounded-lg [&_img]:shadow-md`}
|
||||||
style={{ fontSize: `${previewFontSize}px`, fontFamily: previewFont }}
|
style={{ fontSize: `${previewFontSize}px`, fontFamily: previewFont }}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: marked.parse(processedContent || '', { async: false }) as string
|
__html: marked.parse(processedContent || '', { async: false }) as string
|
||||||
|
|||||||
@@ -16,6 +16,19 @@ code {
|
|||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Override Tailwind prose inline code styling to prevent overlap */
|
||||||
|
.prose code {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
vertical-align: baseline !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose code::before,
|
||||||
|
.prose code::after {
|
||||||
|
content: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* TipTap Editor Styles */
|
/* TipTap Editor Styles */
|
||||||
.ProseMirror {
|
.ProseMirror {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
@@ -113,11 +126,13 @@ code {
|
|||||||
|
|
||||||
.ProseMirror code {
|
.ProseMirror code {
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
padding: 0.125rem 0.25rem;
|
padding: 0.05rem 0.25rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
|
vertical-align: baseline;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .ProseMirror code {
|
.dark .ProseMirror code {
|
||||||
|
|||||||
Reference in New Issue
Block a user