@electerm/electerm-react 5.1.25 → 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/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/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/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 {
|
|
@@ -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
|
|
|
@@ -8,7 +8,9 @@ import { action } from 'manate'
|
|
|
8
8
|
import uid from '../../common/uid'
|
|
9
9
|
import time from '../../common/time'
|
|
10
10
|
import { fixBookmarks } from '../../common/db-fix'
|
|
11
|
-
import
|
|
11
|
+
import { runImportTask } from '../../common/import-task'
|
|
12
|
+
|
|
13
|
+
const e = window.translate
|
|
12
14
|
|
|
13
15
|
function fixBookmarksId (bookmarks) {
|
|
14
16
|
return bookmarks.map(item => {
|
|
@@ -21,6 +23,7 @@ function fixBookmarksId (bookmarks) {
|
|
|
21
23
|
return item
|
|
22
24
|
}).filter(Boolean)
|
|
23
25
|
}
|
|
26
|
+
|
|
24
27
|
export const bookmarkUpload = action(async (file) => {
|
|
25
28
|
const { store } = window
|
|
26
29
|
const { bookmarks, bookmarkGroups } = store
|
|
@@ -58,50 +61,62 @@ export const bookmarkUpload = action(async (file) => {
|
|
|
58
61
|
|
|
59
62
|
const fixed = fixBookmarks(bookmarks1)
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
await runImportTask({
|
|
65
|
+
title: e('import'),
|
|
66
|
+
batch: 200,
|
|
67
|
+
stopWatchers: ['bookmarks', 'bookmarkGroups'],
|
|
68
|
+
steps: [
|
|
69
|
+
{
|
|
70
|
+
label: e('bookmarks'),
|
|
71
|
+
items: fixed,
|
|
72
|
+
process: (chunk) => {
|
|
73
|
+
chunk.forEach(bg => {
|
|
74
|
+
if (!bmTree.has(bg.id)) {
|
|
75
|
+
store.bookmarks.push(bg)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
label: e('bookmarkCategory'),
|
|
82
|
+
items: bookmarkGroups1,
|
|
83
|
+
process: (chunk) => {
|
|
84
|
+
chunk.forEach(bg => {
|
|
85
|
+
if (!bmgTree.has(bg.id)) {
|
|
86
|
+
store.bookmarkGroups.push(bg)
|
|
87
|
+
} else {
|
|
88
|
+
const bg1 = store.bookmarkGroups.find(
|
|
89
|
+
b => b.id === bg.id
|
|
90
|
+
)
|
|
91
|
+
bg1.bookmarkIds = uniq(
|
|
92
|
+
[
|
|
93
|
+
...(bg1.bookmarkIds || []),
|
|
94
|
+
...(bg.bookmarkIds || [])
|
|
95
|
+
]
|
|
96
|
+
)
|
|
97
|
+
bg1.bookmarkGroupIds = uniq(
|
|
98
|
+
[
|
|
99
|
+
...(bg1.bookmarkGroupIds || []),
|
|
100
|
+
...(bg.bookmarkGroupIds || [])
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
label: e('bookmarks'),
|
|
109
|
+
weight: 1,
|
|
110
|
+
run: () => {
|
|
111
|
+
store.fixBookmarkGroups()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
]
|
|
65
115
|
})
|
|
66
116
|
|
|
67
|
-
bookmarkGroups1.forEach(bg => {
|
|
68
|
-
if (!bmgTree.has(bg.id)) {
|
|
69
|
-
store.bookmarkGroups.push(bg)
|
|
70
|
-
} else {
|
|
71
|
-
const bg1 = store.bookmarkGroups.find(
|
|
72
|
-
b => b.id === bg.id
|
|
73
|
-
)
|
|
74
|
-
bg1.bookmarkIds = uniq(
|
|
75
|
-
[
|
|
76
|
-
...(bg1.bookmarkIds || []),
|
|
77
|
-
...(bg.bookmarkIds || [])
|
|
78
|
-
]
|
|
79
|
-
)
|
|
80
|
-
bg1.bookmarkGroupIds = uniq(
|
|
81
|
-
[
|
|
82
|
-
...(bg1.bookmarkGroupIds || []),
|
|
83
|
-
...(bg.bookmarkGroupIds || [])
|
|
84
|
-
]
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
store.fixBookmarkGroups()
|
|
90
|
-
|
|
91
117
|
return false
|
|
92
118
|
})
|
|
93
119
|
|
|
94
120
|
export async function beforeBookmarkUpload (file) {
|
|
95
|
-
|
|
96
|
-
'bookmarks',
|
|
97
|
-
'bookmarkGroups'
|
|
98
|
-
]
|
|
99
|
-
for (const name of names) {
|
|
100
|
-
window[`watch${name}`].stop()
|
|
101
|
-
}
|
|
102
|
-
bookmarkUpload(file)
|
|
103
|
-
await delay(1000)
|
|
104
|
-
for (const name of names) {
|
|
105
|
-
window[`watch${name}`].start()
|
|
106
|
-
}
|
|
121
|
+
return bookmarkUpload(file)
|
|
107
122
|
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import { memo } from 'react'
|
|
1
2
|
import TreeExpander from './tree-expander'
|
|
2
3
|
import TreeListItem from './tree-list-item'
|
|
3
4
|
import TreeItemOp from './tree-item-op'
|
|
4
5
|
import { treeLevelIndent } from './tree-list-layout'
|
|
5
6
|
import createName from '../../common/create-title'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
// memoized: VirtualTreeList re-renders every scroll frame and re-invokes
|
|
9
|
+
// renderItem for the whole visible window; without memo each row (including
|
|
10
|
+
// the antd Popconfirm/Tooltip in TreeItemOp) re-renders per frame.
|
|
11
|
+
// `expanded` is a boolean (not the expandedKeys array) because the array is
|
|
12
|
+
// mutated in place, so identity-based memo comparison would miss changes.
|
|
13
|
+
function TreeListRow (props) {
|
|
8
14
|
const {
|
|
9
15
|
row,
|
|
10
16
|
keyword,
|
|
11
|
-
|
|
17
|
+
expanded,
|
|
12
18
|
activeItemId,
|
|
13
19
|
searchSelectedRowKey,
|
|
14
20
|
staticList,
|
|
@@ -34,7 +40,6 @@ export default function TreeListRow (props) {
|
|
|
34
40
|
item?.bookmarkIds?.length ||
|
|
35
41
|
item?.bookmarkGroupIds?.length
|
|
36
42
|
)
|
|
37
|
-
const isGroupExpanded = Boolean(keyword) || expandedKeys.includes(item.id)
|
|
38
43
|
const itemProps = {
|
|
39
44
|
item,
|
|
40
45
|
isGroup,
|
|
@@ -94,7 +99,7 @@ export default function TreeListRow (props) {
|
|
|
94
99
|
level={parentId}
|
|
95
100
|
group={item}
|
|
96
101
|
hasChildren={groupHasChildren}
|
|
97
|
-
shouldOpen={
|
|
102
|
+
shouldOpen={expanded}
|
|
98
103
|
onExpand={handleExpand}
|
|
99
104
|
onUnExpand={handleUnExpand}
|
|
100
105
|
/>
|
|
@@ -114,3 +119,5 @@ export default function TreeListRow (props) {
|
|
|
114
119
|
</div>
|
|
115
120
|
)
|
|
116
121
|
}
|
|
122
|
+
|
|
123
|
+
export default memo(TreeListRow)
|
|
@@ -37,7 +37,10 @@ export function buildVisibleTreeRows ({
|
|
|
37
37
|
sortTitle,
|
|
38
38
|
sortHost
|
|
39
39
|
}) {
|
|
40
|
-
const groupTree = bookmarkGroupTree ||
|
|
40
|
+
const groupTree = bookmarkGroupTree || new Map()
|
|
41
|
+
const getGroup = id => groupTree instanceof Map
|
|
42
|
+
? groupTree.get(id)
|
|
43
|
+
: groupTree[id]
|
|
41
44
|
const rows = []
|
|
42
45
|
const matchedRowKeys = []
|
|
43
46
|
const expandedKeySet = new Set(expandedKeys || [])
|
|
@@ -72,7 +75,7 @@ export function buildVisibleTreeRows ({
|
|
|
72
75
|
}
|
|
73
76
|
const hasMatch = (group.bookmarkIds || []).some(bookmarkMatches) ||
|
|
74
77
|
(group.bookmarkGroupIds || []).some(id => {
|
|
75
|
-
return groupHasMatchedBookmarks(
|
|
78
|
+
return groupHasMatchedBookmarks(getGroup(id))
|
|
76
79
|
})
|
|
77
80
|
groupMatchCache.set(group.id, hasMatch)
|
|
78
81
|
return hasMatch
|
|
@@ -100,7 +103,7 @@ export function buildVisibleTreeRows ({
|
|
|
100
103
|
: `#${group.id}`
|
|
101
104
|
|
|
102
105
|
for (const groupId of group.bookmarkGroupIds || []) {
|
|
103
|
-
visitGroup(
|
|
106
|
+
visitGroup(getGroup(groupId), nextParentId, depth + 1)
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
const bookmarkIds = group.bookmarkIds || []
|
|
@@ -740,11 +740,13 @@ export default class ItemListTree extends Component {
|
|
|
740
740
|
}
|
|
741
741
|
|
|
742
742
|
renderVirtualRow = (row, editor) => {
|
|
743
|
+
const { keyword } = this.state
|
|
744
|
+
const { expandedKeys } = this.props
|
|
743
745
|
return (
|
|
744
746
|
<TreeListRow
|
|
745
747
|
row={row}
|
|
746
|
-
keyword={
|
|
747
|
-
|
|
748
|
+
keyword={keyword}
|
|
749
|
+
expanded={Boolean(keyword) || expandedKeys.includes(row.item.id)}
|
|
748
750
|
activeItemId={this.props.activeItemId}
|
|
749
751
|
searchSelectedRowKey={this.state.searchSelectedRowKey}
|
|
750
752
|
staticList={this.props.staticList}
|