Merge pull request #663 from patrickpilon/claude/add-clipboard-copy-paste-011CUogNYVLkp3YrnDiwZabG

Add clipboard copy/paste functionality for Chrome
This commit is contained in:
wenjie
2025-11-21 11:44:22 +08:00
committed by GitHub
2 changed files with 100 additions and 20 deletions

View File

@@ -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'
},

View File

@@ -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>}