@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
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
const e = window.translate
|
|
21
21
|
|
|
22
22
|
export default memo(function SidebarPanel (props) {
|
|
23
|
-
const { sidebarPanelTab, pinned } = props
|
|
23
|
+
const { sidebarPanelTab, pinned, isMobile } = props
|
|
24
24
|
const { store } = window
|
|
25
25
|
const [openSelectModal, setOpenSelectModal] = useState(false)
|
|
26
26
|
const prps = {
|
|
@@ -81,6 +81,22 @@ export default memo(function SidebarPanel (props) {
|
|
|
81
81
|
</Tooltip>
|
|
82
82
|
]
|
|
83
83
|
}
|
|
84
|
+
// No pin control on mobile: the panel spans the viewport there, so there is
|
|
85
|
+
// nothing to dock it beside, and the control row is tight enough that the
|
|
86
|
+
// pin would crowd the icons it shares it with.
|
|
87
|
+
function renderPinIcon () {
|
|
88
|
+
if (isMobile) {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
return (
|
|
92
|
+
<Tooltip title={e('pin')}>
|
|
93
|
+
<PushpinOutlined
|
|
94
|
+
{...prps1}
|
|
95
|
+
onClick={store.handlePin}
|
|
96
|
+
/>
|
|
97
|
+
</Tooltip>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
84
100
|
return (
|
|
85
101
|
<div
|
|
86
102
|
className='sidebar-panel bookmarks-panel animate-fast'
|
|
@@ -100,12 +116,9 @@ export default memo(function SidebarPanel (props) {
|
|
|
100
116
|
{
|
|
101
117
|
renderExpandIcons()
|
|
102
118
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
onClick={store.handlePin}
|
|
107
|
-
/>
|
|
108
|
-
</Tooltip>
|
|
119
|
+
{
|
|
120
|
+
renderPinIcon()
|
|
121
|
+
}
|
|
109
122
|
<CloseOutlined
|
|
110
123
|
{...prps}
|
|
111
124
|
onClick={store.handleCloseSidebar}
|
|
@@ -9,11 +9,10 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
ReloadOutlined
|
|
11
11
|
} from '@ant-design/icons'
|
|
12
|
-
import {
|
|
13
|
-
Spin
|
|
14
|
-
} from 'antd'
|
|
15
12
|
import * as ls from '../../common/safe-local-storage'
|
|
16
13
|
import RemoteFloatControl from '../common/remote-float-control'
|
|
14
|
+
import RemoteSessionShell from '../common/remote-session-shell'
|
|
15
|
+
import { eventToRemotePos } from '../common/remote-pointer'
|
|
17
16
|
import SwitchLabel from '../common/switch'
|
|
18
17
|
import './spice.styl'
|
|
19
18
|
|
|
@@ -53,24 +52,19 @@ export default class SpiceSession extends PureComponent {
|
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
// The spice client sends event.offsetX/offsetY to the remote as-is, but when
|
|
56
|
-
// the canvas is CSS-scaled (
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// letterboxed by object-fit: contain) before the lib's handlers run.
|
|
55
|
+
// the canvas is CSS-scaled (fit mode), those are display pixels instead of
|
|
56
|
+
// remote surface pixels. On high resolution remote screens the pointer then
|
|
57
|
+
// only reaches the top-left region. Convert display coords to surface coords
|
|
58
|
+
// before the lib's handlers run (same mapping as rdp, see remote-pointer.js).
|
|
61
59
|
handleMouseScaleFix = (e) => {
|
|
62
60
|
const canvas = this.domRef.current?.querySelector('canvas')
|
|
63
61
|
if (!canvas || !canvas.width || !canvas.height) return
|
|
64
|
-
const
|
|
65
|
-
if (!rect.width || !rect.height) return
|
|
66
|
-
const scale = Math.min(rect.width / canvas.width, rect.height / canvas.height)
|
|
67
|
-
const x = (e.clientX - rect.left - (rect.width - canvas.width * scale) / 2) / scale
|
|
68
|
-
const y = (e.clientY - rect.top - (rect.height - canvas.height * scale) / 2) / scale
|
|
62
|
+
const { x, y } = eventToRemotePos(e, canvas, canvas.width, canvas.height)
|
|
69
63
|
Object.defineProperty(e, 'offsetX', {
|
|
70
|
-
value:
|
|
64
|
+
value: x
|
|
71
65
|
})
|
|
72
66
|
Object.defineProperty(e, 'offsetY', {
|
|
73
|
-
value:
|
|
67
|
+
value: y
|
|
74
68
|
})
|
|
75
69
|
}
|
|
76
70
|
|
|
@@ -123,13 +117,30 @@ export default class SpiceSession extends PureComponent {
|
|
|
123
117
|
}
|
|
124
118
|
|
|
125
119
|
calcCanvasSize = () => {
|
|
126
|
-
const { width, height } = this.
|
|
120
|
+
const { width, height } = this.viewSize || {}
|
|
121
|
+
if (width && height) {
|
|
122
|
+
return { width, height }
|
|
123
|
+
}
|
|
124
|
+
const { width: w, height: h } = this.props
|
|
127
125
|
return {
|
|
128
|
-
width:
|
|
129
|
-
height:
|
|
126
|
+
width: w - 10,
|
|
127
|
+
height: h - 80
|
|
130
128
|
}
|
|
131
129
|
}
|
|
132
130
|
|
|
131
|
+
// fullscreen always fills the screen, the per tab setting only applies
|
|
132
|
+
// windowed; the switch is not reachable in fullscreen anyway
|
|
133
|
+
getFit = () => {
|
|
134
|
+
if (this.props.fullscreen) {
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
return !!this.state.scaleViewport
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
handleViewportResize = (width, height) => {
|
|
141
|
+
this.viewSize = { width, height }
|
|
142
|
+
}
|
|
143
|
+
|
|
133
144
|
handleSendCtrlAltDel = () => {
|
|
134
145
|
if (this.spiceConn && window.spiceHtml5?.sendCtrlAltDel) {
|
|
135
146
|
window.spiceHtml5.sendCtrlAltDel(this.spiceConn)
|
|
@@ -163,12 +174,7 @@ export default class SpiceSession extends PureComponent {
|
|
|
163
174
|
}
|
|
164
175
|
}
|
|
165
176
|
|
|
166
|
-
|
|
167
|
-
const contrlProps = this.getControlProps({
|
|
168
|
-
fixedPosition: false,
|
|
169
|
-
showExitFullscreen: false,
|
|
170
|
-
className: 'mg1l'
|
|
171
|
-
})
|
|
177
|
+
renderControlLeft = () => {
|
|
172
178
|
const scaleProps = {
|
|
173
179
|
checked: this.state.scaleViewport,
|
|
174
180
|
onChange: this.handleScaleViewChange,
|
|
@@ -176,22 +182,30 @@ export default class SpiceSession extends PureComponent {
|
|
|
176
182
|
className: 'mg1l'
|
|
177
183
|
}
|
|
178
184
|
return (
|
|
179
|
-
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
185
|
+
<>
|
|
186
|
+
<ReloadOutlined
|
|
187
|
+
onClick={this.handleReInit}
|
|
188
|
+
className='mg2r mg1l pointer'
|
|
189
|
+
/>
|
|
190
|
+
{this.renderInfo()}
|
|
191
|
+
<SwitchLabel
|
|
192
|
+
{...scaleProps}
|
|
193
|
+
/>
|
|
194
|
+
</>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
renderControlRight = () => {
|
|
199
|
+
const contrlProps = this.getControlProps({
|
|
200
|
+
fixedPosition: false,
|
|
201
|
+
showExitFullscreen: false,
|
|
202
|
+
className: 'mg1l'
|
|
203
|
+
})
|
|
204
|
+
return (
|
|
205
|
+
<>
|
|
206
|
+
{this.props.fullscreenIcon()}
|
|
207
|
+
<RemoteFloatControl {...contrlProps} />
|
|
208
|
+
</>
|
|
195
209
|
)
|
|
196
210
|
}
|
|
197
211
|
|
|
@@ -298,41 +312,24 @@ export default class SpiceSession extends PureComponent {
|
|
|
298
312
|
}
|
|
299
313
|
|
|
300
314
|
render () {
|
|
301
|
-
const {
|
|
302
|
-
const { loading, scaleViewport } = this.state
|
|
303
|
-
const { width: innerWidth, height: innerHeight } = this.calcCanvasSize()
|
|
304
|
-
const wrapperStyle = {
|
|
305
|
-
width: innerWidth + 'px',
|
|
306
|
-
height: innerHeight + 'px',
|
|
307
|
-
overflow: scaleViewport ? 'hidden' : 'auto'
|
|
308
|
-
}
|
|
309
|
-
const cls = `spice-session-wrap session-v-wrap${scaleViewport ? ' scale-viewport' : ''}`
|
|
315
|
+
const { loading } = this.state
|
|
310
316
|
const contrlProps = this.getControlProps()
|
|
311
|
-
const sessProps = {
|
|
312
|
-
className: cls,
|
|
313
|
-
style: {
|
|
314
|
-
width: w + 'px',
|
|
315
|
-
height: h + 'px'
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
317
|
return (
|
|
319
|
-
<
|
|
318
|
+
<RemoteSessionShell
|
|
319
|
+
loading={loading}
|
|
320
|
+
fit={this.getFit()}
|
|
321
|
+
wrapClassName='spice-session-wrap'
|
|
322
|
+
onViewportResize={this.handleViewportResize}
|
|
323
|
+
controlLeft={this.renderControlLeft()}
|
|
324
|
+
controlRight={this.renderControlRight()}
|
|
325
|
+
floatControl={<RemoteFloatControl {...contrlProps} />}
|
|
326
|
+
>
|
|
320
327
|
<div
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
{this.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
style={wrapperStyle}
|
|
327
|
-
className='spice-scroll-wrapper s-scroll-wrapper'
|
|
328
|
-
>
|
|
329
|
-
<div
|
|
330
|
-
ref={this.domRef}
|
|
331
|
-
id={this.screenId}
|
|
332
|
-
/>
|
|
333
|
-
</div>
|
|
334
|
-
</div>
|
|
335
|
-
</Spin>
|
|
328
|
+
className='spice-session-mount'
|
|
329
|
+
id={this.screenId}
|
|
330
|
+
ref={this.domRef}
|
|
331
|
+
/>
|
|
332
|
+
</RemoteSessionShell>
|
|
336
333
|
)
|
|
337
334
|
}
|
|
338
335
|
}
|
|
@@ -1,29 +1,20 @@
|
|
|
1
|
+
// SPICE only styling — the session layout (control bar, viewport, fit mode)
|
|
2
|
+
// lives in the shared remote session shell: common/remote-session-shell.styl
|
|
1
3
|
.spice-session-wrap
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.spice-scroll-wrapper
|
|
4
|
+
// fit: the mount box fills the viewport and the canvas (sized by the spice
|
|
5
|
+
// client to the remote resolution) is scaled to it, keeping the ratio.
|
|
6
|
+
// !important because the client writes the surface size into the mount box
|
|
7
|
+
// inline style (`screen_id` height = surface height).
|
|
8
|
+
&.fit
|
|
9
|
+
.spice-session-mount
|
|
10
|
+
width 100% !important
|
|
11
|
+
height 100% !important
|
|
11
12
|
display flex
|
|
12
13
|
align-items center
|
|
13
14
|
justify-content center
|
|
14
|
-
> div
|
|
15
|
-
width 100% !important
|
|
16
|
-
height 100% !important
|
|
17
|
-
display flex
|
|
18
|
-
align-items center
|
|
19
|
-
justify-content center
|
|
20
15
|
canvas
|
|
21
16
|
width 100% !important
|
|
22
17
|
height 100% !important
|
|
23
18
|
max-width 100% !important
|
|
24
19
|
max-height 100% !important
|
|
25
20
|
object-fit contain
|
|
26
|
-
.spice-scroll-wrapper
|
|
27
|
-
position relative
|
|
28
|
-
background var(--main)
|
|
29
|
-
z-index 299
|
|
@@ -81,6 +81,20 @@
|
|
|
81
81
|
bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
|
|
82
82
|
.remote-monitor-bar
|
|
83
83
|
bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px) + 36px)
|
|
84
|
+
// the right panel stacks on the footer exactly like the monitor bar, so it
|
|
85
|
+
// needs the same lift. Pinned is excluded: a pinned panel is a full-height
|
|
86
|
+
// dock and owns bottom 0. Mobile is excluded as well: there the panel is a
|
|
87
|
+
// full-screen drawer that covers the footer on purpose (mobile.styl).
|
|
88
|
+
&:not(.is-mobile)
|
|
89
|
+
.right-side-panel:not(.right-side-panel-pinned)
|
|
90
|
+
bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px) + 36px)
|
|
91
|
+
// A pinned panel is still a dock, but it yields to the shortcut bar rather
|
|
92
|
+
// than the other way round: the bar is z-index 400 and spans the full
|
|
93
|
+
// viewport width by design (it ignores the left sidebar too), so it would
|
|
94
|
+
// otherwise paint over the dock's bottom edge. The dock stops above the bar
|
|
95
|
+
// only — the footer sits beside the dock, not below it.
|
|
96
|
+
.right-side-panel.right-side-panel-pinned
|
|
97
|
+
bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
|
|
84
98
|
.qm-wrap-tooltip
|
|
85
99
|
bottom calc(var(--footer-stack-height, 36px) + var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
|
|
86
100
|
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Global tooltip that detects Unix timestamps in terminal selections
|
|
3
3
|
* and displays their human-readable date/time near the cursor.
|
|
4
|
+
* Clicking the tooltip (or its copy icon) copies the formatted string.
|
|
4
5
|
* Registered via refsStatic as 'unix-timestamp-tooltip'.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { Component } from 'react'
|
|
9
|
+
import { CopyOutlined } from '@ant-design/icons'
|
|
8
10
|
import { refsStatic } from '../common/ref'
|
|
11
|
+
import { copy } from '../../common/clipboard'
|
|
9
12
|
|
|
10
13
|
export default class UnixTimestampTooltip extends Component {
|
|
11
14
|
state = {
|
|
@@ -56,6 +59,24 @@ export default class UnixTimestampTooltip extends Component {
|
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
handleHide = () => {
|
|
63
|
+
this.setState({ visible: false })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Copy on mousedown instead of click: pressing the tooltip collapses the
|
|
68
|
+
* document selection, and the terminal drops its (DOM backed) selection on
|
|
69
|
+
* that change, which would unmount this tooltip before a click could land.
|
|
70
|
+
* preventDefault also keeps the terminal selection and focus untouched.
|
|
71
|
+
* The whole tooltip is the hit area, the icon is just the affordance.
|
|
72
|
+
*/
|
|
73
|
+
handleCopy = (e) => {
|
|
74
|
+
e.preventDefault()
|
|
75
|
+
e.stopPropagation()
|
|
76
|
+
copy(this.state.text)
|
|
77
|
+
this.handleHide()
|
|
78
|
+
}
|
|
79
|
+
|
|
59
80
|
render () {
|
|
60
81
|
const { visible, x, y, text } = this.state
|
|
61
82
|
if (!visible) {
|
|
@@ -63,6 +84,10 @@ export default class UnixTimestampTooltip extends Component {
|
|
|
63
84
|
}
|
|
64
85
|
return (
|
|
65
86
|
<div
|
|
87
|
+
className='unix-timestamp-tooltip pointer'
|
|
88
|
+
title={window.translate('copy')}
|
|
89
|
+
onMouseDown={this.handleCopy}
|
|
90
|
+
onMouseLeave={this.handleHide}
|
|
66
91
|
style={{
|
|
67
92
|
position: 'fixed',
|
|
68
93
|
left: x,
|
|
@@ -73,12 +98,13 @@ export default class UnixTimestampTooltip extends Component {
|
|
|
73
98
|
borderRadius: 4,
|
|
74
99
|
fontSize: 12,
|
|
75
100
|
whiteSpace: 'nowrap',
|
|
76
|
-
pointerEvents: 'none',
|
|
77
101
|
transform: 'translateX(-50%)',
|
|
102
|
+
userSelect: 'none',
|
|
78
103
|
zIndex: 9999
|
|
79
104
|
}}
|
|
80
105
|
>
|
|
81
106
|
{text}
|
|
107
|
+
<CopyOutlined style={{ marginLeft: 6 }} />
|
|
82
108
|
</div>
|
|
83
109
|
)
|
|
84
110
|
}
|
|
@@ -190,7 +190,7 @@ export default function ThemeAiEditor (props) {
|
|
|
190
190
|
const textAreaProps = {
|
|
191
191
|
value: description,
|
|
192
192
|
onChange: ev => setDescription(ev.target.value),
|
|
193
|
-
placeholder: e('
|
|
193
|
+
placeholder: e('description'),
|
|
194
194
|
autoSize: { minRows: 4, maxRows: 8 },
|
|
195
195
|
disabled: loading || disabled
|
|
196
196
|
}
|
|
@@ -223,7 +223,7 @@ export default function ThemeAiEditor (props) {
|
|
|
223
223
|
<div className='pd1t'>
|
|
224
224
|
<Space>
|
|
225
225
|
<Button {...generateBtnProps}>
|
|
226
|
-
{e('
|
|
226
|
+
{e('submit')}
|
|
227
227
|
</Button>
|
|
228
228
|
<HelpIcon link='https://github.com/electerm/electerm/wiki/Create-theme-by-AI' />
|
|
229
229
|
</Space>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useRef, useState } from 'react'
|
|
2
|
-
import { Button, Input, Form,
|
|
2
|
+
import { Button, Input, Form, Flex } from 'antd'
|
|
3
3
|
import {
|
|
4
4
|
BgColorsOutlined,
|
|
5
5
|
FontSizeOutlined,
|
|
@@ -14,15 +14,16 @@ import {
|
|
|
14
14
|
validThemeProps,
|
|
15
15
|
requiredThemeProps
|
|
16
16
|
} from '../../common/terminal-theme'
|
|
17
|
-
import {
|
|
17
|
+
import { defaultThemeId, defaultThemeLightId } from '../../common/theme-defaults'
|
|
18
18
|
import generate from '../../common/uid'
|
|
19
19
|
import { isAIDisabled } from '../../common/ai-feature'
|
|
20
20
|
import Link from '../common/external-link'
|
|
21
21
|
import InputAutoFocus from '../common/input-auto-focus'
|
|
22
|
+
import ResponsiveTabs from '../common/responsive-tabs'
|
|
22
23
|
import ThemePicker from './theme-editor'
|
|
23
24
|
import ThemeAiEditor from './theme-ai-editor'
|
|
24
25
|
import Upload from '../common/upload'
|
|
25
|
-
|
|
26
|
+
import './theme-form.styl'
|
|
26
27
|
|
|
27
28
|
const { TextArea } = Input
|
|
28
29
|
const FormItem = Form.Item
|
|
@@ -155,6 +156,7 @@ export default function ThemeForm (props) {
|
|
|
155
156
|
<FormItem>
|
|
156
157
|
<span className='mg1r'>src:</span>
|
|
157
158
|
<Link
|
|
159
|
+
className='wordbreak'
|
|
158
160
|
to={url}
|
|
159
161
|
>{url}
|
|
160
162
|
</Link>
|
|
@@ -270,41 +272,36 @@ export default function ThemeForm (props) {
|
|
|
270
272
|
themeName,
|
|
271
273
|
themeText: convertThemeToText(props.formData)
|
|
272
274
|
}
|
|
273
|
-
const isDefaultTheme = id ===
|
|
275
|
+
const isDefaultTheme = id === defaultThemeId || id === defaultThemeLightId
|
|
274
276
|
const disabled = readonly || isDefaultTheme
|
|
275
277
|
const pickerProps = {
|
|
276
278
|
onChange: onPickerChange,
|
|
277
279
|
themeText: txt,
|
|
278
280
|
disabled
|
|
279
281
|
}
|
|
282
|
+
// icon + label, the label text is hidden by css on narrow screens so the
|
|
283
|
+
// three editor tabs always fit, the title keeps it discoverable
|
|
284
|
+
function renderTabLabel (icon, text) {
|
|
285
|
+
return (
|
|
286
|
+
<span className='theme-editor-tab' title={text}>
|
|
287
|
+
{icon}
|
|
288
|
+
<span className='theme-editor-tab-text'>{text}</span>
|
|
289
|
+
</span>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
280
292
|
const tabItems = [
|
|
281
293
|
{
|
|
282
294
|
key: 'theme-editor-color-picker',
|
|
283
|
-
label: (
|
|
284
|
-
<span>
|
|
285
|
-
<BgColorsOutlined className='mg1r' />
|
|
286
|
-
{e('editWithColorPicker')}
|
|
287
|
-
</span>
|
|
288
|
-
)
|
|
295
|
+
label: renderTabLabel(<BgColorsOutlined className='mg1r' />, e('editWithColorPicker'))
|
|
289
296
|
},
|
|
290
297
|
{
|
|
291
298
|
key: 'theme-editor-txt',
|
|
292
|
-
label: (
|
|
293
|
-
<span>
|
|
294
|
-
<FontSizeOutlined className='mg1r' />
|
|
295
|
-
{e('editWithTextEditor')}
|
|
296
|
-
</span>
|
|
297
|
-
)
|
|
299
|
+
label: renderTabLabel(<FontSizeOutlined className='mg1r' />, e('editWithTextEditor'))
|
|
298
300
|
},
|
|
299
301
|
...(!isAIDisabled()
|
|
300
302
|
? [{
|
|
301
303
|
key: 'theme-editor-ai',
|
|
302
|
-
label: (
|
|
303
|
-
<span>
|
|
304
|
-
<RobotOutlined className='mg1r' />
|
|
305
|
-
AI
|
|
306
|
-
</span>
|
|
307
|
-
)
|
|
304
|
+
label: renderTabLabel(<RobotOutlined className='mg1r' />, 'AI')
|
|
308
305
|
}]
|
|
309
306
|
: [])
|
|
310
307
|
]
|
|
@@ -365,7 +362,7 @@ export default function ThemeForm (props) {
|
|
|
365
362
|
</Button>
|
|
366
363
|
</Upload>
|
|
367
364
|
</Flex>
|
|
368
|
-
<
|
|
365
|
+
<ResponsiveTabs {...tabsProps} />
|
|
369
366
|
{
|
|
370
367
|
editor === 'theme-editor-txt'
|
|
371
368
|
? renderTxt()
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
.editor-u-picker
|
|
10
|
-
display block !important
|
|
1
|
+
// setting panel > UI themes > theme form
|
|
2
|
+
//
|
|
3
|
+
// The three editor tabs need ~395px for their translated labels ("Edit with
|
|
4
|
+
// color picker" / "Edit with text editor" / "AI") while a 320px phone leaves
|
|
5
|
+
// the panel only ~257px. Below the app's mobile breakpoint (600px, see
|
|
6
|
+
// constants.js) ResponsiveTabs replaces the bar with a single "current tab +
|
|
7
|
+
// dropdown" selector (common/responsive-tabs.jsx), so the labels stay readable
|
|
8
|
+
// everywhere instead of being collapsed to icons.
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* theme list render
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useState } from 'react'
|
|
5
|
+
import { useEffect, useState } from 'react'
|
|
6
6
|
import {
|
|
7
7
|
CheckCircleOutlined,
|
|
8
|
+
CloseOutlined,
|
|
8
9
|
PlusOutlined,
|
|
9
10
|
SunOutlined,
|
|
10
|
-
MoonOutlined
|
|
11
|
-
EyeOutlined
|
|
11
|
+
MoonOutlined
|
|
12
12
|
} from '@ant-design/icons'
|
|
13
13
|
import { Tag, Tooltip, Button, Space } from 'antd'
|
|
14
14
|
import classnames from 'classnames'
|
|
@@ -30,46 +30,72 @@ export default function ThemeListItem (props) {
|
|
|
30
30
|
const [tooltipVisible, setTooltipVisible] = useState(false)
|
|
31
31
|
const [isPreviewing, setIsPreviewing] = useState(false)
|
|
32
32
|
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
function startPreview () {
|
|
34
|
+
// keep the very first original, hopping between themes must never
|
|
35
|
+
// overwrite it with a theme that is itself only a preview
|
|
36
|
+
if (!window.originalTheme) {
|
|
37
|
+
window.originalTheme = store.config.theme
|
|
38
|
+
}
|
|
37
39
|
store.setTheme(item.id)
|
|
40
|
+
setIsPreviewing(true)
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
function
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
window.originalTheme = currentTheme
|
|
45
|
-
// Apply the preview theme
|
|
46
|
-
store.setTheme(item.id)
|
|
47
|
-
setIsPreviewing(true)
|
|
43
|
+
function cancelPreview () {
|
|
44
|
+
if (window.originalTheme) {
|
|
45
|
+
store.setTheme(window.originalTheme)
|
|
46
|
+
delete window.originalTheme
|
|
48
47
|
}
|
|
48
|
+
setIsPreviewing(false)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
return () => {
|
|
53
|
+
// leaving the list mid preview (switching settings tab, filtering the
|
|
54
|
+
// list, ...) must not leave the user stuck on a theme never applied
|
|
55
55
|
if (window.originalTheme) {
|
|
56
56
|
store.setTheme(window.originalTheme)
|
|
57
57
|
delete window.originalTheme
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
}
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
function handleClickApply () {
|
|
63
|
+
delete window.originalTheme
|
|
64
|
+
setIsPreviewing(false)
|
|
65
|
+
setTooltipVisible(false)
|
|
66
|
+
store.setTheme(item.id)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleClickCancel () {
|
|
70
|
+
cancelPreview()
|
|
71
|
+
setTooltipVisible(false)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleTooltipVisibleChange (visible) {
|
|
75
|
+
setTooltipVisible(visible)
|
|
76
|
+
// opening the popup is the preview itself; every other way of closing it
|
|
77
|
+
// (click elsewhere, esc, clicking the icon again) rolls the preview back
|
|
78
|
+
if (visible) {
|
|
79
|
+
startPreview()
|
|
80
|
+
} else {
|
|
81
|
+
cancelPreview()
|
|
60
82
|
}
|
|
61
83
|
}
|
|
62
84
|
|
|
85
|
+
function handleClickApplyIcon (e) {
|
|
86
|
+
// previewing must not also load the theme into the edit form
|
|
87
|
+
e.stopPropagation()
|
|
88
|
+
}
|
|
89
|
+
|
|
63
90
|
function renderTooltipContent () {
|
|
64
91
|
return (
|
|
65
92
|
<Space.Compact>
|
|
66
93
|
<Button
|
|
67
94
|
size='small'
|
|
68
|
-
icon={<
|
|
69
|
-
onClick={
|
|
70
|
-
type={isPreviewing ? 'primary' : 'default'}
|
|
95
|
+
icon={<CloseOutlined />}
|
|
96
|
+
onClick={handleClickCancel}
|
|
71
97
|
>
|
|
72
|
-
{e('
|
|
98
|
+
{e('cancel')}
|
|
73
99
|
</Button>
|
|
74
100
|
<Button
|
|
75
101
|
size='small'
|
|
@@ -96,7 +122,15 @@ export default function ThemeListItem (props) {
|
|
|
96
122
|
placement='top'
|
|
97
123
|
>
|
|
98
124
|
<CheckCircleOutlined
|
|
99
|
-
className=
|
|
125
|
+
className={
|
|
126
|
+
classnames(
|
|
127
|
+
'pointer list-item-apply',
|
|
128
|
+
{
|
|
129
|
+
'list-item-apply-previewing': isPreviewing
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
onClick={handleClickApplyIcon}
|
|
100
134
|
/>
|
|
101
135
|
</Tooltip>
|
|
102
136
|
)
|