@electerm/electerm-react 2.7.8 → 2.8.6
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/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/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/rdp/rdp-session.jsx +131 -17
- 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/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/data-cols-parser.jsx +3 -2
- package/client/components/terminal-info/network.jsx +3 -2
- package/client/components/vnc/vnc-session.jsx +398 -62
- 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
|
},
|
|
@@ -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
|
|
@@ -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 = {
|
|
@@ -82,16 +85,27 @@ export default class RdpSession extends PureComponent {
|
|
|
82
85
|
})
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
buildWsUrl = (port, type = 'rdp', extra = '') => {
|
|
89
|
+
const { host, tokenElecterm } = this.props.config
|
|
90
|
+
const { id } = this.props.tab
|
|
91
|
+
if (window.et.buildWsUrl) {
|
|
92
|
+
return window.et.buildWsUrl(
|
|
93
|
+
host,
|
|
94
|
+
port,
|
|
95
|
+
tokenElecterm,
|
|
96
|
+
id,
|
|
97
|
+
type,
|
|
98
|
+
extra
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
return `ws://${host}:${port}/${type}/${id}?token=${tokenElecterm}${extra}`
|
|
102
|
+
}
|
|
103
|
+
|
|
85
104
|
remoteInit = async () => {
|
|
86
105
|
this.setState({
|
|
87
106
|
loading: true
|
|
88
107
|
})
|
|
89
108
|
const { config } = this.props
|
|
90
|
-
const {
|
|
91
|
-
host,
|
|
92
|
-
tokenElecterm,
|
|
93
|
-
server = ''
|
|
94
|
-
} = config
|
|
95
109
|
const { id } = this.props
|
|
96
110
|
const tab = window.store.applyProfile(deepCopy(this.props.tab || {}))
|
|
97
111
|
const {
|
|
@@ -127,16 +141,10 @@ export default class RdpSession extends PureComponent {
|
|
|
127
141
|
console.debug('[RDP-CLIENT] Term created, pid=', pid, 'port=', port)
|
|
128
142
|
|
|
129
143
|
// Build the WebSocket proxy address for IronRDP WASM
|
|
130
|
-
const hs = server
|
|
131
|
-
? server.replace(/https?:\/\//, '')
|
|
132
|
-
: `${host}:${port}`
|
|
133
|
-
const pre = server.startsWith('https') ? 'wss' : 'ws'
|
|
134
144
|
const { width, height } = this.state
|
|
135
145
|
// IronRDP connects to the proxy address, which then proxies via RDCleanPath
|
|
136
|
-
|
|
137
|
-
const proxyAddress =
|
|
138
|
-
console.debug('[RDP-CLIENT] Proxy address:', proxyAddress)
|
|
139
|
-
|
|
146
|
+
const extra = `&width=${width}&height=${height}`
|
|
147
|
+
const proxyAddress = this.buildWsUrl(port, 'rdp', extra)
|
|
140
148
|
// Load WASM module if not already loaded
|
|
141
149
|
try {
|
|
142
150
|
await loadWasmModule()
|
|
@@ -171,7 +179,7 @@ export default class RdpSession extends PureComponent {
|
|
|
171
179
|
console.debug('[RDP-CLIENT] desktopSize:', width, 'x', height)
|
|
172
180
|
|
|
173
181
|
const desktopSize = new window.ironRdp.DesktopSize(width, height)
|
|
174
|
-
const enableCredsspExt = new window.ironRdp.Extension('enable_credssp',
|
|
182
|
+
const enableCredsspExt = new window.ironRdp.Extension('enable_credssp', true)
|
|
175
183
|
|
|
176
184
|
const builder = new window.ironRdp.SessionBuilder()
|
|
177
185
|
builder.username(username)
|
|
@@ -183,6 +191,29 @@ export default class RdpSession extends PureComponent {
|
|
|
183
191
|
builder.renderCanvas(canvas)
|
|
184
192
|
builder.extension(enableCredsspExt)
|
|
185
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
|
+
|
|
186
217
|
// Cursor style callback
|
|
187
218
|
builder.setCursorStyleCallbackContext(canvas)
|
|
188
219
|
builder.setCursorStyleCallback(function (style) {
|
|
@@ -241,6 +272,20 @@ export default class RdpSession extends PureComponent {
|
|
|
241
272
|
return e?.message || e?.toString() || String(e)
|
|
242
273
|
}
|
|
243
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
|
+
|
|
244
289
|
onSessionEnd = () => {
|
|
245
290
|
console.debug('[RDP-CLIENT] onSessionEnd called')
|
|
246
291
|
this.session = null
|
|
@@ -353,6 +398,14 @@ export default class RdpSession extends PureComponent {
|
|
|
353
398
|
}, { passive: false })
|
|
354
399
|
|
|
355
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
|
+
})
|
|
356
409
|
}
|
|
357
410
|
|
|
358
411
|
// Get PS/2 scancode from keyboard event code using existing code-scan module
|
|
@@ -382,7 +435,7 @@ export default class RdpSession extends PureComponent {
|
|
|
382
435
|
getAllRes = () => {
|
|
383
436
|
return [
|
|
384
437
|
...this.props.resolutions,
|
|
385
|
-
...resolutions
|
|
438
|
+
...resolutions.slice(1)
|
|
386
439
|
]
|
|
387
440
|
}
|
|
388
441
|
|
|
@@ -397,7 +450,65 @@ export default class RdpSession extends PureComponent {
|
|
|
397
450
|
return null
|
|
398
451
|
}
|
|
399
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
|
+
|
|
400
506
|
renderControl = () => {
|
|
507
|
+
const contrlProps = this.getControlProps({
|
|
508
|
+
fixedPosition: false,
|
|
509
|
+
showExitFullscreen: false,
|
|
510
|
+
className: 'mg1l'
|
|
511
|
+
})
|
|
401
512
|
const {
|
|
402
513
|
id
|
|
403
514
|
} = this.state
|
|
@@ -439,6 +550,7 @@ export default class RdpSession extends PureComponent {
|
|
|
439
550
|
</div>
|
|
440
551
|
<div className='fright'>
|
|
441
552
|
{this.props.fullscreenIcon()}
|
|
553
|
+
<RemoteFloatControl {...contrlProps} />
|
|
442
554
|
</div>
|
|
443
555
|
</div>
|
|
444
556
|
)
|
|
@@ -476,6 +588,7 @@ export default class RdpSession extends PureComponent {
|
|
|
476
588
|
height,
|
|
477
589
|
tabIndex: 0
|
|
478
590
|
}
|
|
591
|
+
const controlProps = this.getControlProps()
|
|
479
592
|
return (
|
|
480
593
|
<Spin spinning={loading}>
|
|
481
594
|
<div
|
|
@@ -483,6 +596,7 @@ export default class RdpSession extends PureComponent {
|
|
|
483
596
|
className='rdp-session-wrap session-v-wrap'
|
|
484
597
|
>
|
|
485
598
|
{this.renderControl()}
|
|
599
|
+
<RemoteFloatControl {...controlProps} />
|
|
486
600
|
<canvas
|
|
487
601
|
{...canvasProps}
|
|
488
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
|
|
|
@@ -184,9 +184,11 @@ class ShortcutControl extends React.PureComponent {
|
|
|
184
184
|
|
|
185
185
|
togglefullscreenShortcut = throttle((e) => {
|
|
186
186
|
e.stopPropagation()
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
if (window.store.fullscreen) {
|
|
188
|
+
window.store.toggleSessFullscreen(false)
|
|
189
|
+
} else {
|
|
190
|
+
window.store.toggleSessFullscreen(true)
|
|
191
|
+
}
|
|
190
192
|
}, 500)
|
|
191
193
|
|
|
192
194
|
zoominShortcut = throttle((e) => {
|
|
@@ -176,9 +176,11 @@ export function shortcutExtend (Cls) {
|
|
|
176
176
|
!altKey &&
|
|
177
177
|
!shiftKey &&
|
|
178
178
|
ctrlKey &&
|
|
179
|
-
this.
|
|
179
|
+
this.zmodemClient &&
|
|
180
|
+
this.zmodemClient.isActive
|
|
180
181
|
) {
|
|
181
|
-
this.
|
|
182
|
+
this.zmodemClient.cancel()
|
|
183
|
+
return false
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
let codeName
|
|
@@ -107,6 +107,19 @@ export default class AttachAddonCustom extends AttachAddon {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
onMsg = (ev) => {
|
|
110
|
+
// Check if it's a JSON zmodem control message
|
|
111
|
+
if (typeof ev.data === 'string') {
|
|
112
|
+
try {
|
|
113
|
+
const msg = JSON.parse(ev.data)
|
|
114
|
+
if (msg.action === 'zmodem-event') {
|
|
115
|
+
// Let zmodem-client handle this, don't write to terminal
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
// Not JSON, continue processing
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
110
123
|
// When in alternate screen mode (like vim, less, or TUI apps like Claude Code),
|
|
111
124
|
// bypass trzsz processing to avoid interference with the application's display
|
|
112
125
|
if (this.term?.buffer?.active?.type === 'alternate') {
|