@electerm/electerm-react 5.1.23 → 5.2.0
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/build-map.js +12 -0
- package/client/common/db.js +7 -5
- package/client/common/import-task.js +174 -0
- package/client/components/bookmark-form/ai-bookmark-form.jsx +18 -15
- package/client/components/bookmark-form/common/bookmark-select.jsx +6 -5
- package/client/components/bookmark-form/common/fields.jsx +1 -1
- package/client/components/bookmark-form/common/profile-item.jsx +12 -2
- package/client/components/bookmark-form/config/rdp.js +1 -1
- package/client/components/bookmark-form/tree-select.jsx +5 -16
- package/client/components/common/import-progress.jsx +90 -0
- package/client/components/layout/layout.jsx +6 -4
- package/client/components/main/main.jsx +2 -0
- package/client/components/profile/profile-form-rdp.jsx +1 -1
- package/client/components/quick-commands/quick-command-transport-mod.jsx +24 -18
- package/client/components/rdp/rdp-cred-form.jsx +55 -0
- package/client/components/rdp/rdp-session.jsx +97 -6
- package/client/components/setting-panel/keywords-transport.jsx +1 -4
- package/client/components/setting-panel/setting-modal.jsx +8 -2
- package/client/components/setting-panel/start-session-select.jsx +5 -16
- package/client/components/spice/spice-session.jsx +40 -0
- package/client/components/terminal/shortcut-bar-entry.jsx +4 -2
- package/client/components/terminal/shortcut-bar.jsx +59 -2
- package/client/components/terminal/shortcut-bar.styl +4 -2
- package/client/components/terminal/terminal-color-query.mjs +32 -27
- package/client/components/terminal/terminal.jsx +13 -9
- package/client/components/tree-list/bookmark-transport.jsx +2 -1
- package/client/components/tree-list/bookmark-upload.js +56 -41
- package/client/components/tree-list/tree-list-row.jsx +11 -4
- package/client/components/tree-list/tree-list-rows.js +6 -3
- package/client/components/tree-list/tree-list.jsx +4 -2
- package/client/store/common.js +19 -1
- package/client/store/init-state.js +3 -0
- package/client/store/load-data.js +6 -1
- package/client/store/store.js +3 -12
- package/client/store/sync.js +31 -2
- package/client/store/watch.js +10 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PureComponent, createRef } from 'react'
|
|
2
2
|
import { createTerm } from '../terminal/terminal-apis'
|
|
3
3
|
import deepCopy from 'json-deep-copy'
|
|
4
|
+
import { pick } from 'lodash-es'
|
|
4
5
|
import clone from '../../common/to-simple-obj'
|
|
5
6
|
import { handleErr } from '../../common/fetch'
|
|
6
7
|
import {
|
|
@@ -24,6 +25,8 @@ import { readClipboardAsync } from '../../common/clipboard'
|
|
|
24
25
|
import RemoteFloatControl from '../common/remote-float-control'
|
|
25
26
|
import SwitchLabel from '../common/switch'
|
|
26
27
|
import HelpIcon from '../common/help-icon'
|
|
28
|
+
import Modal from '../common/modal'
|
|
29
|
+
import RdpCredForm from './rdp-cred-form'
|
|
27
30
|
import { FileTransferManager, createFileLogger } from './file-transfer'
|
|
28
31
|
import { notification } from '../common/notification'
|
|
29
32
|
import message from '../common/message'
|
|
@@ -65,7 +68,8 @@ export default class RdpSession extends PureComponent {
|
|
|
65
68
|
...resObj,
|
|
66
69
|
hasRemoteFiles: false,
|
|
67
70
|
downloadBtnDisabled: true,
|
|
68
|
-
uploadReady: false
|
|
71
|
+
uploadReady: false,
|
|
72
|
+
showCredPrompt: false
|
|
69
73
|
}
|
|
70
74
|
this.session = null
|
|
71
75
|
this.fileTransfer = null
|
|
@@ -121,13 +125,25 @@ export default class RdpSession extends PureComponent {
|
|
|
121
125
|
return `ws://${host}:${port}/${type}/${id}?token=${tokenElecterm}${extra}`
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
remoteInit = async () => {
|
|
128
|
+
remoteInit = async (credOverride) => {
|
|
125
129
|
this.setState({
|
|
126
130
|
loading: true
|
|
127
131
|
})
|
|
128
132
|
const { config } = this.props
|
|
129
133
|
const { id } = this.props
|
|
130
|
-
const tab =
|
|
134
|
+
const tab = {
|
|
135
|
+
...window.store.applyProfile(deepCopy(this.props.tab || {})),
|
|
136
|
+
...credOverride
|
|
137
|
+
}
|
|
138
|
+
// keep resolved credentials for the connect-time prompt
|
|
139
|
+
this.tab = tab
|
|
140
|
+
if (!tab.username || !tab.password) {
|
|
141
|
+
this.setState({
|
|
142
|
+
loading: false,
|
|
143
|
+
showCredPrompt: true
|
|
144
|
+
})
|
|
145
|
+
return
|
|
146
|
+
}
|
|
131
147
|
const {
|
|
132
148
|
type,
|
|
133
149
|
term: terminalType
|
|
@@ -192,6 +208,9 @@ export default class RdpSession extends PureComponent {
|
|
|
192
208
|
const builder = new window.ironRdp.SessionBuilder()
|
|
193
209
|
builder.username(username)
|
|
194
210
|
builder.password(password)
|
|
211
|
+
if (tab.domain) {
|
|
212
|
+
builder.serverDomain(tab.domain)
|
|
213
|
+
}
|
|
195
214
|
builder.destination(destination)
|
|
196
215
|
builder.proxyAddress(proxyAddress)
|
|
197
216
|
builder.authToken('none')
|
|
@@ -277,12 +296,16 @@ export default class RdpSession extends PureComponent {
|
|
|
277
296
|
this.log(`Session ended: ${info.reason()}`, 'info')
|
|
278
297
|
this.onSessionEnd()
|
|
279
298
|
}).catch((e) => {
|
|
280
|
-
this.
|
|
299
|
+
this.showError(e)
|
|
281
300
|
this.onSessionEnd()
|
|
282
301
|
})
|
|
283
302
|
} catch (e) {
|
|
284
|
-
this.
|
|
285
|
-
this.setState({
|
|
303
|
+
this.showError(e)
|
|
304
|
+
this.setState({
|
|
305
|
+
loading: false,
|
|
306
|
+
// let the user retry with different credentials on auth failure
|
|
307
|
+
showCredPrompt: this.isAuthError(e) ? true : this.state.showCredPrompt
|
|
308
|
+
})
|
|
286
309
|
this.setStatus(statusMap.error)
|
|
287
310
|
}
|
|
288
311
|
}
|
|
@@ -307,6 +330,73 @@ export default class RdpSession extends PureComponent {
|
|
|
307
330
|
return e?.message || e?.toString() || String(e)
|
|
308
331
|
}
|
|
309
332
|
|
|
333
|
+
// WrongPassword(1) / LogonFailure(2)
|
|
334
|
+
isAuthError = (err) => {
|
|
335
|
+
if (err && typeof err === 'object' && '__wbg_ptr' in err && err.kind) {
|
|
336
|
+
try {
|
|
337
|
+
const kind = err.kind()
|
|
338
|
+
return kind === 1 || kind === 2
|
|
339
|
+
} catch (_) { }
|
|
340
|
+
}
|
|
341
|
+
return false
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// map known IronErrorKind codes to a user-friendly message
|
|
345
|
+
errText = (err) => {
|
|
346
|
+
if (this.isAuthError(err)) {
|
|
347
|
+
return window.translate('loginFail') + ` (${this.formatError(err)})`
|
|
348
|
+
}
|
|
349
|
+
return this.formatError(err)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
showError = (err) => {
|
|
353
|
+
const text = this.errText(err)
|
|
354
|
+
this.log(`Connection failed: ${this.formatError(err)}`, 'error')
|
|
355
|
+
message.error(text, 10)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
onCredSubmit = (res) => {
|
|
359
|
+
this.setState({
|
|
360
|
+
showCredPrompt: false
|
|
361
|
+
})
|
|
362
|
+
// tear down the failed attempt before reconnecting
|
|
363
|
+
this.cleanup()
|
|
364
|
+
// reconnect with the provided credentials, not persisted to the bookmark
|
|
365
|
+
this.remoteInit(res)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
onCredCancel = () => {
|
|
369
|
+
this.setState({
|
|
370
|
+
showCredPrompt: false
|
|
371
|
+
})
|
|
372
|
+
this.setStatus(statusMap.error)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
renderCredPrompt = () => {
|
|
376
|
+
const {
|
|
377
|
+
showCredPrompt
|
|
378
|
+
} = this.state
|
|
379
|
+
if (!showCredPrompt) {
|
|
380
|
+
return null
|
|
381
|
+
}
|
|
382
|
+
const confirmProps = {
|
|
383
|
+
title: window.translate('credentialsRequired'),
|
|
384
|
+
footer: null,
|
|
385
|
+
open: true,
|
|
386
|
+
onCancel: this.onCredCancel
|
|
387
|
+
}
|
|
388
|
+
return (
|
|
389
|
+
<Modal
|
|
390
|
+
{...confirmProps}
|
|
391
|
+
>
|
|
392
|
+
<RdpCredForm
|
|
393
|
+
initialValues={pick(this.tab, ['username', 'password'])}
|
|
394
|
+
handleFinish={this.onCredSubmit}
|
|
395
|
+
/>
|
|
396
|
+
</Modal>
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
310
400
|
syncLocalToRemote = async () => {
|
|
311
401
|
if (!this.session || this.fileUploadInProgress) {
|
|
312
402
|
return
|
|
@@ -751,6 +841,7 @@ export default class RdpSession extends PureComponent {
|
|
|
751
841
|
ref={this.canvasRef}
|
|
752
842
|
/>
|
|
753
843
|
</div>
|
|
844
|
+
{this.renderCredPrompt()}
|
|
754
845
|
</div>
|
|
755
846
|
</Spin>
|
|
756
847
|
)
|
|
@@ -4,6 +4,7 @@ import time from '../../common/time'
|
|
|
4
4
|
|
|
5
5
|
export default class KeywordsTransport extends BookmarkTransport {
|
|
6
6
|
name = 'keywords-highlight'
|
|
7
|
+
|
|
7
8
|
beforeUpload = async (file) => {
|
|
8
9
|
const { store } = this.props
|
|
9
10
|
const txt = file.fileContent !== undefined
|
|
@@ -20,10 +21,6 @@ export default class KeywordsTransport extends BookmarkTransport {
|
|
|
20
21
|
return false
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
renderEdit () {
|
|
24
|
-
return null
|
|
25
|
-
}
|
|
26
|
-
|
|
27
24
|
handleDownload = () => {
|
|
28
25
|
const { store } = this.props
|
|
29
26
|
const arr = store.config.keywords || []
|
|
@@ -33,14 +33,20 @@ export default auto(function SettingModalWrap (props) {
|
|
|
33
33
|
settingMap.bookmarks,
|
|
34
34
|
settingMap.terminalThemes
|
|
35
35
|
]
|
|
36
|
-
const { settingTab, settingItem,
|
|
36
|
+
const { settingTab, settingItem, bookmarkSelectMode } = store
|
|
37
|
+
// settingSidebarList deep-copies the whole collection; the bookmarks tab
|
|
38
|
+
// ignores `list` (it renders from treeProps), so skip it there — with
|
|
39
|
+
// thousands of bookmarks that copy is the most expensive part of the render
|
|
40
|
+
const list = settingTab === settingMap.bookmarks
|
|
41
|
+
? []
|
|
42
|
+
: store.settingSidebarList
|
|
37
43
|
const props0 = {
|
|
38
44
|
store,
|
|
39
45
|
activeItemId: settingItem.id,
|
|
40
46
|
type: settingTab,
|
|
41
47
|
onClickItem: selectItem,
|
|
42
48
|
shouldConfirmDel: tabsShouldConfirmDel.includes(settingTab),
|
|
43
|
-
list
|
|
49
|
+
list
|
|
44
50
|
}
|
|
45
51
|
const { bookmarks, bookmarkGroups, widgetInstances } = store
|
|
46
52
|
const formProps = {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
BookOutlined
|
|
8
8
|
} from '@ant-design/icons'
|
|
9
9
|
import HelpIcon from '../common/help-icon'
|
|
10
|
+
import buildMap from '../../common/build-map'
|
|
10
11
|
|
|
11
12
|
const e = window.translate
|
|
12
13
|
const { SHOW_CHILD } = TreeSelect
|
|
@@ -21,22 +22,10 @@ function BookmarkSelect (props) {
|
|
|
21
22
|
|
|
22
23
|
const buildData = () => {
|
|
23
24
|
const cats = bookmarkGroups
|
|
24
|
-
const tree = bookmarks
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
...p,
|
|
28
|
-
[k.id]: k
|
|
29
|
-
}
|
|
30
|
-
}, {})
|
|
31
|
-
const btree = cats
|
|
32
|
-
.reduce((p, k) => {
|
|
33
|
-
return {
|
|
34
|
-
...p,
|
|
35
|
-
[k.id]: k
|
|
36
|
-
}
|
|
37
|
-
}, {})
|
|
25
|
+
const tree = buildMap(bookmarks)
|
|
26
|
+
const btree = buildMap(cats)
|
|
38
27
|
function buildSubCats (id) {
|
|
39
|
-
const x = btree
|
|
28
|
+
const x = btree.get(id)
|
|
40
29
|
if (!x) {
|
|
41
30
|
return ''
|
|
42
31
|
}
|
|
@@ -55,7 +44,7 @@ function BookmarkSelect (props) {
|
|
|
55
44
|
return y
|
|
56
45
|
}
|
|
57
46
|
function buildLeaf (id) {
|
|
58
|
-
const x = tree
|
|
47
|
+
const x = tree.get(id)
|
|
59
48
|
if (!x) {
|
|
60
49
|
return ''
|
|
61
50
|
}
|
|
@@ -44,12 +44,52 @@ export default class SpiceSession extends PureComponent {
|
|
|
44
44
|
|
|
45
45
|
componentDidMount () {
|
|
46
46
|
this.remoteInit()
|
|
47
|
+
this.attachMouseScaleFix()
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
componentWillUnmount () {
|
|
51
|
+
this.detachMouseScaleFix()
|
|
50
52
|
this.cleanup()
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
// The spice client sends event.offsetX/offsetY to the remote as-is, but when
|
|
56
|
+
// the canvas is CSS-scaled (scaleViewport or the lib's maxWidth/maxHeight),
|
|
57
|
+
// those are display pixels instead of remote surface pixels. On high
|
|
58
|
+
// resolution remote screens the pointer then only reaches the top-left
|
|
59
|
+
// region. Convert display coords to surface coords (the canvas box is
|
|
60
|
+
// letterboxed by object-fit: contain) before the lib's handlers run.
|
|
61
|
+
handleMouseScaleFix = (e) => {
|
|
62
|
+
const canvas = this.domRef.current?.querySelector('canvas')
|
|
63
|
+
if (!canvas || !canvas.width || !canvas.height) return
|
|
64
|
+
const rect = canvas.getBoundingClientRect()
|
|
65
|
+
if (!rect.width || !rect.height) return
|
|
66
|
+
const scale = Math.min(rect.width / canvas.width, rect.height / canvas.height)
|
|
67
|
+
const x = (e.clientX - rect.left - (rect.width - canvas.width * scale) / 2) / scale
|
|
68
|
+
const y = (e.clientY - rect.top - (rect.height - canvas.height * scale) / 2) / scale
|
|
69
|
+
Object.defineProperty(e, 'offsetX', {
|
|
70
|
+
value: Math.max(0, Math.min(x, canvas.width - 1))
|
|
71
|
+
})
|
|
72
|
+
Object.defineProperty(e, 'offsetY', {
|
|
73
|
+
value: Math.max(0, Math.min(y, canvas.height - 1))
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
attachMouseScaleFix = () => {
|
|
78
|
+
this.domRef.current?.addEventListener(
|
|
79
|
+
'mousemove',
|
|
80
|
+
this.handleMouseScaleFix,
|
|
81
|
+
true
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
detachMouseScaleFix = () => {
|
|
86
|
+
this.domRef.current?.removeEventListener(
|
|
87
|
+
'mousemove',
|
|
88
|
+
this.handleMouseScaleFix,
|
|
89
|
+
true
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
53
93
|
cleanup = () => {
|
|
54
94
|
if (this.spiceConn) {
|
|
55
95
|
try {
|
|
@@ -31,7 +31,9 @@ const ShortcutBar = lazy(() => import('./shortcut-bar'))
|
|
|
31
31
|
// minimum height (px) the visual viewport must lose for us to treat it as
|
|
32
32
|
// "the soft keyboard is now covering part of the screen". A soft keyboard is
|
|
33
33
|
// typically ≥150px; a browser address-bar show/hide is well under this.
|
|
34
|
-
|
|
34
|
+
// Shared with shortcut-bar.jsx, which uses it to decide how far to lift the
|
|
35
|
+
// bar above a keyboard that overlays the page (iOS / HarmonyOS).
|
|
36
|
+
export const KEYBOARD_MIN = 120
|
|
35
37
|
|
|
36
38
|
// kept as a coarse capability fallback for engines without visualViewport.
|
|
37
39
|
// Modern Chromium (Electron) always exposes window.visualViewport, so this is
|
|
@@ -87,7 +89,7 @@ export default function ShortcutBarEntry (props) {
|
|
|
87
89
|
// ignore viewport changes that also changed width (rotation / split) —
|
|
88
90
|
// a keyboard changes height only.
|
|
89
91
|
const sameWidth = Math.abs(vv.width - baseline.width) < 20
|
|
90
|
-
if (sameWidth && baseline.height - vv.height >
|
|
92
|
+
if (sameWidth && baseline.height - vv.height > KEYBOARD_MIN) {
|
|
91
93
|
setKeyboardSeen(true)
|
|
92
94
|
// The user is focused in a terminal with the keyboard up — they need
|
|
93
95
|
// the bar right now. Set visibility here rather than waiting for the
|
|
@@ -10,18 +10,22 @@
|
|
|
10
10
|
* - first (fixed) button collapses the bar
|
|
11
11
|
* - second (fixed) button opens the edit modal (shortcut-bar-edit.jsx)
|
|
12
12
|
* - tapping a shortcut sends it to the active terminal via runQuickCommand
|
|
13
|
+
* - while the system input panel overlays the page without resizing the
|
|
14
|
+
* layout viewport (iOS / HarmonyOS do this, Android resizes instead), the
|
|
15
|
+
* bar lifts itself above the panel — see the visualViewport effect below
|
|
13
16
|
*
|
|
14
17
|
* Only loaded on touch devices — see shortcut-bar-entry.jsx.
|
|
15
18
|
* Data + persistence live in shortcut-bar-defs.js.
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
import { auto } from 'manate/react'
|
|
19
|
-
import { useEffect, useState } from 'react'
|
|
22
|
+
import { useEffect, useRef, useState } from 'react'
|
|
20
23
|
import { createPortal } from 'react-dom'
|
|
21
24
|
import { DownOutlined, EditOutlined } from '@ant-design/icons'
|
|
22
25
|
import { refs } from '../common/ref'
|
|
23
26
|
import { shortcutBarHeight } from '../../common/constants'
|
|
24
27
|
import ShortcutBarEdit from './shortcut-bar-edit'
|
|
28
|
+
import { KEYBOARD_MIN } from './shortcut-bar-entry'
|
|
25
29
|
import { candidates, loadActive, saveActive, ESC } from './shortcut-bar-defs'
|
|
26
30
|
import './shortcut-bar.styl'
|
|
27
31
|
|
|
@@ -89,6 +93,55 @@ function ShortcutBar (props) {
|
|
|
89
93
|
}
|
|
90
94
|
}, [])
|
|
91
95
|
|
|
96
|
+
// On Android the soft keyboard resizes the layout viewport, so a
|
|
97
|
+
// `position: fixed; bottom: 0` bar is pushed up automatically. iOS and
|
|
98
|
+
// HarmonyOS instead keep the layout viewport full-size and let the keyboard
|
|
99
|
+
// overlay it — there the bar sits underneath the keyboard. visualViewport
|
|
100
|
+
// always tracks the *visible* area on every platform, so the gap between it
|
|
101
|
+
// and the layout viewport is exactly the covered height: lift the bar by
|
|
102
|
+
// that much so it rides just above the keyboard.
|
|
103
|
+
const [kbOffset, setKbOffset] = useState(0)
|
|
104
|
+
const kbBaselineRef = useRef({ height: 0, width: 0 })
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const vv = window.visualViewport
|
|
108
|
+
if (!vv) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
// while no keyboard is up, layout viewport == visual viewport (offsetTop
|
|
112
|
+
// 0); keep refreshing the baseline so a later comparison is accurate.
|
|
113
|
+
function sync () {
|
|
114
|
+
if (vv.offsetTop <= 1) {
|
|
115
|
+
kbBaselineRef.current = { height: window.innerHeight, width: vv.width }
|
|
116
|
+
}
|
|
117
|
+
const { height, width } = kbBaselineRef.current
|
|
118
|
+
// a keyboard changes height only — ignore rotation / pinch-zoom, which
|
|
119
|
+
// change width too.
|
|
120
|
+
const sameWidth = Math.abs(vv.width - width) < 20
|
|
121
|
+
const covered = sameWidth ? height - vv.height - vv.offsetTop : 0
|
|
122
|
+
setKbOffset(covered > KEYBOARD_MIN ? Math.ceil(covered) : 0)
|
|
123
|
+
}
|
|
124
|
+
sync()
|
|
125
|
+
vv.addEventListener('resize', sync)
|
|
126
|
+
vv.addEventListener('scroll', sync)
|
|
127
|
+
window.addEventListener('resize', sync)
|
|
128
|
+
return () => {
|
|
129
|
+
vv.removeEventListener('resize', sync)
|
|
130
|
+
vv.removeEventListener('scroll', sync)
|
|
131
|
+
window.removeEventListener('resize', sync)
|
|
132
|
+
}
|
|
133
|
+
}, [])
|
|
134
|
+
|
|
135
|
+
// keep the offset in a CSS var consumed by shortcut-bar.styl, and expose the
|
|
136
|
+
// lifted height so layout.jsx can shrink the terminal accordingly.
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
document.documentElement.style.setProperty(
|
|
139
|
+
'--shortcut-bar-kb-offset',
|
|
140
|
+
kbOffset ? kbOffset + 'px' : '0px'
|
|
141
|
+
)
|
|
142
|
+
store.shortcutBarKbOffset = kbOffset
|
|
143
|
+
}, [kbOffset])
|
|
144
|
+
|
|
92
145
|
// reserve layout space + lift the footer while the bar is shown.
|
|
93
146
|
// expose the bar height as a CSS var so the .styl + footer-lift rule stay
|
|
94
147
|
// in sync with the shortcutBarHeight constant consumed by layout.jsx.
|
|
@@ -135,7 +188,11 @@ function ShortcutBar (props) {
|
|
|
135
188
|
}
|
|
136
189
|
|
|
137
190
|
return createPortal(
|
|
138
|
-
<div
|
|
191
|
+
<div
|
|
192
|
+
className='shortcut-bar'
|
|
193
|
+
role='toolbar'
|
|
194
|
+
style={kbOffset ? { bottom: kbOffset + 'px' } : undefined}
|
|
195
|
+
>
|
|
139
196
|
<div className='shortcut-bar-fixed'>
|
|
140
197
|
<button
|
|
141
198
|
type='button'
|
|
@@ -73,10 +73,12 @@
|
|
|
73
73
|
color var(--text-light)
|
|
74
74
|
|
|
75
75
|
// while the bar is visible, lift the footer + reserve space so nothing is
|
|
76
|
-
// covered (terminal area is shrunk by layout.jsx via store.shortcutBarVisible
|
|
76
|
+
// covered (terminal area is shrunk by layout.jsx via store.shortcutBarVisible;
|
|
77
|
+
// --shortcut-bar-kb-offset raises both bar + footer above a page-overlaying
|
|
78
|
+
// system input panel on iOS / HarmonyOS)
|
|
77
79
|
.shortcut-bar-on
|
|
78
80
|
.main-footer
|
|
79
|
-
bottom var(--shortcut-bar-h, 44px)
|
|
81
|
+
bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
|
|
80
82
|
|
|
81
83
|
// --- edit modal -----------------------------------------------------------
|
|
82
84
|
|
|
@@ -57,7 +57,8 @@ function parseHexColor (color) {
|
|
|
57
57
|
return [
|
|
58
58
|
parseInt(expanded.slice(0, 2), 16),
|
|
59
59
|
parseInt(expanded.slice(2, 4), 16),
|
|
60
|
-
parseInt(expanded.slice(4, 6), 16)
|
|
60
|
+
parseInt(expanded.slice(4, 6), 16),
|
|
61
|
+
alpha
|
|
61
62
|
]
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -82,10 +83,15 @@ function parseRgbColor (color) {
|
|
|
82
83
|
if (alpha === null || alpha <= 0) {
|
|
83
84
|
return null
|
|
84
85
|
}
|
|
85
|
-
return channels
|
|
86
|
+
return [...channels, alpha]
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
export function parseColorToRgb (color) {
|
|
90
|
+
const rgba = parseColorToRgba(color)
|
|
91
|
+
return rgba ? rgba.slice(0, 3) : null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function parseColorToRgba (color) {
|
|
89
95
|
if (typeof color !== 'string') {
|
|
90
96
|
return null
|
|
91
97
|
}
|
|
@@ -105,46 +111,45 @@ export function colorToOscRgb (color) {
|
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
/**
|
|
108
|
-
* Blend a (possibly translucent) selection
|
|
114
|
+
* Blend a (possibly translucent) selection colour over an opaque background,
|
|
109
115
|
* producing the colour in xterm's internal format ({ css, rgba } where
|
|
110
116
|
* rgba = r<<24 | g<<16 | b<<8 | a).
|
|
111
117
|
*
|
|
112
118
|
* xterm derives its DOM selection colour as blend(theme.background,
|
|
113
|
-
* selectionBackground)
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
119
|
+
* selectionBackground) into `selectionBackgroundOpaque`, and separately forces
|
|
120
|
+
* any opaque selectionBackground down to 0.3 alpha in
|
|
121
|
+
* `selectionBackgroundTransparent` (xterm#2737). electerm renders the terminal
|
|
122
|
+
* background via CSS and hands xterm a transparent background, so the blend
|
|
123
|
+
* must be redone over the real visible background — starting from the
|
|
124
|
+
* *configured* colour so an opaque selection stays opaque (xterm's forced
|
|
125
|
+
* 0.3-alpha variant would make every selection translucent).
|
|
117
126
|
*
|
|
118
|
-
* `selectionColor` is
|
|
119
|
-
*
|
|
120
|
-
*
|
|
127
|
+
* `selectionColor` is a CSS colour string from the terminal theme, e.g.
|
|
128
|
+
* themeConfig.selectionBackground; `visibleBackground` is an opaque colour
|
|
129
|
+
* string.
|
|
121
130
|
*/
|
|
122
131
|
export function blendSelectionOverBackground (visibleBackground, selectionColor) {
|
|
123
|
-
const
|
|
124
|
-
const sel = selectionColor
|
|
125
|
-
if (!
|
|
126
|
-
return null
|
|
127
|
-
}
|
|
128
|
-
const [br, bg, bb] = bgRgb
|
|
129
|
-
const a = (sel & 255) / 255
|
|
130
|
-
if (a <= 0) {
|
|
132
|
+
const bg = parseColorToRgb(visibleBackground)
|
|
133
|
+
const sel = parseColorToRgba(selectionColor)
|
|
134
|
+
if (!bg || !sel) {
|
|
131
135
|
return null
|
|
132
136
|
}
|
|
137
|
+
const [br, bgg, bb] = bg
|
|
138
|
+
const [sr, sg, sb, sa] = sel
|
|
133
139
|
const toCss = (r, g, b, alpha) =>
|
|
134
140
|
`#${[r, g, b, alpha].map(toHexByte).join('')}`
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (a >= 1) {
|
|
141
|
+
if (sa >= 1) {
|
|
142
|
+
// Fully opaque: use the configured colour as-is, just normalised to
|
|
143
|
+
// 8-digit hex so both the overlay and per-cell inline styles agree.
|
|
139
144
|
return {
|
|
140
145
|
css: toCss(sr, sg, sb, 255),
|
|
141
|
-
rgba: (
|
|
146
|
+
rgba: (sr << 24 | sg << 16 | sb << 8 | 255) >>> 0
|
|
142
147
|
}
|
|
143
148
|
}
|
|
144
|
-
const r = br + Math.round((sr - br) *
|
|
145
|
-
const g =
|
|
146
|
-
const b = bb + Math.round((sb - bb) *
|
|
147
|
-
const alpha = Math.round(
|
|
149
|
+
const r = br + Math.round((sr - br) * sa)
|
|
150
|
+
const g = bgg + Math.round((sg - bgg) * sa)
|
|
151
|
+
const b = bb + Math.round((sb - bb) * sa)
|
|
152
|
+
const alpha = Math.round(sa * 255)
|
|
148
153
|
return {
|
|
149
154
|
css: toCss(r, g, b, alpha),
|
|
150
155
|
rgba: (r << 24 | g << 16 | b << 8 | alpha) >>> 0
|
|
@@ -1474,31 +1474,35 @@ class Term extends Component {
|
|
|
1474
1474
|
|
|
1475
1475
|
/**
|
|
1476
1476
|
* xterm computes its DOM selection colour as blend(theme.background,
|
|
1477
|
-
* selectionBackground). The
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
1477
|
+
* selectionBackground). The terminal background is forced transparent
|
|
1478
|
+
* (rgba(0,0,0,0)) so electerm's own CSS background / image shows through,
|
|
1479
|
+
* which makes the blend run over transparent-black. Recompute the selection
|
|
1480
|
+
* colours over the real visible background, from the *configured* theme
|
|
1481
|
+
* colours — xterm's internal selectionBackgroundTransparent forces opaque
|
|
1482
|
+
* colours down to 0.3 alpha (xterm#2737), which would make every selection
|
|
1483
|
+
* translucent even when the user configured an opaque one.
|
|
1483
1484
|
*/
|
|
1484
1485
|
fixSelectionColors = (term) => {
|
|
1486
|
+
const themeConfig = this.props.themeConfig || {}
|
|
1485
1487
|
const themeService = term?._core?._themeService
|
|
1486
1488
|
if (!themeService?.modifyColors) {
|
|
1487
1489
|
return
|
|
1488
1490
|
}
|
|
1489
1491
|
const visibleBackground = this.getVisibleTerminalBackground()
|
|
1490
|
-
|
|
1492
|
+
// xterm's DEFAULT_SELECTION, used when the theme omits a selection color
|
|
1493
|
+
const selectionFallback = 'rgba(255, 255, 255, 0.3)'
|
|
1494
|
+
const inactiveFallback = themeConfig.selectionBackground || selectionFallback
|
|
1491
1495
|
themeService.modifyColors((c) => {
|
|
1492
1496
|
const active = blendSelectionOverBackground(
|
|
1493
1497
|
visibleBackground,
|
|
1494
|
-
|
|
1498
|
+
themeConfig.selectionBackground || selectionFallback
|
|
1495
1499
|
)
|
|
1496
1500
|
if (active) {
|
|
1497
1501
|
c.selectionBackgroundOpaque = active
|
|
1498
1502
|
}
|
|
1499
1503
|
const inactive = blendSelectionOverBackground(
|
|
1500
1504
|
visibleBackground,
|
|
1501
|
-
|
|
1505
|
+
themeConfig.selectionInactiveBackground || inactiveFallback
|
|
1502
1506
|
)
|
|
1503
1507
|
if (inactive) {
|
|
1504
1508
|
c.selectionInactiveBackgroundOpaque = inactive
|
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
} from '@ant-design/icons'
|
|
11
11
|
import { Button, Space } from 'antd'
|
|
12
12
|
import Upload from '../common/upload'
|
|
13
|
+
import { beforeBookmarkUpload } from './bookmark-upload'
|
|
13
14
|
|
|
14
15
|
const e = window.translate
|
|
15
16
|
|
|
16
17
|
export default class BookmarkTransport extends PureComponent {
|
|
17
|
-
beforeUpload =
|
|
18
|
+
beforeUpload = beforeBookmarkUpload
|
|
18
19
|
|
|
19
20
|
handleDownload = () => {}
|
|
20
21
|
|