@electerm/electerm-react 3.15.151 → 3.15.153
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/common/constants.js +1 -0
- package/client/components/footer/batch-input.jsx +94 -107
- package/client/components/footer/footer-entry.jsx +27 -4
- package/client/components/footer/footer.styl +17 -13
- package/client/components/layout/layout.jsx +2 -1
- package/client/components/main/main.jsx +3 -1
- package/client/components/quick-commands/quick-command-item.jsx +1 -1
- package/client/components/session/session.jsx +5 -2
- package/client/components/setting-panel/setting-modal.jsx +1 -0
- package/client/components/setting-panel/setting-wrap.jsx +13 -4
- package/client/components/sidebar/info-modal.jsx +1 -1
- package/client/components/sidebar/transfer-list.jsx +1 -1
- package/client/components/tabs/index.jsx +73 -0
- package/client/css/mobile.styl +99 -5
- package/client/store/common.js +12 -3
- package/client/store/init-state.js +3 -1
- package/package.json +1 -1
|
@@ -197,6 +197,7 @@ export const commonLineEndings = commonTxLineEndings
|
|
|
197
197
|
|
|
198
198
|
export const maxBatchInput = 30
|
|
199
199
|
export const windowControlWidth = 94
|
|
200
|
+
export const mobileBreakpoint = 800
|
|
200
201
|
export const baseUpdateCheckUrls = [
|
|
201
202
|
packInfo.homepage,
|
|
202
203
|
'https://gitee.com/github-zxdong262/electerm/raw/gh-pages'
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* batch input module
|
|
3
|
+
*
|
|
4
|
+
* Unified click-to-open behavior for mobile and desktop:
|
|
5
|
+
* - click the compact trigger -> expand the textarea + tab-select panel
|
|
6
|
+
* - click outside the panel -> collapse it
|
|
7
|
+
* - press Enter in the textarea -> send `cmd` to all selected tabs
|
|
8
|
+
*
|
|
9
|
+
* On desktop the compact trigger is rendered as a read-only Input that only
|
|
10
|
+
* serves as a clickable affordance — it is never a real input. On mobile it
|
|
11
|
+
* is a small "B" button to save horizontal space. The open/close logic is
|
|
12
|
+
* identical for both.
|
|
3
13
|
*/
|
|
4
14
|
|
|
5
|
-
import { Component } from 'react'
|
|
15
|
+
import { Component, createRef } from 'react'
|
|
6
16
|
import {
|
|
7
|
-
|
|
17
|
+
Button,
|
|
8
18
|
Input
|
|
9
19
|
} from 'antd'
|
|
10
20
|
import {
|
|
11
|
-
batchInputLsKey,
|
|
12
21
|
terminalWebType,
|
|
13
22
|
terminalRdpType,
|
|
14
23
|
terminalVncType
|
|
@@ -24,97 +33,71 @@ export default class BatchInput extends Component {
|
|
|
24
33
|
super(props)
|
|
25
34
|
this.state = {
|
|
26
35
|
cmd: '',
|
|
27
|
-
open: false
|
|
28
|
-
enter: false
|
|
36
|
+
open: false
|
|
29
37
|
}
|
|
38
|
+
this.outerRef = createRef()
|
|
39
|
+
this.textAreaRef = createRef()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
componentDidMount () {
|
|
43
|
+
document.addEventListener('mousedown', this.handleDocClick)
|
|
30
44
|
}
|
|
31
45
|
|
|
32
46
|
componentWillUnmount () {
|
|
33
|
-
|
|
47
|
+
document.removeEventListener('mousedown', this.handleDocClick)
|
|
34
48
|
}
|
|
35
49
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
// Collapse the panel when the user clicks anywhere outside of it.
|
|
51
|
+
// Clicks inside an antd Popover (e.g. the tab-select popover, which renders
|
|
52
|
+
// in a portal outside this node) are treated as "inside" so interacting with
|
|
53
|
+
// tab selection does not collapse the panel.
|
|
54
|
+
handleDocClick = (ev) => {
|
|
55
|
+
if (!this.state.open) {
|
|
40
56
|
return
|
|
41
57
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
cmd: '',
|
|
46
|
-
open: false
|
|
47
|
-
})
|
|
48
|
-
e.stopPropagation()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
handleChange = (v = '') => {
|
|
52
|
-
let vv = v.replace(/^\d+:/, '').replace(/\n$/, '')
|
|
53
|
-
if (vv === batchInputLsKey) {
|
|
54
|
-
window.store.clearBatchInput()
|
|
55
|
-
vv = ''
|
|
58
|
+
const node = this.outerRef.current
|
|
59
|
+
if (node && node.contains(ev.target)) {
|
|
60
|
+
return
|
|
56
61
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
open: false
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
handleClick = () => {
|
|
64
|
-
this.setState({
|
|
65
|
-
open: true
|
|
66
|
-
})
|
|
67
|
-
window.store.filterBatchInputSelectedTabIds()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
handleBlur = () => {
|
|
71
|
-
this.setState({
|
|
72
|
-
open: false
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
mapper = (v, i) => {
|
|
77
|
-
return {
|
|
78
|
-
value: `${i}:${v}`,
|
|
79
|
-
label: v
|
|
62
|
+
if (ev.target && ev.target.closest && ev.target.closest('.ant-popover')) {
|
|
63
|
+
return
|
|
80
64
|
}
|
|
65
|
+
this.setState({ open: false })
|
|
81
66
|
}
|
|
82
67
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
68
|
+
handleToggle = () => {
|
|
69
|
+
const willOpen = !this.state.open
|
|
70
|
+
if (willOpen) {
|
|
71
|
+
window.store.filterBatchInputSelectedTabIds()
|
|
87
72
|
}
|
|
73
|
+
this.setState({ open: willOpen }, () => {
|
|
74
|
+
if (willOpen && this.textAreaRef.current) {
|
|
75
|
+
this.textAreaRef.current.focus()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
88
78
|
}
|
|
89
79
|
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
this.renderClear()
|
|
96
|
-
]
|
|
80
|
+
handleSubmit = (ev) => {
|
|
81
|
+
const { batchInputSelectedTabIds } = this.props
|
|
82
|
+
const { cmd } = this.state
|
|
83
|
+
if (!cmd.trim()) {
|
|
84
|
+
return
|
|
97
85
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
handleMouseEnter = () => {
|
|
102
|
-
clearTimeout(this.timer)
|
|
86
|
+
window.store.addBatchInput(cmd)
|
|
87
|
+
this.props.input(cmd, Array.from(batchInputSelectedTabIds))
|
|
103
88
|
this.setState({
|
|
104
|
-
|
|
89
|
+
cmd: '',
|
|
90
|
+
open: false
|
|
105
91
|
})
|
|
92
|
+
ev?.stopPropagation()
|
|
106
93
|
}
|
|
107
94
|
|
|
108
|
-
|
|
95
|
+
handleChange = (ev) => {
|
|
109
96
|
this.setState({
|
|
110
|
-
|
|
97
|
+
cmd: ev.target.value
|
|
111
98
|
})
|
|
112
99
|
}
|
|
113
100
|
|
|
114
|
-
handleMouseLeave = () => {
|
|
115
|
-
this.timer = setTimeout(this.leave, 5000)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
101
|
getTabs = () => {
|
|
119
102
|
const { activeTabId } = this.props
|
|
120
103
|
return deepCopy(this.props.tabs.filter(tab => {
|
|
@@ -122,43 +105,54 @@ export default class BatchInput extends Component {
|
|
|
122
105
|
tab.type !== terminalRdpType &&
|
|
123
106
|
tab.type !== terminalVncType
|
|
124
107
|
})).sort((a, b) => {
|
|
125
|
-
//
|
|
108
|
+
// current tab goes first
|
|
126
109
|
if (a.id === activeTabId) return -1
|
|
127
110
|
if (b.id === activeTabId) return 1
|
|
128
111
|
return 0
|
|
129
112
|
})
|
|
130
113
|
}
|
|
131
114
|
|
|
132
|
-
|
|
133
|
-
const {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
115
|
+
renderTrigger () {
|
|
116
|
+
const { isMobile } = this.props
|
|
117
|
+
if (isMobile) {
|
|
118
|
+
return (
|
|
119
|
+
<Button
|
|
120
|
+
size='small'
|
|
121
|
+
type='text'
|
|
122
|
+
onClick={this.handleToggle}
|
|
123
|
+
>
|
|
124
|
+
B
|
|
125
|
+
</Button>
|
|
126
|
+
)
|
|
144
127
|
}
|
|
128
|
+
// Desktop: an input-shaped, read-only affordance. It is NOT a real input —
|
|
129
|
+
// clicking it expands the panel where the user actually types.
|
|
130
|
+
return (
|
|
131
|
+
<Input
|
|
132
|
+
size='small'
|
|
133
|
+
readOnly
|
|
134
|
+
placeholder={e('batchInput')}
|
|
135
|
+
className='batch-input-holder'
|
|
136
|
+
style={{ cursor: 'pointer' }}
|
|
137
|
+
onClick={this.handleToggle}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
render () {
|
|
143
|
+
const { open } = this.state
|
|
144
|
+
const { batchInputSelectedTabIds } = this.props
|
|
145
145
|
const cls = classNames(
|
|
146
146
|
'batch-input-outer',
|
|
147
|
-
{
|
|
148
|
-
'bi-show': open || enter
|
|
149
|
-
}
|
|
147
|
+
{ 'bi-show': open }
|
|
150
148
|
)
|
|
151
|
-
const inputProps = {
|
|
152
|
-
size: 'small',
|
|
153
|
-
placeholder: e('batchInput'),
|
|
154
|
-
className: 'batch-input-holder'
|
|
155
|
-
}
|
|
156
149
|
const textAreaProps = {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
150
|
+
ref: this.textAreaRef,
|
|
151
|
+
onPressEnter: this.handleSubmit,
|
|
152
|
+
onChange: this.handleChange,
|
|
153
|
+
value: this.state.cmd,
|
|
160
154
|
size: 'small',
|
|
161
|
-
autoSize: { minRows:
|
|
155
|
+
autoSize: { minRows: 2, maxRows: 6 },
|
|
162
156
|
placeholder: e('batchInput'),
|
|
163
157
|
allowClear: true
|
|
164
158
|
}
|
|
@@ -173,22 +167,15 @@ export default class BatchInput extends Component {
|
|
|
173
167
|
return (
|
|
174
168
|
<span
|
|
175
169
|
className={cls}
|
|
176
|
-
|
|
177
|
-
onMouseLeave={this.handleMouseLeave}
|
|
170
|
+
ref={this.outerRef}
|
|
178
171
|
>
|
|
179
172
|
<span className='bi-compact'>
|
|
180
|
-
|
|
181
|
-
{...inputProps}
|
|
182
|
-
/>
|
|
173
|
+
{this.renderTrigger()}
|
|
183
174
|
</span>
|
|
184
175
|
<span className='bi-full'>
|
|
185
|
-
<
|
|
186
|
-
{...
|
|
187
|
-
|
|
188
|
-
<Input.TextArea
|
|
189
|
-
{...textAreaProps}
|
|
190
|
-
/>
|
|
191
|
-
</AutoComplete>
|
|
176
|
+
<Input.TextArea
|
|
177
|
+
{...textAreaProps}
|
|
178
|
+
/>
|
|
192
179
|
<TabSelect {...tabSelectProps} />
|
|
193
180
|
</span>
|
|
194
181
|
</span>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { auto } from 'manate/react'
|
|
2
2
|
import {
|
|
3
|
-
Select
|
|
3
|
+
Select,
|
|
4
|
+
Dropdown
|
|
4
5
|
} from 'antd'
|
|
5
|
-
import { InfoCircleOutlined } from '@ant-design/icons'
|
|
6
|
+
import { InfoCircleOutlined, TranslationOutlined } from '@ant-design/icons'
|
|
6
7
|
import './footer.styl'
|
|
7
8
|
import { statusMap } from '../../common/constants'
|
|
8
9
|
import BatchInput from './batch-input'
|
|
@@ -54,9 +55,9 @@ export default auto(function FooterEntry (props) {
|
|
|
54
55
|
const batchProps = {
|
|
55
56
|
input: batchInput,
|
|
56
57
|
tabs: store.tabs,
|
|
57
|
-
batchInputs: store.batchInputs,
|
|
58
58
|
batchInputSelectedTabIds: store.batchInputSelectedTabIds,
|
|
59
|
-
activeTabId: store.activeTabId
|
|
59
|
+
activeTabId: store.activeTabId,
|
|
60
|
+
isMobile: store.isMobile
|
|
60
61
|
}
|
|
61
62
|
return (
|
|
62
63
|
<div className='terminal-footer-unit terminal-footer-center'>
|
|
@@ -96,6 +97,28 @@ export default auto(function FooterEntry (props) {
|
|
|
96
97
|
size: 'small',
|
|
97
98
|
popupMatchSelectWidth: false
|
|
98
99
|
}
|
|
100
|
+
if (props.store.isMobile) {
|
|
101
|
+
const items = encodes.map(k => {
|
|
102
|
+
return {
|
|
103
|
+
key: k,
|
|
104
|
+
label: k.toUpperCase(),
|
|
105
|
+
onClick: () => handleSwitchEncoding(k)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
return (
|
|
109
|
+
<div className='terminal-footer-unit terminal-footer-info'>
|
|
110
|
+
<Dropdown
|
|
111
|
+
menu={{ items }}
|
|
112
|
+
placement='topRight'
|
|
113
|
+
trigger={['click']}
|
|
114
|
+
>
|
|
115
|
+
<TranslationOutlined
|
|
116
|
+
className='pointer font18 mobile-encode-trigger'
|
|
117
|
+
/>
|
|
118
|
+
</Dropdown>
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
99
122
|
return (
|
|
100
123
|
<div className='terminal-footer-unit terminal-footer-info'>
|
|
101
124
|
<div className='fleft relative'>
|
|
@@ -39,22 +39,26 @@
|
|
|
39
39
|
// margin so the history icon stays roughly equidistant from AI and Q
|
|
40
40
|
margin-left 3px
|
|
41
41
|
.batch-input-outer
|
|
42
|
+
position relative
|
|
42
43
|
.bi-full
|
|
43
44
|
display none
|
|
44
45
|
&.bi-show
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
display inline
|
|
46
|
+
// hide the trigger while the popup is open so we don't show two
|
|
47
|
+
// "batch input" fields at once (desktop trigger is an input-shaped affordance)
|
|
48
48
|
.bi-compact
|
|
49
49
|
display none
|
|
50
|
+
.bi-full
|
|
51
|
+
display flex
|
|
52
|
+
flex-direction column
|
|
53
|
+
gap 6px
|
|
54
|
+
position absolute
|
|
55
|
+
bottom 100%
|
|
56
|
+
left 0
|
|
57
|
+
z-index 1000
|
|
58
|
+
background var(--main)
|
|
59
|
+
padding 8px 10px
|
|
60
|
+
box-shadow 0 -2px 8px rgba(0, 0, 0, 0.2)
|
|
61
|
+
border-radius 4px 4px 0 0
|
|
62
|
+
min-width 360px
|
|
50
63
|
.batch-input-holder
|
|
51
|
-
width 100px
|
|
52
|
-
@media (max-width: 500px)
|
|
53
|
-
.batch-input-outer
|
|
54
|
-
&.bi-show
|
|
55
|
-
position fixed
|
|
56
|
-
left 100px
|
|
57
|
-
right 0
|
|
58
|
-
bottom 0
|
|
59
|
-
z-index 100
|
|
60
|
-
background var(--main)
|
|
64
|
+
width 100px
|
|
@@ -113,7 +113,9 @@ export default auto(function Index (props) {
|
|
|
113
113
|
'not-win': !isWin,
|
|
114
114
|
'qm-pinned': pinnedQuickCommandBar,
|
|
115
115
|
fullscreen,
|
|
116
|
-
'is-main': !isSecondInstance
|
|
116
|
+
'is-main': !isSecondInstance,
|
|
117
|
+
'is-mobile': store.isMobile,
|
|
118
|
+
'is-desktop': !store.isMobile
|
|
117
119
|
})
|
|
118
120
|
const ext1 = {
|
|
119
121
|
className: cls
|
|
@@ -111,7 +111,7 @@ export default class QuickCommandsItem extends PureComponent {
|
|
|
111
111
|
title={this.renderTooltip()}
|
|
112
112
|
placement='top'
|
|
113
113
|
mouseEnterDelay={0.5}
|
|
114
|
-
|
|
114
|
+
classNames={{ root: 'qm-tooltip-overlay' }}
|
|
115
115
|
>
|
|
116
116
|
<Button
|
|
117
117
|
key={id}
|
|
@@ -594,6 +594,9 @@ export default class SessionWrapper extends Component {
|
|
|
594
594
|
}
|
|
595
595
|
|
|
596
596
|
renderSplitToggle = () => {
|
|
597
|
+
if (window.store.isMobile) {
|
|
598
|
+
return null
|
|
599
|
+
}
|
|
597
600
|
if (!this.canSplitView() || this.isNotTerminalType()) {
|
|
598
601
|
return null
|
|
599
602
|
}
|
|
@@ -673,8 +676,8 @@ export default class SessionWrapper extends Component {
|
|
|
673
676
|
onClick={() => this.onChangePane(types[i])}
|
|
674
677
|
>
|
|
675
678
|
<span className='type-tab-txt'>
|
|
676
|
-
<span className='
|
|
677
|
-
<span className='
|
|
679
|
+
<span className='w800'>{e(type)}</span>
|
|
680
|
+
<span className='l800'>{simpleMapper[type]}</span>
|
|
678
681
|
<span className='type-tab-line' />
|
|
679
682
|
</span>
|
|
680
683
|
</span>
|
|
@@ -16,6 +16,18 @@ export default class SettingWrap extends Component {
|
|
|
16
16
|
)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
renderRightClose () {
|
|
20
|
+
if (this.props.isMobile) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
return (
|
|
24
|
+
<CloseCircleOutlined
|
|
25
|
+
className='close-setting-wrap-icon close-setting-wrap'
|
|
26
|
+
onClick={this.props.onCancel}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
render () {
|
|
20
32
|
const pops = {
|
|
21
33
|
open: this.props.visible,
|
|
@@ -29,10 +41,7 @@ export default class SettingWrap extends Component {
|
|
|
29
41
|
<Drawer
|
|
30
42
|
{...pops}
|
|
31
43
|
>
|
|
32
|
-
|
|
33
|
-
className='close-setting-wrap-icon close-setting-wrap'
|
|
34
|
-
onClick={this.props.onCancel}
|
|
35
|
-
/>
|
|
44
|
+
{this.renderRightClose()}
|
|
36
45
|
<CloseCircleOutlined
|
|
37
46
|
className='close-setting-wrap alt-close-setting-wrap'
|
|
38
47
|
onClick={this.props.onCancel}
|
|
@@ -39,7 +39,7 @@ export default auto(function InfoModal (props) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const renderCheckUpdate = () => {
|
|
42
|
-
if (window.et.
|
|
42
|
+
if (window.et.disableUpgradeCheck || checkSkipSrc(props.installSrc)) {
|
|
43
43
|
return null
|
|
44
44
|
}
|
|
45
45
|
const {
|
|
@@ -37,7 +37,7 @@ export default memo(function TransferList (props) {
|
|
|
37
37
|
const popProps = {
|
|
38
38
|
placement: 'right',
|
|
39
39
|
destroyOnHidden: true,
|
|
40
|
-
|
|
40
|
+
classNames: { root: 'transfer-list-card' },
|
|
41
41
|
content: <TransferModal {...transferModalProps} />
|
|
42
42
|
}
|
|
43
43
|
return (
|
|
@@ -83,6 +83,9 @@ export default class Tabs extends Component {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
isOverflow = () => {
|
|
86
|
+
if (this.props.isMobile || !this.domRef.current) {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
86
89
|
const { tabs = [] } = this.props
|
|
87
90
|
const len = tabs.length
|
|
88
91
|
const addBtnWidth = 22
|
|
@@ -120,6 +123,9 @@ export default class Tabs extends Component {
|
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
adjustScroll = () => {
|
|
126
|
+
if (this.props.isMobile || !this.domRef.current) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
123
129
|
const { tabs, currentBatchTabId, batch } = this.props
|
|
124
130
|
const index = tabs.findIndex(t => t.id === currentBatchTabId)
|
|
125
131
|
const tabsDomWith = Array.from(
|
|
@@ -139,6 +145,9 @@ export default class Tabs extends Component {
|
|
|
139
145
|
}
|
|
140
146
|
|
|
141
147
|
handleScrollLeft = () => {
|
|
148
|
+
if (!this.domRef.current) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
142
151
|
let { scrollLeft } = this.domRef.current
|
|
143
152
|
scrollLeft = scrollLeft - tabMargin - tabWidth
|
|
144
153
|
if (scrollLeft < 0) {
|
|
@@ -148,6 +157,9 @@ export default class Tabs extends Component {
|
|
|
148
157
|
}
|
|
149
158
|
|
|
150
159
|
handleScrollRight = () => {
|
|
160
|
+
if (!this.domRef.current) {
|
|
161
|
+
return
|
|
162
|
+
}
|
|
151
163
|
let { scrollLeft } = this.domRef.current
|
|
152
164
|
scrollLeft = scrollLeft + tabMargin + tabWidth
|
|
153
165
|
if (scrollLeft < 0) {
|
|
@@ -349,8 +361,69 @@ export default class Tabs extends Component {
|
|
|
349
361
|
return null
|
|
350
362
|
}
|
|
351
363
|
|
|
364
|
+
renderMobileTabsDropdown (items) {
|
|
365
|
+
return (
|
|
366
|
+
<Dropdown
|
|
367
|
+
menu={{ items }}
|
|
368
|
+
placement='bottomRight'
|
|
369
|
+
trigger={['click']}
|
|
370
|
+
>
|
|
371
|
+
<DownOutlined className='tabs-dd-icon' />
|
|
372
|
+
</Dropdown>
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
renderMobileTabsExtra (items) {
|
|
377
|
+
return (
|
|
378
|
+
<div className='mobile-tabs-extra'>
|
|
379
|
+
{this.renderAddBtn()}
|
|
380
|
+
{
|
|
381
|
+
items && items.length
|
|
382
|
+
? this.renderMobileTabsDropdown(items)
|
|
383
|
+
: null
|
|
384
|
+
}
|
|
385
|
+
</div>
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
renderMobileTabs () {
|
|
390
|
+
const { tabs = [], currentBatchTabId } = this.props
|
|
391
|
+
const currentTab = tabs.find(t => t.id === currentBatchTabId)
|
|
392
|
+
const items = tabs.map((t, i) => {
|
|
393
|
+
return {
|
|
394
|
+
key: i + '##' + t.id,
|
|
395
|
+
label: (
|
|
396
|
+
<span><TabTitle tab={t} /></span>
|
|
397
|
+
),
|
|
398
|
+
onClick: () => this.handleClickMenu({ key: i + '##' + t.id })
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
const tabProps = {
|
|
402
|
+
...this.props,
|
|
403
|
+
tab: currentTab,
|
|
404
|
+
isLast: true,
|
|
405
|
+
addTab: this.handleTabAdd,
|
|
406
|
+
tabIndex: 0
|
|
407
|
+
}
|
|
408
|
+
const currentTabEl = currentTab
|
|
409
|
+
? <Tab {...tabProps} key={currentTab.id} />
|
|
410
|
+
: null
|
|
411
|
+
return (
|
|
412
|
+
<div className='tabs mobile-tabs' ref={this.tabsRef}>
|
|
413
|
+
<div className='tabs-inner mobile-tabs-inner'>
|
|
414
|
+
{currentTabEl}
|
|
415
|
+
</div>
|
|
416
|
+
{this.renderMobileTabsExtra(items)}
|
|
417
|
+
{this.renderWindowControl()}
|
|
418
|
+
</div>
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
|
|
352
422
|
renderTabs () {
|
|
353
423
|
const { overflow } = this.state
|
|
424
|
+
if (this.props.isMobile) {
|
|
425
|
+
return this.renderMobileTabs()
|
|
426
|
+
}
|
|
354
427
|
return (
|
|
355
428
|
<div className='tabs' ref={this.tabsRef}>
|
|
356
429
|
{this.renderContent()}
|
package/client/css/mobile.styl
CHANGED
|
@@ -1,8 +1,102 @@
|
|
|
1
|
-
.
|
|
1
|
+
.l800
|
|
2
2
|
display none !important
|
|
3
3
|
|
|
4
|
-
@media (max-width:
|
|
5
|
-
.
|
|
4
|
+
@media (max-width: 800px)
|
|
5
|
+
.w800
|
|
6
6
|
display none !important
|
|
7
|
-
.
|
|
8
|
-
display inline !important
|
|
7
|
+
.l800
|
|
8
|
+
display inline !important
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Mobile UI driven by store.isMobile (is-mobile class on root)
|
|
12
|
+
// Breakpoint: 800px (see mobileBreakpoint in constants.js)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
.is-mobile
|
|
15
|
+
// ---- 1. Tabs: show only current tab + dropdown, close icon always visible
|
|
16
|
+
.tabs.mobile-tabs
|
|
17
|
+
display flex
|
|
18
|
+
align-items center
|
|
19
|
+
overflow hidden
|
|
20
|
+
// leave room for window controls on desktop platforms resized narrow
|
|
21
|
+
&.not-system-ui.not-mac.not-webapp
|
|
22
|
+
.tabs.mobile-tabs
|
|
23
|
+
padding-right 94px
|
|
24
|
+
.mobile-tabs-inner
|
|
25
|
+
flex 1
|
|
26
|
+
min-width 0
|
|
27
|
+
overflow hidden
|
|
28
|
+
display flex
|
|
29
|
+
align-items center
|
|
30
|
+
margin-left 0 !important
|
|
31
|
+
width auto !important
|
|
32
|
+
.tab
|
|
33
|
+
flex 1
|
|
34
|
+
max-width none
|
|
35
|
+
min-width 0
|
|
36
|
+
width auto
|
|
37
|
+
text-align left
|
|
38
|
+
// close icon always visible on mobile
|
|
39
|
+
.tab-close
|
|
40
|
+
display block
|
|
41
|
+
.mobile-tabs-extra
|
|
42
|
+
flex-shrink 0
|
|
43
|
+
display flex
|
|
44
|
+
align-items center
|
|
45
|
+
padding 0 10px
|
|
46
|
+
line-height 36px
|
|
47
|
+
z-index 20
|
|
48
|
+
.tabs-add-btn
|
|
49
|
+
margin 0
|
|
50
|
+
&.empty
|
|
51
|
+
font-size 20px
|
|
52
|
+
margin 0
|
|
53
|
+
.tabs-dd-icon
|
|
54
|
+
margin-left 8px
|
|
55
|
+
font-size 14px
|
|
56
|
+
color var(--text)
|
|
57
|
+
&:hover
|
|
58
|
+
color var(--text-light)
|
|
59
|
+
// tabs-extra must not render on mobile (defensive css too)
|
|
60
|
+
.tabs-extra
|
|
61
|
+
display none !important
|
|
62
|
+
|
|
63
|
+
// ---- 2. Footer batch input: show "B" button, click to expand
|
|
64
|
+
// match the quick-commands "Q" button: no flex-grow, same small margin
|
|
65
|
+
.terminal-footer-center
|
|
66
|
+
flex-grow 0
|
|
67
|
+
margin-left 3px
|
|
68
|
+
.batch-input-outer
|
|
69
|
+
&.bi-show
|
|
70
|
+
.bi-full
|
|
71
|
+
// full-width fixed popup above the footer (mobile is narrow)
|
|
72
|
+
position fixed
|
|
73
|
+
left 0
|
|
74
|
+
right 0
|
|
75
|
+
bottom 36px
|
|
76
|
+
z-index 200
|
|
77
|
+
background var(--main)
|
|
78
|
+
padding 8px 10px
|
|
79
|
+
box-shadow 0 -2px 8px rgba(0, 0, 0, 0.2)
|
|
80
|
+
width auto
|
|
81
|
+
min-width 0
|
|
82
|
+
border-radius 0
|
|
83
|
+
|
|
84
|
+
// ---- 3. Footer encode select: show icon, click to open dropdown
|
|
85
|
+
// match antd Button small icon size (~14px) used by sibling icons
|
|
86
|
+
.mobile-encode-trigger,
|
|
87
|
+
.terminal-info-icon
|
|
88
|
+
font-size 14px !important
|
|
89
|
+
line-height 36px
|
|
90
|
+
padding 0 4px
|
|
91
|
+
&:hover
|
|
92
|
+
color var(--text-light)
|
|
93
|
+
|
|
94
|
+
// ---- 4. Modal: full width / full bleed on mobile
|
|
95
|
+
.custom-modal-container
|
|
96
|
+
padding 0
|
|
97
|
+
.custom-modal-content
|
|
98
|
+
width 100% !important
|
|
99
|
+
max-width 100vw
|
|
100
|
+
margin-top 0
|
|
101
|
+
min-height 100vh
|
|
102
|
+
border-radius 0
|
package/client/store/common.js
CHANGED
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
addPanelWidthLsKey,
|
|
12
12
|
dismissDelKeyTipLsKey,
|
|
13
13
|
connectionMap,
|
|
14
|
-
lastAiChatSessionIdKey
|
|
14
|
+
lastAiChatSessionIdKey,
|
|
15
|
+
mobileBreakpoint,
|
|
16
|
+
splitMap
|
|
15
17
|
} from '../common/constants'
|
|
16
18
|
import * as ls from '../common/safe-local-storage'
|
|
17
19
|
import { refs, refsStatic } from '../components/common/ref'
|
|
@@ -58,14 +60,21 @@ export default Store => {
|
|
|
58
60
|
Store.prototype.onResize = debounce(async function () {
|
|
59
61
|
const { width, height } = await window.pre.runGlobalAsync('getScreenSize')
|
|
60
62
|
const isMaximized = window.pre.runSync('isMaximized')
|
|
63
|
+
const w = window.innerWidth
|
|
64
|
+
const isMobile = w <= mobileBreakpoint
|
|
61
65
|
const update = {
|
|
62
66
|
height: window.innerHeight,
|
|
63
|
-
innerWidth:
|
|
67
|
+
innerWidth: w,
|
|
64
68
|
screenWidth: width,
|
|
65
69
|
screenHeight: height,
|
|
66
|
-
isMaximized
|
|
70
|
+
isMaximized,
|
|
71
|
+
isMobile
|
|
67
72
|
}
|
|
68
73
|
window.store.storeAssign(update)
|
|
74
|
+
// Force single-column layout on mobile
|
|
75
|
+
if (isMobile && window.store.layout !== splitMap.c1) {
|
|
76
|
+
window.store.setLayout(splitMap.c1)
|
|
77
|
+
}
|
|
69
78
|
window.pre.runGlobalAsync('setWindowSize', {
|
|
70
79
|
...update,
|
|
71
80
|
height: window.outerHeight
|
|
@@ -23,7 +23,8 @@ import {
|
|
|
23
23
|
resolutionsLsKey,
|
|
24
24
|
syncServerDataKey,
|
|
25
25
|
splitMap,
|
|
26
|
-
lastAiChatSessionIdKey
|
|
26
|
+
lastAiChatSessionIdKey,
|
|
27
|
+
mobileBreakpoint
|
|
27
28
|
} from '../common/constants'
|
|
28
29
|
import * as ls from '../common/safe-local-storage'
|
|
29
30
|
import { exclude } from 'manate'
|
|
@@ -200,6 +201,7 @@ export default () => {
|
|
|
200
201
|
height: 500,
|
|
201
202
|
isMaximized: window.pre.runSync('isMaximized'),
|
|
202
203
|
hasNodePty: window.pre.runSync('nodePtyCheck'),
|
|
204
|
+
isMobile: window.innerWidth <= mobileBreakpoint,
|
|
203
205
|
fullscreen: false,
|
|
204
206
|
hideDelKeyTip: ls.getItem(dismissDelKeyTipLsKey) === 'y',
|
|
205
207
|
tabsHeight: 36,
|