@electerm/electerm-react 5.5.15 → 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/parse-vv.js +313 -0
- package/client/common/theme-defaults.js +5 -2
- package/client/common/update-check.js +5 -4
- package/client/components/ai/ai-presets.js +13 -2
- package/client/components/bg/css-overwrite.jsx +16 -1
- 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/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/setting-panel/list.jsx +2 -2
- 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/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 +2 -2
- package/client/components/theme/theme-list-item.jsx +59 -25
- 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/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: '',
|
|
@@ -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
|
+
}
|
|
@@ -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,
|
|
@@ -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
|
}
|
|
@@ -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':
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bookmark form - load a .vv (virt-viewer) connection file
|
|
3
|
+
*
|
|
4
|
+
* Registered as field type 'vvFile' in ./fields.jsx and used by
|
|
5
|
+
* ../config/spice.js. Fills host/port/password/title/proxy from the file and
|
|
6
|
+
* reports anything it could not honour instead of dropping it silently.
|
|
7
|
+
*/
|
|
8
|
+
import { useState } from 'react'
|
|
9
|
+
import {
|
|
10
|
+
Form,
|
|
11
|
+
Button,
|
|
12
|
+
Alert
|
|
13
|
+
} from 'antd'
|
|
14
|
+
import { FileAddOutlined } from '@ant-design/icons'
|
|
15
|
+
import fs from '../../../common/fs'
|
|
16
|
+
import { tailFormItemLayout } from '../../../common/form-layout'
|
|
17
|
+
import { parseVv, describeVv } from '../../../common/parse-vv'
|
|
18
|
+
|
|
19
|
+
const FormItem = Form.Item
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* window.translate echoes an unknown key back capitalised, so an unmapped
|
|
23
|
+
* 'loadVvFile' would render as "LoadVvFile". The key IS in
|
|
24
|
+
* ~/dev/electerm-locales, but the app consumes the published
|
|
25
|
+
* @electerm/electerm-locales, so it stays unknown until that ships and the dep
|
|
26
|
+
* is bumped. Fall back to the literal, which is what en_us resolves to anyway
|
|
27
|
+
* (capitalizeFirstLetter('load .vv file')), so nothing changes visually when
|
|
28
|
+
* the translation does land.
|
|
29
|
+
*/
|
|
30
|
+
function vvLabel () {
|
|
31
|
+
const t = window.translate('loadVvFile')
|
|
32
|
+
return t === 'LoadVvFile' ? 'Load .vv file' : t
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Open the native file dialog, filtered to .vv files.
|
|
37
|
+
* window.api.openDialog is dialog.showOpenDialogSync in the main process
|
|
38
|
+
* (see src/app/lib/ipc.js), so the full Electron options object is accepted.
|
|
39
|
+
* @returns {Promise<string|null>} selected path, or null when cancelled
|
|
40
|
+
*/
|
|
41
|
+
async function pickVvFile () {
|
|
42
|
+
const files = await window.api.openDialog({
|
|
43
|
+
title: 'Choose a .vv connection file',
|
|
44
|
+
message: 'Choose a .vv connection file',
|
|
45
|
+
properties: [
|
|
46
|
+
'openFile',
|
|
47
|
+
'showHiddenFiles',
|
|
48
|
+
'noResolveAliases',
|
|
49
|
+
'treatPackageAsDirectory',
|
|
50
|
+
'dontAddToRecent'
|
|
51
|
+
],
|
|
52
|
+
filters: [
|
|
53
|
+
{ name: 'Virt Viewer connection file', extensions: ['vv'] },
|
|
54
|
+
{ name: 'All files', extensions: ['*'] }
|
|
55
|
+
]
|
|
56
|
+
}).catch(() => false)
|
|
57
|
+
if (!files || !files.length) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
return files[0]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default function VvFileField ({ form }) {
|
|
64
|
+
const [hint, setHint] = useState(null)
|
|
65
|
+
const [loading, setLoading] = useState(false)
|
|
66
|
+
|
|
67
|
+
const load = async () => {
|
|
68
|
+
setLoading(true)
|
|
69
|
+
setHint(null)
|
|
70
|
+
try {
|
|
71
|
+
const filePath = await pickVvFile()
|
|
72
|
+
if (!filePath) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
// fs.readFile resolves the file as a utf8 string (src/app/lib/fs.js)
|
|
76
|
+
const text = await fs.readFile(filePath)
|
|
77
|
+
const parsed = parseVv(text)
|
|
78
|
+
|
|
79
|
+
if (!parsed.ok) {
|
|
80
|
+
setHint({ type: 'error', text: describeVv(parsed) })
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Only push the keys the file actually defined, so loading a .vv on top
|
|
85
|
+
// of a partly filled form does not wipe the other fields.
|
|
86
|
+
form.setFieldsValue(parsed.fields)
|
|
87
|
+
|
|
88
|
+
const notes = parsed.warnings.slice()
|
|
89
|
+
if (parsed.ignored.length) {
|
|
90
|
+
notes.push(
|
|
91
|
+
'skipped: ' + parsed.ignored.map(d => d.key).join(', ')
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
if (parsed.unknown.length) {
|
|
95
|
+
notes.push(
|
|
96
|
+
'unrecognised: ' + parsed.unknown.map(d => d.key).join(', ')
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const summary = describeVv(parsed)
|
|
101
|
+
setHint({
|
|
102
|
+
type: notes.length ? 'warning' : 'success',
|
|
103
|
+
text: summary + (notes.length ? ' -- ' + notes.join('; ') : '')
|
|
104
|
+
})
|
|
105
|
+
} catch (err) {
|
|
106
|
+
setHint({ type: 'error', text: 'cannot read file: ' + err.message })
|
|
107
|
+
} finally {
|
|
108
|
+
setLoading(false)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<FormItem
|
|
114
|
+
{...tailFormItemLayout}
|
|
115
|
+
>
|
|
116
|
+
<Button
|
|
117
|
+
icon={<FileAddOutlined />}
|
|
118
|
+
loading={loading}
|
|
119
|
+
onClick={load}
|
|
120
|
+
>
|
|
121
|
+
{vvLabel()}
|
|
122
|
+
</Button>
|
|
123
|
+
{
|
|
124
|
+
hint && (
|
|
125
|
+
<Alert
|
|
126
|
+
className='mg1t'
|
|
127
|
+
type={hint.type}
|
|
128
|
+
title={hint.text}
|
|
129
|
+
showIcon
|
|
130
|
+
closable
|
|
131
|
+
onClose={() => setHint(null)}
|
|
132
|
+
/>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
</FormItem>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
@@ -26,6 +26,7 @@ const spiceConfig = {
|
|
|
26
26
|
fields: [
|
|
27
27
|
commonFields.category,
|
|
28
28
|
commonFields.colorTitle,
|
|
29
|
+
{ type: 'vvFile', name: '__vv__', label: '' },
|
|
29
30
|
{ type: 'input', name: 'host', label: () => e('host'), rules: [{ required: true, message: e('host') + ' required' }] },
|
|
30
31
|
commonFields.port,
|
|
31
32
|
{ type: 'switch', name: 'viewOnly', label: () => e('viewOnly'), valuePropName: 'checked' },
|