@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.
Files changed (29) hide show
  1. package/client/components/bg/custom-css.jsx +10 -7
  2. package/client/components/common/notification.jsx +1 -1
  3. package/client/components/common/opacity.jsx +8 -6
  4. package/client/components/main/ui-theme.jsx +10 -6
  5. package/client/components/profile/profile-list.jsx +1 -1
  6. package/client/components/quick-commands/quick-commands-list.jsx +1 -1
  7. package/client/components/rdp/rdp-session.jsx +24 -15
  8. package/client/components/rdp/rdp.styl +21 -1
  9. package/client/components/setting-panel/list.styl +7 -0
  10. package/client/components/setting-sync/setting-sync-form.jsx +10 -5
  11. package/client/components/sftp/file-item.jsx +1 -0
  12. package/client/components/sftp/file-read.js +58 -3
  13. package/client/components/spice/spice-session.jsx +9 -6
  14. package/client/components/spice/spice.styl +27 -9
  15. package/client/components/ssh-config/load-ssh-configs-item.jsx +99 -0
  16. package/client/components/ssh-config/load-ssh-configs.jsx +38 -9
  17. package/client/components/ssh-config/ssh-config.styl +3 -0
  18. package/client/components/terminal/transfer-client-base.js +28 -0
  19. package/client/components/terminal/trzsz-client.js +8 -10
  20. package/client/components/terminal/zmodem-client.js +8 -10
  21. package/client/components/text-editor/simple-editor.jsx +38 -6
  22. package/client/components/tree-list/bookmark-toolbar.jsx +6 -2
  23. package/client/components/tree-list/tree-list.jsx +0 -5
  24. package/client/components/vnc/vnc-session.jsx +9 -5
  25. package/client/components/vnc/vnc.styl +17 -0
  26. package/client/store/bookmark.js +3 -11
  27. package/client/store/sync.js +2 -3
  28. package/package.json +1 -1
  29. 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
- useConditionalEffect(() => {
25
- applyTheme()
26
- }, delta && !eq(delta.prev, delta.curr))
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
  }
@@ -59,7 +59,7 @@ export function NotificationContainer () {
59
59
  {nots.map(notif => (
60
60
  <NotificationItem
61
61
  key={notif.key}
62
- message={notif.message}
62
+ message={notif.message || notif.type}
63
63
  description={notif.description}
64
64
  type={notif.type}
65
65
  duration={notif.duration}
@@ -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 delta = useDelta(currentOpacity)
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
- useConditionalEffect(() => {
62
- applyOpacity()
63
- }, delta && !eq(delta.prev, delta.curr))
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 delta = useDelta(themeConfig)
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
- useConditionalEffect(() => {
68
- applyTheme()
69
- }, delta && delta.prev && !eq(delta.prev, delta.curr))
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
  }
@@ -28,7 +28,7 @@ export default class ProfileList extends List {
28
28
  const { activeItemId } = this.props
29
29
  const { name, id } = item
30
30
  const cls = classnames(
31
- 'item-list-unit theme-item',
31
+ 'item-list-unit',
32
32
  {
33
33
  active: activeItemId === id
34
34
  }
@@ -63,7 +63,7 @@ export default class QuickCommandsList extends List {
63
63
  const { activeItemId } = this.props
64
64
  const { name, id } = item
65
65
  const cls = classnames(
66
- 'item-list-unit theme-item',
66
+ 'item-list-unit',
67
67
  {
68
68
  active: activeItemId === id
69
69
  }
@@ -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
- {...rdpProps}
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 { useDelta, useConditionalEffect } from 'react-delta-hooks'
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 delta = useDelta(props.formData)
31
- useConditionalEffect(() => {
32
- form.resetFields()
33
- }, delta && delta.prev && !eq(delta.prev, delta.curr))
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) {
@@ -770,6 +770,7 @@ export default class FileSection extends React.Component {
770
770
  typeTo,
771
771
  fromPath: resolve(path, name),
772
772
  toPath,
773
+ fromFile: file,
773
774
  id: generate(),
774
775
  ...createTransferProps(this.props),
775
776
  operation
@@ -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.isDirectory,
58
- isSymbolicLink: stat.isSymbolicLink
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.isDirectory
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
- className={cls}
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.scale-viewport
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
- justify-content: center
11
- align-items: center
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(sshConfigs)
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 (!sshConfigs.length) {
62
+ if (!localConfigs.length) {
44
63
  return (
45
64
  <Empty />
46
65
  )
47
66
  }
48
- return sshConfigs.map((d, i) => {
49
- return (
50
- <SshConfigItem item={d} key={d.title} />
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={!sshConfigs.length || loading}
121
+ disabled={!localConfigs.length || loading}
93
122
  >
94
123
  {e('import')}
95
124
  </Button>
@@ -0,0 +1,3 @@
1
+ .ssh-config-list
2
+ max-height 60vh
3
+ overflow auto
@@ -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
- // Clear line and write progress
245
- const str = `\r\x1b[2K\x1b[32m${displayName}\x1b[0m: ${percent}%, ${formatSize(transferred)}/${formatSize(size)}, ${formatSize(speed)}/s${isComplete ? ' \x1b[32m\x1b[1m[DONE]\x1b[0m' : ''}`
246
- this.writeToTerminal(str + '\r')
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
- // Clear line and write progress
227
- const str = `\r\x1b[2K\x1b[32m${displayName}\x1b[0m: ${percent}%, ${formatSize(transferred)}/${formatSize(size)}, ${formatSize(speed)}/s${isComplete ? ' \x1b[32m\x1b[1m[DONE]\x1b[0m' : ''}`
228
- this.writeToTerminal(str + '\r')
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
- // Only focus the textarea when explicitly navigating between matches
30
- if (isNavigating) {
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, props.value])
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: onImport,
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
- className='rdp-session-wrap pd1'
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
@@ -33,18 +33,10 @@ export default Store => {
33
33
  return {
34
34
  term: 'xterm-256color',
35
35
  id: uid(),
36
- type: 'local',
37
- title: 'ssh config: ' + t.title,
36
+ type: 'ssh',
38
37
  color: '#0088cc',
39
- runScripts: [
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
  }
@@ -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 status = parseJsonSafe(
39
- get(gist, 'files["electerm-status.json"].content')
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "2.13.0",
3
+ "version": "2.15.8",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",
@@ -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
- }