Răsfoiți Sursa

feat: unstylized pdf export

Timothy Jaeryang Baek 5 luni în urmă
părinte
comite
3a79840a87

+ 35 - 0
src/lib/components/chat/Settings/Interface.svelte

@@ -54,6 +54,9 @@
 		height: ''
 	};
 
+	// chat export
+	let stylizedPdfExport = true;
+
 	// Admin - Show Update Available Toast
 	let showUpdateToast = true;
 	let showChangelog = true;
@@ -152,6 +155,11 @@
 		saveSettings({ hapticFeedback: hapticFeedback });
 	};
 
+	const toggleStylizedPdfExport = async () => {
+		stylizedPdfExport = !stylizedPdfExport;
+		saveSettings({ stylizedPdfExport: stylizedPdfExport });
+	};
+
 	const toggleUserLocation = async () => {
 		userLocation = !userLocation;
 
@@ -302,6 +310,11 @@
 		notificationSound = $settings?.notificationSound ?? true;
 		notificationSoundAlways = $settings?.notificationSoundAlways ?? false;
 
+		iframeSandboxAllowSameOrigin = $settings?.iframeSandboxAllowSameOrigin ?? false;
+		iframeSandboxAllowForms = $settings?.iframeSandboxAllowForms ?? false;
+
+		stylizedPdfExport = $settings?.stylizedPdfExport ?? true;
+
 		hapticFeedback = $settings.hapticFeedback ?? false;
 		ctrlEnterToSend = $settings.ctrlEnterToSend ?? false;
 
@@ -964,6 +977,28 @@
 				</div>
 			</div>
 
+			<div>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs">
+						{$i18n.t('Stylized PDF Export')}
+					</div>
+
+					<button
+						class="p-1 px-3 text-xs flex rounded-sm transition"
+						on:click={() => {
+							toggleStylizedPdfExport();
+						}}
+						type="button"
+					>
+						{#if stylizedPdfExport === true}
+							<span class="ml-2 self-center">{$i18n.t('On')}</span>
+						{:else}
+							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
+						{/if}
+					</button>
+				</div>
+			</div>
+
 			<div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
 
 			<div>

+ 113 - 64
src/lib/components/layout/Navbar/Menu.svelte

@@ -19,7 +19,8 @@
 		mobile,
 		temporaryChatEnabled,
 		theme,
-		user
+		user,
+		settings
 	} from '$lib/stores';
 	import { flyAndScale } from '$lib/utils/transitions';
 
@@ -63,76 +64,124 @@
 	};
 
 	const downloadPdf = async () => {
-		const containerElement = document.getElementById('messages-container');
-
-		if (containerElement) {
-			try {
-				const isDarkMode = document.documentElement.classList.contains('dark');
-				const virtualWidth = 800; // Fixed width in px
-				const pagePixelHeight = 1200; // Each slice height (adjust to avoid canvas bugs; generally 2–4k is safe)
-
-				// Clone & style once
-				const clonedElement = containerElement.cloneNode(true);
-				clonedElement.classList.add('text-black');
-				clonedElement.classList.add('dark:text-white');
-				clonedElement.style.width = `${virtualWidth}px`;
-				clonedElement.style.position = 'absolute';
-				clonedElement.style.left = '-9999px'; // Offscreen
-				clonedElement.style.height = 'auto';
-				document.body.appendChild(clonedElement);
-
-				// Get total height after attached to DOM
-				const totalHeight = clonedElement.scrollHeight;
-				let offsetY = 0;
-				let page = 0;
-
-				// Prepare PDF
-				const pdf = new jsPDF('p', 'mm', 'a4');
-				const imgWidth = 210; // A4 mm
-				const pageHeight = 297; // A4 mm
-
-				while (offsetY < totalHeight) {
-					// For each slice, adjust scrollTop to show desired part
-					clonedElement.scrollTop = offsetY;
-
-					// Optionally: mask/hide overflowing content via CSS if needed
-					clonedElement.style.maxHeight = `${pagePixelHeight}px`;
-					// Only render the visible part
-					const canvas = await html2canvas(clonedElement, {
-						backgroundColor: isDarkMode ? '#000' : '#fff',
-						useCORS: true,
-						scale: 2,
-						width: virtualWidth,
-						height: Math.min(pagePixelHeight, totalHeight - offsetY),
-						// Optionally: y offset for correct region?
-						windowWidth: virtualWidth
-						//windowHeight: pagePixelHeight,
-					});
-					const imgData = canvas.toDataURL('image/png');
-					// Maintain aspect ratio
-					const imgHeight = (canvas.height * imgWidth) / canvas.width;
-					const position = 0; // Always first line, since we've clipped vertically
-
-					if (page > 0) pdf.addPage();
-
-					// Set page background for dark mode
-					if (isDarkMode) {
-						pdf.setFillColor(0, 0, 0);
-						pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // black bg
+		if ($settings?.stylizedPdfExport ?? true) {
+			const containerElement = document.getElementById('messages-container');
+
+			if (containerElement) {
+				try {
+					const isDarkMode = document.documentElement.classList.contains('dark');
+					const virtualWidth = 800; // Fixed width in px
+					const pagePixelHeight = 1200; // Each slice height (adjust to avoid canvas bugs; generally 2–4k is safe)
+
+					// Clone & style once
+					const clonedElement = containerElement.cloneNode(true);
+					clonedElement.classList.add('text-black');
+					clonedElement.classList.add('dark:text-white');
+					clonedElement.style.width = `${virtualWidth}px`;
+					clonedElement.style.position = 'absolute';
+					clonedElement.style.left = '-9999px'; // Offscreen
+					clonedElement.style.height = 'auto';
+					document.body.appendChild(clonedElement);
+
+					// Get total height after attached to DOM
+					const totalHeight = clonedElement.scrollHeight;
+					let offsetY = 0;
+					let page = 0;
+
+					// Prepare PDF
+					const pdf = new jsPDF('p', 'mm', 'a4');
+					const imgWidth = 210; // A4 mm
+					const pageHeight = 297; // A4 mm
+
+					while (offsetY < totalHeight) {
+						// For each slice, adjust scrollTop to show desired part
+						clonedElement.scrollTop = offsetY;
+
+						// Optionally: mask/hide overflowing content via CSS if needed
+						clonedElement.style.maxHeight = `${pagePixelHeight}px`;
+						// Only render the visible part
+						const canvas = await html2canvas(clonedElement, {
+							backgroundColor: isDarkMode ? '#000' : '#fff',
+							useCORS: true,
+							scale: 2,
+							width: virtualWidth,
+							height: Math.min(pagePixelHeight, totalHeight - offsetY),
+							// Optionally: y offset for correct region?
+							windowWidth: virtualWidth
+							//windowHeight: pagePixelHeight,
+						});
+						const imgData = canvas.toDataURL('image/png');
+						// Maintain aspect ratio
+						const imgHeight = (canvas.height * imgWidth) / canvas.width;
+						const position = 0; // Always first line, since we've clipped vertically
+
+						if (page > 0) pdf.addPage();
+
+						// Set page background for dark mode
+						if (isDarkMode) {
+							pdf.setFillColor(0, 0, 0);
+							pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // black bg
+						}
+
+						pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+
+						offsetY += pagePixelHeight;
+						page++;
 					}
 
-					pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+					document.body.removeChild(clonedElement);
 
-					offsetY += pagePixelHeight;
-					page++;
+					pdf.save(`chat-${chat.chat.title}.pdf`);
+				} catch (error) {
+					console.error('Error generating PDF', error);
 				}
+			}
+		} else {
+			console.log('Downloading PDF');
+
+			const chatText = await getChatAsText();
+
+			const doc = new jsPDF();
+
+			// Margins
+			const left = 15;
+			const top = 20;
+			const right = 15;
+			const bottom = 20;
 
-				document.body.removeChild(clonedElement);
+			const pageWidth = doc.internal.pageSize.getWidth();
+			const pageHeight = doc.internal.pageSize.getHeight();
+			const usableWidth = pageWidth - left - right;
+			const usableHeight = pageHeight - top - bottom;
 
-				pdf.save(`chat-${chat.chat.title}.pdf`);
-			} catch (error) {
-				console.error('Error generating PDF', error);
+			// Font size and line height
+			const fontSize = 8;
+			doc.setFontSize(fontSize);
+			const lineHeight = fontSize * 1; // adjust if needed
+
+			// Split the markdown into lines (handles \n)
+			const paragraphs = chatText.split('\n');
+
+			let y = top;
+
+			for (let paragraph of paragraphs) {
+				// Wrap each paragraph to fit the width
+				const lines = doc.splitTextToSize(paragraph, usableWidth);
+
+				for (let line of lines) {
+					// If the line would overflow the bottom, add a new page
+					if (y + lineHeight > pageHeight - bottom) {
+						doc.addPage();
+						y = top;
+					}
+					doc.text(line, left, y);
+					y += lineHeight * 0.5;
+				}
+				// Add empty line at paragraph breaks
+				y += lineHeight * 0.1;
 			}
+
+			doc.save(`chat-${chat.chat.title}.pdf`);
 		}
 	};
 

+ 112 - 64
src/lib/components/layout/Sidebar/ChatMenu.svelte

@@ -26,7 +26,7 @@
 		getChatPinnedStatusById,
 		toggleChatPinnedStatusById
 	} from '$lib/apis/chats';
-	import { chats, theme, user } from '$lib/stores';
+	import { chats, settings, theme, user } from '$lib/stores';
 	import { createMessagesList } from '$lib/utils';
 	import { downloadChatAsPDF } from '$lib/apis/utils';
 	import Download from '$lib/components/icons/Download.svelte';
@@ -81,76 +81,124 @@
 	const downloadPdf = async () => {
 		const chat = await getChatById(localStorage.token, chatId);
 
-		const containerElement = document.getElementById('messages-container');
-
-		if (containerElement) {
-			try {
-				const isDarkMode = document.documentElement.classList.contains('dark');
-				const virtualWidth = 800; // Fixed width in px
-				const pagePixelHeight = 1200; // Each slice height (adjust to avoid canvas bugs; generally 2–4k is safe)
-
-				// Clone & style once
-				const clonedElement = containerElement.cloneNode(true);
-				clonedElement.classList.add('text-black');
-				clonedElement.classList.add('dark:text-white');
-				clonedElement.style.width = `${virtualWidth}px`;
-				clonedElement.style.position = 'absolute';
-				clonedElement.style.left = '-9999px'; // Offscreen
-				clonedElement.style.height = 'auto';
-				document.body.appendChild(clonedElement);
-
-				// Get total height after attached to DOM
-				const totalHeight = clonedElement.scrollHeight;
-				let offsetY = 0;
-				let page = 0;
-
-				// Prepare PDF
-				const pdf = new jsPDF('p', 'mm', 'a4');
-				const imgWidth = 210; // A4 mm
-				const pageHeight = 297; // A4 mm
-
-				while (offsetY < totalHeight) {
-					// For each slice, adjust scrollTop to show desired part
-					clonedElement.scrollTop = offsetY;
-
-					// Optionally: mask/hide overflowing content via CSS if needed
-					clonedElement.style.maxHeight = `${pagePixelHeight}px`;
-					// Only render the visible part
-					const canvas = await html2canvas(clonedElement, {
-						backgroundColor: isDarkMode ? '#000' : '#fff',
-						useCORS: true,
-						scale: 2,
-						width: virtualWidth,
-						height: Math.min(pagePixelHeight, totalHeight - offsetY),
-						// Optionally: y offset for correct region?
-						windowWidth: virtualWidth
-						//windowHeight: pagePixelHeight,
-					});
-					const imgData = canvas.toDataURL('image/png');
-					// Maintain aspect ratio
-					const imgHeight = (canvas.height * imgWidth) / canvas.width;
-					const position = 0; // Always first line, since we've clipped vertically
-
-					if (page > 0) pdf.addPage();
-
-					// Set page background for dark mode
-					if (isDarkMode) {
-						pdf.setFillColor(0, 0, 0);
-						pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // black bg
+		if ($settings?.stylizedPdfExport ?? true) {
+			const containerElement = document.getElementById('messages-container');
+
+			if (containerElement) {
+				try {
+					const isDarkMode = document.documentElement.classList.contains('dark');
+					const virtualWidth = 800; // Fixed width in px
+					const pagePixelHeight = 1200; // Each slice height (adjust to avoid canvas bugs; generally 2–4k is safe)
+
+					// Clone & style once
+					const clonedElement = containerElement.cloneNode(true);
+					clonedElement.classList.add('text-black');
+					clonedElement.classList.add('dark:text-white');
+					clonedElement.style.width = `${virtualWidth}px`;
+					clonedElement.style.position = 'absolute';
+					clonedElement.style.left = '-9999px'; // Offscreen
+					clonedElement.style.height = 'auto';
+					document.body.appendChild(clonedElement);
+
+					// Get total height after attached to DOM
+					const totalHeight = clonedElement.scrollHeight;
+					let offsetY = 0;
+					let page = 0;
+
+					// Prepare PDF
+					const pdf = new jsPDF('p', 'mm', 'a4');
+					const imgWidth = 210; // A4 mm
+					const pageHeight = 297; // A4 mm
+
+					while (offsetY < totalHeight) {
+						// For each slice, adjust scrollTop to show desired part
+						clonedElement.scrollTop = offsetY;
+
+						// Optionally: mask/hide overflowing content via CSS if needed
+						clonedElement.style.maxHeight = `${pagePixelHeight}px`;
+						// Only render the visible part
+						const canvas = await html2canvas(clonedElement, {
+							backgroundColor: isDarkMode ? '#000' : '#fff',
+							useCORS: true,
+							scale: 2,
+							width: virtualWidth,
+							height: Math.min(pagePixelHeight, totalHeight - offsetY),
+							// Optionally: y offset for correct region?
+							windowWidth: virtualWidth
+							//windowHeight: pagePixelHeight,
+						});
+						const imgData = canvas.toDataURL('image/png');
+						// Maintain aspect ratio
+						const imgHeight = (canvas.height * imgWidth) / canvas.width;
+						const position = 0; // Always first line, since we've clipped vertically
+
+						if (page > 0) pdf.addPage();
+
+						// Set page background for dark mode
+						if (isDarkMode) {
+							pdf.setFillColor(0, 0, 0);
+							pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // black bg
+						}
+
+						pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+
+						offsetY += pagePixelHeight;
+						page++;
 					}
 
-					pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+					document.body.removeChild(clonedElement);
 
-					offsetY += pagePixelHeight;
-					page++;
+					pdf.save(`chat-${chat.chat.title}.pdf`);
+				} catch (error) {
+					console.error('Error generating PDF', error);
 				}
+			}
+		} else {
+			console.log('Downloading PDF');
+
+			const chatText = await getChatAsText(chat);
+
+			const doc = new jsPDF();
 
-				document.body.removeChild(clonedElement);
+			// Margins
+			const left = 15;
+			const top = 20;
+			const right = 15;
+			const bottom = 20;
 
-				pdf.save(`chat-${chat.chat.title}.pdf`);
-			} catch (error) {
-				console.error('Error generating PDF', error);
+			const pageWidth = doc.internal.pageSize.getWidth();
+			const pageHeight = doc.internal.pageSize.getHeight();
+			const usableWidth = pageWidth - left - right;
+			const usableHeight = pageHeight - top - bottom;
+
+			// Font size and line height
+			const fontSize = 8;
+			doc.setFontSize(fontSize);
+			const lineHeight = fontSize * 1; // adjust if needed
+
+			// Split the markdown into lines (handles \n)
+			const paragraphs = chatText.split('\n');
+
+			let y = top;
+
+			for (let paragraph of paragraphs) {
+				// Wrap each paragraph to fit the width
+				const lines = doc.splitTextToSize(paragraph, usableWidth);
+
+				for (let line of lines) {
+					// If the line would overflow the bottom, add a new page
+					if (y + lineHeight > pageHeight - bottom) {
+						doc.addPage();
+						y = top;
+					}
+					doc.text(line, left, y);
+					y += lineHeight;
+				}
+				// Add empty line at paragraph breaks
+				y += lineHeight * 0.5;
 			}
+
+			doc.save(`chat-${chat.chat.title}.pdf`);
 		}
 	};