mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 09:02:05 -05:00
Merge pull request #663 from patrickpilon/claude/add-clipboard-copy-paste-011CUogNYVLkp3YrnDiwZabG
Add clipboard copy/paste functionality for Chrome
This commit is contained in:
@@ -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'
|
||||
},
|
||||
|
||||
@@ -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<InputRef>(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 = () => {
|
||||
>
|
||||
<div className="pb-3 text-xs text-neutral-500">{t('keyboard.tips')}</div>
|
||||
|
||||
<Select
|
||||
value={langue}
|
||||
onChange={(value) => setLangue(value)}
|
||||
style={{ width: '100%', marginBottom: '12px' }} >
|
||||
<Option value="en">{t('keyboard.dropdownEnglish')}</Option>
|
||||
<Option value="de">{t('keyboard.dropdownGerman')}</Option>
|
||||
</Select>
|
||||
|
||||
<TextArea
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
status={status}
|
||||
showCount
|
||||
maxLength={1024}
|
||||
autoSize={{ minRows: 5, maxRows: 12 }}
|
||||
placeholder={t('keyboard.placeholder')}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Select
|
||||
value={langue}
|
||||
onChange={(value) => setLangue(value)}
|
||||
style={{ width: '100%', marginBottom: '12px' }} >
|
||||
<Option value="en">{t('keyboard.dropdownEnglish')}</Option>
|
||||
<Option value="de">{t('keyboard.dropdownGerman')}</Option>
|
||||
</Select>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
icon={<ClipboardPasteIcon size={16} />}
|
||||
loading={isReadingClipboard}
|
||||
onClick={readFromClipboard}
|
||||
className="flex items-center"
|
||||
>
|
||||
{t('keyboard.readClipboard') || 'Read from Clipboard'}
|
||||
</Button>
|
||||
<span className="text-xs text-neutral-500">
|
||||
{t('keyboard.clipboardHint') || 'Click to paste from your browser clipboard'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
status={status}
|
||||
showCount
|
||||
maxLength={1024}
|
||||
autoSize={{ minRows: 5, maxRows: 12 }}
|
||||
placeholder={t('keyboard.placeholder')}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errMsg && <div className="pt-1 text-sm text-red-500">{errMsg}</div>}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user