diff --git a/package.json b/package.json index eb5b024f..a9237ab9 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@capacitor/android": "^8.0.1", "@capacitor/app": "^8.0.0", "@capacitor/cli": "^8.0.1", + "@capacitor/clipboard": "^8.0.1", "@capacitor/core": "^8.0.1", "@capacitor/filesystem": "^8.1.0", "@capacitor/ios": "^8.0.1", @@ -63,6 +64,7 @@ "@poppanator/sveltekit-svg": "^4.2.1", "@sveltejs/adapter-static": "^3.0.10", "@tiptap/core": "^2.27.2", + "@tiptap/pm": "^2.27.2", "@types/qrcode": "^1.5.6", "@types/throttle-debounce": "^5.0.2", "@vite-pwa/assets-generator": "^0.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 452248b9..9a6e25e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@capacitor/cli': specifier: ^8.0.1 version: 8.0.1 + '@capacitor/clipboard': + specifier: ^8.0.1 + version: 8.0.1(@capacitor/core@8.0.1) '@capacitor/core': specifier: ^8.0.1 version: 8.0.1 @@ -71,6 +74,9 @@ importers: '@tiptap/core': specifier: ^2.27.2 version: 2.27.2(@tiptap/pm@2.27.2) + '@tiptap/pm': + specifier: ^2.27.2 + version: 2.27.2 '@types/qrcode': specifier: ^1.5.6 version: 1.5.6 @@ -791,6 +797,11 @@ packages: engines: {node: '>=22.0.0'} hasBin: true + '@capacitor/clipboard@8.0.1': + resolution: {integrity: sha512-iOlbTi8MojKyLnYE+M27priXid7vHd0PlDwyHohPzkuQ8Rkp6q7ykwZmPEUD+OnU/Ink7Qw/pUOfKgraKmA6Eg==} + peerDependencies: + '@capacitor/core': '>=8.0.0' + '@capacitor/core@8.0.1': resolution: {integrity: sha512-5UqSWxGMp/B8KhYu7rAijqNtYslhcLh+TrbfU48PfdMDsPfaU/VY48sMNzC22xL8BmoFoql/3SKyP+pavTOvOA==} @@ -5969,6 +5980,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@capacitor/clipboard@8.0.1(@capacitor/core@8.0.1)': + dependencies: + '@capacitor/core': 8.0.1 + '@capacitor/core@8.0.1': dependencies: tslib: 2.8.1 diff --git a/src/app/editor/clipboard.ts b/src/app/editor/clipboard.ts new file mode 100644 index 00000000..fa3e8ed5 --- /dev/null +++ b/src/app/editor/clipboard.ts @@ -0,0 +1,81 @@ +import {Clipboard} from "@capacitor/clipboard" +import {Capacitor} from "@capacitor/core" +import {Extension} from "@tiptap/core" +import {Plugin, PluginKey} from "@tiptap/pm/state" + +const nativeClipboardAvailable = () => + Capacitor.isNativePlatform() && Capacitor.isPluginAvailable("Clipboard") + +const hasStandardPastePayload = (event: ClipboardEvent) => { + const clipboardData = event.clipboardData + + if (!clipboardData) { + return false + } + + if (Array.from(clipboardData.items).some(item => item.kind === "file")) { + return true + } + + if (clipboardData.types.includes("text/html")) { + return true + } + + return clipboardData.getData("text/plain") !== "" +} + +const getNativeClipboardImage = async () => { + try { + const {type, value} = await Clipboard.read() + + if (!type.startsWith("image/") || value === "") { + return undefined + } + + const imageData = value.startsWith("data:") ? value : `data:${type};base64,${value}` + const blob = await fetch(imageData).then(res => res.blob()) + + if (!blob.type.startsWith("image/")) { + return undefined + } + + const extension = type.split("/")[1]?.split("+")[0] || "png" + + return new File([blob], `clipboard-image.${extension}`, {type: blob.type || type}) + } catch { + return undefined + } +} + +export const NativeClipboardPasteExtension = Extension.create({ + name: "nativeClipboardPaste", + + addProseMirrorPlugins() { + const editor = this.editor + + return [ + new Plugin({ + key: new PluginKey("nativeClipboardPaste"), + props: { + handlePaste: (_view, event) => { + if (!nativeClipboardAvailable() || hasStandardPastePayload(event)) { + return false + } + + event.preventDefault() + + void getNativeClipboardImage().then(file => { + if (!file) { + return + } + + editor.commands.addFile(file, editor.state.selection.from + 1) + }) + + return true + }, + }, + }), + ] + }, +}) diff --git a/src/app/editor/index.ts b/src/app/editor/index.ts index f56fd4b1..b3a8c858 100644 --- a/src/app/editor/index.ts +++ b/src/app/editor/index.ts @@ -15,6 +15,7 @@ import { } from "@welshman/app" import type {FileAttributes} from "@welshman/editor" import {Editor, MentionSuggestion, WelshmanExtension, editorProps} from "@welshman/editor" +import {NativeClipboardPasteExtension} from "@app/editor/clipboard" import {escapeHtml} from "@lib/html" import {makeMentionNodeView} from "@app/editor/MentionNodeView" import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte" @@ -137,6 +138,7 @@ export const makeEditor = async ({ }, }, }), + NativeClipboardPasteExtension, ], onUpdate({editor}) { wordCount?.set(editor.storage.wordCount.words)