Browse Source

enh: note editor

Timothy Jaeryang Baek 5 months ago
parent
commit
360b5b303a
1 changed files with 134 additions and 5 deletions
  1. 134 5
      src/lib/components/notes/NoteEditor.svelte

+ 134 - 5
src/lib/components/notes/NoteEditor.svelte

@@ -1,6 +1,11 @@
 <script lang="ts">
 	import { getContext, onDestroy, onMount, tick } from 'svelte';
 	import { v4 as uuidv4 } from 'uuid';
+	import fileSaver from 'file-saver';
+	const { saveAs } = fileSaver;
+
+	import jsPDF from 'jspdf';
+	import html2canvas from 'html2canvas-pro';
 
 	const i18n = getContext('i18n');
 
@@ -10,6 +15,8 @@
 	import { goto } from '$app/navigation';
 
 	import { compressImage } from '$lib/utils';
+	import { WEBUI_API_BASE_URL } from '$lib/constants';
+	import { uploadFile } from '$lib/apis/files';
 
 	import dayjs from '$lib/dayjs';
 	import calendar from 'dayjs/plugin/calendar';
@@ -34,22 +41,23 @@
 	// Assuming $i18n.languages is an array of language codes
 	$: loadLocale($i18n.languages);
 
-	import { getNoteById, updateNoteById } from '$lib/apis/notes';
+	import { deleteNoteById, getNoteById, updateNoteById } from '$lib/apis/notes';
 
 	import RichTextInput from '../common/RichTextInput.svelte';
 	import Spinner from '../common/Spinner.svelte';
 	import MicSolid from '../icons/MicSolid.svelte';
 	import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
-	import Tooltip from '../common/Tooltip.svelte';
+	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 
 	import Calendar from '../icons/Calendar.svelte';
 	import Users from '../icons/Users.svelte';
-	import { WEBUI_API_BASE_URL } from '$lib/constants';
-	import { uploadFile } from '$lib/apis/files';
+
 	import Image from '../common/Image.svelte';
 	import FileItem from '../common/FileItem.svelte';
 	import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
 	import RecordMenu from './RecordMenu.svelte';
+	import NoteMenu from './Notes/NoteMenu.svelte';
+	import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
 
 	export let id: null | string = null;
 
@@ -70,9 +78,11 @@
 	};
 
 	let files = [];
+
 	let recording = false;
 	let displayMediaRecord = false;
 
+	let showDeleteConfirm = false;
 	let dragged = false;
 	let loading = false;
 
@@ -245,6 +255,94 @@
 		});
 	};
 
+	const downloadHandler = async (type) => {
+		console.log('downloadHandler', type);
+		if (type === 'md') {
+			const blob = new Blob([note.data.content.md], { type: 'text/markdown' });
+			saveAs(blob, `${note.title}.md`);
+		} else if (type === 'pdf') {
+			await downloadPdf(note);
+		}
+	};
+
+	const downloadPdf = async (note) => {
+		try {
+			// Define a fixed virtual screen size
+			const virtualWidth = 1024; // Fixed width (adjust as needed)
+			const virtualHeight = 1400; // Fixed height (adjust as needed)
+
+			// STEP 1. Get a DOM node to render
+			const html = note.data?.content?.html ?? '';
+			let node;
+			if (html instanceof HTMLElement) {
+				node = html;
+			} else {
+				// If it's HTML string, render to a temporary hidden element
+				node = document.createElement('div');
+				node.innerHTML = html;
+				document.body.appendChild(node);
+			}
+
+			// Render to canvas with predefined width
+			const canvas = await html2canvas(node, {
+				useCORS: true,
+				scale: 2, // Keep at 1x to avoid unexpected enlargements
+				width: virtualWidth, // Set fixed virtual screen width
+				windowWidth: virtualWidth, // Ensure consistent rendering
+				windowHeight: virtualHeight
+			});
+
+			// Remove hidden node if needed
+			if (!(html instanceof HTMLElement)) {
+				document.body.removeChild(node);
+			}
+
+			const imgData = canvas.toDataURL('image/png');
+
+			// A4 page settings
+			const pdf = new jsPDF('p', 'mm', 'a4');
+			const imgWidth = 210; // A4 width in mm
+			const pageHeight = 297; // A4 height in mm
+
+			// Maintain aspect ratio
+			const imgHeight = (canvas.height * imgWidth) / canvas.width;
+			let heightLeft = imgHeight;
+			let position = 0;
+
+			pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+			heightLeft -= pageHeight;
+
+			// Handle additional pages
+			while (heightLeft > 0) {
+				position -= pageHeight;
+				pdf.addPage();
+
+				pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+				heightLeft -= pageHeight;
+			}
+
+			pdf.save(`${note.title}.pdf`);
+		} catch (error) {
+			console.error('Error generating PDF', error);
+
+			toast.error(`${error}`);
+		}
+	};
+
+	const deleteNoteHandler = async (id) => {
+		const res = await deleteNoteById(localStorage.token, id).catch((error) => {
+			toast.error(`${error}`);
+			return null;
+		});
+
+		if (res) {
+			toast.success($i18n.t('Note deleted successfully'));
+			goto('/notes');
+		} else {
+			toast.error($i18n.t('Failed to delete note'));
+		}
+	};
+
 	const onDragOver = (e) => {
 		e.preventDefault();
 
@@ -299,6 +397,19 @@
 
 <FilesOverlay show={dragged} />
 
+<DeleteConfirmDialog
+	bind:show={showDeleteConfirm}
+	title={$i18n.t('Delete note?')}
+	on:confirm={() => {
+		deleteNoteHandler(note.id);
+		showDeleteConfirm = false;
+	}}
+>
+	<div class=" text-sm text-gray-500">
+		{$i18n.t('This will delete')} <span class="  font-semibold">{note.title}</span>.
+	</div>
+</DeleteConfirmDialog>
+
 <div class="relative flex-1 w-full h-full flex justify-center" id="note-editor">
 	{#if loading}
 		<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
@@ -309,7 +420,7 @@
 	{:else}
 		<div class=" w-full flex flex-col {loading ? 'opacity-20' : ''}">
 			<div class="shrink-0 w-full flex justify-between items-center px-4.5 pt-1 mb-1.5">
-				<div class="w-full">
+				<div class="w-full flex">
 					<input
 						class="w-full text-2xl font-medium bg-transparent outline-hidden"
 						type="text"
@@ -317,6 +428,24 @@
 						placeholder={$i18n.t('Title')}
 						required
 					/>
+
+					<div>
+						<NoteMenu
+							onDownload={(type) => {
+								downloadHandler(type);
+							}}
+							onDelete={() => {
+								showDeleteConfirm = true;
+							}}
+						>
+							<button
+								class="self-center w-fit text-sm p-1 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+								type="button"
+							>
+								<EllipsisHorizontal className="size-5" />
+							</button>
+						</NoteMenu>
+					</div>
 				</div>
 			</div>