@electerm/electerm-react 5.5.9 → 5.5.15

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.
@@ -1,5 +1,5 @@
1
1
  import { useRef, useState } from 'react'
2
- import { Button, Input, Form, Tabs, Flex } from 'antd'
2
+ import { Button, Input, Form, Flex } from 'antd'
3
3
  import {
4
4
  BgColorsOutlined,
5
5
  FontSizeOutlined,
@@ -19,10 +19,11 @@ import generate from '../../common/uid'
19
19
  import { isAIDisabled } from '../../common/ai-feature'
20
20
  import Link from '../common/external-link'
21
21
  import InputAutoFocus from '../common/input-auto-focus'
22
+ import ResponsiveTabs from '../common/responsive-tabs'
22
23
  import ThemePicker from './theme-editor'
23
24
  import ThemeAiEditor from './theme-ai-editor'
24
25
  import Upload from '../common/upload'
25
- // import './theme-form.styl'
26
+ import './theme-form.styl'
26
27
 
27
28
  const { TextArea } = Input
28
29
  const FormItem = Form.Item
@@ -155,6 +156,7 @@ export default function ThemeForm (props) {
155
156
  <FormItem>
156
157
  <span className='mg1r'>src:</span>
157
158
  <Link
159
+ className='wordbreak'
158
160
  to={url}
159
161
  >{url}
160
162
  </Link>
@@ -277,34 +279,29 @@ export default function ThemeForm (props) {
277
279
  themeText: txt,
278
280
  disabled
279
281
  }
282
+ // icon + label, the label text is hidden by css on narrow screens so the
283
+ // three editor tabs always fit, the title keeps it discoverable
284
+ function renderTabLabel (icon, text) {
285
+ return (
286
+ <span className='theme-editor-tab' title={text}>
287
+ {icon}
288
+ <span className='theme-editor-tab-text'>{text}</span>
289
+ </span>
290
+ )
291
+ }
280
292
  const tabItems = [
281
293
  {
282
294
  key: 'theme-editor-color-picker',
283
- label: (
284
- <span>
285
- <BgColorsOutlined className='mg1r' />
286
- {e('editWithColorPicker')}
287
- </span>
288
- )
295
+ label: renderTabLabel(<BgColorsOutlined className='mg1r' />, e('editWithColorPicker'))
289
296
  },
290
297
  {
291
298
  key: 'theme-editor-txt',
292
- label: (
293
- <span>
294
- <FontSizeOutlined className='mg1r' />
295
- {e('editWithTextEditor')}
296
- </span>
297
- )
299
+ label: renderTabLabel(<FontSizeOutlined className='mg1r' />, e('editWithTextEditor'))
298
300
  },
299
301
  ...(!isAIDisabled()
300
302
  ? [{
301
303
  key: 'theme-editor-ai',
302
- label: (
303
- <span>
304
- <RobotOutlined className='mg1r' />
305
- AI
306
- </span>
307
- )
304
+ label: renderTabLabel(<RobotOutlined className='mg1r' />, 'AI')
308
305
  }]
309
306
  : [])
310
307
  ]
@@ -365,7 +362,7 @@ export default function ThemeForm (props) {
365
362
  </Button>
366
363
  </Upload>
367
364
  </Flex>
368
- <Tabs {...tabsProps} />
365
+ <ResponsiveTabs {...tabsProps} />
369
366
  {
370
367
  editor === 'theme-editor-txt'
371
368
  ? renderTxt()
@@ -1,10 +1,8 @@
1
- .theme-editor-txt
2
- .editor-u-textarea
3
- display block !important
4
- .editor-u-picker
5
- display none !important
6
- .theme-editor-color-picker
7
- .editor-u-textarea
8
- display none !important
9
- .editor-u-picker
10
- display block !important
1
+ // setting panel > UI themes > theme form
2
+ //
3
+ // The three editor tabs need ~395px for their translated labels ("Edit with
4
+ // color picker" / "Edit with text editor" / "AI") while a 320px phone leaves
5
+ // the panel only ~257px. Below the app's mobile breakpoint (600px, see
6
+ // constants.js) ResponsiveTabs replaces the bar with a single "current tab +
7
+ // dropdown" selector (common/responsive-tabs.jsx), so the labels stay readable
8
+ // everywhere instead of being collapsed to icons.
@@ -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)
@@ -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
 
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.15",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",