latest stages added

This commit is contained in:
felixg
2026-03-01 11:12:14 +01:00
parent 6e9956bce9
commit 7e3a1ceef4
14 changed files with 310 additions and 53 deletions

View File

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

View File

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

View File

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

View File

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

View File

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