@electerm/electerm-react 5.5.9 → 5.5.25
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/README.md +12 -2
- package/client/common/constants.js +14 -0
- package/client/common/default-setting.js +2 -2
- package/client/common/download.jsx +12 -0
- package/client/common/parse-vv.js +313 -0
- package/client/common/resolve.js +20 -14
- package/client/common/theme-defaults.js +5 -2
- package/client/common/update-check.js +5 -4
- package/client/components/ai/ai-history-item.jsx +1 -1
- package/client/components/ai/ai-history.jsx +1 -0
- package/client/components/ai/ai-history.styl +23 -0
- package/client/components/ai/ai-presets.js +13 -2
- package/client/components/bg/css-overwrite.jsx +16 -1
- package/client/components/bookmark-form/common/color-picker.styl +3 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/common/vv-file-field.jsx +137 -0
- package/client/components/bookmark-form/config/spice.js +1 -0
- package/client/components/common/remote-pointer.js +49 -0
- package/client/components/common/remote-session-shell.jsx +137 -0
- package/client/components/common/remote-session-shell.styl +80 -0
- package/client/components/common/responsive-tabs.jsx +89 -0
- package/client/components/common/responsive-tabs.styl +34 -0
- package/client/components/footer/footer-entry.jsx +39 -14
- package/client/components/icons/ai-icon.styl +4 -1
- package/client/components/layout/layout.jsx +7 -1
- package/client/components/main/main.jsx +9 -2
- package/client/components/main/term-fullscreen.styl +7 -0
- package/client/components/quick-commands/quick-command-ai-editor.jsx +255 -0
- package/client/components/quick-commands/quick-command-ai-editor.styl +13 -0
- package/client/components/quick-commands/quick-commands-box.jsx +23 -5
- package/client/components/quick-commands/quick-commands-form-elem.jsx +129 -65
- package/client/components/rdp/rdp-session.jsx +94 -124
- package/client/components/rdp/rdp.styl +13 -36
- package/client/components/session/session.styl +0 -16
- package/client/components/setting-panel/list.jsx +2 -2
- package/client/components/setting-panel/setting-modal.jsx +8 -3
- package/client/components/sftp/sftp-entry.jsx +26 -0
- package/client/components/side-panel-r/right-side-panel.styl +11 -1
- package/client/components/side-panel-r/side-panel-r.jsx +45 -9
- package/client/components/sidebar/bookmark-select.jsx +3 -1
- package/client/components/sidebar/index.jsx +13 -4
- package/client/components/sidebar/side-panel.jsx +8 -3
- package/client/components/sidebar/sidebar-panel.jsx +20 -7
- package/client/components/spice/spice-session.jsx +69 -72
- package/client/components/spice/spice.styl +10 -19
- package/client/components/terminal/shortcut-bar.styl +14 -0
- package/client/components/terminal/unix-timestamp-tooltip.jsx +27 -1
- package/client/components/terminal-info/terminal-info.styl +3 -0
- package/client/components/theme/terminal-theme-list.styl +3 -0
- package/client/components/theme/theme-ai-editor.jsx +2 -2
- package/client/components/theme/theme-form.jsx +20 -23
- package/client/components/theme/theme-form.styl +8 -10
- package/client/components/theme/theme-list-item.jsx +59 -25
- package/client/components/vnc/vnc-session.jsx +72 -104
- package/client/components/vnc/vnc.styl +25 -16
- package/client/css/mobile.styl +28 -0
- package/client/store/common.js +44 -0
- package/client/store/init-state.js +2 -1
- package/client/store/setting.js +4 -0
- package/client/store/sidebar.js +5 -1
- package/client/store/store.js +9 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ This is react components pack for electerm sub projects
|
|
|
16
16
|
|
|
17
17
|
Open-sourced terminal/ssh/sftp/telnet/serialport/RDP/VNC/Spice/ftp client(Linux, Mac, Windows, Android, HarmonyOS, iOS).
|
|
18
18
|
|
|
19
|
-
Besides mainstream Windows/macOS/Linux/Android, electerm also supports HarmonyOS, iOS, and older systems — Ubuntu 18, Windows 7, macOS 10+, and special Chinese Linux distributions such as UOS, Kylin, and LoongArch (both old-world and new-world).
|
|
19
|
+
Besides mainstream Windows/macOS/Linux/Android, electerm also supports HarmonyOS, iOS, and older systems — Ubuntu 18, Windows 7, macOS 10+, and special Chinese Linux distributions such as UOS, Kylin, and LoongArch (both old-world and new-world), as well as RISC-V (riscv64) and PowerPC 64-bit little-endian (ppc64le) Linux.
|
|
20
20
|
|
|
21
21
|
<p>
|
|
22
22
|
<a href="https://electerm.org">Homepage / Downloads</a> ·
|
|
@@ -49,6 +49,16 @@ Besides mainstream Windows/macOS/Linux/Android, electerm also supports HarmonyOS
|
|
|
49
49
|
|
|
50
50
|
----
|
|
51
51
|
|
|
52
|
+
<div align="center">
|
|
53
|
+
<a href="https://fluxionai.world/register?aff=7H7DERHU3GFF&utm_source=github&utm_medium=link&utm_campaign=electerm">
|
|
54
|
+
<img src="https://github.com/electerm/electerm-resource/blob/master/static/images/fluxionai.png?raw=true" alt="FluxionAI" height="180" />
|
|
55
|
+
</a>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
[Fluxion AI](https://fluxionai.world/register?aff=7H7DERHU3GFF&utm_source=github&utm_medium=link&utm_campaign=electerm) — One gateway to the world's leading AI models. [Sign up here to get started](https://fluxionai.world/register?aff=7H7DERHU3GFF&utm_source=github&utm_medium=link&utm_campaign=electerm).
|
|
59
|
+
|
|
60
|
+
----
|
|
61
|
+
|
|
52
62
|
<div align="center">
|
|
53
63
|
<br />
|
|
54
64
|
<br />
|
|
@@ -86,7 +96,7 @@ Thanks to APIMart for sponsoring electerm! APIMart is a low-cost API platform fo
|
|
|
86
96
|
## Features
|
|
87
97
|
|
|
88
98
|
- Works as a terminal/file manager, ssh/sftp/ftp/telnet/serialport/RDP/VNC/Spice client
|
|
89
|
-
- Support Window 7+(X64/ARM64), HarmonyOS,Android, iOS, Mac OS 10.15+(x64/arm64), Linux(x64/arm64/Loong64 new world & old world), even old Linux with glibc 2.17+ like UOS/Kylin/Ubuntu 18.04 etc
|
|
99
|
+
- Support Window 7+(X64/ARM64), HarmonyOS,Android, iOS, Mac OS 10.15+(x64/arm64), Linux(x64/arm64/riscv64/ppc64le/Loong64 new world & old world), even old Linux with glibc 2.17+ like UOS/Kylin/Ubuntu 18.04 etc
|
|
90
100
|
- Global hotkey to toggle window visibility (similar to guake, default is `ctrl + 2`)
|
|
91
101
|
- 🇺🇸 🇨🇳 🇧🇷 🇷🇺 🇪🇸 🇫🇷 🇹🇷 🇭🇰 🇯🇵 🇸🇦 🇩🇪 🇰🇷 🇮🇩 🇵🇱 Multi-language support([electerm-locales](https://github.com/electerm/electerm-locales), contributions/fixes welcome)
|
|
92
102
|
- Double click to directly edit (small) remote files.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* constants
|
|
3
3
|
*/
|
|
4
4
|
import { isMacJs } from './platform.js'
|
|
5
|
+
import { defaultThemeId, defaultThemeLightId } from './theme-defaults.js'
|
|
5
6
|
import logoPath1Ref from '@electerm/electerm-resource/res/imgs/electerm-round-128x128.png'
|
|
6
7
|
import logoPath2Ref from '@electerm/electerm-resource/res/imgs/electerm.png'
|
|
7
8
|
import logoPath3Ref from '@electerm/electerm-resource/res/imgs/electerm-watermark.png'
|
|
@@ -156,6 +157,7 @@ export const leftSideBarOpenKey = 'left-side-bar-open'
|
|
|
156
157
|
export const pinnedQuickCommandBarKey = 'pinned-quick-command-bar'
|
|
157
158
|
export const leftSidePanelWidthKey = 'left-sidebar-width'
|
|
158
159
|
export const rightSidebarWidthKey = 'right-sidebar-width'
|
|
160
|
+
export const rightPanelPinnedKey = 'right-panel-pinned'
|
|
159
161
|
export const addPanelWidthLsKey = 'addPanelWidth'
|
|
160
162
|
export const sftpDefaultSortSettingKey = 'sftp-default-sort'
|
|
161
163
|
export const qmSortByFrequencyKey = 'qm-sort-by-frequency'
|
|
@@ -241,6 +243,18 @@ export const settingShortcutsId = 'setting-shortcuts'
|
|
|
241
243
|
export const settingAiId = 'setting-ai'
|
|
242
244
|
export const settingCommonId = 'setting-common'
|
|
243
245
|
export const settingPasswordsId = 'setting-passwords'
|
|
246
|
+
// Built in items the user is not allowed to delete: the two settings tabs,
|
|
247
|
+
// the default bookmark group and the two built in terminal themes.
|
|
248
|
+
// Keep this an explicit id list. A prefix test like `id.startsWith('default')`
|
|
249
|
+
// would also cover user facing seeds (default quick commands, default local
|
|
250
|
+
// terminal bookmarks) that the user must be able to remove.
|
|
251
|
+
export const undeletableIds = new Set([
|
|
252
|
+
settingSyncId,
|
|
253
|
+
settingCommonId,
|
|
254
|
+
defaultBookmarkGroupId,
|
|
255
|
+
defaultThemeId,
|
|
256
|
+
defaultThemeLightId
|
|
257
|
+
])
|
|
244
258
|
// settings that only apply to the desktop (electron) app,
|
|
245
259
|
// hidden when window.et.isWebApp is true
|
|
246
260
|
export const webAppHiddenSettings = [
|
|
@@ -78,8 +78,8 @@ export default {
|
|
|
78
78
|
hideIP: false,
|
|
79
79
|
dataSyncSelected: 'all',
|
|
80
80
|
nameAI: '',
|
|
81
|
-
baseURLAI: 'https://
|
|
82
|
-
modelAI: 'deepseek-
|
|
81
|
+
baseURLAI: 'https://fluxionai.world/v1',
|
|
82
|
+
modelAI: 'deepseek-v4-flash-0731',
|
|
83
83
|
roleAI: '终端专家,提供不同系统下命令,简要解释用法,用markdown格式',
|
|
84
84
|
apiPathAI: '/chat/completions',
|
|
85
85
|
proxyAI: '',
|
|
@@ -7,6 +7,18 @@ import { chooseSaveDirectory } from './choose-save-folder'
|
|
|
7
7
|
import { DownloadOutlined } from '@ant-design/icons'
|
|
8
8
|
|
|
9
9
|
export default async function download (filename, text) {
|
|
10
|
+
// iOS WKWebView: sandbox targets are invisible to the Files app and
|
|
11
|
+
// blob-anchor downloads are a no-op — save to Documents (visible in
|
|
12
|
+
// Files) via the native bridge instead (see
|
|
13
|
+
// web-components/native-file-save.js).
|
|
14
|
+
if (window.et && window.et.isWebApp && window.et.saveTextNative) {
|
|
15
|
+
try {
|
|
16
|
+
await window.et.saveTextNative(filename, text)
|
|
17
|
+
return
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.log('native save failed, falling back to sandbox save:', e)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
const opts = window.et.isWebApp
|
|
11
23
|
? { filename, content: text }
|
|
12
24
|
: undefined
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .vv connection file parser (virt-viewer / remote-viewer)
|
|
3
|
+
*
|
|
4
|
+
* A .vv file is an INI document with a mandatory [virt-viewer] group and a
|
|
5
|
+
* mandatory `type` key. Everything SPICE-related lives directly in
|
|
6
|
+
* [virt-viewer] -- there is NO [spice] group. An optional [ovirt] group
|
|
7
|
+
* carries oVirt REST integration data.
|
|
8
|
+
*
|
|
9
|
+
* Behaviour is deliberately matched against:
|
|
10
|
+
* - src/virt-viewer-file.c (gitlab.com/virt-viewer/virt-viewer)
|
|
11
|
+
* - remote-viewer(1), "CONNECTION FILE" section
|
|
12
|
+
*
|
|
13
|
+
* Rejection rules copied from virt_viewer_file_new():
|
|
14
|
+
* no [virt-viewer] group -> invalid file
|
|
15
|
+
* no `type` key -> invalid file
|
|
16
|
+
* unknown keys/groups -> kept, never an error (the format is
|
|
17
|
+
* extensible; the convention is an `x-` prefix)
|
|
18
|
+
*
|
|
19
|
+
* Keep in sync with src/client/common/parse-vv.js -- the two differ only in
|
|
20
|
+
* the export statement (see sanitize-filename.js for the same pattern).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const MAIN_GROUP = 'virt-viewer'
|
|
24
|
+
const OVIRT_GROUP = 'ovirt'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* .vv key -> bookmark field understood by
|
|
28
|
+
* src/client/components/bookmark-form/config/spice.js
|
|
29
|
+
*/
|
|
30
|
+
const FIELD_MAP = {
|
|
31
|
+
host: 'host',
|
|
32
|
+
port: 'port',
|
|
33
|
+
password: 'password',
|
|
34
|
+
title: 'title',
|
|
35
|
+
proxy: 'proxy'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Keys the .vv format defines but electerm cannot honour for a spice session.
|
|
40
|
+
* Each becomes a user-visible note instead of being dropped silently.
|
|
41
|
+
*/
|
|
42
|
+
const UNSUPPORTED = {
|
|
43
|
+
'tls-port': 'TLS is not supported (electerm makes a plain TCP connection)',
|
|
44
|
+
'tls-ciphers': 'TLS is not supported',
|
|
45
|
+
ca: 'TLS certificate verification is not supported',
|
|
46
|
+
'host-subject': 'TLS certificate verification is not supported',
|
|
47
|
+
'unix-path': 'Unix-socket transport is not supported',
|
|
48
|
+
username: 'spice sessions authenticate with the password only',
|
|
49
|
+
'disable-channels': 'spice channel selection is not supported',
|
|
50
|
+
'secure-channels': 'spice channel selection is not supported',
|
|
51
|
+
'color-depth': 'colour depth is negotiated with the server',
|
|
52
|
+
'disable-effects': 'remote desktop-effect control is not supported',
|
|
53
|
+
'enable-smartcard': 'smartcard redirection is not supported',
|
|
54
|
+
'enable-usbredir': 'USB redirection is not supported',
|
|
55
|
+
'enable-usb-autoshare': 'USB redirection is not supported',
|
|
56
|
+
'usb-filter': 'USB redirection is not supported',
|
|
57
|
+
'usb-redirect-on-connect': 'USB redirection is not supported',
|
|
58
|
+
fullscreen: 'fullscreen is a session-time toggle',
|
|
59
|
+
version: 'client version requirements are not enforced',
|
|
60
|
+
versions: 'client version requirements are not enforced',
|
|
61
|
+
'newer-version-url': 'client version requirements are not enforced',
|
|
62
|
+
'toggle-fullscreen': 'hotkey rebinding is not supported',
|
|
63
|
+
'release-cursor': 'hotkey rebinding is not supported',
|
|
64
|
+
'secure-attention': 'hotkey rebinding is not supported',
|
|
65
|
+
'zoom-in': 'hotkey rebinding is not supported',
|
|
66
|
+
'zoom-out': 'hotkey rebinding is not supported',
|
|
67
|
+
'zoom-reset': 'hotkey rebinding is not supported',
|
|
68
|
+
'smartcard-insert': 'hotkey rebinding is not supported',
|
|
69
|
+
'smartcard-remove': 'hotkey rebinding is not supported',
|
|
70
|
+
'usb-device-reset': 'hotkey rebinding is not supported'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const BOOL_TRUE = new Set(['1', 'true', 'yes', 'on'])
|
|
74
|
+
|
|
75
|
+
function stripBom (text) {
|
|
76
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isComment (line) {
|
|
80
|
+
return line.startsWith('#') || line.startsWith(';')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse INI text into { groupName: { key: value } }.
|
|
85
|
+
*
|
|
86
|
+
* GKeyFile itself is case-sensitive; group and key names are lowercased on
|
|
87
|
+
* purpose, because hand-written .vv files are inconsistent about casing and
|
|
88
|
+
* the alternative is a confusing "host not set" for a file that clearly
|
|
89
|
+
* sets `Host=`.
|
|
90
|
+
*
|
|
91
|
+
* A trailing backslash continues the value on the next line (GKeyFile rule).
|
|
92
|
+
*
|
|
93
|
+
* @param {string} text
|
|
94
|
+
* @returns {Object} groups
|
|
95
|
+
*/
|
|
96
|
+
function parseIni (text) {
|
|
97
|
+
const groups = {}
|
|
98
|
+
let group = null
|
|
99
|
+
const lines = stripBom(text).split(/\r\n|\r|\n/)
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
102
|
+
let line = lines[i]
|
|
103
|
+
|
|
104
|
+
while (/\\[ \t]*$/.test(line) && i + 1 < lines.length) {
|
|
105
|
+
line = line.replace(/\\[ \t]*$/, '') + lines[++i]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const trimmed = line.trim()
|
|
109
|
+
if (!trimmed || isComment(trimmed)) {
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const groupMatch = trimmed.match(/^\[(.+)\]$/)
|
|
114
|
+
if (groupMatch) {
|
|
115
|
+
group = groupMatch[1].trim().toLowerCase()
|
|
116
|
+
if (!groups[group]) {
|
|
117
|
+
groups[group] = {}
|
|
118
|
+
}
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const eq = line.indexOf('=')
|
|
123
|
+
if (eq < 0 || !group) {
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const key = line.slice(0, eq).trim().toLowerCase()
|
|
128
|
+
// Only leading whitespace is stripped. A trailing space is significant
|
|
129
|
+
// for values such as passwords, so it is preserved.
|
|
130
|
+
const value = line.slice(eq + 1).replace(/^[ \t]+/, '')
|
|
131
|
+
if (key) {
|
|
132
|
+
groups[group][key] = value
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return groups
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse the contents of a .vv file.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} text - raw contents of a .vv file
|
|
143
|
+
* @returns {{
|
|
144
|
+
* ok: boolean, error: string|null, type: string|null,
|
|
145
|
+
* fields: Object, applied: Array, ignored: Array,
|
|
146
|
+
* unknown: Array, warnings: string[], deleteThisFile: boolean,
|
|
147
|
+
* groups: Object
|
|
148
|
+
* }}
|
|
149
|
+
*/
|
|
150
|
+
function parseVv (text) {
|
|
151
|
+
const result = {
|
|
152
|
+
ok: false,
|
|
153
|
+
error: null,
|
|
154
|
+
type: null,
|
|
155
|
+
fields: {},
|
|
156
|
+
applied: [],
|
|
157
|
+
ignored: [],
|
|
158
|
+
unknown: [],
|
|
159
|
+
warnings: [],
|
|
160
|
+
deleteThisFile: false,
|
|
161
|
+
groups: {}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
165
|
+
result.error = 'empty file'
|
|
166
|
+
return result
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let groups
|
|
170
|
+
try {
|
|
171
|
+
groups = parseIni(text)
|
|
172
|
+
} catch (e) {
|
|
173
|
+
result.error = 'cannot parse file: ' + e.message
|
|
174
|
+
return result
|
|
175
|
+
}
|
|
176
|
+
result.groups = groups
|
|
177
|
+
|
|
178
|
+
const main = groups[MAIN_GROUP]
|
|
179
|
+
if (!main) {
|
|
180
|
+
result.error = 'invalid .vv file: missing [' + MAIN_GROUP + '] group'
|
|
181
|
+
return result
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const type = (main.type || '').trim().toLowerCase()
|
|
185
|
+
if (!type) {
|
|
186
|
+
result.error = 'invalid .vv file: missing "type" key in [' + MAIN_GROUP + ']'
|
|
187
|
+
return result
|
|
188
|
+
}
|
|
189
|
+
result.type = type
|
|
190
|
+
if (type !== 'spice') {
|
|
191
|
+
result.error = 'unsupported session type "' + type + '": spice only'
|
|
192
|
+
return result
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
Object.keys(FIELD_MAP).forEach(key => {
|
|
196
|
+
if (!(key in main)) {
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
const raw = main[key]
|
|
200
|
+
const field = FIELD_MAP[key]
|
|
201
|
+
|
|
202
|
+
if (key === 'port') {
|
|
203
|
+
const port = parseInt(raw.trim(), 10)
|
|
204
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
205
|
+
result.warnings.push('ignoring invalid port "' + raw.trim() + '"')
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
result.fields.port = port
|
|
209
|
+
} else if (key === 'password') {
|
|
210
|
+
result.fields.password = raw
|
|
211
|
+
} else {
|
|
212
|
+
const value = raw.trim()
|
|
213
|
+
if (!value) {
|
|
214
|
+
result.warnings.push('ignoring empty "' + key + '"')
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
result.fields[field] = value
|
|
218
|
+
}
|
|
219
|
+
result.applied.push({ key, value: raw, field })
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
if (!result.fields.host) {
|
|
223
|
+
result.error = 'no usable host: [virt-viewer] has no "host" key'
|
|
224
|
+
return result
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// A file that only offers a TLS port has nothing we can dial, because the
|
|
228
|
+
// spice transport here is plain TCP (see src/app/server/spice-proxy.js).
|
|
229
|
+
if (main['tls-port'] && !('port' in main)) {
|
|
230
|
+
result.warnings.push(
|
|
231
|
+
'this file defines only tls-port, and TLS is not supported here, ' +
|
|
232
|
+
'so there is no plain port to connect to'
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// delete-this-file=1 makes remote-viewer unlink the file after loading it
|
|
237
|
+
// (unless VIRT_VIEWER_KEEP_FILE is set). We never delete the user's file;
|
|
238
|
+
// surface it instead so the caller can decide.
|
|
239
|
+
if ('delete-this-file' in main) {
|
|
240
|
+
result.deleteThisFile = BOOL_TRUE.has(
|
|
241
|
+
main['delete-this-file'].trim().toLowerCase()
|
|
242
|
+
)
|
|
243
|
+
if (result.deleteThisFile) {
|
|
244
|
+
result.warnings.push(
|
|
245
|
+
'the file is marked delete-this-file=1; it was NOT deleted'
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
Object.keys(main).forEach(key => {
|
|
251
|
+
if (key === 'type' || key === 'delete-this-file' || key in FIELD_MAP) {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
const value = main[key]
|
|
255
|
+
if (key in UNSUPPORTED) {
|
|
256
|
+
result.ignored.push({ key, value, reason: UNSUPPORTED[key] })
|
|
257
|
+
} else {
|
|
258
|
+
result.unknown.push({ key, value })
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
if (groups[OVIRT_GROUP]) {
|
|
263
|
+
result.ignored.push({
|
|
264
|
+
key: '[' + OVIRT_GROUP + ']',
|
|
265
|
+
value: Object.keys(groups[OVIRT_GROUP]).join(', '),
|
|
266
|
+
reason: 'oVirt REST integration is not supported'
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (result.ignored.length) {
|
|
271
|
+
result.warnings.push(
|
|
272
|
+
result.ignored.length +
|
|
273
|
+
' option(s) in this file are not supported and were skipped'
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
result.ok = true
|
|
278
|
+
return result
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Turn a parse result into a one-line summary for a UI hint.
|
|
283
|
+
*
|
|
284
|
+
* @param {Object} parsed - result of parseVv
|
|
285
|
+
* @returns {string}
|
|
286
|
+
*/
|
|
287
|
+
function describeVv (parsed) {
|
|
288
|
+
if (!parsed || !parsed.ok) {
|
|
289
|
+
return (parsed && parsed.error) || 'invalid .vv file'
|
|
290
|
+
}
|
|
291
|
+
const { host, port, title, proxy } = parsed.fields
|
|
292
|
+
const bits = ['spice://' + host + (port ? ':' + port : '')]
|
|
293
|
+
if (title) {
|
|
294
|
+
bits.push('"' + title + '"')
|
|
295
|
+
}
|
|
296
|
+
if (proxy) {
|
|
297
|
+
bits.push('via ' + proxy)
|
|
298
|
+
}
|
|
299
|
+
if (parsed.ignored.length) {
|
|
300
|
+
bits.push(parsed.ignored.length + ' option(s) skipped')
|
|
301
|
+
}
|
|
302
|
+
return bits.join(' ')
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export {
|
|
306
|
+
parseVv,
|
|
307
|
+
parseIni,
|
|
308
|
+
describeVv,
|
|
309
|
+
MAIN_GROUP,
|
|
310
|
+
OVIRT_GROUP,
|
|
311
|
+
FIELD_MAP,
|
|
312
|
+
UNSUPPORTED
|
|
313
|
+
}
|
package/client/common/resolve.js
CHANGED
|
@@ -13,24 +13,30 @@ export const isWslDistroRoot = (path) => {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export default function resolve (basePath, nameOrDot) {
|
|
16
|
+
// Callers can hand this a value that is not a path yet (a session path that
|
|
17
|
+
// is still loading, a missing tab option, ...). Reading `.includes` off it
|
|
18
|
+
// used to throw and take the whole renderer down with a white screen, so
|
|
19
|
+
// anything that is not a string is treated as an empty path here.
|
|
20
|
+
const base = typeof basePath === 'string' ? basePath : ''
|
|
21
|
+
const name = typeof nameOrDot === 'string' ? nameOrDot : ''
|
|
16
22
|
const hasWinDrive = (path) => /^[a-zA-Z]:/.test(path)
|
|
17
|
-
const isWin =
|
|
23
|
+
const isWin = base.includes('\\') || name.includes('\\') || hasWinDrive(base) || hasWinDrive(name)
|
|
18
24
|
const sep = isWin ? '\\' : '/'
|
|
19
|
-
if (/^[a-zA-Z]:/.test(
|
|
20
|
-
return
|
|
25
|
+
if (/^[a-zA-Z]:/.test(name)) {
|
|
26
|
+
return name.replace(/^\//, '').replace(/\//g, sep)
|
|
21
27
|
}
|
|
22
|
-
if (
|
|
23
|
-
return
|
|
28
|
+
if (name.startsWith('/')) {
|
|
29
|
+
return name.replace(/\\/g, sep)
|
|
24
30
|
}
|
|
25
|
-
if (
|
|
26
|
-
return
|
|
31
|
+
if (name.startsWith('\\\\')) {
|
|
32
|
+
return name
|
|
27
33
|
}
|
|
28
|
-
if (
|
|
29
|
-
if (isWslDistroRoot(
|
|
34
|
+
if (name === '..') {
|
|
35
|
+
if (isWslDistroRoot(base)) {
|
|
30
36
|
return '/'
|
|
31
37
|
}
|
|
32
|
-
const baseEndsWithSep =
|
|
33
|
-
const parts =
|
|
38
|
+
const baseEndsWithSep = base.endsWith(sep)
|
|
39
|
+
const parts = base.split(sep)
|
|
34
40
|
if (parts.length > 1) {
|
|
35
41
|
parts.pop()
|
|
36
42
|
if (isWin && parts.length === 1) {
|
|
@@ -40,10 +46,10 @@ export default function resolve (basePath, nameOrDot) {
|
|
|
40
46
|
}
|
|
41
47
|
return '/'
|
|
42
48
|
}
|
|
43
|
-
if (isWslDistroRoot(
|
|
44
|
-
return
|
|
49
|
+
if (isWslDistroRoot(base) && !base.endsWith(sep)) {
|
|
50
|
+
return base + sep + name
|
|
45
51
|
}
|
|
46
|
-
const result =
|
|
52
|
+
const result = base.endsWith(sep) ? base + name : base + sep + name
|
|
47
53
|
return isWin && result.length === 3 && result.endsWith(':\\') ? '/' : result
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -101,9 +101,12 @@ const defaultThemeDarkTerminal = () => {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
export const defaultThemeId = 'default'
|
|
105
|
+
export const defaultThemeLightId = 'defaultLight'
|
|
106
|
+
|
|
104
107
|
export function defaultTheme () {
|
|
105
108
|
return {
|
|
106
|
-
id:
|
|
109
|
+
id: defaultThemeId,
|
|
107
110
|
name: 'default',
|
|
108
111
|
themeConfig: defaultThemeDarkTerminal(),
|
|
109
112
|
uiThemeConfig: defaultThemeDark()
|
|
@@ -112,7 +115,7 @@ export function defaultTheme () {
|
|
|
112
115
|
|
|
113
116
|
export function defaultThemeLight () {
|
|
114
117
|
return {
|
|
115
|
-
id:
|
|
118
|
+
id: defaultThemeLightId,
|
|
116
119
|
name: 'default light',
|
|
117
120
|
themeConfig: defaultThemeLightTerminal(),
|
|
118
121
|
uiThemeConfig: defaultThemeLightFunc()
|
|
@@ -23,7 +23,7 @@ async function fetchData (url, options) {
|
|
|
23
23
|
|
|
24
24
|
function getInfo (url) {
|
|
25
25
|
const n = Date.now()
|
|
26
|
-
const tail = url.includes('?') ? '' : '?_=' + n
|
|
26
|
+
const tail = url.includes('?') ? '&_=' + n : '?_=' + n
|
|
27
27
|
return fetchData(url + tail, {
|
|
28
28
|
action: 'get-update-info',
|
|
29
29
|
headers: {
|
|
@@ -87,16 +87,17 @@ export function getDownloadUrl (browserDownloadUrl, mirror) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export async function getLatestReleaseInfo () {
|
|
90
|
-
|
|
90
|
+
const { installSrc } = window.store
|
|
91
|
+
const src = installSrc ? '?src=' + encodeURIComponent(installSrc) : ''
|
|
92
|
+
let url = `${baseUpdateCheckUrls[0]}/data/electerm-github-release.json${src}`
|
|
91
93
|
let res = await getInfo(url)
|
|
92
94
|
if (!res?.release?.body) {
|
|
93
|
-
url = `${baseUpdateCheckUrls[1]}/data/electerm-github-release.json`
|
|
95
|
+
url = `${baseUpdateCheckUrls[1]}/data/electerm-github-release.json${src}`
|
|
94
96
|
res = await getInfo(url)
|
|
95
97
|
}
|
|
96
98
|
if (!res || !res.release) {
|
|
97
99
|
return undefined
|
|
98
100
|
}
|
|
99
|
-
const { installSrc } = window.store
|
|
100
101
|
const asset = (res.release.assets || []).find(r => r.name.includes(installSrc))
|
|
101
102
|
return {
|
|
102
103
|
body: res.release.body,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// AI history items (ai-history.jsx / ai-history-item.jsx)
|
|
2
|
+
//
|
|
3
|
+
// Each entry is an antd Tag: nowrap, no max-width, and the text is a user
|
|
4
|
+
// written description up to 50 chars long. Space renders an inline-flex box,
|
|
5
|
+
// which shrink-to-fits its widest *unwrappable* child — so one long entry
|
|
6
|
+
// pushes the whole panel into horizontal scrolling instead of using the width
|
|
7
|
+
// it already has. The label gets its own ellipsis box so the tag can shrink to
|
|
8
|
+
// the available width while the close icon stays reachable.
|
|
9
|
+
.ai-bookmark-history
|
|
10
|
+
.ant-space
|
|
11
|
+
display flex
|
|
12
|
+
.ant-space-item
|
|
13
|
+
min-width 0
|
|
14
|
+
.ant-tag
|
|
15
|
+
max-width 100%
|
|
16
|
+
|
|
17
|
+
.ai-history-label
|
|
18
|
+
display inline-block
|
|
19
|
+
// leave room for the tag close icon
|
|
20
|
+
max-width calc(100% - 16px)
|
|
21
|
+
overflow hidden
|
|
22
|
+
text-overflow ellipsis
|
|
23
|
+
vertical-align middle
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export const defaultAIPresets = [
|
|
2
|
+
{
|
|
3
|
+
id: 'fluxionai',
|
|
4
|
+
nameAI: 'FluxionAI',
|
|
5
|
+
baseURLAI: 'https://fluxionai.world/v1',
|
|
6
|
+
apiPathAI: '/chat/completions',
|
|
7
|
+
modelAI: 'deepseek-v4-flash-0731',
|
|
8
|
+
authHeaderNameAI: 'Authorization: Bearer',
|
|
9
|
+
siteUrl: 'https://fluxionai.world/register?aff=7H7DERHU3GFF&utm_source=github&utm_medium=link&utm_campaign=electerm'
|
|
10
|
+
},
|
|
2
11
|
{
|
|
3
12
|
id: 'atlascloud',
|
|
4
13
|
nameAI: 'AtlasCloud',
|
|
@@ -166,8 +175,10 @@ export const defaultAIPresets = [
|
|
|
166
175
|
|
|
167
176
|
export function getAIPresets () {
|
|
168
177
|
const presets = [...defaultAIPresets]
|
|
169
|
-
|
|
170
|
-
|
|
178
|
+
const customPreset = window.et?.defaultAIPreset
|
|
179
|
+
if (customPreset) {
|
|
180
|
+
// keep the first preset (sponsor) on top, insert after it
|
|
181
|
+
presets.splice(1, 0, customPreset)
|
|
171
182
|
}
|
|
172
183
|
return presets
|
|
173
184
|
}
|
|
@@ -93,10 +93,25 @@ async function createStyleForTab (tab, props) {
|
|
|
93
93
|
'background-image: none'
|
|
94
94
|
)
|
|
95
95
|
}
|
|
96
|
-
} else if (st
|
|
96
|
+
} else if (st === 'none') {
|
|
97
|
+
// The [🚫] opt-out must be spelled out: this rule has a higher specificity
|
|
98
|
+
// than the global one, but an empty declaration block would still let the
|
|
99
|
+
// built-in default bg painted by createGlobalStyle show through.
|
|
100
|
+
styles.push('background-image: none')
|
|
101
|
+
} else {
|
|
97
102
|
styles.push(
|
|
98
103
|
`background-image: ${st}`,
|
|
99
104
|
'background-position: center',
|
|
105
|
+
// createGlobalStyle's built-in default bg rule carries an auto-fit
|
|
106
|
+
// `background-size` (so the 766px wide watermark is not cropped on
|
|
107
|
+
// narrow panes), and for the active tab it targets this very same
|
|
108
|
+
// ::before. `background-size` is not reset by a rule that only sets
|
|
109
|
+
// `background-image`, so it would leak onto the user's image and scale
|
|
110
|
+
// it down to at most 766px, centred — a big image that used to fill the
|
|
111
|
+
// pane would no longer cover it. Stating the initial value keeps user
|
|
112
|
+
// images at their natural size, which is what they rendered at before
|
|
113
|
+
// the auto-fit was introduced.
|
|
114
|
+
'background-size: auto',
|
|
100
115
|
`filter: ${createFilterStyle(props, tab)}`
|
|
101
116
|
)
|
|
102
117
|
}
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
width 92px
|
|
11
11
|
.color-picker-box
|
|
12
12
|
width 420px
|
|
13
|
+
// the popover is portaled to the body, a 420px box simply falls off a phone
|
|
14
|
+
// screen (measured: the swatch list and the antd picker still fit at 280px)
|
|
15
|
+
max-width calc(100vw - 40px)
|
|
13
16
|
|
|
14
17
|
.color-picker-defaults
|
|
15
18
|
width 200px
|
|
@@ -20,6 +20,7 @@ import TriggersField from '../../triggers/triggers-field.jsx'
|
|
|
20
20
|
import ProfileItem from './profile-item.jsx'
|
|
21
21
|
import renderRunScripts from './run-scripts.jsx'
|
|
22
22
|
import SerialPathSelector from './serial-path-selector.jsx'
|
|
23
|
+
import VvFileField from './vv-file-field.jsx'
|
|
23
24
|
import SshHostSelector from './ssh-host-selector.jsx'
|
|
24
25
|
import SshAuthTypeSelector from './ssh-auth-type-selector.jsx'
|
|
25
26
|
import SshAuthSelector from './ssh-auth-selector.jsx'
|
|
@@ -150,6 +151,8 @@ export function renderFormItem (item, formItemLayout, form, ctxProps, index) {
|
|
|
150
151
|
)
|
|
151
152
|
case 'proxy':
|
|
152
153
|
return <ProxyField key={name} bookmarks={ctxProps.bookmarks} />
|
|
154
|
+
case 'vvFile':
|
|
155
|
+
return <VvFileField key={name} form={form} />
|
|
153
156
|
case 'x11':
|
|
154
157
|
return <X11Field key={name} form={form} />
|
|
155
158
|
case 'sshTunnels':
|