kvm/ui/src/components/popovers/PasteModal.tsx

181 lines
6.5 KiB
TypeScript

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 { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
import { keys } from "@/keyboardMappings";
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
import useKeyboard from "../../hooks/useKeyboard";
export default function PasteModal() {
const TextAreaRef = useRef<HTMLTextAreaElement>(null);
const { setPasteModeEnabled } = useHidStore();
const { setDisableVideoFocusTrap } = useUiStore();
const { send } = useJsonRpc();
const { rpcDataChannel } = useRTCStore();
const [invalidChars, setInvalidChars] = useState<string[]>([]);
const close = useClose();
const { handleKeyPress } = useKeyboard();
const { setKeyboardLayout } = useSettingsStore();
const { selectedKeyboard } = useKeyboardLayout();
useEffect(() => {
send("getKeyboardLayout", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) return;
setKeyboardLayout(resp.result as string);
});
}, [send, setKeyboardLayout]);
const onCancelPasteMode = useCallback(() => {
setPasteModeEnabled(false);
setDisableVideoFocusTrap(false);
setInvalidChars([]);
}, [setDisableVideoFocusTrap, setPasteModeEnabled]);
const onConfirmPaste = useCallback(async () => {
setPasteModeEnabled(false);
setDisableVideoFocusTrap(false);
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
if (!selectedKeyboard) return;
for (let i = 0; i < 5; i++) {
for (let i = 0; i < 26; i++) {
handleKeyPress(keys[`Key${String.fromCharCode(65 + i)}`], true);
await new Promise(resolve => setTimeout(resolve, 50));
handleKeyPress(keys[`Key${String.fromCharCode(65 + i)}`], false);
await new Promise(resolve => setTimeout(resolve, 50));
}
await new Promise(resolve => setTimeout(resolve, 50));
handleKeyPress(keys.Enter, true);
await new Promise(resolve => setTimeout(resolve, 50));
handleKeyPress(keys.Enter, false);
await new Promise(resolve => setTimeout(resolve, 50));
}
// for (let index = 0; index < 2; index++) {
// handleKeyPress(keys.KeyA, true);
// await new Promise(resolve => setTimeout(resolve, 3000));
// handleKeyPress(keys.KeyA, false);
// }
}, [setPasteModeEnabled, setDisableVideoFocusTrap, rpcDataChannel?.readyState, selectedKeyboard, handleKeyPress]);
useEffect(() => {
if (TextAreaRef.current) {
TextAreaRef.current.focus();
}
}, []);
return (
<GridCard>
<div className="space-y-4 p-4 py-3">
<div className="grid h-full grid-rows-(--grid-headerBody)">
<div className="h-full space-y-4">
<div className="space-y-4">
<SettingsPageHeader
title="Paste text"
description="Paste text from your client to the remote host"
/>
<div
className="animate-fadeIn opacity-0 space-y-2"
style={{
animationDuration: "0.7s",
animationDelay: "0.1s",
}}
>
<div>
<div className="w-full" onKeyUp={e => e.stopPropagation()} onKeyDown={e => e.stopPropagation()}>
<TextAreaWithLabel
ref={TextAreaRef}
label="Paste from host"
rows={4}
onKeyUp={e => 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 => !selectedKeyboard.chars[char]),
),
];
setInvalidChars(invalidChars);
}}
/>
{invalidChars.length > 0 && (
<div className="mt-2 flex items-center gap-x-2">
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
<span className="text-xs text-red-500 dark:text-red-400">
The following characters won&apos;t be pasted:{" "}
{invalidChars.join(", ")}
</span>
</div>
)}
</div>
</div>
<div className="space-y-4">
<p className="text-xs text-slate-600 dark:text-slate-400">
Sending text using keyboard layout: {selectedKeyboard.isoCode}-{selectedKeyboard.name}
</p>
</div>
</div>
</div>
</div>
</div>
<div
className="flex animate-fadeIn opacity-0 items-center justify-end gap-x-2"
style={{
animationDuration: "0.7s",
animationDelay: "0.2s",
}}
>
<Button
size="SM"
theme="blank"
text="Cancel"
onClick={() => {
onCancelPasteMode();
close();
}}
/>
<Button
size="SM"
theme="primary"
text="Confirm Paste"
onClick={onConfirmPaste}
LeadingIcon={LuCornerDownLeft}
/>
</div>
</div>
</GridCard>
);
}