@electerm/electerm-react 5.5.9 → 5.5.25

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 (62) hide show
  1. package/README.md +12 -2
  2. package/client/common/constants.js +14 -0
  3. package/client/common/default-setting.js +2 -2
  4. package/client/common/download.jsx +12 -0
  5. package/client/common/parse-vv.js +313 -0
  6. package/client/common/resolve.js +20 -14
  7. package/client/common/theme-defaults.js +5 -2
  8. package/client/common/update-check.js +5 -4
  9. package/client/components/ai/ai-history-item.jsx +1 -1
  10. package/client/components/ai/ai-history.jsx +1 -0
  11. package/client/components/ai/ai-history.styl +23 -0
  12. package/client/components/ai/ai-presets.js +13 -2
  13. package/client/components/bg/css-overwrite.jsx +16 -1
  14. package/client/components/bookmark-form/common/color-picker.styl +3 -0
  15. package/client/components/bookmark-form/common/fields.jsx +3 -0
  16. package/client/components/bookmark-form/common/vv-file-field.jsx +137 -0
  17. package/client/components/bookmark-form/config/spice.js +1 -0
  18. package/client/components/common/remote-pointer.js +49 -0
  19. package/client/components/common/remote-session-shell.jsx +137 -0
  20. package/client/components/common/remote-session-shell.styl +80 -0
  21. package/client/components/common/responsive-tabs.jsx +89 -0
  22. package/client/components/common/responsive-tabs.styl +34 -0
  23. package/client/components/footer/footer-entry.jsx +39 -14
  24. package/client/components/icons/ai-icon.styl +4 -1
  25. package/client/components/layout/layout.jsx +7 -1
  26. package/client/components/main/main.jsx +9 -2
  27. package/client/components/main/term-fullscreen.styl +7 -0
  28. package/client/components/quick-commands/quick-command-ai-editor.jsx +255 -0
  29. package/client/components/quick-commands/quick-command-ai-editor.styl +13 -0
  30. package/client/components/quick-commands/quick-commands-box.jsx +23 -5
  31. package/client/components/quick-commands/quick-commands-form-elem.jsx +129 -65
  32. package/client/components/rdp/rdp-session.jsx +94 -124
  33. package/client/components/rdp/rdp.styl +13 -36
  34. package/client/components/session/session.styl +0 -16
  35. package/client/components/setting-panel/list.jsx +2 -2
  36. package/client/components/setting-panel/setting-modal.jsx +8 -3
  37. package/client/components/sftp/sftp-entry.jsx +26 -0
  38. package/client/components/side-panel-r/right-side-panel.styl +11 -1
  39. package/client/components/side-panel-r/side-panel-r.jsx +45 -9
  40. package/client/components/sidebar/bookmark-select.jsx +3 -1
  41. package/client/components/sidebar/index.jsx +13 -4
  42. package/client/components/sidebar/side-panel.jsx +8 -3
  43. package/client/components/sidebar/sidebar-panel.jsx +20 -7
  44. package/client/components/spice/spice-session.jsx +69 -72
  45. package/client/components/spice/spice.styl +10 -19
  46. package/client/components/terminal/shortcut-bar.styl +14 -0
  47. package/client/components/terminal/unix-timestamp-tooltip.jsx +27 -1
  48. package/client/components/terminal-info/terminal-info.styl +3 -0
  49. package/client/components/theme/terminal-theme-list.styl +3 -0
  50. package/client/components/theme/theme-ai-editor.jsx +2 -2
  51. package/client/components/theme/theme-form.jsx +20 -23
  52. package/client/components/theme/theme-form.styl +8 -10
  53. package/client/components/theme/theme-list-item.jsx +59 -25
  54. package/client/components/vnc/vnc-session.jsx +72 -104
  55. package/client/components/vnc/vnc.styl +25 -16
  56. package/client/css/mobile.styl +28 -0
  57. package/client/store/common.js +44 -0
  58. package/client/store/init-state.js +2 -1
  59. package/client/store/setting.js +4 -0
  60. package/client/store/sidebar.js +5 -1
  61. package/client/store/store.js +9 -2
  62. package/package.json +1 -1
