import { useCallback, useEffect, useRef, useState } from "react"; import { LuCornerDownLeft } from "react-icons/lu"; import { ExclamationCircleIcon } from "@heroicons/react/16/solid"; import { useClose } from "@headlessui/react"; import { Button } from "@components/Button"; import { GridCard } from "@components/Card"; import { TextAreaWithLabel } from "@components/TextArea"; import { SettingsPageHeader } from "@components/SettingsPageheader"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useHidStore, useRTCStore, useUiStore, useDeviceSettingsStore } from "@/hooks/stores"; import { keys, modifiers } from "@/keyboardMappings"; import { layouts, chars } from "@/keyboardLayouts"; import notifications from "@/notifications"; const hidKeyboardPayload = (keys: number[], modifier: number) => { return { keys, modifier }; }; const modifierCode = (shift?: boolean, altRight?: boolean) => { return shift ? modifiers["ShiftLeft"] : 0 | (altRight ? modifiers["AltRight"] : 0) } const noModifier = 0 export default function PasteModal() { const TextAreaRef = useRef(null); const setPasteMode = useHidStore(state => state.setPasteModeEnabled); const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); const [send] = useJsonRpc(); const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); const [invalidChars, setInvalidChars] = useState([]); const close = useClose(); const keyboardLayout = useDeviceSettingsStore(state => state.keyboardLayout); const setKeyboardLayout = useDeviceSettingsStore( state => state.setKeyboardLayout, ); useEffect(() => { send("getKeyboardLayout", {}, resp => { if ("error" in resp) return; setKeyboardLayout(resp.result as string); }); }, []); const onCancelPasteMode = useCallback(() => { setPasteMode(false); setDisableVideoFocusTrap(false); setInvalidChars([]); }, [setDisableVideoFocusTrap, setPasteMode]); const onConfirmPaste = useCallback(async () => { setPasteMode(false); setDisableVideoFocusTrap(false); if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return; const text = TextAreaRef.current.value; try { for (const char of text) { if (!keyboardLayout) continue; if (!chars[keyboardLayout]) continue; const { key, shift, altRight, deadKey, accentKey } = chars[keyboardLayout][char] if (!key) continue; const keyz = [ keys[key] ]; const modz = [ modifierCode(shift, altRight) ]; if (deadKey) { keyz.push(keys["Space"]); modz.push(noModifier); } if (accentKey) { keyz.unshift(keys[accentKey.key]) modz.unshift(modifierCode(accentKey.shift, accentKey.altRight)) } for (const [index, kei] of keyz.entries()) { await new Promise((resolve, reject) => { send( "keyboardReport", hidKeyboardPayload([kei], modz[index]), params => { if ("error" in params) return reject(params.error); send("keyboardReport", hidKeyboardPayload([], 0), params => { if ("error" in params) return reject(params.error); resolve(); }); }, ); }); } } } catch (error) { console.error(error); notifications.error("Failed to paste text"); } }, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode]); useEffect(() => { if (TextAreaRef.current) { TextAreaRef.current.focus(); } }, []); return (
e.stopPropagation()}> e.stopPropagation()} onKeyDown={e => { e.stopPropagation(); if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); onConfirmPaste(); } else if (e.key === "Escape") { e.preventDefault(); onCancelPasteMode(); } }} onChange={e => { const value = e.target.value; const invalidChars = [ ...new Set( // @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments [...new Intl.Segmenter().segment(value)] .map(x => x.segment) .filter(char => !chars[keyboardLayout][char]), ), ]; setInvalidChars(invalidChars); }} /> {invalidChars.length > 0 && (
The following characters won't be pasted:{" "} {invalidChars.join(", ")}
)}

Sending key codes using keyboard layout {layouts[keyboardLayout]}

); }