diff --git a/packages/frontend/locales/en.json b/packages/frontend/locales/en.json index 5e3cbc1..f10ff64 100644 --- a/packages/frontend/locales/en.json +++ b/packages/frontend/locales/en.json @@ -36,7 +36,10 @@ "advanced": { "explanation": "By default, a securely generated password is used for each note. You can however also choose your own password, which is not included in the link.", "custom_password": "custom password" - } + }, + "pasting": "Pasting image...", + "pasted_images": "Pasted Images", + "remove": "Remove" }, "show": { "errors": { diff --git a/packages/frontend/src/lib/ui/ShowNote.svelte b/packages/frontend/src/lib/ui/ShowNote.svelte index 61eb1ec..1a3bee0 100644 --- a/packages/frontend/src/lib/ui/ShowNote.svelte +++ b/packages/frontend/src/lib/ui/ShowNote.svelte @@ -80,6 +80,15 @@ {file.type} - {prettyBytes(file.size)} + {#if file.type.startsWith('image/')} + {#key file.name} + {file.name} + {/key} + {/if} {/each} {/if} @@ -130,4 +139,13 @@ margin-bottom: 0.5rem; word-wrap: break-word; } + + .preview { + display: block; + max-width: 100%; + max-height: 60vh; + margin-bottom: 0.5rem; + border-radius: 0.25rem; + border: 2px solid var(--ui-bg-1); + } diff --git a/packages/frontend/src/lib/views/Create.svelte b/packages/frontend/src/lib/views/Create.svelte index a79aad6..80f4104 100644 --- a/packages/frontend/src/lib/views/Create.svelte +++ b/packages/frontend/src/lib/views/Create.svelte @@ -29,6 +29,11 @@ let customPassword: string | null = $state(null) let description = $state('') let loading: string | null = $state(null) + + // Image paste functionality + let pastedImages: { preview: string; file: File }[] = $state([]) + let isPasting = $state(false) + let pasteIndicator = $state(null) $effect(() => { if (!advanced) { @@ -57,6 +62,84 @@ } }) + async function handlePaste(e: ClipboardEvent) { + // Use the standard DataTransfer API to access pasted content + const items = e.clipboardData?.items + if (!items || items.length === 0) return + + isPasting = true + + const imagePromises: Promise[] = [] + for (let i = 0; i < items.length; i++) { + const item = items[i] + if (item.kind === 'file' && item.type.startsWith('image/')) { + const file = item.getAsFile() + if (file) { + const extension = file.type.split('/')[1] || 'png' + const renamed = new File( + [file], + `pasted-image-${Date.now()}-${Math.round(Math.random() * 1000)}.${extension}`, + { type: file.type }, + ) + imagePromises.push(Promise.resolve(renamed)) + } + } + } + + try { + const results = await Promise.all(imagePromises) + const imageFiles = results.filter((file): file is File => file !== null) + + if (imageFiles.length > 0) { + // Switch to file mode if not already + if (!isFile) { + isFile = true + } + + // Process each image for preview and add to files + for (const imageFile of imageFiles) { + // Create preview URL + const previewURL = URL.createObjectURL(imageFile) + + // Add to pasted images for preview + pastedImages = [...pastedImages, { preview: previewURL, file: imageFile }] + + // Convert to FileDTO and add to files array + const arrayBuffer = await imageFile.arrayBuffer() + const fileDTO: FileDTO = { + name: imageFile.name, + size: imageFile.size, + type: imageFile.type, + contents: new Uint8Array(arrayBuffer) + } + + // Add to files if not already present + if (!files.some(f => f.name === imageFile.name && f.size === imageFile.size)) { + files = [...files, fileDTO] + } + } + } + } catch (error) { + console.error('Error processing pasted image:', error) + } finally { + isPasting = false + } + } + + function removePastedImage(index: number) { + const removed = pastedImages.splice(index, 1)[0] + // Revoke the object URL to free memory + URL.revokeObjectURL(removed.preview) + + // Also remove from files array + files = files.filter(file => !(file.name === removed.file.name && file.size === removed.file.size)) + + // If no more pasted images and no other files, switch back to text mode + if (pastedImages.length === 0 && files.length === 0) { + isFile = false + } + } + class EmptyContentError extends Error {} async function submit(e: SubmitEvent) { @@ -110,18 +193,47 @@

{@html $status?.theme_text || $t('home.intro')}

-
+
- {#if isFile} - - {:else} -