@electerm/electerm-react 5.3.5 → 5.3.26
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 +3 -0
- package/client/components/bookmark-form/index.jsx +10 -8
- package/client/components/main/main.jsx +26 -1
- package/client/components/setting-panel/col.jsx +32 -6
- package/client/components/setting-panel/list.styl +12 -0
- package/client/components/setting-panel/setting-mobile-breadcrumb.jsx +29 -0
- package/client/components/setting-panel/setting-modal.jsx +7 -2
- package/client/components/setting-panel/setting-wrap.jsx +0 -3
- package/client/components/setting-panel/setting-wrap.styl +80 -11
- package/client/components/sidebar/index.jsx +28 -21
- package/client/components/sidebar/left-sidebar-icons.jsx +1 -1
- package/client/components/sidebar/sidebar.styl +16 -0
- package/client/components/sys-menu/icons-map.jsx +6 -2
- package/client/components/tabs/index.jsx +16 -3
- package/client/components/tabs/tab.jsx +25 -4
- package/client/components/tabs/tabs.styl +8 -0
- package/client/components/terminal/shortcut-bar-entry.jsx +26 -24
- package/client/components/theme/theme-form.jsx +22 -0
- package/client/components/tree-list/bookmark-transport.jsx +7 -31
- package/client/components/tree-list/tree-list.styl +13 -0
- package/client/store/init-state.js +8 -2
- package/client/store/setting.js +11 -0
- package/client/store/sidebar.js +12 -0
- package/client/store/sync.js +43 -9
- package/client/store/system-menu.js +2 -0
- package/client/store/tab.js +15 -1
- package/package.json +1 -1
|
@@ -201,6 +201,9 @@ export const commonLineEndings = commonTxLineEndings
|
|
|
201
201
|
export const maxBatchInput = 30
|
|
202
202
|
export const windowControlWidth = 94
|
|
203
203
|
export const mobileBreakpoint = 600
|
|
204
|
+
// breakpoint matching the setting panel's `@media (max-width: 800px)` CSS —
|
|
205
|
+
// must be kept in sync with setting-wrap.styl
|
|
206
|
+
export const settingPanelMobileBreakpoint = 800
|
|
204
207
|
export const baseUpdateCheckUrls = [
|
|
205
208
|
packInfo.homepage,
|
|
206
209
|
'https://gitee.com/github-zxdong262/electerm/raw/gh-pages'
|
|
@@ -130,7 +130,7 @@ export default class BookmarkIndex2 extends PureComponent {
|
|
|
130
130
|
return (
|
|
131
131
|
<Button
|
|
132
132
|
size='small'
|
|
133
|
-
className='
|
|
133
|
+
className='mg1l create-ai-btn'
|
|
134
134
|
icon={<RobotOutlined />}
|
|
135
135
|
onClick={this.handleToggleAIMode}
|
|
136
136
|
>
|
|
@@ -171,15 +171,17 @@ export default class BookmarkIndex2 extends PureComponent {
|
|
|
171
171
|
const isNew = id.startsWith(newBookmarkIdPrefix)
|
|
172
172
|
const keys = Object.keys(sessionConfig)
|
|
173
173
|
return (
|
|
174
|
-
<div className='form-wrap pd1x'>
|
|
174
|
+
<div className='form-wrap pd1x mg2b'>
|
|
175
175
|
<div className='form-title pd1t pd1x pd2b bold'>
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
<p>
|
|
177
|
+
<BookOutlined className='mg1r' />
|
|
178
|
+
<span>
|
|
179
|
+
{((!isNew ? e('edit') : e('new')) + ' ' + e(settingMap.bookmarks))}
|
|
180
|
+
</span>
|
|
181
|
+
{this.renderTitle(formData, isNew)}
|
|
182
|
+
{this.renderAIButton(isNew)}
|
|
183
|
+
</p>
|
|
181
184
|
{this.renderTypes(bookmarkType, isNew, keys)}
|
|
182
|
-
{this.renderAIButton(isNew)}
|
|
183
185
|
</div>
|
|
184
186
|
{this.renderForm()}
|
|
185
187
|
</div>
|
|
@@ -86,6 +86,30 @@ export default auto(function Index (props) {
|
|
|
86
86
|
store.checkPendingDeepLink()
|
|
87
87
|
}, [])
|
|
88
88
|
|
|
89
|
+
// Track the actual input modality rather than a static capability probe:
|
|
90
|
+
// a touch-capable laptop is a mouse machine until a real touch happens,
|
|
91
|
+
// and a tablet stays touch even though it can also pair a mouse. Stored in
|
|
92
|
+
// `store.isTouchDevice` (seeded false — no upfront probe, which only says
|
|
93
|
+
// the screen *can* be touched, not what the user operates with); the
|
|
94
|
+
// `is-touch-device` class drives always-visible hover-only action icons, so
|
|
95
|
+
// it must follow the device the user is actually operating.
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const { store } = window
|
|
98
|
+
const handlePointer = (e) => {
|
|
99
|
+
if (e.pointerType === 'mouse') {
|
|
100
|
+
store.isTouchDevice = false
|
|
101
|
+
} else if (e.pointerType === 'touch' || e.pointerType === 'pen') {
|
|
102
|
+
store.isTouchDevice = true
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
document.addEventListener('pointerdown', handlePointer)
|
|
106
|
+
document.addEventListener('pointermove', handlePointer)
|
|
107
|
+
return () => {
|
|
108
|
+
document.removeEventListener('pointerdown', handlePointer)
|
|
109
|
+
document.removeEventListener('pointermove', handlePointer)
|
|
110
|
+
}
|
|
111
|
+
}, [])
|
|
112
|
+
|
|
89
113
|
const { store } = props
|
|
90
114
|
const {
|
|
91
115
|
configLoaded,
|
|
@@ -122,7 +146,8 @@ export default auto(function Index (props) {
|
|
|
122
146
|
'fs-with-footer': fullscreen && store.inActiveTerminal,
|
|
123
147
|
'is-main': !isSecondInstance,
|
|
124
148
|
'is-mobile': store.isMobile,
|
|
125
|
-
'is-desktop': !store.isMobile
|
|
149
|
+
'is-desktop': !store.isMobile,
|
|
150
|
+
'is-touch-device': store.isTouchDevice
|
|
126
151
|
})
|
|
127
152
|
const ext1 = {
|
|
128
153
|
className: cls,
|
|
@@ -1,18 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* two column layout, left column fixed
|
|
2
|
+
* two column layout, left column fixed width, right column auto width.
|
|
3
|
+
* the DOM is identical on desktop and mobile — which part is visible is
|
|
4
|
+
* pure CSS driven by the 800px media query in setting-wrap.styl (not the
|
|
5
|
+
* global `.is-mobile` class, which matches a different breakpoint), so a
|
|
6
|
+
* resize across the breakpoint never remounts the forms:
|
|
7
|
+
* - desktop (>800px): both columns side by side, breadcrumb hidden
|
|
8
|
+
* - mobile menu view (`setting-view-menu`): left menu fills the panel
|
|
9
|
+
* - mobile content view (`setting-view-content`): breadcrumb on top
|
|
10
|
+
* (click it to go back to the menu) + right content scrolls below
|
|
3
11
|
*/
|
|
4
12
|
|
|
13
|
+
import { auto } from 'manate/react'
|
|
5
14
|
import Placeholder from '../common/placeholder'
|
|
15
|
+
import SettingMobileBreadcrumb from './setting-mobile-breadcrumb'
|
|
6
16
|
|
|
7
|
-
|
|
17
|
+
const e = window.translate
|
|
18
|
+
|
|
19
|
+
function SettingCol (props) {
|
|
20
|
+
const store = window.store
|
|
21
|
+
const {
|
|
22
|
+
title,
|
|
23
|
+
name
|
|
24
|
+
} = store.settingItem
|
|
25
|
+
const label = title || name || e('new')
|
|
26
|
+
const viewCls = store.settingMobileView === 'content'
|
|
27
|
+
? 'setting-view-content'
|
|
28
|
+
: 'setting-view-menu'
|
|
8
29
|
return (
|
|
9
|
-
<div className=
|
|
30
|
+
<div className={`setting-col ${viewCls}`}>
|
|
31
|
+
<SettingMobileBreadcrumb
|
|
32
|
+
onBack={() => store.backToSettingMenu()}
|
|
33
|
+
parentLabel={e(store.settingTab)}
|
|
34
|
+
currentLabel={label}
|
|
35
|
+
/>
|
|
10
36
|
<div className='setting-row setting-row-left'>
|
|
11
37
|
{props.children[0]}
|
|
12
38
|
</div>
|
|
13
|
-
<div
|
|
14
|
-
className='setting-row setting-row-right'
|
|
15
|
-
>
|
|
39
|
+
<div className='setting-row setting-row-right'>
|
|
16
40
|
<div className='setting-col-content'>
|
|
17
41
|
{props.children[1]}
|
|
18
42
|
</div>
|
|
@@ -23,3 +47,5 @@ export default function SettingCol (props) {
|
|
|
23
47
|
</div>
|
|
24
48
|
)
|
|
25
49
|
}
|
|
50
|
+
|
|
51
|
+
export default auto(SettingCol)
|
|
@@ -45,6 +45,18 @@
|
|
|
45
45
|
.theme-item:hover
|
|
46
46
|
.list-item-remove
|
|
47
47
|
right 24px
|
|
48
|
+
// touch screens have no hover — always reveal the hover-only action icons
|
|
49
|
+
.is-touch-device
|
|
50
|
+
.list-item-edit
|
|
51
|
+
.list-item-apply
|
|
52
|
+
.list-item-remove
|
|
53
|
+
.list-item-bookmark
|
|
54
|
+
.list-item-duplicate
|
|
55
|
+
.list-item-autorun
|
|
56
|
+
display block
|
|
57
|
+
.theme-item
|
|
58
|
+
.list-item-remove
|
|
59
|
+
right 24px
|
|
48
60
|
.setting-tabs-setting
|
|
49
61
|
.item-list-unit
|
|
50
62
|
.list-item-remove
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* breadcrumb shown at the top of the settings content view on mobile.
|
|
3
|
+
* - parent crumb = the active setting tab, clickable, returns to the menu list
|
|
4
|
+
* - current crumb = the selected item name, plain text
|
|
5
|
+
* `onBack` + labels are props so it stays reusable for other panels
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ArrowLeftOutlined } from '@ant-design/icons'
|
|
9
|
+
|
|
10
|
+
export default function SettingMobileBreadcrumb (props) {
|
|
11
|
+
const {
|
|
12
|
+
onBack,
|
|
13
|
+
parentLabel,
|
|
14
|
+
currentLabel
|
|
15
|
+
} = props
|
|
16
|
+
return (
|
|
17
|
+
<div className='setting-mobile-back'>
|
|
18
|
+
<button
|
|
19
|
+
className='setting-mobile-back-btn'
|
|
20
|
+
onClick={onBack}
|
|
21
|
+
>
|
|
22
|
+
<ArrowLeftOutlined />
|
|
23
|
+
<span className='setting-mobile-back-parent'>{parentLabel}</span>
|
|
24
|
+
</button>
|
|
25
|
+
<span className='setting-mobile-back-sep'>></span>
|
|
26
|
+
<span className='setting-mobile-back-current elli'>{currentLabel}</span>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -9,7 +9,8 @@ import { lazy, Suspense } from 'react'
|
|
|
9
9
|
import SettingModal from './setting-wrap'
|
|
10
10
|
import {
|
|
11
11
|
settingMap,
|
|
12
|
-
modals
|
|
12
|
+
modals,
|
|
13
|
+
settingPanelMobileBreakpoint
|
|
13
14
|
} from '../../common/constants'
|
|
14
15
|
const TabBookmarks = lazy(() => import('./tab-bookmarks'))
|
|
15
16
|
const TabQuickCommands = lazy(() => import('./tab-quick-commands'))
|
|
@@ -25,6 +26,11 @@ const e = window.translate
|
|
|
25
26
|
export default auto(function SettingModalWrap (props) {
|
|
26
27
|
const selectItem = (item) => {
|
|
27
28
|
window.store.setSettingItem(item)
|
|
29
|
+
// narrow panels drill into the right col content after picking a menu
|
|
30
|
+
// item — same 800px breakpoint as the setting panel media query
|
|
31
|
+
if (window.store.innerWidth <= settingPanelMobileBreakpoint) {
|
|
32
|
+
window.store.settingMobileView = 'content'
|
|
33
|
+
}
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
function renderTabs () {
|
|
@@ -188,7 +194,6 @@ export default auto(function SettingModalWrap (props) {
|
|
|
188
194
|
visible={show}
|
|
189
195
|
useSystemTitleBar={useSystemTitleBar}
|
|
190
196
|
innerWidth={innerWidth}
|
|
191
|
-
isMobile={props.store.isMobile}
|
|
192
197
|
>
|
|
193
198
|
{renderTabs()}
|
|
194
199
|
</SettingModal>
|
|
@@ -67,34 +67,103 @@
|
|
|
67
67
|
.setting-col
|
|
68
68
|
::-webkit-scrollbar
|
|
69
69
|
width 0
|
|
70
|
-
@media (max-width:
|
|
70
|
+
@media (max-width: 800px)
|
|
71
71
|
.setting-col
|
|
72
72
|
position fixed
|
|
73
73
|
top 128px
|
|
74
74
|
bottom 0
|
|
75
75
|
left var(--left-side-bar-width, 43px)
|
|
76
76
|
right 0
|
|
77
|
-
|
|
77
|
+
display flex
|
|
78
|
+
flex-direction column
|
|
79
|
+
overflow hidden
|
|
78
80
|
.setting-row
|
|
79
81
|
position static
|
|
80
82
|
top auto
|
|
83
|
+
// right close button is redundant with the left one on mobile
|
|
84
|
+
.close-setting-wrap:not(.alt-close-setting-wrap)
|
|
85
|
+
display none
|
|
86
|
+
// menu view by default: the left col list fills the panel
|
|
81
87
|
.setting-row-left
|
|
88
|
+
flex 1
|
|
89
|
+
min-height 0
|
|
90
|
+
overflow-y auto
|
|
91
|
+
position static
|
|
92
|
+
top auto
|
|
93
|
+
bottom auto
|
|
82
94
|
left auto
|
|
83
95
|
width auto
|
|
84
|
-
|
|
85
|
-
padding 138px 10px 10px 10px
|
|
96
|
+
padding 10px
|
|
86
97
|
.setting-row-right
|
|
87
|
-
|
|
98
|
+
display none
|
|
99
|
+
position static
|
|
100
|
+
top auto
|
|
88
101
|
right auto
|
|
89
102
|
bottom auto
|
|
90
|
-
|
|
103
|
+
left auto
|
|
104
|
+
width auto
|
|
91
105
|
padding 10px
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
// content view: fixed breadcrumb on top, right col content scrolls below
|
|
107
|
+
.setting-view-content
|
|
108
|
+
.setting-mobile-back
|
|
109
|
+
display flex
|
|
110
|
+
.setting-row-left
|
|
111
|
+
display none
|
|
112
|
+
.setting-row-right
|
|
113
|
+
display flex
|
|
114
|
+
flex-direction column
|
|
115
|
+
flex 1
|
|
116
|
+
min-height 0
|
|
117
|
+
overflow hidden
|
|
118
|
+
position static
|
|
119
|
+
top auto
|
|
120
|
+
right auto
|
|
121
|
+
bottom auto
|
|
122
|
+
left auto
|
|
123
|
+
width auto
|
|
124
|
+
padding 10px
|
|
125
|
+
.setting-col-content
|
|
126
|
+
flex 1
|
|
127
|
+
overflow-y auto
|
|
128
|
+
min-height 0
|
|
96
129
|
.setting-tabs-bookmarks
|
|
97
130
|
.setting-row-left
|
|
98
131
|
width auto
|
|
99
132
|
.setting-row-right
|
|
100
|
-
left auto
|
|
133
|
+
left auto
|
|
134
|
+
|
|
135
|
+
// breadcrumb used on the narrow (mobile) content view; hidden by default and
|
|
136
|
+
// re-shown by `.setting-view-content` inside the 800px media query above
|
|
137
|
+
.setting-mobile-back
|
|
138
|
+
display none
|
|
139
|
+
flex-shrink 0
|
|
140
|
+
align-items center
|
|
141
|
+
gap 4px
|
|
142
|
+
padding 8px 10px
|
|
143
|
+
background var(--main)
|
|
144
|
+
border-bottom 1px solid var(--main-light)
|
|
145
|
+
z-index 10
|
|
146
|
+
.setting-mobile-back-btn
|
|
147
|
+
display flex
|
|
148
|
+
align-items center
|
|
149
|
+
gap 4px
|
|
150
|
+
height 24px
|
|
151
|
+
padding 0 6px
|
|
152
|
+
font-size 13px
|
|
153
|
+
color var(--primary)
|
|
154
|
+
background transparent
|
|
155
|
+
border 0
|
|
156
|
+
border-radius 4px
|
|
157
|
+
cursor pointer
|
|
158
|
+
&:hover
|
|
159
|
+
background var(--primary)
|
|
160
|
+
color var(--primary-contrast)
|
|
161
|
+
.setting-mobile-back-sep
|
|
162
|
+
color var(--text-dark)
|
|
163
|
+
font-size 13px
|
|
164
|
+
line-height 24px
|
|
165
|
+
padding 0 2px
|
|
166
|
+
.setting-mobile-back-current
|
|
167
|
+
font-size 13px
|
|
168
|
+
min-width 0
|
|
169
|
+
max-width 60%
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
1
2
|
import {
|
|
2
3
|
InfoCircleOutlined,
|
|
3
4
|
UpCircleOutlined,
|
|
@@ -48,37 +49,43 @@ export default function Sidebar (props) {
|
|
|
48
49
|
const { store } = window
|
|
49
50
|
|
|
50
51
|
const handleClickOutside = (event) => {
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const { store } = window
|
|
53
|
+
// Nothing to dismiss when the panel is closed or pinned; also ignore
|
|
54
|
+
// clicks while typing in an input so the panel is not yanked away
|
|
55
|
+
// mid-interaction.
|
|
56
|
+
if (!store.openedSideBar || store.pinned || hasActiveInput()) {
|
|
53
57
|
return
|
|
54
58
|
}
|
|
59
|
+
const target = event.target
|
|
60
|
+
// Never treat the panel itself or the bookmark toggle button as "outside".
|
|
61
|
+
// The very click that toggles the panel open lands on that button, so this
|
|
62
|
+
// guard makes the toggle immune to event-ordering races — and, in dev hot
|
|
63
|
+
// reloads, to stale listeners left behind by an older build.
|
|
64
|
+
if (target.closest && (
|
|
65
|
+
target.closest('.sidebar-panel') ||
|
|
66
|
+
target.closest('.bookmark-sidebar-toggle')
|
|
67
|
+
)) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
store.setOpenedSideBar('')
|
|
71
|
+
}
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
// One always-on document listener for click-outside dismissal, attached
|
|
74
|
+
// once. Previously the listener was added/removed from inside the bookmark
|
|
75
|
+
// click handler, so it leaked when the panel was closed via the ✕ icon and
|
|
76
|
+
// then instantly re-closed the panel on the next bookmark-icon click.
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
document.addEventListener('click', handleClickOutside)
|
|
79
|
+
return () => {
|
|
60
80
|
document.removeEventListener('click', handleClickOutside)
|
|
61
81
|
}
|
|
62
|
-
}
|
|
82
|
+
}, [])
|
|
63
83
|
|
|
64
84
|
const handleClickBookmark = () => {
|
|
65
85
|
if (showModal) {
|
|
66
86
|
store.showModal = 0
|
|
67
87
|
}
|
|
68
|
-
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
if (openedSideBar === 'bookmarks') {
|
|
72
|
-
// Remove listener when closing
|
|
73
|
-
document.removeEventListener('click', handleClickOutside)
|
|
74
|
-
store.setOpenedSideBar('')
|
|
75
|
-
} else {
|
|
76
|
-
// Add listener when opening, with slight delay to avoid conflict with this click
|
|
77
|
-
setTimeout(() => {
|
|
78
|
-
document.addEventListener('click', handleClickOutside)
|
|
79
|
-
}, 0)
|
|
80
|
-
store.setOpenedSideBar('bookmarks')
|
|
81
|
-
}
|
|
88
|
+
store.openLeftSidePanel()
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
const handleShowUpgrade = () => {
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
z-index 100
|
|
24
24
|
bottom 0
|
|
25
25
|
background var(--main-dark)
|
|
26
|
+
display flex
|
|
27
|
+
flex-direction column
|
|
26
28
|
|
|
27
29
|
.item-list
|
|
28
30
|
padding-right 0
|
|
@@ -105,6 +107,20 @@
|
|
|
105
107
|
z-index 200
|
|
106
108
|
width 0
|
|
107
109
|
|
|
110
|
+
// icon bar: fills sidebar height, scrolls when overflow on small screens,
|
|
111
|
+
// scrollbar hidden but scrolling still works
|
|
112
|
+
.sidebar-bar
|
|
113
|
+
flex 1
|
|
114
|
+
min-height 0
|
|
115
|
+
overflow-y auto
|
|
116
|
+
overflow-x hidden
|
|
117
|
+
scrollbar-width none // Firefox
|
|
118
|
+
-ms-overflow-style none // IE/Edge
|
|
119
|
+
&::-webkit-scrollbar
|
|
120
|
+
width 0
|
|
121
|
+
height 0
|
|
122
|
+
display none
|
|
123
|
+
|
|
108
124
|
// mobile: icon bar hidden — hide its buttons
|
|
109
125
|
.sidebar.collapsed
|
|
110
126
|
.sidebar-bar
|
|
@@ -29,7 +29,9 @@ import {
|
|
|
29
29
|
PlayCircleFilled,
|
|
30
30
|
StopOutlined,
|
|
31
31
|
DownloadOutlined,
|
|
32
|
-
SwapOutlined
|
|
32
|
+
SwapOutlined,
|
|
33
|
+
PushpinOutlined,
|
|
34
|
+
PushpinFilled
|
|
33
35
|
} from '@ant-design/icons'
|
|
34
36
|
import IconHolder from './icon-holder'
|
|
35
37
|
|
|
@@ -62,5 +64,7 @@ export default {
|
|
|
62
64
|
PlayCircleFilled,
|
|
63
65
|
StopOutlined,
|
|
64
66
|
DownloadOutlined,
|
|
65
|
-
SwapOutlined
|
|
67
|
+
SwapOutlined,
|
|
68
|
+
PushpinOutlined,
|
|
69
|
+
PushpinFilled
|
|
66
70
|
}
|
|
@@ -122,11 +122,23 @@ export default class Tabs extends Component {
|
|
|
122
122
|
)
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// desktop tabs bar renders pinned tabs first: stable partition that keeps
|
|
126
|
+
// the original relative order inside the pinned and unpinned groups.
|
|
127
|
+
// store.tabs order itself is untouched by pinning.
|
|
128
|
+
getSortedTabs = () => {
|
|
129
|
+
const { tabs = [] } = this.props
|
|
130
|
+
return [
|
|
131
|
+
...tabs.filter(t => t.isPinned),
|
|
132
|
+
...tabs.filter(t => !t.isPinned)
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
|
|
125
136
|
adjustScroll = () => {
|
|
126
137
|
if (this.props.isMobile || !this.domRef.current) {
|
|
127
138
|
return
|
|
128
139
|
}
|
|
129
|
-
const {
|
|
140
|
+
const { currentBatchTabId, batch } = this.props
|
|
141
|
+
const tabs = this.getSortedTabs()
|
|
130
142
|
const index = tabs.findIndex(t => t.id === currentBatchTabId)
|
|
131
143
|
const tabsDomWith = Array.from(
|
|
132
144
|
document.querySelectorAll(`.v${batch + 1} .tab`)
|
|
@@ -209,7 +221,7 @@ export default class Tabs extends Component {
|
|
|
209
221
|
}
|
|
210
222
|
|
|
211
223
|
renderExtra () {
|
|
212
|
-
const items = this.
|
|
224
|
+
const items = this.getSortedTabs().map((t, i) => {
|
|
213
225
|
return {
|
|
214
226
|
key: i + '##' + t.id,
|
|
215
227
|
label: (
|
|
@@ -261,7 +273,8 @@ export default class Tabs extends Component {
|
|
|
261
273
|
}
|
|
262
274
|
|
|
263
275
|
renderContentInner () {
|
|
264
|
-
const {
|
|
276
|
+
const { width, config } = this.props
|
|
277
|
+
const tabs = this.getSortedTabs()
|
|
265
278
|
const len = tabs.length
|
|
266
279
|
const tabsWidthAll = tabMargin * len + 10 + this.tabsWidth()
|
|
267
280
|
const { overflow } = this.state
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
CloseOutlined,
|
|
10
10
|
Loading3QuartersOutlined,
|
|
11
11
|
BorderlessTableOutlined,
|
|
12
|
-
LockOutlined
|
|
12
|
+
LockOutlined,
|
|
13
|
+
PushpinFilled
|
|
13
14
|
} from '@ant-design/icons'
|
|
14
15
|
import {
|
|
15
16
|
Tooltip,
|
|
@@ -239,8 +240,15 @@ class Tab extends Component {
|
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
|
|
242
|
-
handleReloadTab = async () => {
|
|
243
|
-
|
|
243
|
+
handleReloadTab = async (e) => {
|
|
244
|
+
// stop propagation so the parent tab-title click doesn't re-activate
|
|
245
|
+
// this tab by its now-stale (removed) id after reloadTab replaces it
|
|
246
|
+
e && e.stopPropagation()
|
|
247
|
+
const { tab, batch } = this.props
|
|
248
|
+
const newTab = window.store.reloadTab(tab.id)
|
|
249
|
+
if (newTab) {
|
|
250
|
+
window.store.clickTab(newTab.id, batch)
|
|
251
|
+
}
|
|
244
252
|
}
|
|
245
253
|
|
|
246
254
|
handleReloadAll = () => {
|
|
@@ -309,8 +317,13 @@ class Tab extends Component {
|
|
|
309
317
|
window.store.closeTabsRight(this.props.tab.id)
|
|
310
318
|
}
|
|
311
319
|
|
|
320
|
+
togglePin = () => {
|
|
321
|
+
const { tab } = this.props
|
|
322
|
+
window.store.pinTab(tab.id, !tab.isPinned)
|
|
323
|
+
}
|
|
324
|
+
|
|
312
325
|
renderContext = () => {
|
|
313
|
-
const { tabs, tabIndex } = this.props
|
|
326
|
+
const { tabs, tabIndex, tab } = this.props
|
|
314
327
|
const len = tabs.length
|
|
315
328
|
const index = tabIndex
|
|
316
329
|
const noRight = index >= len - 1
|
|
@@ -359,6 +372,13 @@ class Tab extends Component {
|
|
|
359
372
|
icon: <iconsMap.EditOutlined />,
|
|
360
373
|
label: e('rename')
|
|
361
374
|
},
|
|
375
|
+
!window.store.isMobile && {
|
|
376
|
+
key: 'togglePin',
|
|
377
|
+
icon: tab.isPinned
|
|
378
|
+
? <iconsMap.PushpinFilled />
|
|
379
|
+
: <iconsMap.PushpinOutlined />,
|
|
380
|
+
label: tab.isPinned ? e('unpin') : e('pin')
|
|
381
|
+
},
|
|
362
382
|
{
|
|
363
383
|
key: 'handleReloadTab',
|
|
364
384
|
icon: <iconsMap.ReloadOutlined />,
|
|
@@ -542,6 +562,7 @@ class Tab extends Component {
|
|
|
542
562
|
}
|
|
543
563
|
<span className='tab-title'>
|
|
544
564
|
<span className='iblock mg1r tab-count' style={styleTag}>{tabCount}</span>
|
|
565
|
+
{tab.isPinned && <PushpinFilled className='tab-pin' />}
|
|
545
566
|
<span className='mg1r'>{title}</span>
|
|
546
567
|
</span>
|
|
547
568
|
</div>
|
|
@@ -135,6 +135,10 @@
|
|
|
135
135
|
font-size 10px
|
|
136
136
|
&:hover
|
|
137
137
|
background var(--error)
|
|
138
|
+
// touch screens have no hover — always reveal the close icon
|
|
139
|
+
.is-touch-device
|
|
140
|
+
.tab-close
|
|
141
|
+
display block
|
|
138
142
|
|
|
139
143
|
.tabs-add-btn
|
|
140
144
|
display inline-block
|
|
@@ -225,6 +229,10 @@
|
|
|
225
229
|
height 20px
|
|
226
230
|
line-height 21px
|
|
227
231
|
|
|
232
|
+
.tab-pin
|
|
233
|
+
margin-right 3px
|
|
234
|
+
font-size 11px
|
|
235
|
+
|
|
228
236
|
.ssh-tunnel-list-wrapper
|
|
229
237
|
max-height 300px
|
|
230
238
|
overflow-y auto
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
import { lazy, Suspense, useEffect, useState } from 'react'
|
|
40
|
+
import { auto } from 'manate/react'
|
|
40
41
|
|
|
41
42
|
const ShortcutBar = lazy(() => import('./shortcut-bar'))
|
|
42
43
|
|
|
@@ -52,38 +53,37 @@ export const KEYBOARD_MIN = 120
|
|
|
52
53
|
// keyboard from a mouse-driven window resize on touch-capable desktops.
|
|
53
54
|
const TOUCH_GRACE_MS = 3000
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// effectively only reached on very old runtimes — where remote-control
|
|
58
|
-
// misreporting is not a concern.
|
|
59
|
-
export function isTouchDevice () {
|
|
60
|
-
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
61
|
-
return false
|
|
62
|
-
}
|
|
63
|
-
const coarse = typeof window.matchMedia === 'function' &&
|
|
64
|
-
window.matchMedia('(pointer: coarse)').matches
|
|
65
|
-
const hasTouch = (navigator.maxTouchPoints || 0) > 0 ||
|
|
66
|
-
('ontouchstart' in window)
|
|
67
|
-
return Boolean(coarse || hasTouch)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export default function ShortcutBarEntry (props) {
|
|
71
|
-
// mount the lazy bar only after a soft keyboard has been seen at least once,
|
|
72
|
-
// so its code (and antd/icon deps) stays out of the main bundle on devices
|
|
73
|
-
// that never show one (desktops, remote-control clients mis-reported as
|
|
74
|
-
// touch, etc.).
|
|
75
|
-
//
|
|
56
|
+
function ShortcutBarEntry (props) {
|
|
57
|
+
const { store } = props
|
|
76
58
|
// opt out entirely when the user has disabled the shortcut bar in common
|
|
77
59
|
// settings — bail before any listener is attached or component imported.
|
|
78
|
-
const { store } = props
|
|
79
60
|
const disabled = store.config.disableShortcutBar
|
|
61
|
+
// follow the store's live touch state (flipped by main.jsx from real
|
|
62
|
+
// pointer input — mouse → false, touch/pen → true) instead of probing screen
|
|
63
|
+
// capability, which only says the screen *can* be touched, not whether the
|
|
64
|
+
// user is actually using touch, so the bar reacts when a touch-capable
|
|
65
|
+
// machine starts being used by touch.
|
|
66
|
+
const isTouch = store.isTouchDevice
|
|
67
|
+
// very old runtimes expose no visualViewport — the lazy "wait for a real
|
|
68
|
+
// keyboard" signal below can never fire there, so fall back straight to the
|
|
69
|
+
// touch state.
|
|
70
|
+
const noVisualViewport = typeof window === 'undefined' || !window.visualViewport
|
|
80
71
|
const [keyboardSeen, setKeyboardSeen] = useState(() => {
|
|
81
|
-
if (
|
|
82
|
-
return disabled
|
|
72
|
+
if (noVisualViewport) {
|
|
73
|
+
return !disabled && isTouch
|
|
83
74
|
}
|
|
84
75
|
return false
|
|
85
76
|
})
|
|
86
77
|
|
|
78
|
+
// keep the no-visualViewport fallback current as the store's touch state
|
|
79
|
+
// flips at runtime; on modern engines keyboardSeen is driven by the resize
|
|
80
|
+
// listener below and this never runs.
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (noVisualViewport) {
|
|
83
|
+
setKeyboardSeen(!disabled && isTouch)
|
|
84
|
+
}
|
|
85
|
+
}, [disabled, isTouch, noVisualViewport])
|
|
86
|
+
|
|
87
87
|
useEffect(() => {
|
|
88
88
|
const vv = window.visualViewport
|
|
89
89
|
if (!vv) {
|
|
@@ -171,3 +171,5 @@ export default function ShortcutBarEntry (props) {
|
|
|
171
171
|
</Suspense>
|
|
172
172
|
)
|
|
173
173
|
}
|
|
174
|
+
|
|
175
|
+
export default auto(ShortcutBarEntry)
|
|
@@ -40,6 +40,9 @@ export default function ThemeForm (props) {
|
|
|
40
40
|
action.current = 'saveOnly'
|
|
41
41
|
form.submit()
|
|
42
42
|
}
|
|
43
|
+
function applyOnly () {
|
|
44
|
+
props.store.setTheme(props.formData.id)
|
|
45
|
+
}
|
|
43
46
|
// A function to validate the input text
|
|
44
47
|
async function validateInput (_, value) {
|
|
45
48
|
const input = value
|
|
@@ -189,6 +192,25 @@ export default function ThemeForm (props) {
|
|
|
189
192
|
if (!id) {
|
|
190
193
|
return null
|
|
191
194
|
}
|
|
195
|
+
if (readonly) {
|
|
196
|
+
return (
|
|
197
|
+
<FormItem>
|
|
198
|
+
<Button
|
|
199
|
+
type='primary'
|
|
200
|
+
className='mg1r mg1b'
|
|
201
|
+
onClick={applyOnly}
|
|
202
|
+
>
|
|
203
|
+
{e('apply')}
|
|
204
|
+
</Button>
|
|
205
|
+
<Button
|
|
206
|
+
type='dashed'
|
|
207
|
+
onClick={exporter}
|
|
208
|
+
>
|
|
209
|
+
{e('export')}
|
|
210
|
+
</Button>
|
|
211
|
+
</FormItem>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
192
214
|
return (
|
|
193
215
|
<FormItem>
|
|
194
216
|
<Button
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
import { PureComponent } from 'react'
|
|
6
6
|
import {
|
|
7
7
|
ExportOutlined,
|
|
8
|
-
ImportOutlined
|
|
9
|
-
EditOutlined
|
|
8
|
+
ImportOutlined
|
|
10
9
|
} from '@ant-design/icons'
|
|
11
10
|
import { Button, Space } from 'antd'
|
|
12
11
|
import Upload from '../common/upload'
|
|
@@ -19,21 +18,6 @@ export default class BookmarkTransport extends PureComponent {
|
|
|
19
18
|
|
|
20
19
|
handleDownload = () => {}
|
|
21
20
|
|
|
22
|
-
handleToggleEdit = () => {
|
|
23
|
-
window.store.bookmarkSelectMode = true
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
renderEdit () {
|
|
27
|
-
return (
|
|
28
|
-
<Button
|
|
29
|
-
icon={<EditOutlined />}
|
|
30
|
-
onClick={this.handleToggleEdit}
|
|
31
|
-
title={e('edit')}
|
|
32
|
-
key='edit-and-del'
|
|
33
|
-
/>
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
21
|
renderExport () {
|
|
38
22
|
return (
|
|
39
23
|
<Button
|
|
@@ -63,19 +47,11 @@ export default class BookmarkTransport extends PureComponent {
|
|
|
63
47
|
}
|
|
64
48
|
|
|
65
49
|
render () {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
</Space.Compact>
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
return [
|
|
76
|
-
edit,
|
|
77
|
-
this.renderExport(),
|
|
78
|
-
this.renderImport()
|
|
79
|
-
]
|
|
50
|
+
return (
|
|
51
|
+
<Space.Compact>
|
|
52
|
+
{this.renderExport()}
|
|
53
|
+
{this.renderImport()}
|
|
54
|
+
</Space.Compact>
|
|
55
|
+
)
|
|
80
56
|
}
|
|
81
57
|
}
|
|
@@ -187,3 +187,16 @@
|
|
|
187
187
|
&.is-active
|
|
188
188
|
opacity 1
|
|
189
189
|
pointer-events auto
|
|
190
|
+
// touch screens have no hover — always reveal the hover-only action icons
|
|
191
|
+
.is-touch-device
|
|
192
|
+
.tree-list-row
|
|
193
|
+
.tree-item-title
|
|
194
|
+
padding-right 60px
|
|
195
|
+
.tree-item-op-wrap
|
|
196
|
+
opacity 1
|
|
197
|
+
pointer-events auto
|
|
198
|
+
.add-menu-wrap
|
|
199
|
+
.sidebar-panel
|
|
200
|
+
.tree-list-row
|
|
201
|
+
.tree-item-title
|
|
202
|
+
padding-right 20px
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
import * as ls from '../common/safe-local-storage'
|
|
30
30
|
import { exclude } from 'manate'
|
|
31
31
|
import initSettingItem from '../common/init-setting-item'
|
|
32
|
-
import { getRandomDefaultColor } from '../common/rand-hex-color'
|
|
33
32
|
|
|
34
33
|
const e = window.translate
|
|
35
34
|
|
|
@@ -39,7 +38,7 @@ function getDefaultBookmarkGroups (bookmarks) {
|
|
|
39
38
|
title: e(defaultBookmarkGroupId),
|
|
40
39
|
id: defaultBookmarkGroupId,
|
|
41
40
|
bookmarkIds: bookmarks.map(d => d.id),
|
|
42
|
-
color:
|
|
41
|
+
color: '#0088cc'
|
|
43
42
|
})
|
|
44
43
|
]
|
|
45
44
|
}
|
|
@@ -135,6 +134,8 @@ export default () => {
|
|
|
135
134
|
settingTab: settingMap.bookmarks, // setting tab
|
|
136
135
|
bookmarkId: undefined,
|
|
137
136
|
showModal: 0,
|
|
137
|
+
// mobile settings view: 'menu' (left col list, default) or 'content' (right col)
|
|
138
|
+
settingMobileView: 'menu',
|
|
138
139
|
|
|
139
140
|
// setting sync related
|
|
140
141
|
autoSyncReady: false,
|
|
@@ -214,6 +215,11 @@ export default () => {
|
|
|
214
215
|
isMaximized: window.pre.runSync('isMaximized'),
|
|
215
216
|
hasNodePty: window.pre.runSync('nodePtyCheck'),
|
|
216
217
|
isMobile: window.innerWidth <= mobileBreakpoint,
|
|
218
|
+
// reflects the input device actually in use, not screen capability — a
|
|
219
|
+
// capability probe only proves the screen can be touched, not whether the
|
|
220
|
+
// user operates it by touch. Seeded false and left to main.jsx, which sets
|
|
221
|
+
// mouse → false / touch → true on the first real pointer event.
|
|
222
|
+
isTouchDevice: false,
|
|
217
223
|
fullscreen: false,
|
|
218
224
|
tabsHeight: 36,
|
|
219
225
|
|
package/client/store/setting.js
CHANGED
|
@@ -40,6 +40,7 @@ export default Store => {
|
|
|
40
40
|
})
|
|
41
41
|
store.setSettingItem(item)
|
|
42
42
|
store.openSettingModal()
|
|
43
|
+
store.settingMobileView = 'content'
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
Store.prototype.handleOpenQuickCommandsSetting = function () {
|
|
@@ -136,12 +137,20 @@ export default Store => {
|
|
|
136
137
|
)
|
|
137
138
|
}
|
|
138
139
|
store.showModal = modals.setting
|
|
140
|
+
// mobile always lands on the left col menu first
|
|
141
|
+
store.settingMobileView = 'menu'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
Store.prototype.backToSettingMenu = function () {
|
|
145
|
+
window.store.settingMobileView = 'menu'
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
Store.prototype.hideSettingModal = function () {
|
|
142
149
|
const { store } = window
|
|
143
150
|
store.showModal = modals.hide
|
|
144
151
|
store.setSettingItem({})
|
|
152
|
+
// reset so a later open starts from the menu on mobile
|
|
153
|
+
store.settingMobileView = 'menu'
|
|
145
154
|
}
|
|
146
155
|
|
|
147
156
|
Store.prototype.loadFontList = async function () {
|
|
@@ -161,5 +170,7 @@ export default Store => {
|
|
|
161
170
|
settingTab
|
|
162
171
|
})
|
|
163
172
|
store.setSettingItem(item)
|
|
173
|
+
// a new tab means a new menu to pick from on mobile
|
|
174
|
+
store.settingMobileView = 'menu'
|
|
164
175
|
}
|
|
165
176
|
}
|
package/client/store/sidebar.js
CHANGED
|
@@ -19,6 +19,18 @@ export default Store => {
|
|
|
19
19
|
store.expandedKeys = []
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Single entry point for opening/closing the left side panel from the
|
|
23
|
+
// sidebar icons. The panel is a toggle: call again to close. It also
|
|
24
|
+
// re-centers the pin behaviour so every open/close transition goes
|
|
25
|
+
// through this one method.
|
|
26
|
+
Store.prototype.openLeftSidePanel = function (field = 'bookmarks') {
|
|
27
|
+
const { store } = window
|
|
28
|
+
if (store.pinned) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
store.setOpenedSideBar(store.openedSideBar ? '' : field)
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
Store.prototype.handlePin = function (pinned) {
|
|
23
35
|
const { store } = window
|
|
24
36
|
const current = !store.pinned
|
package/client/store/sync.js
CHANGED
|
@@ -52,6 +52,46 @@ function isJSON (str = '') {
|
|
|
52
52
|
return str.startsWith('[')
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Names of data types that are actually encrypted on upload when a
|
|
57
|
+
* sync password is set. Only these types trigger the fail-closed
|
|
58
|
+
* check on download.
|
|
59
|
+
*/
|
|
60
|
+
const encryptedDataNames = new Set([
|
|
61
|
+
settingMap.bookmarks,
|
|
62
|
+
settingMap.profiles
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decrypt sync data with fail-closed semantics.
|
|
67
|
+
*
|
|
68
|
+
* Only bookmarks and profiles are encrypted on upload when a sync
|
|
69
|
+
* password is configured. For those two types, plaintext JSON
|
|
70
|
+
* (content starting with '[') must never be silently accepted when a
|
|
71
|
+
* password is set — it could be attacker-injected data from a
|
|
72
|
+
* compromised sync backend. Instead, we reject it so the user is
|
|
73
|
+
* alerted rather than silently importing untrusted data.
|
|
74
|
+
*
|
|
75
|
+
* For all other data types, plaintext JSON is expected even when a
|
|
76
|
+
* password is set, so we accept it without error.
|
|
77
|
+
*/
|
|
78
|
+
async function decryptSyncData (str, pass, dataName) {
|
|
79
|
+
if (!str) {
|
|
80
|
+
return str
|
|
81
|
+
}
|
|
82
|
+
if (isJSON(str)) {
|
|
83
|
+
if (pass && encryptedDataNames.has(dataName)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
'Sync data is plaintext but a sync password is configured. ' +
|
|
86
|
+
'This may indicate the data was tampered with or the sync backend is compromised. ' +
|
|
87
|
+
'Aborting sync download for safety.'
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
return str
|
|
91
|
+
}
|
|
92
|
+
return window.pre.runGlobalAsync('decryptAsync', str, pass)
|
|
93
|
+
}
|
|
94
|
+
|
|
55
95
|
async function fetchData (type, func, args, token, proxy) {
|
|
56
96
|
const data = {
|
|
57
97
|
type,
|
|
@@ -337,9 +377,7 @@ export default (Store) => {
|
|
|
337
377
|
let serverItems = []
|
|
338
378
|
if (serverStr) {
|
|
339
379
|
try {
|
|
340
|
-
|
|
341
|
-
serverStr = await window.pre.runGlobalAsync('decryptAsync', serverStr, pass)
|
|
342
|
-
}
|
|
380
|
+
serverStr = await decryptSyncData(serverStr, pass, n)
|
|
343
381
|
serverItems = JSON.parse(serverStr)
|
|
344
382
|
} catch (e) {
|
|
345
383
|
console.error(`Failed to parse server data for ${n}:`, e)
|
|
@@ -520,9 +558,7 @@ export default (Store) => {
|
|
|
520
558
|
continue
|
|
521
559
|
}
|
|
522
560
|
}
|
|
523
|
-
|
|
524
|
-
str = await window.pre.runGlobalAsync('decryptAsync', str, pass)
|
|
525
|
-
}
|
|
561
|
+
str = await decryptSyncData(str, pass, n)
|
|
526
562
|
let arr = JSON.parse(str)
|
|
527
563
|
if (n === settingMap.terminalThemes) {
|
|
528
564
|
arr = store.fixThemes(arr)
|
|
@@ -585,9 +621,7 @@ export default (Store) => {
|
|
|
585
621
|
continue
|
|
586
622
|
}
|
|
587
623
|
}
|
|
588
|
-
|
|
589
|
-
str = await window.pre.runGlobalAsync('decryptAsync', str, pass)
|
|
590
|
-
}
|
|
624
|
+
str = await decryptSyncData(str, pass, n)
|
|
591
625
|
let arr = JSON.parse(
|
|
592
626
|
str
|
|
593
627
|
)
|
|
@@ -60,6 +60,8 @@ export default Store => {
|
|
|
60
60
|
})
|
|
61
61
|
store.setSettingItem(getInitItem([], settingMap.bookmarks))
|
|
62
62
|
store.openSettingModal()
|
|
63
|
+
// on mobile, jump straight to the new bookmark form instead of the menu
|
|
64
|
+
store.settingMobileView = 'content'
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
Store.prototype.onNewSshAI = function () {
|
package/client/store/tab.js
CHANGED
|
@@ -127,6 +127,8 @@ export default Store => {
|
|
|
127
127
|
if (store[batchProp] === tabId) {
|
|
128
128
|
store[batchProp] = newTab.id
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
return newTab
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
Store.prototype.reloadAllTabs = function () {
|
|
@@ -168,6 +170,17 @@ export default Store => {
|
|
|
168
170
|
store[`activeTabId${sourceTab.batch}`] = duplicatedTab.id
|
|
169
171
|
}
|
|
170
172
|
|
|
173
|
+
// only toggles the flag: the tabs bar renders pinned tabs first,
|
|
174
|
+
// store.tabs order itself is never changed by pinning
|
|
175
|
+
Store.prototype.pinTab = function (tabId, isPinned = true) {
|
|
176
|
+
const { store } = window
|
|
177
|
+
const tab = store.tabs.find(t => t.id === tabId)
|
|
178
|
+
if (!tab || Boolean(tab.isPinned) === isPinned) {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
tab.isPinned = isPinned
|
|
182
|
+
}
|
|
183
|
+
|
|
171
184
|
Store.prototype.closeOtherTabs = function (id) {
|
|
172
185
|
const { store } = window
|
|
173
186
|
const { tabs } = store
|
|
@@ -534,7 +547,8 @@ export default Store => {
|
|
|
534
547
|
'sshSftpSplitView',
|
|
535
548
|
'sshTunnelResults',
|
|
536
549
|
'displayRaw',
|
|
537
|
-
'autoReConnect'
|
|
550
|
+
'autoReConnect',
|
|
551
|
+
'isPinned'
|
|
538
552
|
]
|
|
539
553
|
const { history } = store
|
|
540
554
|
const index = history.filter(d => d.id && d.tab).findIndex(d => {
|