diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 084bfa3..f109274 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -74,6 +74,16 @@ const en = { submit: 'Submit', virtual: 'Keyboard', ctrlaltdel: 'Ctrl+Alt+Del', + readClipboard: 'Read from Clipboard', + clipboardHint: 'Click to paste from your browser clipboard', + clipboardNotSupported: 'Clipboard API is not supported in this browser', + clipboardEmpty: 'Clipboard is empty', + clipboardRead: 'Clipboard content loaded', + clipboardPermissionDenied: + 'Clipboard permission denied. Please allow clipboard access in your browser.', + clipboardReadError: 'Failed to read clipboard', + clipboardTooLong: 'Clipboard content exceeds 1024 characters', + nonAsciiError: 'Only ASCII characters are supported' dropdownEnglish: 'English', dropdownGerman: 'German' }, diff --git a/web/src/pages/desktop/menu/keyboard/paste.tsx b/web/src/pages/desktop/menu/keyboard/paste.tsx index d4bbf9b..ad5327c 100644 --- a/web/src/pages/desktop/menu/keyboard/paste.tsx +++ b/web/src/pages/desktop/menu/keyboard/paste.tsx @@ -1,8 +1,8 @@ import { ChangeEvent, useRef, useState } from 'react'; -import { Button, Input, Modal, Select, type InputRef } from 'antd'; +import { Button, Input, Modal, Select, message, type InputRef } from 'antd'; import clsx from 'clsx'; import { useSetAtom } from 'jotai'; -import { ClipboardIcon } from 'lucide-react'; +import { ClipboardIcon, ClipboardPasteIcon } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { paste } from '@/api/hid'; @@ -21,6 +21,7 @@ export const Paste = () => { const [status, setStatus] = useState<'' | 'error'>(''); const [isLoading, setIsLoading] = useState(false); const [errMsg, setErrMsg] = useState(''); + const [isReadingClipboard, setIsReadingClipboard] = useState(false); const [langue, setLangue] = useState('en'); const inputRef = useRef(null); @@ -31,6 +32,60 @@ export const Paste = () => { setInputValue(value); } + async function readFromClipboard() { + if (isReadingClipboard) return; + + setIsReadingClipboard(true); + try { + // Check if clipboard API is available + if (!navigator.clipboard || !navigator.clipboard.readText) { + message.error(t('keyboard.clipboardNotSupported') || 'Clipboard API is not supported in this browser'); + return; + } + + // Read text from clipboard + const text = await navigator.clipboard.readText(); + + if (!text) { + message.warning(t('keyboard.clipboardEmpty') || 'Clipboard is empty'); + return; + } + + // Validate ASCII + if (!isASCII(text)) { + setStatus('error'); + setInputValue(text); + message.error(t('keyboard.nonAsciiError') || 'Only ASCII characters are supported'); + return; + } + + // Check length + if (text.length > 1024) { + message.warning(t('keyboard.clipboardTooLong') || 'Clipboard content exceeds 1024 characters'); + setInputValue(text.substring(0, 1024)); + setStatus(''); + return; + } + + setInputValue(text); + setStatus(''); + message.success(t('keyboard.clipboardRead') || 'Clipboard content loaded'); + } catch (error) { + console.error('Failed to read clipboard:', error); + if (error instanceof Error) { + if (error.name === 'NotAllowedError') { + message.error(t('keyboard.clipboardPermissionDenied') || 'Clipboard permission denied. Please allow clipboard access in your browser.'); + } else { + message.error(t('keyboard.clipboardReadError') || `Failed to read clipboard: ${error.message}`); + } + } else { + message.error(t('keyboard.clipboardReadError') || 'Failed to read clipboard'); + } + } finally { + setIsReadingClipboard(false); + } + } + function submit() { if (isLoading) return; setIsLoading(true); @@ -91,24 +146,39 @@ export const Paste = () => { >
{t('keyboard.tips')}
- - -