@electerm/electerm-react 5.5.6 → 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.
Files changed (47) hide show
  1. package/client/common/default-setting.js +1 -0
  2. package/client/common/download.jsx +12 -0
  3. package/client/common/resolve.js +20 -14
  4. package/client/components/ai/ai-history-item.jsx +1 -1
  5. package/client/components/ai/ai-history.jsx +1 -0
  6. package/client/components/ai/ai-history.styl +23 -0
  7. package/client/components/bookmark-form/common/color-picker.styl +3 -0
  8. package/client/components/common/remote-pointer.js +49 -0
  9. package/client/components/common/remote-session-shell.jsx +137 -0
  10. package/client/components/common/remote-session-shell.styl +80 -0
  11. package/client/components/common/responsive-tabs.jsx +89 -0
  12. package/client/components/common/responsive-tabs.styl +34 -0
  13. package/client/components/footer/cmd-history.jsx +132 -45
  14. package/client/components/footer/cmd-history.styl +42 -4
  15. package/client/components/footer/multi-tab-run-modal.jsx +83 -0
  16. package/client/components/footer/tab-select.jsx +34 -17
  17. package/client/components/quick-commands/quick-command-create-modal.jsx +60 -0
  18. package/client/components/quick-commands/quick-commands-form-elem.jsx +10 -4
  19. package/client/components/rdp/rdp-session.jsx +94 -124
  20. package/client/components/rdp/rdp.styl +13 -36
  21. package/client/components/session/session.styl +0 -16
  22. package/client/components/setting-panel/setting-modal.jsx +8 -3
  23. package/client/components/setting-panel/setting-terminal.jsx +1 -0
  24. package/client/components/sftp/sftp-entry.jsx +30 -0
  25. package/client/components/sidebar/side-panel.jsx +1 -1
  26. package/client/components/spice/spice-session.jsx +69 -72
  27. package/client/components/spice/spice.styl +10 -19
  28. package/client/components/terminal/mixins/term-content.js +38 -0
  29. package/client/components/terminal/mixins/term-context-menu.jsx +35 -3
  30. package/client/components/terminal/mixins/term-socket.js +5 -5
  31. package/client/components/terminal/mixins/term-touch.js +444 -57
  32. package/client/components/terminal/shortcut-bar-defs.js +30 -4
  33. package/client/components/terminal/shortcut-bar-edit.jsx +40 -9
  34. package/client/components/terminal/shortcut-bar.jsx +13 -4
  35. package/client/components/terminal/ssh-reload-state.js +41 -1
  36. package/client/components/terminal/startup-queue.js +67 -13
  37. package/client/components/terminal/terminal-select-text.jsx +69 -0
  38. package/client/components/terminal/terminal.jsx +72 -4
  39. package/client/components/terminal/terminal.styl +61 -0
  40. package/client/components/theme/theme-form.jsx +18 -21
  41. package/client/components/theme/theme-form.styl +8 -10
  42. package/client/components/vnc/vnc-session.jsx +72 -104
  43. package/client/components/vnc/vnc.styl +25 -16
  44. package/client/css/mobile.styl +20 -7
  45. package/client/store/setting.js +4 -0
  46. package/client/store/tab.js +5 -4
  47. package/package.json +1 -1
@@ -14,7 +14,6 @@ import {
14
14
  DownloadOutlined
15
15
  } from '@ant-design/icons'
16
16
  import {
17
- Spin,
18
17
  Select,
19
18
  Tooltip
20
19
  } from 'antd'
@@ -23,6 +22,8 @@ import scanCode from './code-scan'
23
22
  import resolutions from './resolutions'
24
23
  import { readClipboardAsync } from '../../common/clipboard'
25
24
  import RemoteFloatControl from '../common/remote-float-control'
25
+ import RemoteSessionShell from '../common/remote-session-shell'
26
+ import { eventToRemotePos } from '../common/remote-pointer'
26
27
  import SwitchLabel from '../common/switch'
27
28
  import HelpIcon from '../common/help-icon'
