@electerm/electerm-react 2.7.9 → 2.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/common/pre.js +38 -11
- package/client/components/ai/ai-config.jsx +1 -1
- package/client/components/bookmark-form/common/run-scripts.jsx +1 -1
- package/client/components/bookmark-form/config/rdp.js +1 -0
- package/client/components/bookmark-form/config/vnc.js +5 -0
- package/client/components/common/remote-float-control.jsx +79 -0
- package/client/components/common/remote-float-control.styl +28 -0
- package/client/components/common/upload.jsx +75 -0
- package/client/components/layout/layout.jsx +2 -1
- package/client/components/main/main.jsx +3 -6
- package/client/components/main/term-fullscreen.styl +1 -10
- package/client/components/quick-commands/quick-commands-list-form.jsx +1 -2
- package/client/components/rdp/rdp-session.jsx +113 -4
- package/client/components/rdp/resolutions.js +6 -0
- package/client/components/session/session.jsx +4 -3
- package/client/components/session/session.styl +18 -5
- package/client/components/session/sessions.jsx +2 -1
- package/client/components/shortcuts/shortcut-control.jsx +5 -3
- package/client/components/shortcuts/shortcut-handler.js +4 -2
- package/client/components/tabs/workspace-save-modal.jsx +1 -1
- package/client/components/terminal/attach-addon-custom.js +13 -0
- package/client/components/terminal/event-emitter.js +27 -0
- package/client/components/terminal/terminal.jsx +10 -297
- package/client/components/terminal/zmodem-client.js +385 -0
- package/client/components/terminal-info/base.jsx +1 -1
- package/client/components/terminal-info/data-cols-parser.jsx +3 -2
- package/client/components/terminal-info/network.jsx +3 -2
- package/client/components/vnc/vnc-session.jsx +397 -52
- package/client/css/basic.styl +3 -0
- package/client/store/event.js +2 -2
- package/client/store/init-state.js +1 -1
- package/package.json +1 -1
- package/client/common/byte-format.js +0 -14
- package/client/components/main/term-fullscreen-control.jsx +0 -21
- package/client/components/terminal/xterm-zmodem.js +0 -55
package/client/common/pre.js
CHANGED
|
@@ -89,26 +89,47 @@ const fs = {
|
|
|
89
89
|
},
|
|
90
90
|
open: (...args) => {
|
|
91
91
|
const cb = args.pop()
|
|
92
|
-
window.
|
|
92
|
+
if (window.et.isWebApp) {
|
|
93
|
+
window.fs.openCustom(...args)
|
|
94
|
+
.then((data) => cb(undefined, data))
|
|
95
|
+
.catch((err) => cb(err))
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
runGlobalAsync('fsOpen', ...args)
|
|
93
99
|
.then((data) => cb(undefined, data))
|
|
94
100
|
.catch((err) => cb(err))
|
|
95
101
|
},
|
|
96
102
|
read: (p1, arr, ...args) => {
|
|
97
103
|
const cb = args.pop()
|
|
98
|
-
window.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
if (window.et.isWebApp) {
|
|
105
|
+
window.fs.readCustom(
|
|
106
|
+
p1,
|
|
107
|
+
arr.length,
|
|
108
|
+
...args
|
|
109
|
+
)
|
|
110
|
+
.then((data) => {
|
|
111
|
+
const { n, newArr } = data
|
|
112
|
+
const newArr1 = decodeBase64String(newArr)
|
|
113
|
+
cb(undefined, n, newArr1)
|
|
114
|
+
})
|
|
115
|
+
.catch(err => cb(err))
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
runGlobalAsync('fsRead', p1, arr.length, ...args)
|
|
103
119
|
.then((data) => {
|
|
104
|
-
const { n,
|
|
105
|
-
|
|
106
|
-
cb(undefined, n, newArr1)
|
|
120
|
+
const { n, buffer } = data
|
|
121
|
+
cb(undefined, n, buffer)
|
|
107
122
|
})
|
|
108
123
|
.catch(err => cb(err))
|
|
109
124
|
},
|
|
110
125
|
close: (fd, cb) => {
|
|
111
|
-
window.
|
|
126
|
+
if (window.et.isWebApp) {
|
|
127
|
+
window.fs.closeCustom(fd)
|
|
128
|
+
.then((data) => cb(undefined, data))
|
|
129
|
+
.catch((err) => cb(err))
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
runGlobalAsync('fsClose', fd)
|
|
112
133
|
.then((data) => cb(undefined, data))
|
|
113
134
|
.catch((err) => cb(err))
|
|
114
135
|
},
|
|
@@ -124,7 +145,13 @@ const fs = {
|
|
|
124
145
|
.catch((err) => cb(err))
|
|
125
146
|
},
|
|
126
147
|
write: (p1, buf, cb) => {
|
|
127
|
-
window.
|
|
148
|
+
if (window.et.isWebApp) {
|
|
149
|
+
window.fs.writeCustom(p1, encodeUint8Array(buf))
|
|
150
|
+
.then((data) => cb(undefined, data))
|
|
151
|
+
.catch((err) => cb(err))
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
runGlobalAsync('fsWrite', p1, buf)
|
|
128
155
|
.then((data) => cb(undefined, data))
|
|
129
156
|
.catch((err) => cb(err))
|
|
130
157
|
},
|
|
@@ -100,7 +100,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
100
100
|
className='ai-config-form'
|
|
101
101
|
>
|
|
102
102
|
<Form.Item label='API URL' required>
|
|
103
|
-
<Space.Compact
|
|
103
|
+
<Space.Compact className='width-100'>
|
|
104
104
|
<Form.Item
|
|
105
105
|
label='API URL'
|
|
106
106
|
name='baseURLAI'
|
|
@@ -31,6 +31,7 @@ const rdpConfig = {
|
|
|
31
31
|
{ ...commonFields.password, rules: [{ required: true, message: e('password') + ' required' }] },
|
|
32
32
|
commonFields.description,
|
|
33
33
|
{ type: 'input', name: 'domain', label: () => e('domain') },
|
|
34
|
+
commonFields.proxy,
|
|
34
35
|
commonFields.type
|
|
35
36
|
]
|
|
36
37
|
}
|
|
@@ -15,6 +15,9 @@ const vncConfig = {
|
|
|
15
15
|
viewOnly: false,
|
|
16
16
|
clipViewport: true,
|
|
17
17
|
scaleViewport: true,
|
|
18
|
+
qualityLevel: 3, // 0-9, lower = faster performance, default 6
|
|
19
|
+
compressionLevel: 1, // 0-9, lower = faster performance, default 2
|
|
20
|
+
shared: true,
|
|
18
21
|
connectionHoppings: [],
|
|
19
22
|
...getAuthTypeDefault(props)
|
|
20
23
|
})
|
|
@@ -33,6 +36,8 @@ const vncConfig = {
|
|
|
33
36
|
{ type: 'switch', name: 'viewOnly', label: () => e('viewOnly'), valuePropName: 'checked' },
|
|
34
37
|
{ type: 'switch', name: 'clipViewport', label: () => e('clipViewport'), valuePropName: 'checked' },
|
|
35
38
|
{ type: 'switch', name: 'scaleViewport', label: () => e('scaleViewport'), valuePropName: 'checked' },
|
|
39
|
+
{ type: 'number', name: 'qualityLevel', label: () => e('qualityLevel') + ' (0-9)', min: 0, max: 9, step: 1 },
|
|
40
|
+
{ type: 'number', name: 'compressionLevel', label: () => e('compressionLevel') + ' (0-9)', min: 0, max: 9, step: 1 },
|
|
36
41
|
{ type: 'profileItem', name: '__profile__', label: '', profileFilter: d => !isEmpty(d.vnc) },
|
|
37
42
|
commonFields.username,
|
|
38
43
|
commonFields.password,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
FullscreenExitOutlined,
|
|
4
|
+
AppstoreOutlined,
|
|
5
|
+
DesktopOutlined,
|
|
6
|
+
MoreOutlined,
|
|
7
|
+
DownOutlined
|
|
8
|
+
} from '@ant-design/icons'
|
|
9
|
+
import { Dropdown } from 'antd'
|
|
10
|
+
import './remote-float-control.styl'
|
|
11
|
+
|
|
12
|
+
const e = window.translate
|
|
13
|
+
|
|
14
|
+
export default function RemoteFloatControl (props) {
|
|
15
|
+
const {
|
|
16
|
+
isFullScreen,
|
|
17
|
+
onSendCtrlAltDel,
|
|
18
|
+
screens = [],
|
|
19
|
+
onSelectScreen,
|
|
20
|
+
currentScreen,
|
|
21
|
+
fixedPosition = true,
|
|
22
|
+
showExitFullscreen = true,
|
|
23
|
+
className = ''
|
|
24
|
+
} = props
|
|
25
|
+
|
|
26
|
+
if (fixedPosition && !isFullScreen) return null
|
|
27
|
+
|
|
28
|
+
function onExitFullScreen () {
|
|
29
|
+
window.store.toggleSessFullscreen(false)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const items = []
|
|
33
|
+
|
|
34
|
+
if (showExitFullscreen && isFullScreen) {
|
|
35
|
+
items.push({
|
|
36
|
+
key: 'exit-fullscreen',
|
|
37
|
+
label: e('exitFullscreen') || 'Exit Fullscreen',
|
|
38
|
+
icon: <FullscreenExitOutlined />,
|
|
39
|
+
onClick: onExitFullScreen
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (onSendCtrlAltDel) {
|
|
44
|
+
items.push({
|
|
45
|
+
key: 'ctrl-alt-del',
|
|
46
|
+
label: 'Send Ctrl+Alt+Del',
|
|
47
|
+
icon: <AppstoreOutlined />,
|
|
48
|
+
onClick: onSendCtrlAltDel
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (screens && screens.length > 0) {
|
|
53
|
+
items.push({
|
|
54
|
+
key: 'screens',
|
|
55
|
+
label: 'Select Screen',
|
|
56
|
+
icon: <DesktopOutlined />,
|
|
57
|
+
children: screens.map(s => ({
|
|
58
|
+
key: s.id,
|
|
59
|
+
label: s.name,
|
|
60
|
+
onClick: () => onSelectScreen(s.id),
|
|
61
|
+
icon: currentScreen === s.id ? <DownOutlined /> : null
|
|
62
|
+
}))
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const containerClassName = (fixedPosition ? 'remote-float-control' : 'remote-float-control-inline') + (className ? ' ' + className : '')
|
|
67
|
+
const buttonClassName = fixedPosition ? 'remote-float-btn' : 'remote-float-btn-inline'
|
|
68
|
+
const iconClassName = fixedPosition ? 'font20' : ''
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className={containerClassName}>
|
|
72
|
+
<Dropdown menu={{ items }} trigger={['click']}>
|
|
73
|
+
<div className={buttonClassName}>
|
|
74
|
+
<MoreOutlined className={iconClassName} />
|
|
75
|
+
</div>
|
|
76
|
+
</Dropdown>
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.remote-float-control
|
|
2
|
+
position fixed
|
|
3
|
+
top 10px
|
|
4
|
+
right 10px
|
|
5
|
+
z-index 9999
|
|
6
|
+
|
|
7
|
+
.remote-float-control-inline
|
|
8
|
+
display inline-block
|
|
9
|
+
position relative
|
|
10
|
+
|
|
11
|
+
.remote-float-btn-inline
|
|
12
|
+
display inline
|
|
13
|
+
cursor pointer
|
|
14
|
+
|
|
15
|
+
.remote-float-btn
|
|
16
|
+
padding 8px
|
|
17
|
+
background rgba(0, 0, 0, 0.5)
|
|
18
|
+
border-radius 4px
|
|
19
|
+
color #fff
|
|
20
|
+
cursor pointer
|
|
21
|
+
opacity 0.8
|
|
22
|
+
transition opacity 0.3s
|
|
23
|
+
display flex
|
|
24
|
+
align-items center
|
|
25
|
+
justify-content center
|
|
26
|
+
&:hover
|
|
27
|
+
&.ant-dropdown-open
|
|
28
|
+
opacity 1
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Upload component that uses Electron's native file dialog
|
|
3
|
+
* This replaces antd Upload to get absolute file paths instead of browser-based file selection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PureComponent } from 'react'
|
|
7
|
+
import { getLocalFileInfo } from '../sftp/file-read'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Open a single file select dialog
|
|
11
|
+
* @returns {Promise<Object|null>} - File object with path info or null if cancelled
|
|
12
|
+
*/
|
|
13
|
+
const openFileSelect = async () => {
|
|
14
|
+
const properties = [
|
|
15
|
+
'openFile',
|
|
16
|
+
'showHiddenFiles',
|
|
17
|
+
'noResolveAliases',
|
|
18
|
+
'treatPackageAsDirectory',
|
|
19
|
+
'dontAddToRecent'
|
|
20
|
+
]
|
|
21
|
+
const files = await window.api.openDialog({
|
|
22
|
+
title: 'Choose a file',
|
|
23
|
+
message: 'Choose a file',
|
|
24
|
+
properties
|
|
25
|
+
}).catch(() => false)
|
|
26
|
+
if (!files || !files.length) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
const filePath = files[0]
|
|
30
|
+
const stat = await getLocalFileInfo(filePath)
|
|
31
|
+
return { ...stat, filePath, path: filePath }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Custom Upload component
|
|
36
|
+
* Uses Electron's native file dialog for file selection
|
|
37
|
+
* API compatible with antd Upload for the use cases in this project
|
|
38
|
+
*/
|
|
39
|
+
export default class Upload extends PureComponent {
|
|
40
|
+
handleClick = async () => {
|
|
41
|
+
const { beforeUpload, disabled } = this.props
|
|
42
|
+
if (disabled) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
const file = await openFileSelect()
|
|
46
|
+
if (!file) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
if (beforeUpload) {
|
|
50
|
+
beforeUpload(file)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
render () {
|
|
55
|
+
const {
|
|
56
|
+
children,
|
|
57
|
+
className,
|
|
58
|
+
style,
|
|
59
|
+
disabled
|
|
60
|
+
} = this.props
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
className={className}
|
|
65
|
+
style={style}
|
|
66
|
+
onClick={this.handleClick}
|
|
67
|
+
role='button'
|
|
68
|
+
tabIndex={disabled ? -1 : 0}
|
|
69
|
+
aria-disabled={disabled}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -19,7 +19,6 @@ import TransportsActionStore from '../file-transfer/transports-action-store.jsx'
|
|
|
19
19
|
import classnames from 'classnames'
|
|
20
20
|
import ShortcutControl from '../shortcuts/shortcut-control.jsx'
|
|
21
21
|
import { isMac, isWin, textTerminalBgValue } from '../../common/constants'
|
|
22
|
-
import TermFullscreenControl from './term-fullscreen-control'
|
|
23
22
|
import TerminalInfo from '../terminal-info/terminal-info'
|
|
24
23
|
import { ConfigProvider } from 'antd'
|
|
25
24
|
import { NotificationContainer } from '../common/notification'
|
|
@@ -36,6 +35,7 @@ import WorkspaceSaveModal from '../tabs/workspace-save-modal'
|
|
|
36
35
|
import { pick } from 'lodash-es'
|
|
37
36
|
import deepCopy from 'json-deep-copy'
|
|
38
37
|
import './wrapper.styl'
|
|
38
|
+
import './term-fullscreen.styl'
|
|
39
39
|
|
|
40
40
|
export default auto(function Index (props) {
|
|
41
41
|
useEffect(() => {
|
|
@@ -79,7 +79,7 @@ export default auto(function Index (props) {
|
|
|
79
79
|
const {
|
|
80
80
|
configLoaded,
|
|
81
81
|
config,
|
|
82
|
-
|
|
82
|
+
fullscreen,
|
|
83
83
|
pinned,
|
|
84
84
|
isSecondInstance,
|
|
85
85
|
pinnedQuickCommandBar,
|
|
@@ -105,7 +105,7 @@ export default auto(function Index (props) {
|
|
|
105
105
|
pinned,
|
|
106
106
|
'not-win': !isWin,
|
|
107
107
|
'qm-pinned': pinnedQuickCommandBar,
|
|
108
|
-
|
|
108
|
+
fullscreen,
|
|
109
109
|
'is-main': !isSecondInstance
|
|
110
110
|
})
|
|
111
111
|
const ext1 = {
|
|
@@ -243,9 +243,6 @@ export default auto(function Index (props) {
|
|
|
243
243
|
<div {...ext1}>
|
|
244
244
|
<InputContextMenu />
|
|
245
245
|
<ShortcutControl config={config} />
|
|
246
|
-
<TermFullscreenControl
|
|
247
|
-
terminalFullScreen={terminalFullScreen}
|
|
248
|
-
/>
|
|
249
246
|
<CssOverwrite
|
|
250
247
|
{...confsCss}
|
|
251
248
|
wsInited={wsInited}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.
|
|
1
|
+
.fullscreen
|
|
2
2
|
.sidebar
|
|
3
3
|
.tabs
|
|
4
4
|
.terminal-footer
|
|
@@ -7,13 +7,6 @@
|
|
|
7
7
|
.main-footer
|
|
8
8
|
.session-v-info
|
|
9
9
|
display none
|
|
10
|
-
.term-fullscreen-control
|
|
11
|
-
display block
|
|
12
|
-
right 10px
|
|
13
|
-
top 10px
|
|
14
|
-
position fixed
|
|
15
|
-
z-index 100
|
|
16
|
-
background #2df56c
|
|
17
10
|
// Hide all sessions first
|
|
18
11
|
.session-wrap
|
|
19
12
|
display none
|
|
@@ -37,5 +30,3 @@
|
|
|
37
30
|
top 10px !important
|
|
38
31
|
right 10px !important
|
|
39
32
|
bottom 10px !important
|
|
40
|
-
.term-fullscreen-control
|
|
41
|
-
display none
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
import * as ls from '../../common/safe-local-storage'
|
|
18
18
|
import scanCode from './code-scan'
|
|
19
19
|
import resolutions from './resolutions'
|
|
20
|
+
import { readClipboardAsync } from '../../common/clipboard'
|
|
21
|
+
import RemoteFloatControl from '../common/remote-float-control'
|
|
20
22
|
|
|
21
23
|
const { Option } = Select
|
|
22
24
|
|
|
@@ -32,7 +34,8 @@ async function loadWasmModule () {
|
|
|
32
34
|
DesktopSize: mod.DesktopSize,
|
|
33
35
|
InputTransaction: mod.InputTransaction,
|
|
34
36
|
DeviceEvent: mod.DeviceEvent,
|
|
35
|
-
Extension: mod.Extension
|
|
37
|
+
Extension: mod.Extension,
|
|
38
|
+
ClipboardData: mod.ClipboardData
|
|
36
39
|
}
|
|
37
40
|
await window.ironRdp.wasmInit()
|
|
38
41
|
window.ironRdp.wasmSetup('info')
|
|
@@ -42,7 +45,7 @@ async function loadWasmModule () {
|
|
|
42
45
|
export default class RdpSession extends PureComponent {
|
|
43
46
|
constructor (props) {
|
|
44
47
|
const id = `rdp-reso-${props.tab.host}`
|
|
45
|
-
const resObj = ls.getItemJSON(id, resolutions[
|
|
48
|
+
const resObj = ls.getItemJSON(id, resolutions[1])
|
|
46
49
|
super(props)
|
|
47
50
|
this.canvasRef = createRef()
|
|
48
51
|
this.state = {
|
|
@@ -176,7 +179,7 @@ export default class RdpSession extends PureComponent {
|
|
|
176
179
|
console.debug('[RDP-CLIENT] desktopSize:', width, 'x', height)
|
|
177
180
|
|
|
178
181
|
const desktopSize = new window.ironRdp.DesktopSize(width, height)
|
|
179
|
-
const enableCredsspExt = new window.ironRdp.Extension('enable_credssp',
|
|
182
|
+
const enableCredsspExt = new window.ironRdp.Extension('enable_credssp', true)
|
|
180
183
|
|
|
181
184
|
const builder = new window.ironRdp.SessionBuilder()
|
|
182
185
|
builder.username(username)
|
|
@@ -188,6 +191,29 @@ export default class RdpSession extends PureComponent {
|
|
|
188
191
|
builder.renderCanvas(canvas)
|
|
189
192
|
builder.extension(enableCredsspExt)
|
|
190
193
|
|
|
194
|
+
// Clipboard callbacks
|
|
195
|
+
builder.remoteClipboardChangedCallback((clipboardData) => {
|
|
196
|
+
try {
|
|
197
|
+
if (clipboardData.isEmpty()) {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
const items = clipboardData.items()
|
|
201
|
+
for (const item of items) {
|
|
202
|
+
if (item.mimeType() === 'text/plain') {
|
|
203
|
+
const text = item.value()
|
|
204
|
+
console.debug('[RDP-CLIENT] Received clipboard text:', text)
|
|
205
|
+
window.pre.writeClipboard(text)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
console.error('[RDP-CLIENT] Clipboard error:', e)
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
builder.forceClipboardUpdateCallback(() => {
|
|
214
|
+
this.syncLocalToRemote()
|
|
215
|
+
})
|
|
216
|
+
|
|
191
217
|
// Cursor style callback
|
|
192
218
|
builder.setCursorStyleCallbackContext(canvas)
|
|
193
219
|
builder.setCursorStyleCallback(function (style) {
|
|
@@ -246,6 +272,20 @@ export default class RdpSession extends PureComponent {
|
|
|
246
272
|
return e?.message || e?.toString() || String(e)
|
|
247
273
|
}
|
|
248
274
|
|
|
275
|
+
syncLocalToRemote = async () => {
|
|
276
|
+
if (!this.session) return
|
|
277
|
+
try {
|
|
278
|
+
const text = await readClipboardAsync()
|
|
279
|
+
if (text) {
|
|
280
|
+
const data = new window.ironRdp.ClipboardData()
|
|
281
|
+
data.addText('text/plain', text)
|
|
282
|
+
await this.session.onClipboardPaste(data)
|
|
283
|
+
}
|
|
284
|
+
} catch (e) {
|
|
285
|
+
console.error('[RDP-CLIENT] Local clipboard sync error:', e)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
249
289
|
onSessionEnd = () => {
|
|
250
290
|
console.debug('[RDP-CLIENT] onSessionEnd called')
|
|
251
291
|
this.session = null
|
|
@@ -358,6 +398,14 @@ export default class RdpSession extends PureComponent {
|
|
|
358
398
|
}, { passive: false })
|
|
359
399
|
|
|
360
400
|
canvas.addEventListener('contextmenu', (e) => e.preventDefault())
|
|
401
|
+
|
|
402
|
+
canvas.addEventListener('paste', () => {
|
|
403
|
+
this.syncLocalToRemote()
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
canvas.addEventListener('focus', () => {
|
|
407
|
+
this.syncLocalToRemote()
|
|
408
|
+
})
|
|
361
409
|
}
|
|
362
410
|
|
|
363
411
|
// Get PS/2 scancode from keyboard event code using existing code-scan module
|
|
@@ -387,7 +435,7 @@ export default class RdpSession extends PureComponent {
|
|
|
387
435
|
getAllRes = () => {
|
|
388
436
|
return [
|
|
389
437
|
...this.props.resolutions,
|
|
390
|
-
...resolutions
|
|
438
|
+
...resolutions.slice(1)
|
|
391
439
|
]
|
|
392
440
|
}
|
|
393
441
|
|
|
@@ -402,7 +450,65 @@ export default class RdpSession extends PureComponent {
|
|
|
402
450
|
return null
|
|
403
451
|
}
|
|
404
452
|
|
|
453
|
+
getControlProps = (options = {}) => {
|
|
454
|
+
const {
|
|
455
|
+
fixedPosition = true,
|
|
456
|
+
showExitFullscreen = true,
|
|
457
|
+
className = ''
|
|
458
|
+
} = options
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
isFullScreen: this.props.fullscreen,
|
|
462
|
+
onSendCtrlAltDel: this.handleSendCtrlAltDel,
|
|
463
|
+
screens: [], // RDP doesn't have multi-screen support like VNC
|
|
464
|
+
currentScreen: null,
|
|
465
|
+
onSelectScreen: () => {}, // No-op for RDP
|
|
466
|
+
fixedPosition,
|
|
467
|
+
showExitFullscreen,
|
|
468
|
+
className
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
handleSendCtrlAltDel = () => {
|
|
473
|
+
if (!this.session) return
|
|
474
|
+
try {
|
|
475
|
+
// Send Ctrl+Alt+Del sequence using IronRDP
|
|
476
|
+
const tx = new window.ironRdp.InputTransaction()
|
|
477
|
+
|
|
478
|
+
// Ctrl key press
|
|
479
|
+
const ctrlScancode = 0x1D // Left Ctrl scancode
|
|
480
|
+
tx.addEvent(window.ironRdp.DeviceEvent.keyPressed(ctrlScancode))
|
|
481
|
+
|
|
482
|
+
// Alt key press
|
|
483
|
+
const altScancode = 0x38 // Left Alt scancode
|
|
484
|
+
tx.addEvent(window.ironRdp.DeviceEvent.keyPressed(altScancode))
|
|
485
|
+
|
|
486
|
+
// Del key press
|
|
487
|
+
const delScancode = 0x53 // Delete scancode
|
|
488
|
+
tx.addEvent(window.ironRdp.DeviceEvent.keyPressed(delScancode))
|
|
489
|
+
|
|
490
|
+
// Del key release
|
|
491
|
+
tx.addEvent(window.ironRdp.DeviceEvent.keyReleased(delScancode))
|
|
492
|
+
|
|
493
|
+
// Alt key release
|
|
494
|
+
tx.addEvent(window.ironRdp.DeviceEvent.keyReleased(altScancode))
|
|
495
|
+
|
|
496
|
+
// Ctrl key release
|
|
497
|
+
tx.addEvent(window.ironRdp.DeviceEvent.keyReleased(ctrlScancode))
|
|
498
|
+
|
|
499
|
+
this.session.applyInputs(tx)
|
|
500
|
+
console.log('[RDP-CLIENT] Sent Ctrl+Alt+Del')
|
|
501
|
+
} catch (err) {
|
|
502
|
+
console.error('[RDP-CLIENT] Failed to send Ctrl+Alt+Del:', err)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
405
506
|
renderControl = () => {
|
|
507
|
+
const contrlProps = this.getControlProps({
|
|
508
|
+
fixedPosition: false,
|
|
509
|
+
showExitFullscreen: false,
|
|
510
|
+
className: 'mg1l'
|
|
511
|
+
})
|
|
406
512
|
const {
|
|
407
513
|
id
|
|
408
514
|
} = this.state
|
|
@@ -444,6 +550,7 @@ export default class RdpSession extends PureComponent {
|
|
|
444
550
|
</div>
|
|
445
551
|
<div className='fright'>
|
|
446
552
|
{this.props.fullscreenIcon()}
|
|
553
|
+
<RemoteFloatControl {...contrlProps} />
|
|
447
554
|
</div>
|
|
448
555
|
</div>
|
|
449
556
|
)
|
|
@@ -481,6 +588,7 @@ export default class RdpSession extends PureComponent {
|
|
|
481
588
|
height,
|
|
482
589
|
tabIndex: 0
|
|
483
590
|
}
|
|
591
|
+
const controlProps = this.getControlProps()
|
|
484
592
|
return (
|
|
485
593
|
<Spin spinning={loading}>
|
|
486
594
|
<div
|
|
@@ -488,6 +596,7 @@ export default class RdpSession extends PureComponent {
|
|
|
488
596
|
className='rdp-session-wrap session-v-wrap'
|
|
489
597
|
>
|
|
490
598
|
{this.renderControl()}
|
|
599
|
+
<RemoteFloatControl {...controlProps} />
|
|
491
600
|
<canvas
|
|
492
601
|
{...canvasProps}
|
|
493
602
|
ref={this.canvasRef}
|
|
@@ -283,7 +283,8 @@ export default class SessionWrapper extends Component {
|
|
|
283
283
|
'delTab',
|
|
284
284
|
'config',
|
|
285
285
|
'reloadTab',
|
|
286
|
-
'editTab'
|
|
286
|
+
'editTab',
|
|
287
|
+
'fullscreen'
|
|
287
288
|
]),
|
|
288
289
|
...pick(
|
|
289
290
|
this,
|
|
@@ -462,7 +463,7 @@ export default class SessionWrapper extends Component {
|
|
|
462
463
|
handleFullscreen = () => {
|
|
463
464
|
// Make this tab the active tab before fullscreening
|
|
464
465
|
window.store.activeTabId = this.props.tab.id
|
|
465
|
-
window.store.
|
|
466
|
+
window.store.toggleSessFullscreen(true, this.props.tab.id)
|
|
466
467
|
}
|
|
467
468
|
|
|
468
469
|
toggleBroadcastInput = () => {
|
|
@@ -492,7 +493,7 @@ export default class SessionWrapper extends Component {
|
|
|
492
493
|
return (
|
|
493
494
|
<Tooltip title={title} placement='bottomLeft'>
|
|
494
495
|
<FullscreenOutlined
|
|
495
|
-
className='mg1r icon-info iblock pointer spliter
|
|
496
|
+
className='mg1r icon-info iblock pointer spliter fullscreen-control-icon'
|
|
496
497
|
onClick={this.handleFullscreen}
|
|
497
498
|
/>
|
|
498
499
|
</Tooltip>
|
|
@@ -60,14 +60,27 @@
|
|
|
60
60
|
.web-session-wrap
|
|
61
61
|
height 100vh
|
|
62
62
|
background var(--main)
|
|
63
|
-
.session-v-
|
|
63
|
+
.session-v-info
|
|
64
|
+
line-height 30px
|
|
65
|
+
.vnc-scroll-wrapper
|
|
66
|
+
position relative
|
|
64
67
|
background var(--main)
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
z-index 299
|
|
69
|
+
&::-webkit-scrollbar
|
|
70
|
+
width 16px
|
|
71
|
+
height 16px
|
|
72
|
+
background var(--main-darker)
|
|
73
|
+
&::-webkit-scrollbar-track
|
|
74
|
+
background var(--main-darker)
|
|
75
|
+
box-shadow inset 0 0 5px var(--main-darker)
|
|
76
|
+
&::-webkit-scrollbar-thumb
|
|
77
|
+
background var(--primary)
|
|
78
|
+
border-radius 0
|
|
79
|
+
&::-webkit-scrollbar-corner
|
|
80
|
+
background var(--main-darker)
|
|
67
81
|
.vnc-session-wrap > div
|
|
68
82
|
display block !important
|
|
69
|
-
|
|
70
|
-
height auto !important
|
|
83
|
+
overflow hidden !important
|
|
71
84
|
.not-split-view > .ant-splitter-bar .ant-splitter-bar-dragger
|
|
72
85
|
display none
|
|
73
86
|
|