@electerm/electerm-react 2.13.0 → 2.15.8
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/components/bg/custom-css.jsx +10 -7
- package/client/components/common/notification.jsx +1 -1
- package/client/components/common/opacity.jsx +8 -6
- package/client/components/main/ui-theme.jsx +10 -6
- package/client/components/profile/profile-list.jsx +1 -1
- package/client/components/quick-commands/quick-commands-list.jsx +1 -1
- package/client/components/rdp/rdp-session.jsx +24 -15
- package/client/components/rdp/rdp.styl +21 -1
- package/client/components/setting-panel/list.styl +7 -0
- package/client/components/setting-sync/setting-sync-form.jsx +10 -5
- package/client/components/sftp/file-item.jsx +1 -0
- package/client/components/sftp/file-read.js +58 -3
- package/client/components/spice/spice-session.jsx +9 -6
- package/client/components/spice/spice.styl +27 -9
- package/client/components/ssh-config/load-ssh-configs-item.jsx +99 -0
- package/client/components/ssh-config/load-ssh-configs.jsx +38 -9
- package/client/components/ssh-config/ssh-config.styl +3 -0
- package/client/components/terminal/transfer-client-base.js +28 -0
- package/client/components/terminal/trzsz-client.js +8 -10
- package/client/components/terminal/zmodem-client.js +8 -10
- package/client/components/text-editor/simple-editor.jsx +38 -6
- package/client/components/tree-list/bookmark-toolbar.jsx +6 -2
- package/client/components/tree-list/tree-list.jsx +0 -5
- package/client/components/vnc/vnc-session.jsx +9 -5
- package/client/components/vnc/vnc.styl +17 -0
- package/client/store/bookmark.js +3 -11
- package/client/store/sync.js +2 -3
- package/package.json +1 -1
- package/client/components/ssh-config/ssh-config-item.jsx +0 -24
|
@@ -2,16 +2,14 @@
|
|
|
2
2
|
* ui theme
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useEffect } from 'react'
|
|
6
|
-
import { useDelta, useConditionalEffect } from 'react-delta-hooks'
|
|
5
|
+
import { useEffect, useRef } from 'react'
|
|
7
6
|
import eq from 'fast-deep-equal'
|
|
8
7
|
|
|
9
8
|
const themeDomId = 'custom-css'
|
|
10
9
|
|
|
11
10
|
export default function CustomCss (props) {
|
|
12
11
|
const { customCss } = props
|
|
13
|
-
|
|
14
|
-
const delta = useDelta(customCss)
|
|
12
|
+
const prevRef = useRef(null)
|
|
15
13
|
|
|
16
14
|
async function applyTheme () {
|
|
17
15
|
const style = document.getElementById(themeDomId)
|
|
@@ -21,8 +19,13 @@ export default function CustomCss (props) {
|
|
|
21
19
|
useEffect(() => {
|
|
22
20
|
applyTheme()
|
|
23
21
|
}, [])
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (prevRef.current && !eq(prevRef.current, customCss)) {
|
|
25
|
+
applyTheme()
|
|
26
|
+
}
|
|
27
|
+
prevRef.current = customCss
|
|
28
|
+
}, [customCss])
|
|
29
|
+
|
|
27
30
|
return null
|
|
28
31
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
2
|
-
import { useDelta, useConditionalEffect } from 'react-delta-hooks'
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
3
2
|
import eq from 'fast-deep-equal'
|
|
4
3
|
|
|
5
4
|
const opacityDomId = 'opacity-style'
|
|
@@ -14,7 +13,7 @@ const opacityDomId = 'opacity-style'
|
|
|
14
13
|
export default function Opacity ({ opacity }) {
|
|
15
14
|
// Default to 1 if opacity is not provided
|
|
16
15
|
const currentOpacity = opacity !== undefined ? opacity : 1
|
|
17
|
-
const
|
|
16
|
+
const prevRef = useRef(null)
|
|
18
17
|
|
|
19
18
|
function applyOpacity () {
|
|
20
19
|
let styleElement = document.getElementById(opacityDomId)
|
|
@@ -58,9 +57,12 @@ export default function Opacity ({ opacity }) {
|
|
|
58
57
|
}
|
|
59
58
|
}, [])
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (prevRef.current && !eq(prevRef.current, currentOpacity)) {
|
|
62
|
+
applyOpacity()
|
|
63
|
+
}
|
|
64
|
+
prevRef.current = currentOpacity
|
|
65
|
+
}, [currentOpacity])
|
|
64
66
|
|
|
65
67
|
return null
|
|
66
68
|
}
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* ui theme
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useEffect } from 'react'
|
|
6
|
-
import { useDelta, useConditionalEffect } from 'react-delta-hooks'
|
|
5
|
+
import { useEffect, useRef } from 'react'
|
|
7
6
|
import eq from 'fast-deep-equal'
|
|
8
7
|
import isColorDark from '../../common/is-color-dark'
|
|
9
8
|
|
|
@@ -53,7 +52,7 @@ function buildTheme (themeConfig) {
|
|
|
53
52
|
export default function UiTheme (props) {
|
|
54
53
|
const { themeConfig } = props
|
|
55
54
|
|
|
56
|
-
const
|
|
55
|
+
const prevRef = useRef(null)
|
|
57
56
|
|
|
58
57
|
async function applyTheme () {
|
|
59
58
|
const style = document.getElementById(themeDomId)
|
|
@@ -64,8 +63,13 @@ export default function UiTheme (props) {
|
|
|
64
63
|
useEffect(() => {
|
|
65
64
|
applyTheme()
|
|
66
65
|
}, [])
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (prevRef.current && !eq(prevRef.current, themeConfig)) {
|
|
69
|
+
applyTheme()
|
|
70
|
+
}
|
|
71
|
+
prevRef.current = themeConfig
|
|
72
|
+
}, [themeConfig])
|
|
73
|
+
|
|
70
74
|
return null
|
|
71
75
|
}
|
|
@@ -601,35 +601,44 @@ export default class RdpSession extends PureComponent {
|
|
|
601
601
|
|
|
602
602
|
render () {
|
|
603
603
|
const { width: w, height: h } = this.props
|
|
604
|
-
const rdpProps = {
|
|
605
|
-
style: {
|
|
606
|
-
width: w + 'px',
|
|
607
|
-
height: h + 'px'
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
604
|
const { width, height, loading, scaleViewport } = this.state
|
|
605
|
+
const innerWidth = w - 10
|
|
606
|
+
const innerHeight = h - 80
|
|
607
|
+
const wrapperStyle = {
|
|
608
|
+
width: innerWidth + 'px',
|
|
609
|
+
height: innerHeight + 'px',
|
|
610
|
+
overflow: scaleViewport ? 'hidden' : 'auto'
|
|
611
|
+
}
|
|
611
612
|
const canvasProps = {
|
|
612
613
|
width,
|
|
613
614
|
height,
|
|
614
615
|
tabIndex: 0
|
|
615
616
|
}
|
|
616
|
-
if (scaleViewport) {
|
|
617
|
-
canvasProps.className = 'scale-viewport'
|
|
618
|
-
}
|
|
619
617
|
const cls = `rdp-session-wrap session-v-wrap${scaleViewport ? ' scale-viewport' : ''}`
|
|
618
|
+
const sessProps = {
|
|
619
|
+
className: cls,
|
|
620
|
+
style: {
|
|
621
|
+
width: w + 'px',
|
|
622
|
+
height: h + 'px'
|
|
623
|
+
}
|
|
624
|
+
}
|
|
620
625
|
const controlProps = this.getControlProps()
|
|
621
626
|
return (
|
|
622
627
|
<Spin spinning={loading}>
|
|
623
628
|
<div
|
|
624
|
-
{...
|
|
625
|
-
className={cls}
|
|
629
|
+
{...sessProps}
|
|
626
630
|
>
|
|
627
631
|
{this.renderControl()}
|
|
628
|
-
<canvas
|
|
629
|
-
{...canvasProps}
|
|
630
|
-
ref={this.canvasRef}
|
|
631
|
-
/>
|
|
632
632
|
<RemoteFloatControl {...controlProps} />
|
|
633
|
+
<div
|
|
634
|
+
style={wrapperStyle}
|
|
635
|
+
className='rdp-scroll-wrapper s-scroll-wrapper'
|
|
636
|
+
>
|
|
637
|
+
<canvas
|
|
638
|
+
{...canvasProps}
|
|
639
|
+
ref={this.canvasRef}
|
|
640
|
+
/>
|
|
641
|
+
</div>
|
|
633
642
|
</div>
|
|
634
643
|
</Spin>
|
|
635
644
|
)
|
|
@@ -9,7 +9,27 @@
|
|
|
9
9
|
left: 0
|
|
10
10
|
width: 100%
|
|
11
11
|
&.scale-viewport
|
|
12
|
-
canvas
|
|
12
|
+
.rdp-scroll-wrapper canvas
|
|
13
13
|
width: 100% !important
|
|
14
|
+
height: 100% !important
|
|
14
15
|
object-fit: contain
|
|
15
16
|
|
|
17
|
+
.s-scroll-wrapper
|
|
18
|
+
&::-webkit-scrollbar
|
|
19
|
+
width 16px
|
|
20
|
+
height 16px
|
|
21
|
+
background var(--main-darker)
|
|
22
|
+
&::-webkit-scrollbar-track
|
|
23
|
+
background var(--main-darker)
|
|
24
|
+
box-shadow inset 0 0 5px var(--main-darker)
|
|
25
|
+
&::-webkit-scrollbar-thumb
|
|
26
|
+
background var(--primary)
|
|
27
|
+
border-radius 0
|
|
28
|
+
&::-webkit-scrollbar-corner
|
|
29
|
+
background var(--main-darker)
|
|
30
|
+
.rdp-scroll-wrapper
|
|
31
|
+
position relative
|
|
32
|
+
background var(--main)
|
|
33
|
+
z-index 299
|
|
34
|
+
|
|
35
|
+
|
|
@@ -36,6 +36,13 @@
|
|
|
36
36
|
.list-item-remove
|
|
37
37
|
.list-item-bookmark
|
|
38
38
|
display block
|
|
39
|
+
.theme-item:hover
|
|
40
|
+
.list-item-remove
|
|
41
|
+
right 24px
|
|
42
|
+
.setting-tabs-setting
|
|
43
|
+
.item-list-unit
|
|
44
|
+
.list-item-remove
|
|
45
|
+
display none
|
|
39
46
|
// .item-list
|
|
40
47
|
// .list-item-edit
|
|
41
48
|
// .list-item-apply
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* bookmark form
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { useEffect, useRef } from 'react'
|
|
9
9
|
import { ArrowDownOutlined, ArrowUpOutlined, SaveOutlined, ClearOutlined } from '@ant-design/icons'
|
|
10
10
|
import { Button, Input, Form, Alert } from 'antd'
|
|
11
11
|
import { notification } from '../common/notification'
|
|
@@ -27,10 +27,15 @@ function trim (str) {
|
|
|
27
27
|
|
|
28
28
|
export default function SyncForm (props) {
|
|
29
29
|
const [form] = Form.useForm()
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const prevRef = useRef(null)
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (prevRef.current && !eq(prevRef.current, props.formData)) {
|
|
34
|
+
form.resetFields()
|
|
35
|
+
}
|
|
36
|
+
prevRef.current = props.formData
|
|
37
|
+
}, [props.formData])
|
|
38
|
+
|
|
34
39
|
const { syncType } = props
|
|
35
40
|
function disabled () {
|
|
36
41
|
if (syncType === syncTypes.cloud) {
|
|
@@ -22,6 +22,61 @@ export const getFileExt = fileName => {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
const modeDirectoryMask = 0o170000
|
|
26
|
+
const modeDirectoryValue = 0o040000
|
|
27
|
+
|
|
28
|
+
const toIsDirectory = (stat) => {
|
|
29
|
+
if (!stat) {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof stat.isDirectory === 'function') {
|
|
34
|
+
return stat.isDirectory()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof stat.isDirectory === 'boolean') {
|
|
38
|
+
return stat.isDirectory
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof stat.type === 'string') {
|
|
42
|
+
return stat.type === 'd'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (typeof stat.type === 'number') {
|
|
46
|
+
return stat.type === 2
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof stat.mode === 'number') {
|
|
50
|
+
return (stat.mode & modeDirectoryMask) === modeDirectoryValue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const toIsSymbolicLink = (stat) => {
|
|
57
|
+
if (!stat) {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof stat.isSymbolicLink === 'function') {
|
|
62
|
+
return stat.isSymbolicLink()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof stat.isSymbolicLink === 'boolean') {
|
|
66
|
+
return stat.isSymbolicLink
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof stat.type === 'string') {
|
|
70
|
+
return stat.type === 'l'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof stat.type === 'number') {
|
|
74
|
+
return stat.type === 3
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
25
80
|
export const getFolderFromFilePath = (filePath, isRemote) => {
|
|
26
81
|
const sep = isRemote ? '/' : window.pre.sep
|
|
27
82
|
const arr = filePath.split(sep)
|
|
@@ -54,8 +109,8 @@ export const getLocalFileInfo = async (filePath) => {
|
|
|
54
109
|
type: 'local',
|
|
55
110
|
...getFolderFromFilePath(filePath, false),
|
|
56
111
|
id: generate(),
|
|
57
|
-
isDirectory: statr
|
|
58
|
-
isSymbolicLink: stat
|
|
112
|
+
isDirectory: toIsDirectory(statr),
|
|
113
|
+
isSymbolicLink: toIsSymbolicLink(stat)
|
|
59
114
|
}
|
|
60
115
|
}
|
|
61
116
|
|
|
@@ -71,6 +126,6 @@ export const getRemoteFileInfo = async (sftp, filePath) => {
|
|
|
71
126
|
type: 'remote',
|
|
72
127
|
...getFolderFromFilePath(filePath, true),
|
|
73
128
|
id: generate(),
|
|
74
|
-
isDirectory: stat
|
|
129
|
+
isDirectory: toIsDirectory(stat)
|
|
75
130
|
}
|
|
76
131
|
}
|
|
@@ -269,20 +269,23 @@ export default class SpiceSession extends PureComponent {
|
|
|
269
269
|
}
|
|
270
270
|
const cls = `spice-session-wrap session-v-wrap${scaleViewport ? ' scale-viewport' : ''}`
|
|
271
271
|
const contrlProps = this.getControlProps()
|
|
272
|
+
const sessProps = {
|
|
273
|
+
className: cls,
|
|
274
|
+
style: {
|
|
275
|
+
width: w + 'px',
|
|
276
|
+
height: h + 'px'
|
|
277
|
+
}
|
|
278
|
+
}
|
|
272
279
|
return (
|
|
273
280
|
<Spin spinning={loading}>
|
|
274
281
|
<div
|
|
275
|
-
|
|
276
|
-
style={{
|
|
277
|
-
width: w + 'px',
|
|
278
|
-
height: h + 'px'
|
|
279
|
-
}}
|
|
282
|
+
{...sessProps}
|
|
280
283
|
>
|
|
281
284
|
{this.renderControl()}
|
|
282
285
|
<RemoteFloatControl {...contrlProps} />
|
|
283
286
|
<div
|
|
284
287
|
style={wrapperStyle}
|
|
285
|
-
className='spice-scroll-wrapper'
|
|
288
|
+
className='spice-scroll-wrapper s-scroll-wrapper'
|
|
286
289
|
>
|
|
287
290
|
<div
|
|
288
291
|
ref={this.domRef}
|
|
@@ -1,11 +1,29 @@
|
|
|
1
|
-
.spice-session-wrap
|
|
2
|
-
canvas
|
|
3
|
-
width: 100% !important
|
|
4
|
-
object-fit: contain
|
|
5
|
-
.spice-scroll-wrapper
|
|
6
|
-
display block
|
|
7
|
-
.spice-scroll-wrapper
|
|
1
|
+
.spice-session-wrap
|
|
8
2
|
display: flex
|
|
9
3
|
flex-direction: column
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
align-items: center
|
|
5
|
+
.session-v-info
|
|
6
|
+
position: relative
|
|
7
|
+
width: 100%
|
|
8
|
+
z-index: 300
|
|
9
|
+
&.scale-viewport
|
|
10
|
+
.spice-scroll-wrapper
|
|
11
|
+
display flex
|
|
12
|
+
align-items center
|
|
13
|
+
justify-content center
|
|
14
|
+
> div
|
|
15
|
+
width 100% !important
|
|
16
|
+
height 100% !important
|
|
17
|
+
display flex
|
|
18
|
+
align-items center
|
|
19
|
+
justify-content center
|
|
20
|
+
canvas
|
|
21
|
+
width 100% !important
|
|
22
|
+
height 100% !important
|
|
23
|
+
max-width 100% !important
|
|
24
|
+
max-height 100% !important
|
|
25
|
+
object-fit contain
|
|
26
|
+
.spice-scroll-wrapper
|
|
27
|
+
position relative
|
|
28
|
+
background var(--main)
|
|
29
|
+
z-index 299
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Input
|
|
4
|
+
} from 'antd'
|
|
5
|
+
import {
|
|
6
|
+
EditOutlined,
|
|
7
|
+
DeleteOutlined,
|
|
8
|
+
CheckOutlined,
|
|
9
|
+
CloseOutlined
|
|
10
|
+
} from '@ant-design/icons'
|
|
11
|
+
|
|
12
|
+
const { TextArea } = Input
|
|
13
|
+
|
|
14
|
+
export default function LoadSshConfigsItem (props) {
|
|
15
|
+
const { item, index, onDelete, onUpdate } = props
|
|
16
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
17
|
+
const [editValue, setEditValue] = useState(JSON.stringify(item, null, 2))
|
|
18
|
+
|
|
19
|
+
const handleToggleEdit = function () {
|
|
20
|
+
if (isEditing) {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(editValue)
|
|
23
|
+
onUpdate(index, parsed)
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error('Invalid JSON:', err)
|
|
26
|
+
setEditValue(JSON.stringify(item, null, 2))
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
setEditValue(JSON.stringify(item, null, 2))
|
|
30
|
+
}
|
|
31
|
+
setIsEditing(!isEditing)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handleDelete = function () {
|
|
35
|
+
onDelete(index)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handleCancelEdit = function () {
|
|
39
|
+
setEditValue(JSON.stringify(item, null, 2))
|
|
40
|
+
setIsEditing(false)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderActions () {
|
|
44
|
+
if (isEditing) {
|
|
45
|
+
return [
|
|
46
|
+
<CheckOutlined
|
|
47
|
+
className='mg1r pointer icon-success'
|
|
48
|
+
onClick={handleToggleEdit}
|
|
49
|
+
key='confirm-ssh-config-item'
|
|
50
|
+
/>,
|
|
51
|
+
<CloseOutlined
|
|
52
|
+
className='mg1r pointer icon-warning'
|
|
53
|
+
onClick={handleCancelEdit}
|
|
54
|
+
key='cancel-ssh-config-item'
|
|
55
|
+
/>
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
return [
|
|
59
|
+
<EditOutlined
|
|
60
|
+
className='mg1r pointer ssh-config-item-edit-icon'
|
|
61
|
+
onClick={handleToggleEdit}
|
|
62
|
+
key='edit-ssh-config-item'
|
|
63
|
+
/>,
|
|
64
|
+
<DeleteOutlined
|
|
65
|
+
className='pointer icon-danger ssh-config-item-delete-icon'
|
|
66
|
+
onClick={handleDelete}
|
|
67
|
+
key='del-ssh-config-item'
|
|
68
|
+
/>
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderContent () {
|
|
73
|
+
if (isEditing) {
|
|
74
|
+
return (
|
|
75
|
+
<TextArea
|
|
76
|
+
value={editValue}
|
|
77
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
78
|
+
rows={10}
|
|
79
|
+
className='mg1t'
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
return (
|
|
84
|
+
<pre className='ssh-config-item-content'>
|
|
85
|
+
{JSON.stringify(item, null, 2)}
|
|
86
|
+
</pre>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className='ssh-config-item pd1'>
|
|
92
|
+
<div className='pd1b ssh-config-item-header'>
|
|
93
|
+
<b className='mg1r'>[{index + 1}]</b>
|
|
94
|
+
{renderActions()}
|
|
95
|
+
</div>
|
|
96
|
+
{renderContent()}
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
@@ -5,18 +5,20 @@ import {
|
|
|
5
5
|
Empty
|
|
6
6
|
} from 'antd'
|
|
7
7
|
import { useState, useEffect } from 'react'
|
|
8
|
-
import SshConfigItem from './ssh-config-item'
|
|
9
8
|
import * as ls from '../../common/safe-local-storage'
|
|
10
9
|
import {
|
|
11
10
|
sshConfigLoadKey
|
|
12
11
|
} from '../../common/constants'
|
|
13
12
|
import { ReloadOutlined } from '@ant-design/icons'
|
|
13
|
+
import LoadSshConfigsItem from './load-ssh-configs-item'
|
|
14
|
+
import './ssh-config.styl'
|
|
14
15
|
|
|
15
16
|
const e = window.translate
|
|
16
17
|
|
|
17
18
|
export default function LoadSshConfigs (props) {
|
|
18
19
|
const [loading, setLoading] = useState(false)
|
|
19
20
|
const { sshConfigs } = props
|
|
21
|
+
const [localConfigs, setLocalConfigs] = useState([])
|
|
20
22
|
|
|
21
23
|
const {
|
|
22
24
|
store
|
|
@@ -24,6 +26,11 @@ export default function LoadSshConfigs (props) {
|
|
|
24
26
|
const {
|
|
25
27
|
showSshConfigModal
|
|
26
28
|
} = props
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setLocalConfigs(sshConfigs)
|
|
32
|
+
}, [sshConfigs])
|
|
33
|
+
|
|
27
34
|
const handleCancel = function () {
|
|
28
35
|
store.showSshConfigModal = false
|
|
29
36
|
}
|
|
@@ -35,21 +42,43 @@ export default function LoadSshConfigs (props) {
|
|
|
35
42
|
|
|
36
43
|
const handleLoadSshConfig = function () {
|
|
37
44
|
store.showSshConfigModal = false
|
|
38
|
-
store.addSshConfigs(
|
|
45
|
+
store.addSshConfigs(localConfigs)
|
|
39
46
|
ls.setItem(sshConfigLoadKey, 'yes')
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
const handleDeleteItem = function (index) {
|
|
50
|
+
const newConfigs = [...localConfigs]
|
|
51
|
+
newConfigs.splice(index, 1)
|
|
52
|
+
setLocalConfigs(newConfigs)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const handleUpdateItem = function (index, newItem) {
|
|
56
|
+
const newConfigs = [...localConfigs]
|
|
57
|
+
newConfigs[index] = newItem
|
|
58
|
+
setLocalConfigs(newConfigs)
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
const renderList = function () {
|
|
43
|
-
if (!
|
|
62
|
+
if (!localConfigs.length) {
|
|
44
63
|
return (
|
|
45
64
|
<Empty />
|
|
46
65
|
)
|
|
47
66
|
}
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
return (
|
|
68
|
+
<div className='pd1b ssh-config-list'>
|
|
69
|
+
{
|
|
70
|
+
localConfigs.map((item, index) => (
|
|
71
|
+
<LoadSshConfigsItem
|
|
72
|
+
key={index}
|
|
73
|
+
item={item}
|
|
74
|
+
index={index}
|
|
75
|
+
onDelete={handleDeleteItem}
|
|
76
|
+
onUpdate={handleUpdateItem}
|
|
77
|
+
/>
|
|
78
|
+
))
|
|
79
|
+
}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
53
82
|
}
|
|
54
83
|
|
|
55
84
|
useEffect(() => {
|
|
@@ -89,7 +118,7 @@ export default function LoadSshConfigs (props) {
|
|
|
89
118
|
type='primary'
|
|
90
119
|
className='mg1r mg1b'
|
|
91
120
|
onClick={handleLoadSshConfig}
|
|
92
|
-
disabled={!
|
|
121
|
+
disabled={!localConfigs.length || loading}
|
|
93
122
|
>
|
|
94
123
|
{e('import')}
|
|
95
124
|
</Button>
|
|
@@ -107,6 +107,34 @@ export class TransferClientBase {
|
|
|
107
107
|
this.writeToTerminal(`\x1b[32m\x1b[1m${this.getProtocolDisplayName()}::${type}::START\x1b[0m\r\n`)
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Write progress bar to terminal
|
|
112
|
+
* @param {Object} options - Progress options
|
|
113
|
+
* @param {string} options.name - File name
|
|
114
|
+
* @param {number} options.size - Total size in bytes
|
|
115
|
+
* @param {number} options.transferred - Transferred bytes
|
|
116
|
+
* @param {number} options.speed - Transfer speed in bytes/s
|
|
117
|
+
* @param {boolean} options.isComplete - Whether transfer is complete
|
|
118
|
+
* @param {Function} options.formatSize - Function to format size
|
|
119
|
+
* @returns {string} The progress string written to terminal
|
|
120
|
+
*/
|
|
121
|
+
writeProgressBar ({ name, size, transferred, speed, isComplete = false, formatSize = (b) => b }) {
|
|
122
|
+
const percent = size > 0 ? Math.floor(transferred * 100 / size) : 100
|
|
123
|
+
const barWidth = 30
|
|
124
|
+
const filledWidth = Math.floor(percent / 100 * barWidth)
|
|
125
|
+
const emptyWidth = barWidth - filledWidth
|
|
126
|
+
|
|
127
|
+
const bar = '\x1b[32m' + '\u2588'.repeat(filledWidth) + '\x1b[90m' + '\u2591'.repeat(emptyWidth) + '\x1b[0m'
|
|
128
|
+
|
|
129
|
+
const sizeStr = `${formatSize(transferred)}/${formatSize(size)}`
|
|
130
|
+
const speedStr = speed > 0 ? `, ${formatSize(speed)}/s` : ''
|
|
131
|
+
const doneStr = isComplete ? ' \x1b[32m\x1b[1m[DONE]\x1b[0m' : ''
|
|
132
|
+
|
|
133
|
+
const str = `\r\x1b[2K\x1b[32m${name}\x1b[0m: ${percent}% ${bar} ${sizeStr}${speedStr}${doneStr}`
|
|
134
|
+
this.writeToTerminal(str + '\r')
|
|
135
|
+
return str
|
|
136
|
+
}
|
|
137
|
+
|
|
110
138
|
/**
|
|
111
139
|
* Get protocol display name
|
|
112
140
|
* Should be overridden by subclass
|
|
@@ -230,20 +230,18 @@ export class TrzszClient extends TransferClientBase {
|
|
|
230
230
|
if (!this.currentTransfer || !this.terminal?.term) return
|
|
231
231
|
|
|
232
232
|
const { name, size, transferred, path, serverSpeed } = this.currentTransfer
|
|
233
|
-
const percent = size > 0 ? Math.floor(transferred * 100 / size) : 100
|
|
234
|
-
|
|
235
|
-
// Use server's speed if available, otherwise calculate locally
|
|
236
233
|
const speed = serverSpeed || 0
|
|
237
|
-
|
|
238
|
-
// Use full path if available, otherwise just name
|
|
239
234
|
const displayName = path || name
|
|
240
|
-
|
|
241
|
-
// filesize expects bytes and formats to human readable
|
|
242
235
|
const formatSize = (bytes) => filesize(bytes)
|
|
243
236
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
237
|
+
this.writeProgressBar({
|
|
238
|
+
name: displayName,
|
|
239
|
+
size,
|
|
240
|
+
transferred,
|
|
241
|
+
speed,
|
|
242
|
+
isComplete,
|
|
243
|
+
formatSize
|
|
244
|
+
})
|
|
247
245
|
}
|
|
248
246
|
|
|
249
247
|
/**
|
|
@@ -212,20 +212,18 @@ export class ZmodemClient extends TransferClientBase {
|
|
|
212
212
|
if (!this.currentTransfer || !this.terminal?.term) return
|
|
213
213
|
|
|
214
214
|
const { name, size, transferred, path, serverSpeed } = this.currentTransfer
|
|
215
|
-
const percent = size > 0 ? Math.floor(transferred * 100 / size) : 100
|
|
216
|
-
|
|
217
|
-
// Use server's speed if available, otherwise calculate locally
|
|
218
215
|
const speed = serverSpeed || 0
|
|
219
|
-
|
|
220
|
-
// Use full path if available, otherwise just name
|
|
221
216
|
const displayName = path || name
|
|
222
|
-
|
|
223
|
-
// filesize expects bytes and formats to human readable
|
|
224
217
|
const formatSize = (bytes) => filesize(bytes)
|
|
225
218
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
219
|
+
this.writeProgressBar({
|
|
220
|
+
name: displayName,
|
|
221
|
+
size,
|
|
222
|
+
transferred,
|
|
223
|
+
speed,
|
|
224
|
+
isComplete,
|
|
225
|
+
formatSize
|
|
226
|
+
})
|
|
229
227
|
}
|
|
230
228
|
}
|
|
231
229
|
|
|
@@ -18,6 +18,11 @@ export default function SimpleEditor (props) {
|
|
|
18
18
|
|
|
19
19
|
// When currentMatch changes, highlight the match in textarea
|
|
20
20
|
useEffect(() => {
|
|
21
|
+
// Only process navigation when explicitly triggered (not when text changes)
|
|
22
|
+
if (!isNavigating) {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
if (currentMatch >= 0 && occurrences.length > 0) {
|
|
22
27
|
const match = occurrences[currentMatch]
|
|
23
28
|
if (editorRef.current) {
|
|
@@ -26,10 +31,8 @@ export default function SimpleEditor (props) {
|
|
|
26
31
|
// Set selection range to select the matched text
|
|
27
32
|
textarea.setSelectionRange(match.start, match.end)
|
|
28
33
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
textarea.focus()
|
|
32
|
-
}
|
|
34
|
+
// Focus the textarea when explicitly navigating between matches
|
|
35
|
+
textarea.focus()
|
|
33
36
|
|
|
34
37
|
// Scroll to the selection position
|
|
35
38
|
// Use setTimeout to ensure the selection is rendered before scrolling
|
|
@@ -53,10 +56,17 @@ export default function SimpleEditor (props) {
|
|
|
53
56
|
setIsNavigating(false)
|
|
54
57
|
}, [currentMatch, occurrences])
|
|
55
58
|
|
|
56
|
-
// Auto-search when keyword changes
|
|
59
|
+
// Auto-search when keyword changes (but not when text is being edited)
|
|
57
60
|
useEffect(() => {
|
|
61
|
+
// Set navigating to true so first match is highlighted when searching
|
|
62
|
+
setIsNavigating(true)
|
|
58
63
|
findMatches()
|
|
59
|
-
}, [searchKeyword
|
|
64
|
+
}, [searchKeyword])
|
|
65
|
+
|
|
66
|
+
// Update matches when text changes, but don't change currentMatch position
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
updateMatchesOnly()
|
|
69
|
+
}, [props.value])
|
|
60
70
|
|
|
61
71
|
// Copy the editor content to clipboard
|
|
62
72
|
const copyEditorContent = () => {
|
|
@@ -87,6 +97,28 @@ export default function SimpleEditor (props) {
|
|
|
87
97
|
setCurrentMatch(matches.length ? 0 : -1)
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
// Update matches only (without changing currentMatch position)
|
|
101
|
+
const updateMatchesOnly = () => {
|
|
102
|
+
if (!searchKeyword) {
|
|
103
|
+
setOccurrences([])
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const matches = []
|
|
108
|
+
const text = props.value || ''
|
|
109
|
+
const escapedKeyword = escapeRegExp(searchKeyword)
|
|
110
|
+
const regex = new RegExp(escapedKeyword, 'gi')
|
|
111
|
+
let match
|
|
112
|
+
|
|
113
|
+
while ((match = regex.exec(text)) !== null) {
|
|
114
|
+
matches.push({
|
|
115
|
+
start: match.index,
|
|
116
|
+
end: match.index + searchKeyword.length
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
setOccurrences(matches)
|
|
120
|
+
}
|
|
121
|
+
|
|
90
122
|
// Handle search action when user presses enter or clicks the search button
|
|
91
123
|
const handleSearch = (e) => {
|
|
92
124
|
if (e && e.stopPropagation) {
|
|
@@ -23,7 +23,6 @@ export default function BookmarkToolbar (props) {
|
|
|
23
23
|
const {
|
|
24
24
|
onNewBookmark,
|
|
25
25
|
onNewBookmarkGroup,
|
|
26
|
-
onImport,
|
|
27
26
|
onExport,
|
|
28
27
|
onSshConfigs,
|
|
29
28
|
bookmarkGroups,
|
|
@@ -121,7 +120,12 @@ export default function BookmarkToolbar (props) {
|
|
|
121
120
|
},
|
|
122
121
|
{
|
|
123
122
|
label: e('import'),
|
|
124
|
-
onClick:
|
|
123
|
+
onClick: () => {
|
|
124
|
+
const fileInput = document.querySelector('.upload-bookmark-icon')
|
|
125
|
+
if (fileInput) {
|
|
126
|
+
fileInput.click()
|
|
127
|
+
}
|
|
128
|
+
},
|
|
125
129
|
icon: <ImportOutlined />
|
|
126
130
|
},
|
|
127
131
|
{
|
|
@@ -680,10 +680,6 @@ export default class ItemListTree extends Component {
|
|
|
680
680
|
)
|
|
681
681
|
}
|
|
682
682
|
|
|
683
|
-
handleImport = () => {
|
|
684
|
-
document.querySelector('.upload-bookmark-icon input')?.click()
|
|
685
|
-
}
|
|
686
|
-
|
|
687
683
|
handleExport = () => {
|
|
688
684
|
document.querySelector('.download-bookmark-icon')?.click()
|
|
689
685
|
}
|
|
@@ -697,7 +693,6 @@ export default class ItemListTree extends Component {
|
|
|
697
693
|
<NewButtonsGroup
|
|
698
694
|
onNewBookmark={this.handleNewBookmark}
|
|
699
695
|
onNewBookmarkGroup={this.handleNewBookmarkGroup}
|
|
700
|
-
onImport={this.handleImport}
|
|
701
696
|
onExport={this.handleExport}
|
|
702
697
|
onSshConfigs={this.handleSshConfigs}
|
|
703
698
|
bookmarkGroups={this.props.bookmarkGroups}
|
|
@@ -18,6 +18,7 @@ import Modal from '../common/modal'
|
|
|
18
18
|
import { copy } from '../../common/clipboard'
|
|
19
19
|
import VncForm from './vnc-form'
|
|
20
20
|
import RemoteFloatControl from '../common/remote-float-control'
|
|
21
|
+
import './vnc.styl'
|
|
21
22
|
|
|
22
23
|
// noVNC module imports — loaded dynamically
|
|
23
24
|
async function loadVncModule () {
|
|
@@ -602,14 +603,17 @@ export default class VncSession extends PureComponent {
|
|
|
602
603
|
className: 'vnc-session-wrap session-v-wrap'
|
|
603
604
|
}
|
|
604
605
|
const contrlProps = this.getControlProps()
|
|
606
|
+
const sessProps = {
|
|
607
|
+
className: 'vnc-session-wrap',
|
|
608
|
+
style: {
|
|
609
|
+
width: w + 'px',
|
|
610
|
+
height: h + 'px'
|
|
611
|
+
}
|
|
612
|
+
}
|
|
605
613
|
return (
|
|
606
614
|
<Spin spinning={loading}>
|
|
607
615
|
<div
|
|
608
|
-
|
|
609
|
-
style={{
|
|
610
|
-
width: w + 'px',
|
|
611
|
-
height: h + 'px'
|
|
612
|
-
}}
|
|
616
|
+
{...sessProps}
|
|
613
617
|
>
|
|
614
618
|
{this.renderControl()}
|
|
615
619
|
<RemoteFloatControl
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.vnc-session-wrap
|
|
2
|
+
display: flex
|
|
3
|
+
flex-direction: column
|
|
4
|
+
justify-content: center
|
|
5
|
+
align-items: center
|
|
6
|
+
.session-v-info
|
|
7
|
+
position: absolute
|
|
8
|
+
top: 0
|
|
9
|
+
left: 0
|
|
10
|
+
width: 100%
|
|
11
|
+
&.scale-viewport
|
|
12
|
+
.rdp-scroll-wrapper canvas
|
|
13
|
+
width: 100% !important
|
|
14
|
+
height: 100% !important
|
|
15
|
+
object-fit: contain
|
|
16
|
+
> div
|
|
17
|
+
background: transparent !important
|
package/client/store/bookmark.js
CHANGED
|
@@ -33,18 +33,10 @@ export default Store => {
|
|
|
33
33
|
return {
|
|
34
34
|
term: 'xterm-256color',
|
|
35
35
|
id: uid(),
|
|
36
|
-
type: '
|
|
37
|
-
title: 'ssh config: ' + t.title,
|
|
36
|
+
type: 'ssh',
|
|
38
37
|
color: '#0088cc',
|
|
39
|
-
|
|
40
|
-
{
|
|
41
|
-
script: `ssh ${t.title}`,
|
|
42
|
-
delay: 500
|
|
43
|
-
}
|
|
44
|
-
]
|
|
38
|
+
...t
|
|
45
39
|
}
|
|
46
|
-
}).filter(d => {
|
|
47
|
-
return !store.bookmarks.find(t => t.title === d.title)
|
|
48
40
|
})
|
|
49
41
|
const ids = bookmarksToAdd.map(d => d.id)
|
|
50
42
|
let sshConfigGroup = store.bookmarkGroups.find(d => d.id === 'sshConfig')
|
|
@@ -61,7 +53,7 @@ export default Store => {
|
|
|
61
53
|
...ids,
|
|
62
54
|
...(sshConfigGroup.bookmarkIds || [])
|
|
63
55
|
]
|
|
64
|
-
})
|
|
56
|
+
}, 'bookmarkGroups')
|
|
65
57
|
}
|
|
66
58
|
return store.addItems(bookmarksToAdd, 'bookmarks')
|
|
67
59
|
}
|
package/client/store/sync.js
CHANGED
|
@@ -35,9 +35,8 @@ async function fetchData (type, func, args, token, proxy) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function updateSyncServerStatusFromGist (store, gist, type) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
)
|
|
38
|
+
const statusContent = get(gist, 'files["electerm-status.json"].content')
|
|
39
|
+
const status = statusContent ? parseJsonSafe(statusContent) : undefined
|
|
41
40
|
store.syncServerStatus[type] = status
|
|
42
41
|
}
|
|
43
42
|
|
package/package.json
CHANGED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { Tooltip } from 'antd'
|
|
2
|
-
|
|
3
|
-
export default function SshConfigItem (props) {
|
|
4
|
-
const { item } = props
|
|
5
|
-
|
|
6
|
-
const generateTooltipContent = (item) => {
|
|
7
|
-
return Object.entries(item)
|
|
8
|
-
.filter(([key]) => key !== 'id')
|
|
9
|
-
.map(([key, value]) => (
|
|
10
|
-
<div key={key}>
|
|
11
|
-
<b className='mg1r'>{key}:</b>
|
|
12
|
-
<span>{value}</span>
|
|
13
|
-
</div>
|
|
14
|
-
))
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<Tooltip title={generateTooltipContent(item)}>
|
|
19
|
-
<div className='elli pd1y pd2x'>
|
|
20
|
-
ssh {item.title}
|
|
21
|
-
</div>
|
|
22
|
-
</Tooltip>
|
|
23
|
-
)
|
|
24
|
-
}
|