@@ -7,7 +7,6 @@ import {
7
7
  statusMap
8
8
  } from '../../common/constants'
9
9
  import {
10
- Spin,
11
10
  Select
12
11
  } from 'antd'
13
12
  import {
@@ -18,6 +17,7 @@ import Modal from '../common/modal'
18
17
  import { copy, readClipboard } from '../../common/clipboard'
19
18
  import VncForm from './vnc-form'
20
19
  import RemoteFloatControl from '../common/remote-float-control'
20
+ import RemoteSessionShell from '../common/remote-session-shell'
21
21
  import './vnc.styl'
22
22
 
23
23
  // noVNC module imports — loaded dynamically
@@ -71,23 +71,35 @@ export default class VncSession extends PureComponent {
71
71
  })
72
72
  }
73
73
 
74
- getScreenSize = () => {
75
- const { screens, currentScreen } = this.state
76
- const currentScreenData = screens.find(s => s.id === currentScreen)
77
- return {
78
- remoteWidth: currentScreenData ? currentScreenData.width : null,
79
- remoteHeight: currentScreenData ? currentScreenData.height : null
74
+ // fullscreen always fills the screen, the per tab setting only applies
75
+ // windowed; the switch is not reachable in fullscreen anyway
76
+ getFit = () => {
77
+ if (this.props.fullscreen) {
78
+ return true
80
79
  }
80
+ const { scaleViewport = true } = this.props.tab
81
+ return !!scaleViewport
81
82
  }
82
83
 
84
+ // viewport box measured by the shell, used as the size hint for the server.
85
+ // Falls back to the layout props before the first measurement.
83
86
  calcCanvasSize = () => {
84
- const { width, height } = this.props
87
+ const { width, height } = this.viewSize || {}
88
+ if (width && height) {
89
+ return { width, height }
90
+ }
91
+ const { width: w, height: h } = this.props
85
92
  return {
86
- width: width - 10,
87
- height: height - 80
93
+ width: w - 10,
94
+ height: h - 80
88
95
  }
89
96
  }
90
97
 
98
+ handleViewportResize = (width, height) => {
99
+ this.viewSize = { width, height }
100
+ this.updateViewLayout()
101
+ }
102
+
91
103
  buildWsUrl = (port, type = 'vnc', extra = '') => {
92
104
  const { host, tokenElecterm } = this.props.config
93
105
  const { id } = this.props.tab
@@ -127,30 +139,6 @@ export default class VncSession extends PureComponent {
127
139
  }
128
140
  }
129
141
 
130
- renderControl = () => {
131
- const contrlProps = this.getControlProps({
132
- fixedPosition: false,
133
- showExitFullscreen: false,
134
- className: 'mg1l'
135
- })
136
- return (
137
- <div className='pd1 fix session-v-info'>
138
- <div className='fleft'>
139
- <ReloadOutlined
140
- onClick={this.handleReInit}
141
- className='mg2r mg1l pointer'
142
- />
143
- {this.renderScreensSelect()}
144
- {this.renderInfo()}
145
- </div>
146
- <div className='fright'>
147
- {this.props.fullscreenIcon()}
148
- <RemoteFloatControl {...contrlProps} />
149
- </div>
150
- </div>
151
- )
152
- }
153
-
154
142
  renderScreensSelect = () => {
155
143
  const {
156
144
  screens,
@@ -232,8 +220,9 @@ export default class VncSession extends PureComponent {
232
220
 
233
221
  const { width, height } = this.calcCanvasSize()
234
222
  const wsUrl = this.buildWsUrl(port, 'vnc', `&width=${width}&height=${height}`)
235
- // When scaleViewport is false, we don't set fixed dimensions on the canvas
236
- // so it can render at the actual remote screen size
223
+ // noVNC sizes its own canvas (`_screen` fills whatever box it is mounted
224
+ // into, the canvas is centered in it), so scaling is entirely driven by
225
+ // `scaleViewport` plus the size of the mount node.
237
226
  const vncOpts = {
238
227
  clipViewport,
239
228
  scaleViewport,
@@ -241,13 +230,6 @@ export default class VncSession extends PureComponent {
241
230
  qualityLevel, // JPEG quality 0-9, lower = faster
242
231
  compressionLevel, // Compression 0-9, lower = faster
243
232
  shared, // Allow shared connections
244
- style: scaleViewport
245
- ? {
246
- width: width + 'px',
247
- height: height + 'px',
248
- overflow: 'hidden'
249
- }
250
- : {},
251
233
  credentials: {}
252
234
  }
253
235
  if (username) {
@@ -378,7 +360,7 @@ export default class VncSession extends PureComponent {
378
360
  for (const event of events) {
379
361
  rfb.addEventListener(event, this[`on${window.capitalizeFirstLetter(event)}`])
380
362
  }
381
- rfb.scaleViewport = scaleViewport
363
+ rfb.scaleViewport = this.getFit()
382
364
  rfb.clipViewport = clipViewport
383
365
  rfb.qualityLevel = qualityLevel
384
366
  rfb.compressionLevel = compressionLevel
@@ -550,9 +532,10 @@ export default class VncSession extends PureComponent {
550
532
  }
551
533
 
552
534
  componentDidUpdate (prevProps, prevState) {
535
+ // viewport size changes are reported by the shell, the layout props are
536
+ // stale in fullscreen so they are not used here
553
537
  if (
554
- prevProps.width !== this.props.width ||
555
- prevProps.height !== this.props.height ||
538
+ prevProps.fullscreen !== this.props.fullscreen ||
556
539
  prevState.currentScreen !== this.state.currentScreen
557
540
  ) {
558
541
  this.updateViewLayout()
@@ -561,24 +544,25 @@ export default class VncSession extends PureComponent {
561
544
 
562
545
  updateViewLayout = () => {
563
546
  const { currentScreen, screens } = this.state
564
- const { scaleViewport = true, clipViewport = false } = this.props.tab
547
+ const { clipViewport = false } = this.props.tab
565
548
  const container = this.domRef.current ? this.domRef.current.firstElementChild : null
566
549
 
567
550
  if (!this.rfb || !container) {
568
551
  return
569
552
  }
570
553
 
571
- // Force re-apply scaleViewport to trigger autoscale logic (if enabled)
572
- // or reset to 1.0 (if disabled)
573
- this.rfb.scaleViewport = scaleViewport
554
+ // keep noVNC scaling in sync with our fit state, then re-run autoscale so
555
+ // our patched single screen logic sees the new viewport size
556
+ const fit = this.getFit()
557
+ this.rfb.scaleViewport = fit
574
558
  this.rfb.clipViewport = clipViewport
575
559
 
576
- // Explicitly trigger autoscale to ensure our patched logic runs
577
- if (scaleViewport && this.rfb._display) {
560
+ if (fit && this.rfb._display) {
578
561
  this.rfb._display.autoscale(container.clientWidth, container.clientHeight)
579
562
  }
580
563
 
581
564
  // Handle scrolling to the selected screen
565
+ clearTimeout(this.timer)
582
566
  this.timer = setTimeout(() => {
583
567
  if (currentScreen === 'all') {
584
568
  container.scrollTo(0, 0)
@@ -653,61 +637,45 @@ export default class VncSession extends PureComponent {
653
637
  }
654
638
 
655
639
  render () {
656
- const { width: w, height: h } = this.props
657
640
  const { loading } = this.state
658
- const { remoteWidth, remoteHeight } = this.getScreenSize()
659
- const { scaleViewport = true } = this.props.tab
660
- // When not in scale mode, we need a wrapper with container size,
661
- // and the inner div should have remote screen size to show scrollbars
662
- const isScaled = scaleViewport
663
-
664
- const {
665
- width: innerWidth,
666
- height: innerHeight
667
- } = this.calcCanvasSize()
668
- const wrapperStyle = {
669
- width: innerWidth + 'px',
670
- height: innerHeight + 'px',
671
- overflow: isScaled ? 'hidden' : 'auto'
672
- }
673
- const remoteStyle = {
674
- width: isScaled ? '100%' : remoteWidth + 'px',
675
- height: isScaled ? '100%' : remoteHeight + 'px',
676
- overflow: 'hidden'
677
- }
678
- const divProps = {
679
- style: remoteStyle,
680
- className: 'vnc-session-wrap session-v-wrap'
681
- }
682
- const contrlProps = this.getControlProps()
683
- const sessProps = {
684
- className: 'vnc-session-wrap',
685
- style: {
686
- width: w + 'px',
687
- height: h + 'px'
688
- }
689
- }
641
+ const floatProps = this.getControlProps()
642
+ const inlineProps = this.getControlProps({
643
+ fixedPosition: false,
644
+ showExitFullscreen: false,
645
+ className: 'mg1l'
646
+ })
647
+ const controlLeft = (
648
+ <>
649
+ <ReloadOutlined
650
+ onClick={this.handleReInit}
651
+ className='mg2r mg1l pointer'
652
+ />
653
+ {this.renderScreensSelect()}
654
+ {this.renderInfo()}
655
+ </>
656
+ )
657
+ const controlRight = (
658
+ <>
659
+ {this.props.fullscreenIcon()}
660
+ <RemoteFloatControl {...inlineProps} />
661
+ </>
662
+ )
690
663
  return (
691
- <Spin spinning={loading}>
664
+ <RemoteSessionShell
665
+ loading={loading}
666
+ fit={this.getFit()}
667
+ wrapClassName='vnc-session-wrap'
668
+ onViewportResize={this.handleViewportResize}
669
+ controlLeft={controlLeft}
670
+ controlRight={controlRight}
671
+ floatControl={<RemoteFloatControl {...floatProps} />}
672
+ overlay={this.renderConfirm()}
673
+ >
692
674
  <div
693
- {...sessProps}
694
- >
695
- {this.renderControl()}
696
- <RemoteFloatControl
697
- {...contrlProps}
698
- />
699
- <div
700
- style={wrapperStyle}
701
- className='vnc-scroll-wrapper'
702
- >
703
- <div
704
- {...divProps}
705
- ref={this.domRef}
706
- />
707
- </div>
708
- {this.renderConfirm()}
709
- </div>
710
- </Spin>
675
+ className='remote-session-fill vnc-session-mount'
676
+ ref={this.domRef}
677
+ />
678
+ </RemoteSessionShell>
711
679
  )
712
680
  }
713
681
  }
@@ -1,17 +1,26 @@
1
+ // VNC only styling — the session layout (control bar, viewport, fit mode)
2
+ // lives in the shared remote session shell: common/remote-session-shell.styl
1
3
  .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
4
+ .vnc-session-mount
5
+ // noVNC mounts its own screen element in here and sizes its canvas
6
+ // against this box, so it has to keep a definite size
7
+ width 100%
8
+ height 100%
9
+ // noVNC's screen element carries no class of its own: letterbox area
10
+ // follows the app theme instead of noVNC's own background
11
+ > div
12
+ background transparent !important
13
+ // scrolled when fit is off and the remote screen is bigger than
14
+ // the window
15
+ &::-webkit-scrollbar
16
+ width 16px
17
+ height 16px
18
+ background var(--main-darker)
19
+ &::-webkit-scrollbar-track
20
+ background var(--main-darker)
21
+ box-shadow inset 0 0 5px var(--main-darker)
22
+ &::-webkit-scrollbar-thumb
23
+ background var(--primary)
24
+ border-radius 0
25
+ &::-webkit-scrollbar-corner
26
+ background var(--main-darker)
@@ -11,6 +11,34 @@
11
11
  .remote-monitor-bar
12
12
  position fixed
13
13
 
14
+ // Side panels are full-screen drawers on mobile: they start at the viewport
15
+ // edge (the left one covers the icon bar too, so no strip of terminal is left
16
+ // showing) and run to the bottom edge — footer included. On desktop the
17
+ // footer is reserved *beside* the panel because it holds the panel's own
18
+ // trigger; on mobile the drawer is dismissed from its header (✕ / the
19
+ // bookmark/history tabs) instead, so there is nothing to keep clear. The
20
+ // inline width already comes from store.leftSidePanelWidth / rightPanelWidth,
21
+ // which are the viewport width here.
22
+ // Both are on the fixed frame (the right panel explicitly: its bottom edge
23
+ // must not drift from the fixed footer when the visual viewport changes), and
24
+ // both have to outrank the footer's 200 — it comes later in the DOM, so at an
25
+ // equal rank it would paint over the drawer. The left drawer lives inside
26
+ // .sidebar, which is its own stacking context (z-index 100), so lifting the
27
+ // drawer alone is not enough: the whole sidebar column has to clear the
28
+ // footer. The right panel is a root-level child, so its own z-index suffices.
29
+ // The icon bar takes no extra room from this: it is 43px wide and the footer
30
+ // starts at 44px.
31
+ .sidebar
32
+ z-index 250
33
+ .sidebar-list
34
+ left 0
35
+ bottom 0
36
+ z-index 250
37
+ .right-side-panel
38
+ position fixed
39
+ bottom 0
40
+ z-index 250
41
+
14
42
  // ---- 1. Tabs: show only current tab + dropdown, close icon always visible
15
43
  .tabs.mobile-tabs
16
44
  display flex
@@ -10,6 +10,7 @@ import {
10
10
  leftSidePanelWidthKey,
11
11
  leftSideBarOpenKey,
12
12
  rightSidebarWidthKey,
13
+ rightPanelPinnedKey,
13
14
  addPanelWidthLsKey,
14
15
  connectionMap,
15
16
  lastAiChatSessionIdKey,
@@ -42,6 +43,18 @@ export default Store => {
42
43
  window.store.setConfig(ext)
43
44
  }
44
45
 
46
+ // The footer info icon is a toggle: the same click opens and closes the
47
+ // panel, so the trigger never has to be hunted down in the panel header.
48
+ Store.prototype.toggleInfoPanel = action(function () {
49
+ const { store } = window
50
+ const isOpen = store.rightPanelVisible && store.rightPanelTab === 'info'
51
+ store.rightPanelVisible = !isOpen
52
+ store.rightPanelTab = 'info'
53
+ if (!isOpen) {
54
+ store.openInfoPanelAction()
55
+ }
56
+ })
57
+
45
58
  Store.prototype.openInfoPanel = action(function () {
46
59
  const { store } = window
47
60
  store.rightPanelVisible = true
@@ -122,7 +135,13 @@ export default Store => {
122
135
  Object.assign(window.store._termSearchOptions, update)
123
136
  }
124
137
 
138
+ // Both panel widths are desktop-only preferences: on mobile the panel is a
139
+ // full-width drawer whose width is fixed, so a drag (or any other caller)
140
+ // must not overwrite the desktop value that is still in localStorage.
125
141
  Store.prototype.setLeftSidePanelWidth = function (v) {
142
+ if (window.store.isMobile) {
143
+ return
144
+ }
126
145
  ls.setItem(leftSidePanelWidthKey, v)
127
146
  window.store._leftSidePanelWidth = v
128
147
  }
@@ -144,9 +163,22 @@ export default Store => {
144
163
  }
145
164
 
146
165
  Store.prototype.setRightSidePanelWidth = function (v) {
166
+ if (window.store.isMobile) {
167
+ return
168
+ }
147
169
  ls.setItem(rightSidebarWidthKey, v)
148
170
  window.store._rightPanelWidth = v
149
171
  }
172
+
173
+ // Persist the pin the same way the left sidebar does (sidebarPinnedKey):
174
+ // pinned is a durable layout preference, not a per-session toggle. It matters
175
+ // more here because pinned and unpinned are visibly different modes — the
176
+ // unpinned panel is an overlay that clears the footer, the pinned one is a
177
+ // full-height dock.
178
+ Store.prototype.setRightPanelPinned = function (v) {
179
+ ls.setItem(rightPanelPinnedKey, v + '')
180
+ window.store.rightPanelPinned = v
181
+ }
150
182
  Store.prototype.beforeExit = function (evt) {
151
183
  const { confirmBeforeExit } = window.store.config
152
184
  if (
@@ -286,6 +318,18 @@ export default Store => {
286
318
  }
287
319
  }
288
320
 
321
+ // Same toggle contract as toggleInfoPanel: the footer AI button opens and
322
+ // closes its own panel. The open path must go through handleOpenAIPanel so
323
+ // the missing-config prompt still fires; closing must not touch config.
324
+ Store.prototype.toggleAIPanel = function () {
325
+ const { store } = window
326
+ if (store.rightPanelVisible && store.rightPanelTab === 'ai') {
327
+ store.rightPanelVisible = false
328
+ return
329
+ }
330
+ store.handleOpenAIPanel()
331
+ }
332
+
289
333
  Store.prototype.explainWithAi = function (txt) {
290
334
  const { store } = window
291
335
  store.handleOpenAIPanel()
@@ -18,6 +18,7 @@ import {
18
18
  localAddrBookmarkLsKey,
19
19
  leftSidePanelWidthKey,
20
20
  rightSidebarWidthKey,
21
+ rightPanelPinnedKey,
21
22
  addPanelWidthLsKey,
22
23
  qmSortByFrequencyKey,
23
24
  resolutionsLsKey,
@@ -125,7 +126,7 @@ export default () => {
125
126
  terminalInfoProps: {},
126
127
  rightPanelVisible: false,
127
128
  rightPanelTab: 'info',
128
- rightPanelPinned: false,
129
+ rightPanelPinned: ls.getItem(rightPanelPinnedKey) === 'true',
129
130
  _rightPanelWidth: parseInt(ls.getItem(rightSidebarWidthKey), 10) || 500,
130
131
  showAIConfigModal: false,
131
132
 
@@ -111,6 +111,10 @@ export default Store => {
111
111
  })
112
112
  store.setSettingItem(settingList().find(d => d.id === settingSyncId))
113
113
  store.openSettingModal()
114
+ // on mobile, jump straight to the sync form instead of the menu list
115
+ if (store.isMobile) {
116
+ store.settingMobileView = 'content'
117
+ }
114
118
  })
115
119
  }
116
120
 
@@ -25,7 +25,11 @@ export default Store => {
25
25
  // through this one method.
26
26
  Store.prototype.openLeftSidePanel = function (field = 'bookmarks') {
27
27
  const { store } = window
28
- if (store.pinned) {
28
+ // A pinned panel is already open, so there is nothing to toggle — except
29
+ // on mobile, where pinning is not a reachable mode (no pin control) and
30
+ // the panel is a full-width overlay. Honouring a stale desktop pin here
31
+ // would leave the bookmark button dead on arrival.
32
+ if (store.pinned && !store.isMobile) {
29
33
  return
30
34
  }
31
35
  store.setOpenedSideBar(store.openedSideBar ? '' : field)
@@ -61,10 +61,15 @@ class Store {
61
61
  return window.store.innerWidth
62
62
  }
63
63
 
64
+ // Mobile turns a side panel into a full-screen drawer: it spans the whole
65
+ // viewport and its width is not user-adjustable. So the stored desktop width
66
+ // is ignored here, and so is the icon bar (the drawer covers it) — and the
67
+ // old 500px cap, which left a strip of terminal showing on windows between
68
+ // 500 and 600px, i.e. the top of the mobile range.
64
69
  get leftSidePanelWidth () {
65
70
  const { store } = window
66
71
  if (store.isMobile) {
67
- return Math.min(store.innerWidth - store.leftSideBarWidth, 500)
72
+ return store.innerWidth
68
73
  }
69
74
  return store._leftSidePanelWidth
70
75
  }
@@ -78,10 +83,12 @@ class Store {
78
83
  return store._leftSideBarOpen ? sidebarWidth : 0
79
84
  }
80
85
 
86
+ // Same as leftSidePanelWidth: full-screen drawer on mobile, width not
87
+ // user-adjustable.
81
88
  get rightPanelWidth () {
82
89
  const { store } = window
83
90
  if (store.isMobile) {
84
- return Math.min(store.innerWidth, 500)
91
+ return store.innerWidth
85
92
  }
86
93
  return store._rightPanelWidth
87
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "5.5.9",
3
+ "version": "5.5.25",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",