latest stages added
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
Select,
|
||||
Button,
|
||||
Space,
|
||||
Switch,
|
||||
Divider,
|
||||
Row,
|
||||
Col,
|
||||
@@ -33,7 +34,7 @@ export const ConnectionModal: React.FC<Props> = ({
|
||||
}) => {
|
||||
const [form] = Form.useForm<ConnectionFormValues>();
|
||||
const protocol = Form.useWatch('protocol', form);
|
||||
const isEdit = !!connection;
|
||||
const isEdit = !!connection?.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -48,11 +49,12 @@ export const ConnectionModal: React.FC<Props> = ({
|
||||
domain: connection.domain ?? undefined,
|
||||
osType: connection.osType ?? undefined,
|
||||
notes: connection.notes ?? undefined,
|
||||
clipboardEnabled: connection.clipboardEnabled !== false,
|
||||
folderId: connection.folderId ?? null,
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ protocol: 'ssh', port: 22 });
|
||||
form.setFieldsValue({ protocol: 'ssh', port: 22, clipboardEnabled: true });
|
||||
}
|
||||
}
|
||||
}, [open, connection, form]);
|
||||
@@ -166,6 +168,12 @@ export const ConnectionModal: React.FC<Props> = ({
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{protocol === 'rdp' && (
|
||||
<Form.Item label="Clipboard" name="clipboardEnabled" valuePropName="checked">
|
||||
<Switch checkedChildren="Enabled" unCheckedChildren="Disabled" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
|
||||
<Form.Item label="OS Type" name="osType">
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Button,
|
||||
Tag,
|
||||
Typography,
|
||||
Switch,
|
||||
message,
|
||||
Divider,
|
||||
Row,
|
||||
@@ -59,6 +60,7 @@ export const ConnectionProperties: React.FC = () => {
|
||||
domain: connection.domain ?? undefined,
|
||||
osType: connection.osType ?? undefined,
|
||||
notes: connection.notes ?? undefined,
|
||||
clipboardEnabled: connection.clipboardEnabled !== false,
|
||||
folderId: connection.folderId ?? null,
|
||||
});
|
||||
} else {
|
||||
@@ -156,6 +158,12 @@ export const ConnectionProperties: React.FC = () => {
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{protocol === 'rdp' && (
|
||||
<Form.Item label="Clipboard" name="clipboardEnabled" valuePropName="checked">
|
||||
<Switch checkedChildren="On" unCheckedChildren="Off" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Divider style={{ margin: '4px 0 8px' }} />
|
||||
|
||||
<Form.Item label="Name" name="name" rules={[{ required: true }]}>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
MoreOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
CopyOutlined,
|
||||
FolderAddOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
@@ -233,6 +234,20 @@ export const ConnectionTree: React.FC = () => {
|
||||
setModalOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'duplicate',
|
||||
icon: <CopyOutlined />,
|
||||
label: 'Duplicate',
|
||||
onClick: () => {
|
||||
const conn = node.itemData.connection!;
|
||||
setEditingConnection({
|
||||
...conn,
|
||||
id: '', // empty id → modal treats it as create
|
||||
name: `${conn.name} (Copy)`,
|
||||
});
|
||||
setModalOpen(true);
|
||||
},
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: 'delete',
|
||||
|
||||
@@ -250,6 +250,30 @@ export const RdpTab: React.FC<Props> = ({ session }) => {
|
||||
|
||||
// --- Keyboard event handlers ---
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
// Ctrl+V / Cmd+V: read local clipboard → send to remote → then forward keystroke
|
||||
if ((e.ctrlKey || e.metaKey) && e.code === 'KeyV') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.readText().then((text) => {
|
||||
if (text) {
|
||||
sendJson({ type: 'clipboardWrite', text });
|
||||
}
|
||||
// Forward Ctrl+V keystrokes after clipboard is set
|
||||
sendJson({ type: 'keyDown', keySym: e.ctrlKey ? 0xffe3 : 0xffeb }); // Ctrl/Meta
|
||||
sendJson({ type: 'keyDown', keySym: 0x76 }); // v
|
||||
sendJson({ type: 'keyUp', keySym: 0x76 });
|
||||
sendJson({ type: 'keyUp', keySym: e.ctrlKey ? 0xffe3 : 0xffeb });
|
||||
}).catch(() => {
|
||||
// Clipboard access denied — just forward the keystroke
|
||||
sendJson({ type: 'keyDown', keySym: 0xffe3 });
|
||||
sendJson({ type: 'keyDown', keySym: 0x76 });
|
||||
sendJson({ type: 'keyUp', keySym: 0x76 });
|
||||
sendJson({ type: 'keyUp', keySym: 0xffe3 });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+C / Cmd+C: forward to remote, clipboard sync handled by rdpd monitor
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const keySym = getKeySym(e);
|
||||
@@ -259,6 +283,12 @@ export const RdpTab: React.FC<Props> = ({ session }) => {
|
||||
};
|
||||
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
// Skip keyup for Ctrl+V since we sent synthetic keystrokes above
|
||||
if ((e.ctrlKey || e.metaKey) && e.code === 'KeyV') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const keySym = getKeySym(e);
|
||||
@@ -270,18 +300,24 @@ export const RdpTab: React.FC<Props> = ({ session }) => {
|
||||
canvas.addEventListener('keydown', onKeyDown);
|
||||
canvas.addEventListener('keyup', onKeyUp);
|
||||
|
||||
// --- Clipboard: paste handler ---
|
||||
const onPaste = (e: ClipboardEvent) => {
|
||||
const text = e.clipboardData?.getData('text');
|
||||
if (text) {
|
||||
sendJson({ type: 'clipboardWrite', text });
|
||||
}
|
||||
};
|
||||
canvas.addEventListener('paste', onPaste);
|
||||
|
||||
// --- Resize observer to keep scale in sync ---
|
||||
// --- Resize observer: update scale + send dynamic resize to rdpd ---
|
||||
let resizeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateScale();
|
||||
|
||||
// Debounce resize messages (300ms) to avoid spamming during drag
|
||||
if (resizeTimer) clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
const w = container.clientWidth;
|
||||
const h = container.clientHeight;
|
||||
if (w > 0 && h > 0 && ws.readyState === WebSocket.OPEN) {
|
||||
// Use device pixel ratio for crisp rendering on HiDPI
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const rw = Math.round(w * dpr);
|
||||
const rh = Math.round(h * dpr);
|
||||
ws.send(JSON.stringify({ type: 'resize', width: rw, height: rh }));
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
resizeObserver.observe(container);
|
||||
|
||||
@@ -293,8 +329,8 @@ export const RdpTab: React.FC<Props> = ({ session }) => {
|
||||
canvas.removeEventListener('contextmenu', onContextMenu);
|
||||
canvas.removeEventListener('keydown', onKeyDown);
|
||||
canvas.removeEventListener('keyup', onKeyUp);
|
||||
canvas.removeEventListener('paste', onPaste);
|
||||
resizeObserver.disconnect();
|
||||
if (resizeTimer) clearTimeout(resizeTimer);
|
||||
ws.close();
|
||||
wsRef.current = null;
|
||||
};
|
||||
@@ -318,9 +354,8 @@ export const RdpTab: React.FC<Props> = ({ session }) => {
|
||||
ref={canvasRef}
|
||||
tabIndex={0}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'contain',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
outline: 'none',
|
||||
cursor: 'default',
|
||||
}}
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface Connection {
|
||||
domain?: string | null;
|
||||
osType?: string | null;
|
||||
notes?: string | null;
|
||||
clipboardEnabled?: boolean;
|
||||
folderId?: string | null;
|
||||
privateKey?: string | null;
|
||||
createdAt: string;
|
||||
@@ -37,6 +38,7 @@ export interface ConnectionFormValues {
|
||||
domain?: string;
|
||||
osType?: string;
|
||||
notes?: string;
|
||||
clipboardEnabled?: boolean;
|
||||
folderId?: string | null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user