28
29
  import Modal from '../common/modal'
@@ -102,6 +103,12 @@ export default class RdpSession extends PureComponent {
102
103
 
103
104
  runInitScript = () => { }
104
105
 
106
+ // fullscreen always fills the screen, the per tab setting only applies
107
+ // windowed; the switch is not reachable in fullscreen anyway
108
+ getFit = () => {
109
+ return !!this.props.fullscreen || !!this.state.scaleViewport
110
+ }
111
+
105
112
  setStatus = status => {
106
113
  const id = this.props.tab?.id
107
114
  this.props.editTab(id, {
@@ -458,30 +465,14 @@ export default class RdpSession extends PureComponent {
458
465
  canvas.addEventListener('mousemove', (e) => {
459
466
  if (!this.session) return
460
467
  try {
461
- const rect = canvas.getBoundingClientRect()
462
- const { scaleViewport } = this.state
463
- let scaleX = canvas.width / rect.width
464
- let scaleY = canvas.height / rect.height
465
- let offsetX = 0
466
- let offsetY = 0
467
- if (scaleViewport) {
468
- const containerRatio = rect.width / rect.height
469
- const canvasRatio = canvas.width / canvas.height
470
- let renderWidth, renderHeight
471
- if (containerRatio > canvasRatio) {
472
- renderHeight = rect.height
473
- renderWidth = rect.height * canvasRatio
474
- offsetX = (rect.width - renderWidth) / 2
475
- } else {
476
- renderWidth = rect.width
477
- renderHeight = rect.width / canvasRatio
478
- offsetY = (rect.height - renderHeight) / 2
479
- }
480
- scaleX = canvas.width / renderWidth
481
- scaleY = canvas.height / renderHeight
482
- }
483
- const x = Math.round((e.clientX - rect.left - offsetX) * scaleX)
484
- const y = Math.round((e.clientY - rect.top - offsetY) * scaleY)
468
+ // the canvas is rendered with object-fit: contain, so the pointer has
469
+ // to be mapped through the real surface rect (see remote-pointer.js)
470
+ const { x, y } = eventToRemotePos(
471
+ e,
472
+ canvas,
473
+ canvas.width,
474
+ canvas.height
475
+ )
485
476
  const event = window.ironRdp.DeviceEvent.mouseMove(x, y)
486
477
  const tx = new window.ironRdp.InputTransaction()
487
478
  tx.addEvent(event)
@@ -705,12 +696,7 @@ export default class RdpSession extends PureComponent {
705
696
  this.setupInputHandlers()
706
697
  }
707
698
 
708
- renderControl = () => {
709
- const contrlProps = this.getControlProps({
710
- fixedPosition: false,
711
- showExitFullscreen: false,
712
- className: 'mg1l'
713
- })
699
+ renderControlLeft = () => {
714
700
  const {
715
701
  id,
716
702
  hasRemoteFiles,
@@ -730,61 +716,67 @@ export default class RdpSession extends PureComponent {
730
716
  const uploadTitle = window.translate('upload') || 'Upload files to remote'
731
717
  const downloadTitle = window.translate('download') || 'Download files from remote'
732
718
  return (
733
- <div
734
- className='pd1 fix session-v-info block'
735
- >
736
- <div className='fleft'>
737
- <ReloadOutlined
738
- onClick={this.handleReInit}
739
- className='mg2r mg1l pointer'
740
- />
741
- <Select
742
- {...sleProps}
743
- >
744
- {
745
- this.getAllRes().map(d => {
746
- const v = d.id
747
- return (
748
- <Option
749
- key={v}
750
- value={v}
751
- >
752
- {d.width}x{d.height}
753
- </Option>
754
- )
755
- })
756
- }
757
- </Select>
758
- <EditOutlined
759
- onClick={this.handleEditResolutions}
760
- className='mg2r mg1l pointer'
761
- />
762
- {this.renderInfo()}
763
- <SwitchLabel
764
- {...scaleProps}
719
+ <>
720
+ <ReloadOutlined
721
+ onClick={this.handleReInit}
722
+ className='mg2r mg1l pointer'
723
+ />
724
+ <Select
725
+ {...sleProps}
726
+ >
727
+ {
728
+ this.getAllRes().map(d => {
729
+ const v = d.id
730
+ return (
731
+ <Option
732
+ key={v}
733
+ value={v}
734
+ >
735
+ {d.width}x{d.height}
736
+ </Option>
737
+ )
738
+ })
739
+ }
740
+ </Select>
741
+ <EditOutlined
742
+ onClick={this.handleEditResolutions}
743
+ className='mg2r mg1l pointer'
744
+ />
745
+ {this.renderInfo()}
746
+ <SwitchLabel
747
+ {...scaleProps}
748
+ />
749
+ <Tooltip title={uploadTitle}>
750
+ <UploadOutlined
751
+ onClick={this.handleUploadButtonClick}
752
+ className={`mg1r mg2l pointer rdp-file-transfer-btn${uploadReady ? ' rdp-download-flash' : ''}`}
765
753
  />
766
- <Tooltip title={uploadTitle}>
767
- <UploadOutlined
768
- onClick={this.handleUploadButtonClick}
769
- className={`mg1r mg2l pointer rdp-file-transfer-btn${uploadReady ? ' rdp-download-flash' : ''}`}
770
- />
771
- </Tooltip>
772
- <Tooltip title={downloadTitle}>
773
- <DownloadOutlined
774
- onClick={this.handleDownloadButtonClick}
775
- className={`mg2r mg1l pointer rdp-file-transfer-btn${hasRemoteFiles ? ' rdp-download-flash' : ' rdp-download-disabled'}`}
776
- />
777
- </Tooltip>
778
- <HelpIcon
779
- link='https://github.com/electerm/electerm/wiki/RDP-File-Transfer'
780
- className='mg2r mg1l'
754
+ </Tooltip>
755
+ <Tooltip title={downloadTitle}>
756
+ <DownloadOutlined
757
+ onClick={this.handleDownloadButtonClick}
758
+ className={`mg2r mg1l pointer rdp-file-transfer-btn${hasRemoteFiles ? ' rdp-download-flash' : ' rdp-download-disabled'}`}
781
759
  />
782
- </div>
783
- <div className='fright'>
784
- {this.props.fullscreenIcon()}
785
- <RemoteFloatControl {...contrlProps} />
786
- </div>
787
- </div>
760
+ </Tooltip>
761
+ <HelpIcon
762
+ link='https://github.com/electerm/electerm/wiki/RDP-File-Transfer'
763
+ className='mg2r mg1l'
764
+ />
765
+ </>
766
+ )
767
+ }
768
+
769
+ renderControlRight = () => {
770
+ const contrlProps = this.getControlProps({
771
+ fixedPosition: false,
772
+ showExitFullscreen: false,
773
+ className: 'mg1l'
774
+ })
775
+ return (
776
+ <>
777
+ {this.props.fullscreenIcon()}
778
+ <RemoteFloatControl {...contrlProps} />
779
+ </>
788
780
  )
789
781
  }
790
782
 
@@ -802,48 +794,26 @@ export default class RdpSession extends PureComponent {
802
794
  }
803
795
 
804
796
  render () {
805
- const { width: w, height: h } = this.props
806
- const { width, height, loading, scaleViewport } = this.state
807
- const innerWidth = w - 10
808
- const innerHeight = h - 80
809
- const wrapperStyle = {
810
- width: innerWidth + 'px',
811
- height: innerHeight + 'px',
812
- overflow: scaleViewport ? 'hidden' : 'auto'
813
- }
814
- const canvasProps = {
815
- width,
816
- height,
817
- tabIndex: 0
818
- }
819
- const cls = `rdp-session-wrap session-v-wrap${scaleViewport ? ' scale-viewport' : ''}`
820
- const sessProps = {
821
- className: cls,
822
- style: {
823
- width: w + 'px',
824
- height: h + 'px'
825
- }
826
- }
797
+ const { width, height, loading } = this.state
827
798
  const controlProps = this.getControlProps()
828
799
  return (
829
- <Spin spinning={loading}>
830
- <div
831
- {...sessProps}
832
- >
833
- {this.renderControl()}
834
- <RemoteFloatControl {...controlProps} />
835
- <div
836
- style={wrapperStyle}
837
- className='rdp-scroll-wrapper s-scroll-wrapper'
838
- >
839
- <canvas
840
- {...canvasProps}
841
- ref={this.canvasRef}
842
- />
843
- </div>
844
- {this.renderCredPrompt()}
845
- </div>
846
- </Spin>
800
+ <RemoteSessionShell
801
+ loading={loading}
802
+ fit={this.getFit()}
803
+ wrapClassName='rdp-session-wrap'
804
+ controlLeft={this.renderControlLeft()}
805
+ controlRight={this.renderControlRight()}
806
+ floatControl={<RemoteFloatControl {...controlProps} />}
807
+ overlay={this.renderCredPrompt()}
808
+ >
809
+ <canvas
810
+ className='rdp-canvas'
811
+ width={width}
812
+ height={height}
813
+ tabIndex={0}
814
+ ref={this.canvasRef}
815
+ />
816
+ </RemoteSessionShell>
847
817
  )
848
818
  }
849
819
  }
@@ -1,36 +1,18 @@
1
+ // RDP only styling — the session layout (control bar, viewport, fit mode)
2
+ // lives in the shared remote session shell: common/remote-session-shell.styl
3
+
4
+ // fit: the canvas box fills the viewport and the remote surface is letterboxed
5
+ // inside it with object-fit: contain, so the aspect ratio is kept and the
6
+ // letterbox keeps the app background
1
7
  .rdp-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
8
+ &.fit
9
+ .rdp-canvas
10
+ object-fit contain
16
11
 
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
12
+ // fit off: rendered at the remote desktop size, the viewport scrolls
13
+ .rdp-canvas
14
+ display block
15
+ outline none
34
16
 
35
17
  .rdp-file-transfer-btn
36
18
  font-size: 16px
@@ -45,9 +27,6 @@
45
27
  &:hover
46
28
  color: var(--text)
47
29
 
48
- canvas
49
- outline: none
50
-
51
30
  @keyframes rdp-download-flash
52
31
  0%, 100%
53
32
  color: var(--primary)
@@ -58,5 +37,3 @@ canvas
58
37
 
59
38
  .rdp-download-flash
60
39
  animation: rdp-download-flash 1s ease-in-out infinite
61
-
62
-
@@ -71,22 +71,6 @@
71
71
  overflow hidden
72
72
  .session-v-info
73
73
  line-height 30px
74
- .vnc-scroll-wrapper
75
- position relative
76
- background var(--main)
77
- z-index 299
78
- &::-webkit-scrollbar
79
- width 16px
80
- height 16px
81
- background var(--main-darker)
82
- &::-webkit-scrollbar-track
83
- background var(--main-darker)
84
- box-shadow inset 0 0 5px var(--main-darker)
85
- &::-webkit-scrollbar-thumb
86
- background var(--primary)
87
- border-radius 0
88
- &::-webkit-scrollbar-corner
89
- background var(--main-darker)
90
74
 
91
75
  .not-split-view > .ant-splitter-bar .ant-splitter-bar-dragger
92
76
  display none
@@ -4,10 +4,11 @@
4
4
 
5
5
  import { auto } from 'manate/react'
6
6
  import { pick } from 'lodash-es'
7
- import { Tabs, Spin } from 'antd'
7
+ import { Spin } from 'antd'
8
8
  import { lazy, Suspense } from 'react'
9
9
  import SettingModal from './setting-wrap'
10
10
  import LazyBoundary from '../common/lazy-boundary'
11
+ import ResponsiveTabs from '../common/responsive-tabs'
11
12
  import {
12
13
  settingMap,
13
14
  modals,
@@ -132,11 +133,15 @@ export default auto(function SettingModalWrap (props) {
132
133
  onChange: store.handleChangeSettingTab,
133
134
  destroyOnHidden: true,
134
135
  className: 'setting-tabs',
135
- type: 'card'
136
+ type: 'card',
137
+ // the panel turns into a phone style drill-down at 800px (same
138
+ // breakpoint as the media query in setting-wrap.styl), collapse the tab
139
+ // bar there too instead of letting antd hide the overflow tabs
140
+ isMobile: store.innerWidth <= settingPanelMobileBreakpoint
136
141
  }
137
142
  return (
138
143
  <>
139
- <Tabs
144
+ <ResponsiveTabs
140
145
  {...tabsProps}
141
146
  />
142
147
  <LazyBoundary>
@@ -571,6 +571,7 @@ export default class SettingTerminal extends Component {
571
571
  'rightClickSelectsWord',
572
572
  'pasteWhenContextMenu',
573
573
  'copyWhenSelect',
574
+ 'disableConfirmForLargeClipboardContent',
574
575
  'ctrlOrMetaOpenTerminalLink',
575
576
  'sftpPathFollowSsh',
576
577
  'sshSftpSplitView',
@@ -104,8 +104,12 @@ export default class Sftp extends Component {
104
104
  refs.remove(this.id)
105
105
  this.sftp && this.sftp.destroy()
106
106
  this.sftp = null
107
+ clearTimeout(this.timer)
108
+ this.timer = null
107
109
  clearTimeout(this.timer4)
108
110
  this.timer4 = null
111
+ clearTimeout(this.retryHandler)
112
+ this.retryHandler = null
109
113
  // Clear sort cache to prevent memory leaks
110
114
  this._sortCache?.clear()
111
115
  this._lastSortArgs = null
@@ -289,11 +293,29 @@ export default class Sftp extends Component {
289
293
  if (!path && this.sftp) {
290
294
  path = await this.getPwd(this.props.tab.username)
291
295
  }
296
+ if (!path) {
297
+ // No sftp connection yet (still connecting, or the previous attempt
298
+ // failed). There is no remote home to resolve, and navigating to an
299
+ // unknown path would blank the panel, so (re)start the connection and
300
+ // let its own listing decide the path.
301
+ if (
302
+ !this.sftp &&
303
+ !this.state.loadingSftp &&
304
+ !this.state.remoteLoading
305
+ ) {
306
+ this.initData(this.terminalId, this.port)
307
+ }
308
+ return
309
+ }
292
310
  path = normalizeRemotePath(path)
293
311
  } else {
294
312
  path = this.getLocalHome()
295
313
  }
296
314
 
315
+ if (typeof path !== 'string' || !path) {
316
+ return
317
+ }
318
+
297
319
  this.setState({
298
320
  [n]: path,
299
321
  [nt]: path
@@ -1029,6 +1051,9 @@ export default class Sftp extends Component {
1029
1051
  goParent = (type) => {
1030
1052
  const n = `${type}Path`
1031
1053
  const p = this.state[n]
1054
+ if (typeof p !== 'string' || !p) {
1055
+ return
1056
+ }
1032
1057
  let np = resolve(p, '..')
1033
1058
  if (type === typeMap.remote) {
1034
1059
  np = normalizeRemotePath(np)
@@ -1132,6 +1157,11 @@ export default class Sftp extends Component {
1132
1157
 
1133
1158
  renderParentItem = (type) => {
1134
1159
  const currentPath = this.state[`${type}Path`]
1160
+ // The path can be momentarily unknown (e.g. the remote home while the sftp
1161
+ // client is still connecting). Show no parent item rather than throwing.
1162
+ if (typeof currentPath !== 'string') {
1163
+ return null
1164
+ }
1135
1165
  const parentPath = resolve(currentPath, '..')
1136
1166
  // Don't render parent item if we're at the root
1137
1167
  if (parentPath === currentPath) {
@@ -19,7 +19,7 @@ export default function SidePanel (props) {
19
19
  }
20
20
  }, [props.leftSidePanelWidth, props.leftSideBarWidth])
21
21
  const dragProps = {
22
- min: 343,
22
+ min: 200,
23
23
  max: 600,
24
24
  width: props.leftSidePanelWidth,
25
25
  onDragEnd,
@@ -9,11 +9,10 @@ import {
9
9
  import {
10
10
  ReloadOutlined
11
11
  } from '@ant-design/icons'
12
- import {
13
- Spin
14
- } from 'antd'
15
12
  import * as ls from '../../common/safe-local-storage'
16
13
  import RemoteFloatControl from '../common/remote-float-control'
14
+ import RemoteSessionShell from '../common/remote-session-shell'
15
+ import { eventToRemotePos } from '../common/remote-pointer'
17
16
  import SwitchLabel from '../common/switch'
18
17
  import './spice.styl'
19
18
 
@@ -53,24 +52,19 @@ export default class SpiceSession extends PureComponent {
53
52
  }
54
53
 
55
54
  // The spice client sends event.offsetX/offsetY to the remote as-is, but when
56
- // the canvas is CSS-scaled (scaleViewport or the lib's maxWidth/maxHeight),
57
- // those are display pixels instead of remote surface pixels. On high
58
- // resolution remote screens the pointer then only reaches the top-left
59
- // region. Convert display coords to surface coords (the canvas box is
60
- // letterboxed by object-fit: contain) before the lib's handlers run.
55
+ // the canvas is CSS-scaled (fit mode), those are display pixels instead of
56
+ // remote surface pixels. On high resolution remote screens the pointer then
57
+ // only reaches the top-left region. Convert display coords to surface coords
58
+ // before the lib's handlers run (same mapping as rdp, see remote-pointer.js).
61
59
  handleMouseScaleFix = (e) => {
62
60
  const canvas = this.domRef.current?.querySelector('canvas')
63
61
  if (!canvas || !canvas.width || !canvas.height) return
64
- const rect = canvas.getBoundingClientRect()
65
- if (!rect.width || !rect.height) return
66
- const scale = Math.min(rect.width / canvas.width, rect.height / canvas.height)
67
- const x = (e.clientX - rect.left - (rect.width - canvas.width * scale) / 2) / scale
68
- const y = (e.clientY - rect.top - (rect.height - canvas.height * scale) / 2) / scale
62
+ const { x, y } = eventToRemotePos(e, canvas, canvas.width, canvas.height)
69
63
  Object.defineProperty(e, 'offsetX', {
70
- value: Math.max(0, Math.min(x, canvas.width - 1))
64
+ value: x
71
65
  })
72
66
  Object.defineProperty(e, 'offsetY', {
73
- value: Math.max(0, Math.min(y, canvas.height - 1))
67
+ value: y
74
68
  })
75
69
  }
76
70
 
@@ -123,13 +117,30 @@ export default class SpiceSession extends PureComponent {
123
117
  }
124
118
 
125
119
  calcCanvasSize = () => {
126
- const { width, height } = this.props
120
+ const { width, height } = this.viewSize || {}
121
+ if (width && height) {
122
+ return { width, height }
123
+ }
124
+ const { width: w, height: h } = this.props
127
125
  return {
128
- width: width - 10,
129
- height: height - 80
126
+ width: w - 10,
127
+ height: h - 80
130
128
  }
131
129
  }
132
130
 
131
+ // fullscreen always fills the screen, the per tab setting only applies
132
+ // windowed; the switch is not reachable in fullscreen anyway
133
+ getFit = () => {
134
+ if (this.props.fullscreen) {
135
+ return true
136
+ }
137
+ return !!this.state.scaleViewport
138
+ }
139
+
140
+ handleViewportResize = (width, height) => {
141
+ this.viewSize = { width, height }
142
+ }
143
+
133
144
  handleSendCtrlAltDel = () => {
134
145
  if (this.spiceConn && window.spiceHtml5?.sendCtrlAltDel) {
135
146
  window.spiceHtml5.sendCtrlAltDel(this.spiceConn)
@@ -163,12 +174,7 @@ export default class SpiceSession extends PureComponent {
163
174
  }
164
175
  }
165
176
 
166
- renderControl = () => {
167
- const contrlProps = this.getControlProps({
168
- fixedPosition: false,
169
- showExitFullscreen: false,
170
- className: 'mg1l'
171
- })
177
+ renderControlLeft = () => {
172
178
  const scaleProps = {
173
179
  checked: this.state.scaleViewport,
174
180
  onChange: this.handleScaleViewChange,
@@ -176,22 +182,30 @@ export default class SpiceSession extends PureComponent {
176
182
  className: 'mg1l'
177
183
  }
178
184
  return (
179
- <div className='pd1 fix session-v-info'>
180
- <div className='fleft'>
181
- <ReloadOutlined
182
- onClick={this.handleReInit}
183
- className='mg2r mg1l pointer'
184
- />
185
- {this.renderInfo()}
186
- <SwitchLabel
187
- {...scaleProps}
188
- />
189
- </div>
190
- <div className='fright'>
191
- {this.props.fullscreenIcon()}
192
- <RemoteFloatControl {...contrlProps} />
193
- </div>
194
- </div>
185
+ <>
186
+ <ReloadOutlined
187
+ onClick={this.handleReInit}
188
+ className='mg2r mg1l pointer'
189
+ />
190
+ {this.renderInfo()}
191
+ <SwitchLabel
192
+ {...scaleProps}
193
+ />
194
+ </>
195
+ )
196
+ }
197
+
198
+ renderControlRight = () => {
199
+ const contrlProps = this.getControlProps({
200
+ fixedPosition: false,
201
+ showExitFullscreen: false,
202
+ className: 'mg1l'
203
+ })
204
+ return (
205
+ <>
206
+ {this.props.fullscreenIcon()}
207
+ <RemoteFloatControl {...contrlProps} />
208
+ </>
195
209
  )
196
210
  }
197
211
 
@@ -298,41 +312,24 @@ export default class SpiceSession extends PureComponent {
298
312
  }
299
313
 
300
314
  render () {
301
- const { width: w, height: h } = this.props
302
- const { loading, scaleViewport } = this.state
303
- const { width: innerWidth, height: innerHeight } = this.calcCanvasSize()
304
- const wrapperStyle = {
305
- width: innerWidth + 'px',
306
- height: innerHeight + 'px',
307
- overflow: scaleViewport ? 'hidden' : 'auto'
308
- }
309
- const cls = `spice-session-wrap session-v-wrap${scaleViewport ? ' scale-viewport' : ''}`
315
+ const { loading } = this.state
310
316
  const contrlProps = this.getControlProps()
311
- const sessProps = {
312
- className: cls,
313
- style: {
314
- width: w + 'px',
315
- height: h + 'px'
316
- }
317
- }
318
317
  return (
319
- <Spin spinning={loading}>
318
+ <RemoteSessionShell
319
+ loading={loading}
320
+ fit={this.getFit()}
321
+ wrapClassName='spice-session-wrap'
322
+ onViewportResize={this.handleViewportResize}
323
+ controlLeft={this.renderControlLeft()}
324
+ controlRight={this.renderControlRight()}
325
+ floatControl={<RemoteFloatControl {...contrlProps} />}
326
+ >
320
327
  <div
321
- {...sessProps}
322
- >
323
- {this.renderControl()}
324
- <RemoteFloatControl {...contrlProps} />
325
- <div
326
- style={wrapperStyle}
327
- className='spice-scroll-wrapper s-scroll-wrapper'
328
- >
329
- <div
330
- ref={this.domRef}
331
- id={this.screenId}
332
- />
333
- </div>
334
- </div>
335
- </Spin>
328
+ className='spice-session-mount'
329
+ id={this.screenId}
330
+ ref={this.domRef}
331
+ />
332
+ </RemoteSessionShell>
336
333
  )
337
334
  }
338
335
  }