From d30b98c987fefd7ecd0b45569b9e008d63cca14b Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Sat, 11 Jul 2026 21:41:11 +1000 Subject: [PATCH] chore: mark subproject as dirty --- overlay/src/app/hooks/useFilePasteHandler.ts | 79 ++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 overlay/src/app/hooks/useFilePasteHandler.ts diff --git a/overlay/src/app/hooks/useFilePasteHandler.ts b/overlay/src/app/hooks/useFilePasteHandler.ts new file mode 100644 index 0000000..4b929a6 --- /dev/null +++ b/overlay/src/app/hooks/useFilePasteHandler.ts @@ -0,0 +1,79 @@ +import { useCallback, ClipboardEventHandler } from 'react'; +import { getDataTransferFiles } from '../utils/dom'; +import { readClipboardImage, isTauri, isLinux } from '../utils/tauri'; + +const filesFromClipboardItems = (data: DataTransfer | null): File[] => { + if (!data?.items) return []; + + const files: File[] = []; + for (let i = 0; i < data.items.length; i += 1) { + const item = data.items[i]; + if (item.kind !== 'file') continue; + const file = item.getAsFile(); + if (file) files.push(file); + } + return files; +}; + +const filesFromHtmlImage = (data: DataTransfer | null): File[] => { + const html = data?.getData('text/html'); + if (!html) return []; + + const match = html.match(/]+src=["'](data:image\/[a-zA-Z+]+;base64,[^"']+)["']/i); + if (!match?.[1]) return []; + + try { + const dataUrl = match[1]; + const [meta, base64] = dataUrl.split(','); + const mime = meta.match(/data:(image\/[a-zA-Z+]+);base64/i)?.[1] ?? 'image/png'; + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i += 1) { + bytes[i] = binary.charCodeAt(i); + } + const ext = mime.split('/')[1]?.replace('+xml', '') || 'png'; + return [new File([bytes], `clipboard-image.${ext}`, { type: mime })]; + } catch { + return []; + } +}; + +const getClipboardFiles = (data: DataTransfer | null): File[] => { + if (!data) return []; + + const fromFiles = getDataTransferFiles(data); + if (fromFiles && fromFiles.length > 0) return fromFiles; + + const fromItems = filesFromClipboardItems(data); + if (fromItems.length > 0) return fromItems; + + return filesFromHtmlImage(data); +}; + +/** + * Paste handler for RoomInput. Supports clipboard images via files, DataTransferItem + * list, and HTML data-URL fallbacks (common on mobile WebViews). + */ +export const useFilePasteHandler = (onPaste: (file: File[]) => void): ClipboardEventHandler => + useCallback( + async (evt) => { + const files = getClipboardFiles(evt.clipboardData); + if (files.length > 0) { + evt.preventDefault(); + evt.stopPropagation(); + onPaste(files); + return; + } + + // On Linux with Tauri, browser clipboard API doesn't work for images. + if (isTauri() && isLinux()) { + const clipboardImage = await readClipboardImage(); + if (clipboardImage) { + evt.preventDefault(); + evt.stopPropagation(); + onPaste([clipboardImage]); + } + } + }, + [onPaste] + );