@electerm/electerm-react 2.15.8 → 2.16.6
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/components/ai/ai-chat.jsx +44 -2
- package/client/components/ai/ai-stop-icon.jsx +13 -0
- package/client/components/ai/ai.styl +10 -0
- package/client/components/bg/css-overwrite.jsx +158 -187
- package/client/components/bg/custom-css.jsx +8 -17
- package/client/components/bookmark-form/bookmark-schema.js +7 -1
- package/client/components/bookmark-form/common/color-picker.jsx +4 -8
- package/client/components/bookmark-form/common/exec-settings-field.jsx +44 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/config/common-fields.js +1 -0
- package/client/components/bookmark-form/config/local.js +3 -1
- package/client/components/common/animate-text.jsx +22 -23
- package/client/components/common/modal.jsx +2 -0
- package/client/components/common/password.jsx +19 -32
- package/client/components/footer/cmd-history.jsx +154 -0
- package/client/components/footer/cmd-history.styl +73 -0
- package/client/components/footer/footer-entry.jsx +15 -1
- package/client/components/main/main.jsx +2 -3
- package/client/components/quick-commands/quick-commands-select.jsx +1 -4
- package/client/components/rdp/rdp-session.jsx +23 -4
- package/client/components/session/session.styl +1 -3
- package/client/components/setting-panel/terminal-bg-config.jsx +2 -0
- package/client/components/setting-panel/text-bg-modal.jsx +9 -9
- package/client/components/sftp/file-item.jsx +22 -0
- package/client/components/sidebar/history-item.jsx +6 -3
- package/client/components/sidebar/history.jsx +48 -5
- package/client/components/sidebar/sidebar-panel.jsx +0 -13
- package/client/components/sidebar/sidebar.styl +19 -0
- package/client/components/tabs/add-btn-menu.jsx +28 -4
- package/client/components/tabs/add-btn.jsx +1 -1
- package/client/components/tabs/add-btn.styl +8 -0
- package/client/components/terminal/terminal.jsx +28 -11
- package/client/components/terminal/transfer-client-base.js +18 -2
- package/client/components/terminal/trzsz-client.js +2 -1
- package/client/components/terminal/zmodem-client.js +2 -1
- package/client/components/text-editor/edit-with-custom-editor.jsx +49 -0
- package/client/components/text-editor/text-editor-form.jsx +13 -5
- package/client/components/text-editor/text-editor.jsx +20 -1
- package/client/components/vnc/vnc-session.jsx +3 -0
- package/client/components/vnc/vnc.styl +1 -1
- package/client/store/common.js +31 -4
- package/client/store/init-state.js +26 -1
- package/client/store/store.js +1 -1
- package/client/store/watch.js +8 -1
- package/package.json +1 -1
|
@@ -2,31 +2,74 @@
|
|
|
2
2
|
* history select
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import React, { useState, useEffect } from 'react'
|
|
5
6
|
import { auto } from 'manate/react'
|
|
7
|
+
import { Switch } from 'antd'
|
|
8
|
+
import { UnorderedListOutlined } from '@ant-design/icons'
|
|
6
9
|
import HistoryItem from './history-item'
|
|
10
|
+
import { getItemJSON, setItemJSON } from '../../common/safe-local-storage.js'
|
|
11
|
+
|
|
12
|
+
const SORT_BY_FREQ_KEY = 'electerm-history-sort-by-frequency'
|
|
7
13
|
|
|
8
14
|
export default auto(function HistoryPanel (props) {
|
|
9
15
|
const { store } = window
|
|
10
16
|
if (store.config.disableConnectionHistory) {
|
|
11
17
|
return null
|
|
12
18
|
}
|
|
19
|
+
const [sortByFrequency, setSortByFrequency] = useState(() => {
|
|
20
|
+
return getItemJSON(SORT_BY_FREQ_KEY, false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setItemJSON(SORT_BY_FREQ_KEY, sortByFrequency)
|
|
25
|
+
}, [sortByFrequency])
|
|
26
|
+
|
|
13
27
|
const {
|
|
14
28
|
history
|
|
15
29
|
} = store
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
30
|
+
let arr = [...history]
|
|
31
|
+
if (sortByFrequency) {
|
|
32
|
+
arr = arr.sort((a, b) => { return b.count - a.count })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleSortByFrequencyChange = (checked) => {
|
|
36
|
+
setSortByFrequency(checked)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const handleClearHistory = () => {
|
|
40
|
+
store.clearHistory()
|
|
41
|
+
}
|
|
42
|
+
const e = window.translate
|
|
43
|
+
const switchProps = {
|
|
44
|
+
checkedChildren: e('sortByFrequency'),
|
|
45
|
+
unCheckedChildren: e('sortByFrequency'),
|
|
46
|
+
checked: sortByFrequency,
|
|
47
|
+
onChange: handleSortByFrequencyChange,
|
|
48
|
+
size: 'small'
|
|
49
|
+
}
|
|
50
|
+
const clearIconProps = {
|
|
51
|
+
className: 'history-clear-icon pointer clear-ai-icon icon-hover',
|
|
52
|
+
title: window.translate('clear'),
|
|
53
|
+
onClick: handleClearHistory
|
|
54
|
+
}
|
|
19
55
|
return (
|
|
20
56
|
<div
|
|
21
57
|
className='sidebar-panel-history'
|
|
22
58
|
>
|
|
23
|
-
<div className='pd2x'>
|
|
59
|
+
<div className='history-header pd2x pd2b'>
|
|
60
|
+
<Switch
|
|
61
|
+
{...switchProps}
|
|
62
|
+
/>
|
|
63
|
+
<UnorderedListOutlined
|
|
64
|
+
{...clearIconProps}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
<div className='history-body'>
|
|
24
68
|
{
|
|
25
69
|
arr.map((item, i) => {
|
|
26
70
|
return (
|
|
27
71
|
<HistoryItem
|
|
28
72
|
key={item.id}
|
|
29
|
-
index={i}
|
|
30
73
|
item={item}
|
|
31
74
|
/>
|
|
32
75
|
)
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
PlusCircleOutlined,
|
|
14
14
|
ShrinkOutlined,
|
|
15
15
|
PushpinOutlined,
|
|
16
|
-
UnorderedListOutlined,
|
|
17
16
|
SelectOutlined
|
|
18
17
|
} from '@ant-design/icons'
|
|
19
18
|
|
|
@@ -29,21 +28,9 @@ export default memo(function SidebarPanel (props) {
|
|
|
29
28
|
const prps1 = {
|
|
30
29
|
className: prps.className + (pinned ? ' pinned' : '')
|
|
31
30
|
}
|
|
32
|
-
const props2 = {
|
|
33
|
-
onClick: store.clearHistory,
|
|
34
|
-
className: 'mg2x pointer clear-ai-icon icon-hover'
|
|
35
|
-
}
|
|
36
|
-
const tabBarExtraContent = sidebarPanelTab === 'history'
|
|
37
|
-
? (
|
|
38
|
-
<UnorderedListOutlined
|
|
39
|
-
{...props2}
|
|
40
|
-
/>
|
|
41
|
-
)
|
|
42
|
-
: null
|
|
43
31
|
const tabsProps = {
|
|
44
32
|
activeKey: sidebarPanelTab,
|
|
45
33
|
onChange: store.handleSidebarPanelTab,
|
|
46
|
-
tabBarExtraContent,
|
|
47
34
|
items: [
|
|
48
35
|
{
|
|
49
36
|
key: 'bookmarks',
|
|
@@ -48,6 +48,25 @@
|
|
|
48
48
|
flex-direction column
|
|
49
49
|
overflow hidden
|
|
50
50
|
min-height 0
|
|
51
|
+
.history-header
|
|
52
|
+
flex-shrink 0
|
|
53
|
+
display flex
|
|
54
|
+
align-items center
|
|
55
|
+
border-bottom 1px solid var(--border)
|
|
56
|
+
position sticky
|
|
57
|
+
top 0
|
|
58
|
+
z-index 10
|
|
59
|
+
background var(--main)
|
|
60
|
+
.history-clear-icon
|
|
61
|
+
margin-left auto
|
|
62
|
+
margin-right 0
|
|
63
|
+
color var(--text-dark)
|
|
64
|
+
&:hover
|
|
65
|
+
color var(--error)
|
|
66
|
+
.history-body
|
|
67
|
+
flex 1
|
|
68
|
+
overflow-y auto
|
|
69
|
+
overflow-x hidden
|
|
51
70
|
.not-system-ui.is-mac
|
|
52
71
|
.sidebar-bar
|
|
53
72
|
margin-top 20px
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
* Add button menu component
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import React, { useCallback } from 'react'
|
|
5
|
+
import React, { useCallback, useState } from 'react'
|
|
6
|
+
import { Tabs } from 'antd'
|
|
6
7
|
import {
|
|
7
8
|
CodeFilled,
|
|
8
9
|
RightSquareFilled,
|
|
9
10
|
RobotOutlined
|
|
10
11
|
} from '@ant-design/icons'
|
|
11
12
|
import BookmarksList from '../sidebar/bookmark-select'
|
|
13
|
+
import History from '../sidebar/history'
|
|
12
14
|
import DragHandle from '../common/drag-handle'
|
|
13
15
|
import QuickConnect from './quick-connect'
|
|
14
16
|
|
|
@@ -26,6 +28,7 @@ export default function AddBtnMenu ({
|
|
|
26
28
|
setAddPanelWidth
|
|
27
29
|
}) {
|
|
28
30
|
const { onNewSsh, onNewSshAI } = window.store
|
|
31
|
+
const [activeTab, setActiveTab] = useState('bookmarks')
|
|
29
32
|
const cls = 'pd2x pd1y context-item pointer'
|
|
30
33
|
const addTabBtn = window.store.hasNodePty
|
|
31
34
|
? (
|
|
@@ -59,6 +62,24 @@ export default function AddBtnMenu ({
|
|
|
59
62
|
left: menuPosition === 'right'
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
const tabItems = [
|
|
66
|
+
{
|
|
67
|
+
key: 'bookmarks',
|
|
68
|
+
label: e('bookmarks')
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: 'history',
|
|
72
|
+
label: e('history')
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
let listContent
|
|
77
|
+
if (activeTab === 'bookmarks') {
|
|
78
|
+
listContent = <BookmarksList store={window.store} />
|
|
79
|
+
} else {
|
|
80
|
+
listContent = <History store={window.store} />
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
return (
|
|
63
84
|
<div
|
|
64
85
|
ref={menuRef}
|
|
@@ -89,11 +110,14 @@ export default function AddBtnMenu ({
|
|
|
89
110
|
<RobotOutlined /> {e('createBookmarkByAI')}
|
|
90
111
|
</div>
|
|
91
112
|
<QuickConnect batch={batch} inputOnly />
|
|
113
|
+
<Tabs
|
|
114
|
+
activeKey={activeTab}
|
|
115
|
+
onChange={setActiveTab}
|
|
116
|
+
items={tabItems}
|
|
117
|
+
/>
|
|
92
118
|
</div>
|
|
93
119
|
<div className='add-menu-list'>
|
|
94
|
-
|
|
95
|
-
store={window.store}
|
|
96
|
-
/>
|
|
120
|
+
{listContent}
|
|
97
121
|
</div>
|
|
98
122
|
</div>
|
|
99
123
|
)
|
|
@@ -118,7 +118,7 @@ export default class AddBtn extends Component {
|
|
|
118
118
|
focusSearchInput = () => {
|
|
119
119
|
// Focus the search input after the menu renders
|
|
120
120
|
this.focusTimeout = setTimeout(() => {
|
|
121
|
-
const searchInput = this.menuRef.current?.querySelector('.ant-input')
|
|
121
|
+
const searchInput = this.menuRef.current?.querySelector('.add-menu-list .ant-input')
|
|
122
122
|
if (searchInput) {
|
|
123
123
|
searchInput.focus()
|
|
124
124
|
searchInput.select()
|
|
@@ -82,7 +82,6 @@ class Term extends Component {
|
|
|
82
82
|
this.currentInput = ''
|
|
83
83
|
this.shellInjected = false
|
|
84
84
|
this.shellType = null
|
|
85
|
-
this.manualCommandHistory = new Set()
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
domRef = createRef()
|
|
@@ -737,7 +736,6 @@ class Term extends Component {
|
|
|
737
736
|
if (d === '\r' || d === '\n') {
|
|
738
737
|
const currentCmd = this.getCurrentInput()
|
|
739
738
|
if (currentCmd && currentCmd.trim() && this.shouldUseManualHistory()) {
|
|
740
|
-
this.manualCommandHistory.add(currentCmd.trim())
|
|
741
739
|
window.store.addCmdHistory(currentCmd.trim())
|
|
742
740
|
}
|
|
743
741
|
this.closeSuggestions()
|
|
@@ -903,9 +901,7 @@ class Term extends Component {
|
|
|
903
901
|
}
|
|
904
902
|
|
|
905
903
|
shouldUseManualHistory = () => {
|
|
906
|
-
|
|
907
|
-
(this.shellType === 'sh' || (isWin && this.isLocal()))
|
|
908
|
-
return useManual
|
|
904
|
+
return !this.cmdAddon || !this.cmdAddon.hasShellIntegration()
|
|
909
905
|
}
|
|
910
906
|
|
|
911
907
|
canInjectShellIntegration = () => {
|
|
@@ -1078,6 +1074,32 @@ class Term extends Component {
|
|
|
1078
1074
|
const { savePassword } = this.state
|
|
1079
1075
|
const termType = type
|
|
1080
1076
|
const extra = this.props.sessionOptions
|
|
1077
|
+
// Determine if this is a local terminal (no host)
|
|
1078
|
+
const isLocalType = !tab.host
|
|
1079
|
+
// Build exec settings: only for local type, prefer tab settings over config
|
|
1080
|
+
let execOpts = {}
|
|
1081
|
+
let execPropName = 'execLinux'
|
|
1082
|
+
if (isWin) {
|
|
1083
|
+
execPropName = 'execWindows'
|
|
1084
|
+
} else if (isMac) {
|
|
1085
|
+
execPropName = 'execMac'
|
|
1086
|
+
}
|
|
1087
|
+
if (isLocalType) {
|
|
1088
|
+
// Check flat properties on tab first (bookmark data), then fall back to config
|
|
1089
|
+
if (tab[execPropName]) {
|
|
1090
|
+
// Use bookmark's exec setting directly
|
|
1091
|
+
execOpts = {
|
|
1092
|
+
[execPropName]: tab[execPropName],
|
|
1093
|
+
[`${execPropName}Args`]: tab[`${execPropName}Args`] || []
|
|
1094
|
+
}
|
|
1095
|
+
} else if (config[execPropName]) {
|
|
1096
|
+
// Use global config exec settings
|
|
1097
|
+
execOpts = {
|
|
1098
|
+
[execPropName]: config[execPropName],
|
|
1099
|
+
[`${execPropName}Args`]: config[`${execPropName}Args`] || []
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1081
1103
|
const opts = clone({
|
|
1082
1104
|
cols,
|
|
1083
1105
|
rows,
|
|
@@ -1085,18 +1107,13 @@ class Term extends Component {
|
|
|
1085
1107
|
saveTerminalLogToFile: config.saveTerminalLogToFile,
|
|
1086
1108
|
...tab,
|
|
1087
1109
|
...extra,
|
|
1110
|
+
...execOpts,
|
|
1088
1111
|
logName,
|
|
1089
1112
|
sessionLogPath: config.sessionLogPath || createDefaultLogPath(),
|
|
1090
1113
|
...pick(config, [
|
|
1091
1114
|
'addTimeStampToTermLog',
|
|
1092
1115
|
'keepaliveInterval',
|
|
1093
1116
|
'keepaliveCountMax',
|
|
1094
|
-
'execWindows',
|
|
1095
|
-
'execMac',
|
|
1096
|
-
'execLinux',
|
|
1097
|
-
'execWindowsArgs',
|
|
1098
|
-
'execMacArgs',
|
|
1099
|
-
'execLinuxArgs',
|
|
1100
1117
|
'keyword2FA',
|
|
1101
1118
|
'debug'
|
|
1102
1119
|
]),
|
|
@@ -18,6 +18,7 @@ export class TransferClientBase {
|
|
|
18
18
|
this.currentTransfer = null
|
|
19
19
|
this.savePath = null
|
|
20
20
|
this.messageHandler = null
|
|
21
|
+
this._prevProgressRows = 0
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -130,8 +131,23 @@ export class TransferClientBase {
|
|
|
130
131
|
const speedStr = speed > 0 ? `, ${formatSize(speed)}/s` : ''
|
|
131
132
|
const doneStr = isComplete ? ' \x1b[32m\x1b[1m[DONE]\x1b[0m' : ''
|
|
132
133
|
|
|
133
|
-
const str = `\
|
|
134
|
-
|
|
134
|
+
const str = `\x1b[32m${name}\x1b[0m: ${percent}% ${bar} ${sizeStr}${speedStr}${doneStr}`
|
|
135
|
+
|
|
136
|
+
// Calculate visible length (no ANSI codes) to detect line wrapping
|
|
137
|
+
const visibleLen = name.length + 2 + String(percent).length + 2 + barWidth + 1 + sizeStr.length + speedStr.length + (isComplete ? 7 : 0)
|
|
138
|
+
const cols = this.terminal?.term?.cols || 80
|
|
139
|
+
const currentRows = Math.max(1, Math.ceil(visibleLen / cols))
|
|
140
|
+
|
|
141
|
+
// Move cursor back up to the start of the previous progress block, then
|
|
142
|
+
// erase everything from there to end-of-display so wrapped lines are gone.
|
|
143
|
+
let clearSeq = '\r'
|
|
144
|
+
for (let i = 0; i < this._prevProgressRows; i++) {
|
|
145
|
+
clearSeq += '\x1b[A' // cursor up one row
|
|
146
|
+
}
|
|
147
|
+
clearSeq += '\x1b[J' // erase from cursor to end of display
|
|
148
|
+
|
|
149
|
+
this._prevProgressRows = currentRows - 1
|
|
150
|
+
this.writeToTerminal(clearSeq + str + '\r')
|
|
135
151
|
return str
|
|
136
152
|
}
|
|
137
153
|
|
|
@@ -208,8 +208,9 @@ export class TrzszClient extends TransferClientBase {
|
|
|
208
208
|
this.currentTransfer.path = path
|
|
209
209
|
// Call directly to ensure 100% is displayed immediately
|
|
210
210
|
this._doWriteProgress(true)
|
|
211
|
-
// Add newline after completion
|
|
211
|
+
// Add newline after completion and reset row tracker for next file
|
|
212
212
|
this.writeToTerminal('\r\n')
|
|
213
|
+
this._prevProgressRows = 0
|
|
213
214
|
}
|
|
214
215
|
this.currentTransfer = null
|
|
215
216
|
}
|
|
@@ -180,8 +180,9 @@ export class ZmodemClient extends TransferClientBase {
|
|
|
180
180
|
this.currentTransfer.path = path
|
|
181
181
|
// Call directly to ensure 100% is displayed immediately
|
|
182
182
|
this._doWriteProgress(true)
|
|
183
|
-
// Add newline after completion
|
|
183
|
+
// Add newline after completion and reset row tracker for next file
|
|
184
184
|
this.writeToTerminal('\r\n')
|
|
185
|
+
this._prevProgressRows = 0
|
|
185
186
|
}
|
|
186
187
|
this.currentTransfer = null
|
|
187
188
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit with custom editor - input + button component
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import { Button, Input, Space } from 'antd'
|
|
7
|
+
|
|
8
|
+
const LS_KEY = 'customEditorCommand'
|
|
9
|
+
const e = window.translate
|
|
10
|
+
|
|
11
|
+
export default function EditWithCustomEditor ({ loading, editWithCustom }) {
|
|
12
|
+
const [editorCommand, setEditorCommand] = useState(
|
|
13
|
+
() => window.localStorage.getItem(LS_KEY) || ''
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
function handleChange (ev) {
|
|
17
|
+
const val = ev.target.value
|
|
18
|
+
setEditorCommand(val)
|
|
19
|
+
window.localStorage.setItem(LS_KEY, val)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function handleClick () {
|
|
23
|
+
const cmd = editorCommand.trim()
|
|
24
|
+
if (cmd) {
|
|
25
|
+
editWithCustom(cmd)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (window.et.isWebApp) {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Space.Compact className='mg1b'>
|
|
35
|
+
<Button
|
|
36
|
+
type='primary'
|
|
37
|
+
disabled={loading || !editorCommand.trim()}
|
|
38
|
+
onClick={handleClick}
|
|
39
|
+
>
|
|
40
|
+
{e('editWith')}
|
|
41
|
+
</Button>
|
|
42
|
+
<Input
|
|
43
|
+
value={editorCommand}
|
|
44
|
+
onChange={handleChange}
|
|
45
|
+
disabled={loading}
|
|
46
|
+
/>
|
|
47
|
+
</Space.Compact>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { useEffect } from 'react'
|
|
6
6
|
import { Form, Button } from 'antd'
|
|
7
7
|
import SimpleEditor from './simple-editor'
|
|
8
|
+
import EditWithCustomEditor from './edit-with-custom-editor'
|
|
8
9
|
|
|
9
10
|
const FormItem = Form.Item
|
|
10
11
|
const e = window.translate
|
|
@@ -38,7 +39,7 @@ export default function TextEditorForm (props) {
|
|
|
38
39
|
} = props
|
|
39
40
|
const popsEdit = {
|
|
40
41
|
type: 'primary',
|
|
41
|
-
className: '
|
|
42
|
+
className: 'mg1r mg1b',
|
|
42
43
|
disabled: loading,
|
|
43
44
|
onClick: props.editWith
|
|
44
45
|
}
|
|
@@ -62,10 +63,6 @@ export default function TextEditorForm (props) {
|
|
|
62
63
|
<SimpleEditor />
|
|
63
64
|
</FormItem>
|
|
64
65
|
<div className='pd1t pd2b'>
|
|
65
|
-
<Button
|
|
66
|
-
{...popsEdit}
|
|
67
|
-
>{e('editWithSystemEditor')}
|
|
68
|
-
</Button>
|
|
69
66
|
<Button
|
|
70
67
|
type='primary'
|
|
71
68
|
className='mg1r mg1b'
|
|
@@ -85,6 +82,17 @@ export default function TextEditorForm (props) {
|
|
|
85
82
|
>{e('cancel')}
|
|
86
83
|
</Button>
|
|
87
84
|
</div>
|
|
85
|
+
<div className='pd1t pd2b'>
|
|
86
|
+
<Button
|
|
87
|
+
{...popsEdit}
|
|
88
|
+
>
|
|
89
|
+
{e('editWithSystemEditor')}
|
|
90
|
+
</Button>
|
|
91
|
+
<EditWithCustomEditor
|
|
92
|
+
loading={loading}
|
|
93
|
+
editWithCustom={props.editWithCustom}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
88
96
|
</Form>
|
|
89
97
|
)
|
|
90
98
|
}
|
|
@@ -118,6 +118,24 @@ export default class TextEditor extends PureComponent {
|
|
|
118
118
|
fileRef.editWithSystemEditor(text)
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
editWithCustom = async (editorCommand) => {
|
|
122
|
+
this.setStateProxy({
|
|
123
|
+
loading: true
|
|
124
|
+
})
|
|
125
|
+
const {
|
|
126
|
+
id, text
|
|
127
|
+
} = this.state
|
|
128
|
+
const fileRef = refs.get(id)
|
|
129
|
+
if (!fileRef) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
await fileRef.editWithCustomEditor(text, editorCommand)
|
|
133
|
+
.catch(err => {
|
|
134
|
+
this.setStateProxy({ loading: false })
|
|
135
|
+
window.store.onError(err)
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
cancel = () => {
|
|
122
140
|
this.setStateProxy({
|
|
123
141
|
id: '',
|
|
@@ -150,7 +168,8 @@ export default class TextEditor extends PureComponent {
|
|
|
150
168
|
submit: this.handleSubmit,
|
|
151
169
|
text,
|
|
152
170
|
cancel: this.cancel,
|
|
153
|
-
editWith: this.editWith
|
|
171
|
+
editWith: this.editWith,
|
|
172
|
+
editWithCustom: this.editWithCustom
|
|
154
173
|
}
|
|
155
174
|
return (
|
|
156
175
|
<Modal
|
|
@@ -373,6 +373,9 @@ export default class VncSession extends PureComponent {
|
|
|
373
373
|
}
|
|
374
374
|
rfb.scaleViewport = scaleViewport
|
|
375
375
|
rfb.clipViewport = clipViewport
|
|
376
|
+
rfb.qualityLevel = qualityLevel
|
|
377
|
+
rfb.compressionLevel = compressionLevel
|
|
378
|
+
rfb.viewOnly = viewOnly
|
|
376
379
|
this.rfb = rfb
|
|
377
380
|
}
|
|
378
381
|
|
package/client/store/common.js
CHANGED
|
@@ -345,13 +345,40 @@ export default Store => {
|
|
|
345
345
|
return
|
|
346
346
|
}
|
|
347
347
|
const { terminalCommandHistory } = window.store
|
|
348
|
-
terminalCommandHistory.
|
|
348
|
+
const existing = terminalCommandHistory.get(cmd)
|
|
349
|
+
if (existing) {
|
|
350
|
+
// Use set() to trigger reactivity
|
|
351
|
+
terminalCommandHistory.set(cmd, {
|
|
352
|
+
count: existing.count + 1,
|
|
353
|
+
lastUseTime: new Date().toISOString()
|
|
354
|
+
})
|
|
355
|
+
} else {
|
|
356
|
+
terminalCommandHistory.set(cmd, {
|
|
357
|
+
count: 1,
|
|
358
|
+
lastUseTime: new Date().toISOString()
|
|
359
|
+
})
|
|
360
|
+
}
|
|
349
361
|
if (terminalCommandHistory.size > 100) {
|
|
350
362
|
// Delete oldest 20 items when history exceeds 100
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
363
|
+
const entries = Array.from(terminalCommandHistory.entries())
|
|
364
|
+
entries.sort((a, b) => new Date(a[1].lastUseTime).getTime() - new Date(b[1].lastUseTime).getTime())
|
|
365
|
+
for (let i = 0; i < 20 && i < entries.length; i++) {
|
|
366
|
+
terminalCommandHistory.delete(entries[i][0])
|
|
354
367
|
}
|
|
355
368
|
}
|
|
356
369
|
})
|
|
370
|
+
|
|
371
|
+
Store.prototype.deleteCmdHistory = function (cmd) {
|
|
372
|
+
const { terminalCommandHistory } = window.store
|
|
373
|
+
terminalCommandHistory.delete(cmd)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
Store.prototype.clearAllCmdHistory = function () {
|
|
377
|
+
window.store.terminalCommandHistory = new Map()
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
Store.prototype.runCmdFromHistory = function (cmd) {
|
|
381
|
+
window.store.runQuickCommand(cmd)
|
|
382
|
+
window.store.addCmdHistory(cmd)
|
|
383
|
+
}
|
|
357
384
|
}
|
|
@@ -74,7 +74,32 @@ export default () => {
|
|
|
74
74
|
addressBookmarksLocal: ls.getItemJSON(localAddrBookmarkLsKey, []),
|
|
75
75
|
openResolutionEdit: false,
|
|
76
76
|
resolutions: ls.getItemJSON(resolutionsLsKey, []),
|
|
77
|
-
terminalCommandHistory:
|
|
77
|
+
// terminalCommandHistory: Map<cmdString, {count: Number, lastUseTime: DateString}>
|
|
78
|
+
// Load from localStorage and migrate from old format (Set of strings) if needed
|
|
79
|
+
terminalCommandHistory: (() => {
|
|
80
|
+
const savedData = ls.getItemJSON(cmdHistoryKey, [])
|
|
81
|
+
const map = new Map()
|
|
82
|
+
if (Array.isArray(savedData)) {
|
|
83
|
+
// Check if old format (array of strings) or new format (array of objects)
|
|
84
|
+
if (savedData.length > 0 && typeof savedData[0] === 'string') {
|
|
85
|
+
// Old format: migrate to new format
|
|
86
|
+
savedData.forEach(cmd => {
|
|
87
|
+
map.set(cmd, { count: 1, lastUseTime: new Date().toISOString() })
|
|
88
|
+
})
|
|
89
|
+
} else {
|
|
90
|
+
// New format: array of {cmd, count, lastUseTime}
|
|
91
|
+
savedData.forEach(item => {
|
|
92
|
+
if (item.cmd) {
|
|
93
|
+
map.set(item.cmd, {
|
|
94
|
+
count: item.count || 1,
|
|
95
|
+
lastUseTime: item.lastUseTime || new Date().toISOString()
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return map
|
|
102
|
+
})(),
|
|
78
103
|
|
|
79
104
|
// workspaces
|
|
80
105
|
workspaces: [],
|
package/client/store/store.js
CHANGED
|
@@ -176,7 +176,7 @@ class Store {
|
|
|
176
176
|
|
|
177
177
|
get terminalCommandSuggestions () {
|
|
178
178
|
const { store } = window
|
|
179
|
-
const historyCommands = Array.from(store.terminalCommandHistory)
|
|
179
|
+
const historyCommands = Array.from(store.terminalCommandHistory.keys())
|
|
180
180
|
const batchInputCommands = store.batchInputs
|
|
181
181
|
const quickCommands = store.quickCommands.reduce(
|
|
182
182
|
(p, q) => {
|
package/client/store/watch.js
CHANGED
|
@@ -145,7 +145,14 @@ export default store => {
|
|
|
145
145
|
}).start()
|
|
146
146
|
|
|
147
147
|
autoRun(() => {
|
|
148
|
-
|
|
148
|
+
const history = store.terminalCommandHistory
|
|
149
|
+
// Save in new format: array of {cmd, count, lastUseTime}
|
|
150
|
+
const data = Array.from(history.entries()).map(([cmd, info]) => ({
|
|
151
|
+
cmd,
|
|
152
|
+
count: info.count,
|
|
153
|
+
lastUseTime: info.lastUseTime
|
|
154
|
+
}))
|
|
155
|
+
ls.setItemJSON(cmdHistoryKey, data)
|
|
149
156
|
return store.terminalCommandHistory
|
|
150
157
|
}).start()
|
|
151
158
|
|