@electerm/electerm-react 2.2.0 → 2.3.18
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 -1
- package/client/common/ui-theme.js +25 -12
- package/client/components/ai/ai.styl +1 -3
- package/client/components/bg/custom-css.jsx +2 -2
- package/client/components/bookmark-form/common/serial-path-selector.jsx +1 -1
- package/client/components/common/highlight.styl +1 -2
- package/client/components/common/input-context-menu.jsx +294 -0
- package/client/components/common/opacity.jsx +66 -0
- package/client/components/file-transfer/transfer.styl +6 -9
- package/client/components/footer/footer.styl +2 -5
- package/client/components/layout/layout.styl +2 -3
- package/client/components/main/error-wrapper.jsx +3 -3
- package/client/components/main/main.jsx +4 -7
- package/client/components/main/term-fullscreen.styl +1 -1
- package/client/components/main/ui-theme.jsx +46 -6
- package/client/components/main/upgrade.styl +4 -6
- package/client/components/main/wrapper.styl +5 -27
- package/client/components/quick-commands/qm.styl +3 -6
- package/client/components/session/session.styl +11 -13
- package/client/components/setting-panel/list.styl +5 -5
- package/client/components/setting-panel/setting-wrap.styl +1 -6
- package/client/components/setting-panel/terminal-bg-config.jsx +3 -0
- package/client/components/setting-sync/setting-sync-form.jsx +0 -1
- package/client/components/sftp/file-item.jsx +3 -0
- package/client/components/sftp/sftp-entry.jsx +50 -9
- package/client/components/sftp/sftp.styl +21 -34
- package/client/components/sftp/transfer-tag.styl +3 -5
- package/client/components/side-panel-r/right-side-panel.styl +7 -9
- package/client/components/sidebar/info.styl +1 -14
- package/client/components/sidebar/sidebar.styl +16 -18
- package/client/components/sys-menu/sys-menu.styl +8 -11
- package/client/components/tabs/tabs.styl +34 -35
- package/client/components/terminal/term-search.styl +3 -3
- package/client/components/terminal/terminal-search-bar.jsx +1 -1
- package/client/components/terminal/terminal.jsx +2 -2
- package/client/components/terminal/terminal.styl +22 -27
- package/client/components/terminal-info/terminal-info.styl +8 -8
- package/client/components/theme/terminal-theme-list.styl +2 -2
- package/client/components/theme/theme-list-item.jsx +62 -20
- package/client/components/tree-list/tree-list.styl +1 -1
- package/client/css/basic.styl +6 -12
- package/client/css/includes/theme.styl +16 -0
- package/client/store/ui-theme.js +0 -35
- package/client/views/index.pug +8 -1
- package/package.json +1 -1
- package/client/components/common/native-input.styl +0 -7
- package/client/components/setting-sync/sync.styl +0 -7
- package/client/components/terminal/zmodem.styl +0 -14
- package/client/css/antd-overwrite.styl +0 -10
- package/client/css/includes/theme-default.styl +0 -20
|
@@ -2,19 +2,32 @@
|
|
|
2
2
|
* ui theme related
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
const defaultUiThemeStylus = `
|
|
6
|
+
--main #141314
|
|
7
|
+
--main-dark #000
|
|
8
|
+
--main-light #2E3338
|
|
9
|
+
--text #ddd
|
|
10
|
+
--text-light #fff
|
|
11
|
+
--text-dark #888
|
|
12
|
+
--text-disabled #777
|
|
13
|
+
--primary #08c
|
|
14
|
+
--info #FFD166
|
|
15
|
+
--success #06D6A0
|
|
16
|
+
--error #EF476F
|
|
17
|
+
--warn #E55934
|
|
18
|
+
`
|
|
19
|
+
|
|
20
|
+
export function getUiThemeConfig (conf = defaultUiThemeStylus) {
|
|
21
|
+
const lines = conf.split('\n').filter(line => line.trim())
|
|
22
|
+
return lines.reduce((p, line) => {
|
|
23
|
+
const [k, v] = line.trim().replace('--', '').split(' ')
|
|
24
|
+
if (k && v) {
|
|
25
|
+
return {
|
|
26
|
+
...p,
|
|
27
|
+
[k]: v
|
|
28
|
+
}
|
|
17
29
|
}
|
|
30
|
+
return p
|
|
18
31
|
}, {})
|
|
19
32
|
}
|
|
20
33
|
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
@require '../../css/includes/theme-default'
|
|
2
1
|
.ai-chat-container
|
|
3
2
|
height 100%
|
|
4
3
|
display flex
|
|
5
4
|
flex-direction column
|
|
6
|
-
color text
|
|
7
5
|
|
|
8
6
|
.ai-chat-history
|
|
9
7
|
flex-grow 1
|
|
@@ -17,7 +15,7 @@
|
|
|
17
15
|
|
|
18
16
|
.chat-history-item
|
|
19
17
|
.code-block
|
|
20
|
-
border 1px dashed
|
|
18
|
+
border 1px dashed var(--main-darker)
|
|
21
19
|
padding 5px
|
|
22
20
|
border-radius 3px
|
|
23
21
|
pre
|
|
@@ -14,8 +14,8 @@ export default function CustomCss (props) {
|
|
|
14
14
|
const delta = useDelta(customCss)
|
|
15
15
|
|
|
16
16
|
async function applyTheme () {
|
|
17
|
-
const
|
|
18
|
-
|
|
17
|
+
const style = document.getElementById(themeDomId)
|
|
18
|
+
style.innerHTML = customCss
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
useEffect(() => {
|
|
@@ -30,7 +30,7 @@ export default function SerialPathSelector ({
|
|
|
30
30
|
/>
|
|
31
31
|
</FormItem>
|
|
32
32
|
<Spin spinning={loaddingSerials}>
|
|
33
|
-
<span onClick={store.handleGetSerials}>
|
|
33
|
+
<span onClick={store.handleGetSerials} className='pointer'>
|
|
34
34
|
<ReloadOutlined /> {e('reload')} serials
|
|
35
35
|
</span>
|
|
36
36
|
</Spin>
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone Input/TextArea Context Menu Module
|
|
3
|
+
* Automatically adds context menus to all input/textarea elements without code modification
|
|
4
|
+
* Just import this module and render <InputContextMenu /> in your React app to activate
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useEffect, useRef } from 'react'
|
|
8
|
+
import { Dropdown } from 'antd'
|
|
9
|
+
import iconsMap from '../sys-menu/icons-map.jsx'
|
|
10
|
+
import { copy, readClipboard, readClipboardAsync } from '../../common/clipboard.js'
|
|
11
|
+
|
|
12
|
+
const e = window.translate
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if element is input or textarea
|
|
16
|
+
*/
|
|
17
|
+
function isInputElement (element) {
|
|
18
|
+
if (!element) return false
|
|
19
|
+
const tagName = element.tagName.toLowerCase()
|
|
20
|
+
return tagName === 'input' || tagName === 'textarea'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get input element state
|
|
25
|
+
*/
|
|
26
|
+
function getInputState (element) {
|
|
27
|
+
if (!isInputElement(element)) return null
|
|
28
|
+
|
|
29
|
+
const hasText = element.value && element.value.length > 0
|
|
30
|
+
const hasSelection = element.selectionStart !== element.selectionEnd
|
|
31
|
+
const isReadOnly = element.readOnly || element.disabled
|
|
32
|
+
|
|
33
|
+
let hasClipboard = false
|
|
34
|
+
try {
|
|
35
|
+
const content = readClipboard()
|
|
36
|
+
hasClipboard = content && content.length > 0
|
|
37
|
+
} catch (error) {
|
|
38
|
+
hasClipboard = false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
hasText,
|
|
43
|
+
hasSelection,
|
|
44
|
+
hasClipboard,
|
|
45
|
+
isReadOnly
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Handle copy operation
|
|
51
|
+
*/
|
|
52
|
+
function handleCopy (element) {
|
|
53
|
+
if (!isInputElement(element)) return
|
|
54
|
+
|
|
55
|
+
const selectedText = element.value.substring(element.selectionStart, element.selectionEnd)
|
|
56
|
+
if (selectedText) {
|
|
57
|
+
copy(selectedText)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Handle paste operation
|
|
63
|
+
*/
|
|
64
|
+
async function handlePaste (element) {
|
|
65
|
+
if (!isInputElement(element) || element.readOnly || element.disabled) return
|
|
66
|
+
|
|
67
|
+
element.focus()
|
|
68
|
+
const clipboardText = await readClipboardAsync()
|
|
69
|
+
|
|
70
|
+
if (clipboardText) {
|
|
71
|
+
insertTextAtCursor(element, clipboardText)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Insert text at cursor position in input/textarea
|
|
77
|
+
*/
|
|
78
|
+
function insertTextAtCursor (element, text) {
|
|
79
|
+
if (!isInputElement(element)) return
|
|
80
|
+
|
|
81
|
+
element.focus()
|
|
82
|
+
|
|
83
|
+
// Try modern approach first - using execCommand
|
|
84
|
+
if (document.execCommand && document.queryCommandSupported('insertText')) {
|
|
85
|
+
try {
|
|
86
|
+
element.setSelectionRange(element.selectionStart, element.selectionEnd)
|
|
87
|
+
const success = document.execCommand('insertText', false, text)
|
|
88
|
+
if (success) {
|
|
89
|
+
triggerInputEvents(element)
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.log('execCommand insertText failed:', error)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Trigger input events for React and other frameworks
|
|
99
|
+
*/
|
|
100
|
+
function triggerInputEvents (element) {
|
|
101
|
+
const inputEvent = new Event('input', { bubbles: true, cancelable: true })
|
|
102
|
+
element.dispatchEvent(inputEvent)
|
|
103
|
+
|
|
104
|
+
const changeEvent = new Event('change', { bubbles: true, cancelable: true })
|
|
105
|
+
element.dispatchEvent(changeEvent)
|
|
106
|
+
|
|
107
|
+
// Handle React's internal event handlers
|
|
108
|
+
const reactHandlers = Object.keys(element).find(key => key.startsWith('__reactProps') || key.startsWith('__reactEventHandlers'))
|
|
109
|
+
if (reactHandlers) {
|
|
110
|
+
const handler = element[reactHandlers]
|
|
111
|
+
if (handler?.onChange) {
|
|
112
|
+
handler.onChange({ target: element, currentTarget: element })
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Handle cut operation
|
|
119
|
+
*/
|
|
120
|
+
function handleCut (element) {
|
|
121
|
+
if (!isInputElement(element) || element.readOnly || element.disabled) return
|
|
122
|
+
|
|
123
|
+
const { selectionStart, selectionEnd } = element
|
|
124
|
+
const selectedText = element.value.substring(selectionStart, selectionEnd)
|
|
125
|
+
|
|
126
|
+
if (selectedText) {
|
|
127
|
+
element.focus()
|
|
128
|
+
element.setSelectionRange(selectionStart, selectionEnd)
|
|
129
|
+
|
|
130
|
+
// Try using execCommand for cut
|
|
131
|
+
if (document.execCommand && document.queryCommandSupported('cut')) {
|
|
132
|
+
try {
|
|
133
|
+
const success = document.execCommand('cut')
|
|
134
|
+
if (success) {
|
|
135
|
+
triggerInputEvents(element)
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.log('execCommand cut failed:', error)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handle select all operation
|
|
146
|
+
*/
|
|
147
|
+
function handleSelectAll (element) {
|
|
148
|
+
if (!isInputElement(element)) return
|
|
149
|
+
element.select()
|
|
150
|
+
element.focus()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create context menu items based on input state
|
|
155
|
+
*/
|
|
156
|
+
function createContextMenuItems (element) {
|
|
157
|
+
const state = getInputState(element)
|
|
158
|
+
if (!state) return []
|
|
159
|
+
|
|
160
|
+
const { hasText, hasSelection, hasClipboard, isReadOnly } = state
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
key: 'copy',
|
|
165
|
+
icon: 'CopyOutlined',
|
|
166
|
+
label: e('copy'),
|
|
167
|
+
disabled: !hasSelection
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
key: 'cut',
|
|
171
|
+
icon: 'FileExcelOutlined',
|
|
172
|
+
label: e('cut'),
|
|
173
|
+
disabled: !hasSelection || isReadOnly
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
key: 'paste',
|
|
177
|
+
icon: 'SwitcherOutlined',
|
|
178
|
+
label: e('paste'),
|
|
179
|
+
disabled: !hasClipboard || isReadOnly
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
key: 'selectAll',
|
|
183
|
+
icon: 'CheckSquareOutlined',
|
|
184
|
+
label: e('selectall'),
|
|
185
|
+
disabled: !hasText
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* React Component for Input Context Menu
|
|
192
|
+
* Mount this component in your app to enable context menus on all input/textarea elements
|
|
193
|
+
*/
|
|
194
|
+
const InputContextMenu = () => {
|
|
195
|
+
const [visible, setVisible] = useState(false)
|
|
196
|
+
const [position, setPosition] = useState({ x: 0, y: 0 })
|
|
197
|
+
const [targetElement, setTargetElement] = useState(null)
|
|
198
|
+
const menuRef = useRef(null)
|
|
199
|
+
|
|
200
|
+
const handleMenuClick = async ({ key }) => {
|
|
201
|
+
// Keep reference to current element before closing menu
|
|
202
|
+
const currentElement = targetElement
|
|
203
|
+
// Close menu first
|
|
204
|
+
setVisible(false)
|
|
205
|
+
setTargetElement(null)
|
|
206
|
+
// Execute action with preserved element reference
|
|
207
|
+
if (currentElement) {
|
|
208
|
+
setTimeout(async () => {
|
|
209
|
+
try {
|
|
210
|
+
switch (key) {
|
|
211
|
+
case 'copy':
|
|
212
|
+
handleCopy(currentElement)
|
|
213
|
+
break
|
|
214
|
+
case 'cut':
|
|
215
|
+
handleCut(currentElement)
|
|
216
|
+
break
|
|
217
|
+
case 'paste':
|
|
218
|
+
await handlePaste(currentElement)
|
|
219
|
+
break
|
|
220
|
+
case 'selectAll':
|
|
221
|
+
handleSelectAll(currentElement)
|
|
222
|
+
break
|
|
223
|
+
default:
|
|
224
|
+
console.warn('Unknown menu action:', key)
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('Menu action failed:', error)
|
|
228
|
+
}
|
|
229
|
+
}, 10) // Small delay to ensure menu is closed
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const handleContextMenu = (event) => {
|
|
235
|
+
const target = event.target
|
|
236
|
+
if (isInputElement(target)) {
|
|
237
|
+
event.preventDefault()
|
|
238
|
+
event.stopPropagation()
|
|
239
|
+
setPosition({ x: event.clientX, y: event.clientY })
|
|
240
|
+
setTargetElement(target)
|
|
241
|
+
setVisible(true)
|
|
242
|
+
} else {
|
|
243
|
+
setVisible(false)
|
|
244
|
+
setTargetElement(null)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const handleClick = (event) => {
|
|
249
|
+
if (visible && menuRef.current && !menuRef.current.contains(event.target)) {
|
|
250
|
+
setVisible(false)
|
|
251
|
+
setTargetElement(null)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
document.addEventListener('contextmenu', handleContextMenu, true)
|
|
256
|
+
document.addEventListener('click', handleClick, true)
|
|
257
|
+
return () => {
|
|
258
|
+
document.removeEventListener('contextmenu', handleContextMenu, true)
|
|
259
|
+
document.removeEventListener('click', handleClick, true)
|
|
260
|
+
}
|
|
261
|
+
}, [visible])
|
|
262
|
+
const items = createContextMenuItems(targetElement) || []
|
|
263
|
+
const menuItems = items.map(item => {
|
|
264
|
+
const IconComponent = item.icon && iconsMap[item.icon]
|
|
265
|
+
return {
|
|
266
|
+
...item,
|
|
267
|
+
icon: IconComponent ? <IconComponent /> : null
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
return (
|
|
271
|
+
<div
|
|
272
|
+
ref={menuRef}
|
|
273
|
+
style={{
|
|
274
|
+
position: 'fixed',
|
|
275
|
+
left: position.x,
|
|
276
|
+
top: position.y,
|
|
277
|
+
zIndex: 9999
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
<Dropdown
|
|
281
|
+
menu={{
|
|
282
|
+
items: menuItems,
|
|
283
|
+
onClick: handleMenuClick
|
|
284
|
+
}}
|
|
285
|
+
open={visible}
|
|
286
|
+
placement='bottomLeft'
|
|
287
|
+
>
|
|
288
|
+
<div style={{ width: 1, height: 1 }} />
|
|
289
|
+
</Dropdown>
|
|
290
|
+
</div>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export default InputContextMenu
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { useDelta, useConditionalEffect } from 'react-delta-hooks'
|
|
3
|
+
import eq from 'fast-deep-equal'
|
|
4
|
+
|
|
5
|
+
const opacityDomId = 'opacity-style'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Opacity component
|
|
9
|
+
* Handles conditional CSS rendering based on opacity setting
|
|
10
|
+
* @param {Object} props
|
|
11
|
+
* @param {number} props.opacity - Opacity value from store.config
|
|
12
|
+
* @returns {null}
|
|
13
|
+
*/
|
|
14
|
+
export default function Opacity ({ opacity }) {
|
|
15
|
+
// Default to 1 if opacity is not provided
|
|
16
|
+
const currentOpacity = opacity !== undefined ? opacity : 1
|
|
17
|
+
const delta = useDelta(currentOpacity)
|
|
18
|
+
|
|
19
|
+
function applyOpacity () {
|
|
20
|
+
let styleElement = document.getElementById(opacityDomId)
|
|
21
|
+
|
|
22
|
+
// Create style element if it doesn't exist
|
|
23
|
+
if (!styleElement) {
|
|
24
|
+
styleElement = document.createElement('style')
|
|
25
|
+
styleElement.id = opacityDomId
|
|
26
|
+
document.head.appendChild(styleElement)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Update style content based on opacity value
|
|
30
|
+
if (currentOpacity === 1) {
|
|
31
|
+
styleElement.innerHTML = ''
|
|
32
|
+
window.pre.runGlobalAsync('setBackgroundColor', '#333333')
|
|
33
|
+
} else {
|
|
34
|
+
window.pre.runGlobalAsync('setBackgroundColor', '#33333300')
|
|
35
|
+
styleElement.innerHTML = `
|
|
36
|
+
html {
|
|
37
|
+
background: transparent !important;
|
|
38
|
+
}
|
|
39
|
+
body {
|
|
40
|
+
background: transparent !important;
|
|
41
|
+
}
|
|
42
|
+
#outside-context {
|
|
43
|
+
opacity: ${currentOpacity} !important;
|
|
44
|
+
}
|
|
45
|
+
`
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
applyOpacity()
|
|
51
|
+
|
|
52
|
+
// Cleanup function
|
|
53
|
+
return () => {
|
|
54
|
+
const styleElement = document.getElementById(opacityDomId)
|
|
55
|
+
if (styleElement) {
|
|
56
|
+
document.head.removeChild(styleElement)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}, [])
|
|
60
|
+
|
|
61
|
+
useConditionalEffect(() => {
|
|
62
|
+
applyOpacity()
|
|
63
|
+
}, delta && !eq(delta.prev, delta.curr))
|
|
64
|
+
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
@require '../../css/includes/theme-default'
|
|
2
1
|
.transports-wrap
|
|
3
2
|
position absolute
|
|
4
3
|
left 60px
|
|
5
4
|
top 3px
|
|
6
5
|
right 60px
|
|
7
6
|
height 36px
|
|
8
|
-
|
|
7
|
+
|
|
9
8
|
.transports-circle-wrap
|
|
10
9
|
text-align center
|
|
11
10
|
position relative
|
|
@@ -16,8 +15,8 @@
|
|
|
16
15
|
right 0
|
|
17
16
|
overflow-y scroll
|
|
18
17
|
z-index 11
|
|
19
|
-
background main
|
|
20
|
-
|
|
18
|
+
background var(--main)
|
|
19
|
+
|
|
21
20
|
.transports-wrap
|
|
22
21
|
.transports-dd
|
|
23
22
|
display none
|
|
@@ -25,7 +24,7 @@
|
|
|
25
24
|
display block
|
|
26
25
|
|
|
27
26
|
.transports-title
|
|
28
|
-
border-bottom 1px solid primary
|
|
27
|
+
border-bottom 1px solid var(--primary)
|
|
29
28
|
padding 10px 15px
|
|
30
29
|
.transports-content
|
|
31
30
|
max-height 200px
|
|
@@ -34,11 +33,9 @@
|
|
|
34
33
|
display flex
|
|
35
34
|
padding 8px 5px
|
|
36
35
|
&:hover
|
|
37
|
-
background main-
|
|
36
|
+
background var(--main-darker)
|
|
38
37
|
.sftp-transport .transfer-control-icon:hover
|
|
39
|
-
color primary
|
|
40
|
-
.transports-count
|
|
41
|
-
color text
|
|
38
|
+
color var(--primary)
|
|
42
39
|
|
|
43
40
|
.flex-child + .flex-child
|
|
44
41
|
margin-left 5px
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
@require '../../css/includes/theme-default'
|
|
2
|
-
|
|
3
1
|
.main-footer
|
|
4
|
-
background main
|
|
2
|
+
background var(--main)
|
|
5
3
|
height 36px
|
|
6
4
|
position absolute
|
|
7
5
|
left 44px
|
|
8
6
|
right 0
|
|
9
7
|
bottom 0
|
|
10
|
-
color text
|
|
11
8
|
z-index 200
|
|
12
9
|
.pinned
|
|
13
10
|
.main-footer
|
|
@@ -52,4 +49,4 @@
|
|
|
52
49
|
right 0
|
|
53
50
|
bottom 0
|
|
54
51
|
z-index 100
|
|
55
|
-
background main
|
|
52
|
+
background var(--main)
|
|
@@ -20,9 +20,9 @@ const troubleshootContent = {
|
|
|
20
20
|
windows: 'path\\to\\electerm.exe'
|
|
21
21
|
},
|
|
22
22
|
clearConfig: {
|
|
23
|
-
mac: 'rm -rf ~/Library/Application\\ Support/electerm/users/default_user/electerm.data.nedb',
|
|
24
|
-
linux: 'rm -rf ~/.config/electerm/users/default_user/electerm.data.nedb',
|
|
25
|
-
windows: 'Delete C:\\Users\\your-user-name\\AppData\\Roaming\\electerm\\users\\default_user\\electerm.data.nedb'
|
|
23
|
+
mac: 'rm -rf ~/Library/Application\\ Support/electerm/users/default_user/electerm_data.db && rm -rf ~/Library/Application\\ Support/electerm/users/default_user/electerm.data.nedb',
|
|
24
|
+
linux: 'rm -rf ~/.config/electerm/users/default_user/electerm_data.db && rm -rf ~/.config/electerm/users/default_user/electerm.data.nedb',
|
|
25
|
+
windows: 'Delete C:\\Users\\your-user-name\\AppData\\Roaming\\electerm\\users\\default_user\\electerm_data.db && Delete C:\\Users\\your-user-name\\AppData\\Roaming\\electerm\\users\\default_user\\electerm.data.nedb'
|
|
26
26
|
},
|
|
27
27
|
clearData: {
|
|
28
28
|
mac: 'rm -rf ~/Library/Application\\ Support/electerm*',
|
|
@@ -28,6 +28,8 @@ import ConnectionHoppingWarning from './connection-hopping-warnning'
|
|
|
28
28
|
import SshConfigLoadNotify from '../ssh-config/ssh-config-load-notify'
|
|
29
29
|
import LoadSshConfigs from '../ssh-config/load-ssh-configs'
|
|
30
30
|
import AIChat from '../ai/ai-chat'
|
|
31
|
+
import Opacity from '../common/opacity'
|
|
32
|
+
import InputContextMenu from '../common/input-context-menu'
|
|
31
33
|
import { pick } from 'lodash-es'
|
|
32
34
|
import deepCopy from 'json-deep-copy'
|
|
33
35
|
import './wrapper.styl'
|
|
@@ -152,11 +154,6 @@ export default auto(function Index (props) {
|
|
|
152
154
|
const themeProps = {
|
|
153
155
|
themeConfig: store.getUiThemeConfig()
|
|
154
156
|
}
|
|
155
|
-
const outerProps = {
|
|
156
|
-
style: {
|
|
157
|
-
opacity: config.opacity
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
157
|
const copiedTransfer = deepCopy(fileTransfers)
|
|
161
158
|
const copiedHistory = deepCopy(transferHistory)
|
|
162
159
|
const sidebarProps = {
|
|
@@ -252,6 +249,7 @@ export default auto(function Index (props) {
|
|
|
252
249
|
theme={uiThemeConfig}
|
|
253
250
|
>
|
|
254
251
|
<div {...ext1}>
|
|
252
|
+
<InputContextMenu />
|
|
255
253
|
<ShortcutControl config={config} />
|
|
256
254
|
<TermFullscreenControl
|
|
257
255
|
terminalFullScreen={terminalFullScreen}
|
|
@@ -260,10 +258,10 @@ export default auto(function Index (props) {
|
|
|
260
258
|
{...confsCss}
|
|
261
259
|
wsInited={wsInited}
|
|
262
260
|
/>
|
|
261
|
+
<Opacity opacity={config.opacity} />
|
|
263
262
|
<TerminalInteractive />
|
|
264
263
|
<UiTheme
|
|
265
264
|
{...themeProps}
|
|
266
|
-
buildTheme={store.buildTheme}
|
|
267
265
|
/>
|
|
268
266
|
<CustomCss customCss={config.customCss} />
|
|
269
267
|
<TextEditor />
|
|
@@ -277,7 +275,6 @@ export default auto(function Index (props) {
|
|
|
277
275
|
<BatchOp {...batchOpProps} />
|
|
278
276
|
<div
|
|
279
277
|
id='outside-context'
|
|
280
|
-
{...outerProps}
|
|
281
278
|
>
|
|
282
279
|
<Sidebar {...sidebarProps} />
|
|
283
280
|
<Layout
|
|
@@ -5,20 +5,60 @@
|
|
|
5
5
|
import { useEffect } from 'react'
|
|
6
6
|
import { useDelta, useConditionalEffect } from 'react-delta-hooks'
|
|
7
7
|
import eq from 'fast-deep-equal'
|
|
8
|
+
import isColorDark from '../../common/is-color-dark'
|
|
8
9
|
|
|
9
10
|
const themeDomId = 'theme-css'
|
|
10
11
|
|
|
12
|
+
function darker (color, amount = 0.1) {
|
|
13
|
+
let usePound = false
|
|
14
|
+
|
|
15
|
+
if (color[0] === '#') {
|
|
16
|
+
color = color.slice(1)
|
|
17
|
+
usePound = true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const num = parseInt(color, 16)
|
|
21
|
+
|
|
22
|
+
let r = (num >> 16) - Math.round(255 * amount)
|
|
23
|
+
if (r < 0) r = 0
|
|
24
|
+
let b = ((num >> 8) & 0x00FF) - Math.round(255 * amount)
|
|
25
|
+
if (b < 0) b = 0
|
|
26
|
+
let g = (num & 0x0000FF) - Math.round(255 * amount)
|
|
27
|
+
if (g < 0) g = 0
|
|
28
|
+
|
|
29
|
+
return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildTheme (themeConfig) {
|
|
33
|
+
const keys = Object.keys(themeConfig || {})
|
|
34
|
+
const themeCss = keys.map(key => {
|
|
35
|
+
const val = themeConfig[key]
|
|
36
|
+
if (key === 'primary') {
|
|
37
|
+
const contrast = isColorDark(val) ? '#fff' : '#000'
|
|
38
|
+
return `--${key}-contrast: ${contrast};\n--${key}: ${val};`
|
|
39
|
+
} else if (key === 'main') {
|
|
40
|
+
const darkerMain = darker(val, 0.3)
|
|
41
|
+
const lighterMain = darker(val, -0.3)
|
|
42
|
+
return `--${key}-darker: ${darkerMain};\n--${key}-lighter: ${lighterMain};\n--${key}: ${val};`
|
|
43
|
+
}
|
|
44
|
+
return `--${key}: ${val};`
|
|
45
|
+
}).join('\n')
|
|
46
|
+
if (themeCss) {
|
|
47
|
+
const css = `:root {\n${themeCss}\n}\n`
|
|
48
|
+
return Promise.resolve(css)
|
|
49
|
+
}
|
|
50
|
+
return Promise.resolve('')
|
|
51
|
+
}
|
|
52
|
+
|
|
11
53
|
export default function UiTheme (props) {
|
|
12
|
-
const { themeConfig
|
|
54
|
+
const { themeConfig } = props
|
|
13
55
|
|
|
14
56
|
const delta = useDelta(themeConfig)
|
|
15
57
|
|
|
16
58
|
async function applyTheme () {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
} = await buildTheme(themeConfig)
|
|
21
|
-
stylus.innerHTML = stylusCss
|
|
59
|
+
const style = document.getElementById(themeDomId)
|
|
60
|
+
const css = await buildTheme(themeConfig)
|
|
61
|
+
style.innerHTML = css
|
|
22
62
|
}
|
|
23
63
|
|
|
24
64
|
useEffect(() => {
|