dsh-clean-desktop-shell 0.1.11 → 0.1.13
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/CONTRIBUTORS.md +25 -19
- package/README.en.md +422 -333
- package/README.md +363 -278
- package/electron/aumid.js +17 -17
- package/electron/crashGuard.js +60 -60
- package/electron/error.html +113 -113
- package/electron/main.js +203 -187
- package/electron/outage.js +60 -0
- package/electron/preload.js +216 -49
- package/electron/progress-preload.js +11 -11
- package/electron/progress.html +79 -79
- package/electron/progress.js +56 -56
- package/electron/service.js +559 -458
- package/electron/shortcut.js +122 -122
- package/electron/tray.js +232 -232
- package/electron/window.js +603 -264
- package/lib/client.js +41 -6
- package/lib/common.js +74 -74
- package/lib/icon.js +85 -51
- package/lib/index.js +90 -15
- package/lib/runtime.js +423 -423
- package/package.json +140 -127
- package/scripts/check-syntax.mjs +54 -54
- package/scripts/selftest-recovery.mjs +140 -0
- package/scripts/selftest-runtime.mjs +90 -90
- package/src/client/client.js +41 -6
- package/src/host/common.js +74 -74
- package/src/host/icon.js +85 -51
- package/src/host/index.js +90 -15
- package/src/host/runtime.js +423 -423
- package/version.txt +1 -1
package/electron/preload.js
CHANGED
|
@@ -1,49 +1,216 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Preload: makes the frameless window draggable + exposes the minimal
|
|
3
|
-
* shell API to the loaded page (sandbox-safe contextBridge).
|
|
4
|
-
*
|
|
5
|
-
* A frameless window has no title bar, so dragging is provided by a
|
|
6
|
-
* `-webkit-app-region: drag` strip along the top of the loaded page.
|
|
7
|
-
* The right side is left for the native window-controls overlay.
|
|
8
|
-
*
|
|
9
|
-
* The strip is a transparent overlay, so it never changes page layout —
|
|
10
|
-
* but it sits above the page top edge, so page top-bar buttons can be
|
|
11
|
-
* reached by the strip being only 1px tall at the very edge... Instead we
|
|
12
|
-
* use a pragmatic height and rely on the page's own top padding for the
|
|
13
|
-
* DSH top bar area. Overlap is acceptable: the strip is click-through for
|
|
14
|
-
* everything except dragging (app-region drag areas swallow mouse events).
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Preload: makes the frameless window draggable + exposes the minimal
|
|
3
|
+
* shell API to the loaded page (sandbox-safe contextBridge).
|
|
4
|
+
*
|
|
5
|
+
* A frameless window has no title bar, so dragging is provided by a
|
|
6
|
+
* `-webkit-app-region: drag` strip along the top of the loaded page.
|
|
7
|
+
* The right side is left for the native window-controls overlay.
|
|
8
|
+
*
|
|
9
|
+
* The strip is a transparent overlay, so it never changes page layout —
|
|
10
|
+
* but it sits above the page top edge, so page top-bar buttons can be
|
|
11
|
+
* reached by the strip being only 1px tall at the very edge... Instead we
|
|
12
|
+
* use a pragmatic height and rely on the page's own top padding for the
|
|
13
|
+
* DSH top bar area. Overlap is acceptable: the strip is click-through for
|
|
14
|
+
* everything except dragging (app-region drag areas swallow mouse events).
|
|
15
|
+
*
|
|
16
|
+
* The in-page connection notice is the non-destructive counterpart of the
|
|
17
|
+
* offline screen: while a real page is loaded, backend liveness failures
|
|
18
|
+
* never navigate away — the main process pushes watch results here and we
|
|
19
|
+
* show/clear a small fixed banner instead. Draft, scroll and selection in
|
|
20
|
+
* the loaded page are untouched.
|
|
21
|
+
*/
|
|
22
|
+
const { contextBridge, ipcRenderer } = require('electron')
|
|
23
|
+
|
|
24
|
+
// Page-side handler invoked when the main process asks the DSH client
|
|
25
|
+
// runtime (injected page module, src/client/client.js) to reconnect. One
|
|
26
|
+
// handler per page context — navigation resets it with the page.
|
|
27
|
+
let reconnectHandler = null
|
|
28
|
+
|
|
29
|
+
ipcRenderer.on('shell:client-reconnect', () => {
|
|
30
|
+
try {
|
|
31
|
+
if (typeof reconnectHandler === 'function') reconnectHandler()
|
|
32
|
+
} catch {
|
|
33
|
+
// page handler errors must not break the shell
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
contextBridge.exposeInMainWorld('shellAPI', {
|
|
38
|
+
// Ask the main process to (re)load the real target URL. Used by the
|
|
39
|
+
// offline screen's retry button.
|
|
40
|
+
reload: () => ipcRenderer.send('shell:reload'),
|
|
41
|
+
// Offline-screen quick actions (mirror the tray backend controls).
|
|
42
|
+
startBackend: () => ipcRenderer.send('shell:start-backend'),
|
|
43
|
+
detectBackend: () => ipcRenderer.send('shell:detect-backend'),
|
|
44
|
+
chooseBackendFolder: () => ipcRenderer.send('shell:choose-backend-folder'),
|
|
45
|
+
// DSH client-runtime connection bridge (see src/client/client.js):
|
|
46
|
+
// - connectionReport(state) forwards the page connection lifecycle
|
|
47
|
+
// ('connected' | 'connecting' | 'disconnected') to the main process so
|
|
48
|
+
// HTTP health alone can never hide a terminal disconnect;
|
|
49
|
+
// - onReconnectRequest(cb) registers the page-side reconnect action (the
|
|
50
|
+
// runtime's own reconnect(), never a page reload); returns an
|
|
51
|
+
// unregister function.
|
|
52
|
+
connectionReport: (state) => ipcRenderer.send('shell:client-connection', state),
|
|
53
|
+
onReconnectRequest: (cb) => {
|
|
54
|
+
if (typeof cb !== 'function') return () => {}
|
|
55
|
+
reconnectHandler = cb
|
|
56
|
+
return () => {
|
|
57
|
+
if (reconnectHandler === cb) reconnectHandler = null
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// ---------- in-page connection notice ----------
|
|
63
|
+
|
|
64
|
+
// Latest push from window.js refreshNotice: { state: 'ok' } or
|
|
65
|
+
// { state: 'degraded', persistent: boolean }, computed from merged HTTP
|
|
66
|
+
// probe + client-runtime connection state. Kept until the DOM is ready
|
|
67
|
+
// (messages can arrive before DOMContentLoaded on very fast loads).
|
|
68
|
+
let connState = null
|
|
69
|
+
let connUi = null
|
|
70
|
+
|
|
71
|
+
ipcRenderer.on('shell:connection-state', (_event, state) => {
|
|
72
|
+
connState = state
|
|
73
|
+
applyConnState()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
function applyConnState() {
|
|
77
|
+
if (!connState || !connUi) return
|
|
78
|
+
if (!connState.state || connState.state === 'ok') {
|
|
79
|
+
connUi.el.style.display = 'none'
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
const persistent = connState.persistent === true
|
|
83
|
+
connUi.title.textContent = persistent
|
|
84
|
+
? '后端暂时无法访问'
|
|
85
|
+
: '与后端的连接暂时中断'
|
|
86
|
+
connUi.sub.textContent = persistent
|
|
87
|
+
? '当前页面已保留,连接恢复后本页会自动继续,也可手动重试。'
|
|
88
|
+
: '正在自动重连,当前页面已保留。'
|
|
89
|
+
connUi.retryBtn.hidden = false
|
|
90
|
+
// Full reload is only offered once the outage is persistent: it is
|
|
91
|
+
// destructive to unsaved page state, so it stays an explicit user choice.
|
|
92
|
+
connUi.reloadBtn.hidden = !persistent
|
|
93
|
+
connUi.el.style.display = 'flex'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build the notice banner below the drag strip (top-center, fixed). Uses
|
|
98
|
+
* inline styles like the drag strip — no <style> injection, so the loaded
|
|
99
|
+
* page's Content-Security-Policy cannot block it. The element itself is
|
|
100
|
+
* inert while hidden and never steals focus while visible.
|
|
101
|
+
*/
|
|
102
|
+
function buildConnUi() {
|
|
103
|
+
const base = {
|
|
104
|
+
position: 'fixed',
|
|
105
|
+
top: '0',
|
|
106
|
+
left: '50%',
|
|
107
|
+
transform: 'translateX(-50%)',
|
|
108
|
+
display: 'none',
|
|
109
|
+
alignItems: 'center',
|
|
110
|
+
gap: '14px',
|
|
111
|
+
maxWidth: '76vw',
|
|
112
|
+
padding: '8px 14px',
|
|
113
|
+
background: 'rgba(16, 19, 26, 0.94)',
|
|
114
|
+
border: '1px solid #33405a',
|
|
115
|
+
borderRadius: '10px',
|
|
116
|
+
color: '#e8eaf0',
|
|
117
|
+
font: '13px/1.4 system-ui, "Segoe UI", "Microsoft YaHei", sans-serif',
|
|
118
|
+
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.45)',
|
|
119
|
+
zIndex: '2147483646',
|
|
120
|
+
pointerEvents: 'auto',
|
|
121
|
+
userSelect: 'none',
|
|
122
|
+
WebkitUserSelect: 'none',
|
|
123
|
+
}
|
|
124
|
+
const el = document.createElement('div')
|
|
125
|
+
el.id = 'dsh-clean-shell-conn'
|
|
126
|
+
el.setAttribute('role', 'status')
|
|
127
|
+
el.setAttribute('aria-live', 'polite')
|
|
128
|
+
Object.assign(el.style, base)
|
|
129
|
+
|
|
130
|
+
const copy = document.createElement('div')
|
|
131
|
+
copy.style.display = 'flex'
|
|
132
|
+
copy.style.flexDirection = 'column'
|
|
133
|
+
copy.style.gap = '2px'
|
|
134
|
+
|
|
135
|
+
const title = document.createElement('div')
|
|
136
|
+
title.id = 'dsh-clean-shell-conn-title'
|
|
137
|
+
title.style.fontWeight = '600'
|
|
138
|
+
title.style.color = '#e8eaf0'
|
|
139
|
+
const sub = document.createElement('div')
|
|
140
|
+
sub.id = 'dsh-clean-shell-conn-sub'
|
|
141
|
+
sub.style.fontSize = '12px'
|
|
142
|
+
sub.style.opacity = '0.75'
|
|
143
|
+
copy.append(title, sub)
|
|
144
|
+
|
|
145
|
+
const actions = document.createElement('div')
|
|
146
|
+
actions.style.display = 'flex'
|
|
147
|
+
actions.style.gap = '8px'
|
|
148
|
+
|
|
149
|
+
const btnStyle = {
|
|
150
|
+
padding: '5px 12px',
|
|
151
|
+
fontSize: '12px',
|
|
152
|
+
color: '#e8eaf0',
|
|
153
|
+
background: '#232d3d',
|
|
154
|
+
border: '1px solid #33405a',
|
|
155
|
+
borderRadius: '7px',
|
|
156
|
+
cursor: 'pointer',
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const retryBtn = document.createElement('button')
|
|
160
|
+
retryBtn.id = 'dsh-clean-shell-conn-retry'
|
|
161
|
+
retryBtn.type = 'button'
|
|
162
|
+
retryBtn.textContent = '立即重试'
|
|
163
|
+
Object.assign(retryBtn.style, btnStyle)
|
|
164
|
+
retryBtn.addEventListener('mouseenter', () => { retryBtn.style.background = '#2b3750' })
|
|
165
|
+
retryBtn.addEventListener('mouseleave', () => { retryBtn.style.background = '#232d3d' })
|
|
166
|
+
retryBtn.addEventListener('click', () => ipcRenderer.send('shell:retry-connection'))
|
|
167
|
+
|
|
168
|
+
const reloadBtn = document.createElement('button')
|
|
169
|
+
reloadBtn.id = 'dsh-clean-shell-conn-reload'
|
|
170
|
+
reloadBtn.type = 'button'
|
|
171
|
+
reloadBtn.textContent = '重新加载页面'
|
|
172
|
+
Object.assign(reloadBtn.style, btnStyle)
|
|
173
|
+
reloadBtn.hidden = true
|
|
174
|
+
reloadBtn.addEventListener('mouseenter', () => { reloadBtn.style.background = '#2b3750' })
|
|
175
|
+
reloadBtn.addEventListener('mouseleave', () => { reloadBtn.style.background = '#232d3d' })
|
|
176
|
+
reloadBtn.addEventListener('click', () => ipcRenderer.send('shell:reload'))
|
|
177
|
+
|
|
178
|
+
actions.append(retryBtn, reloadBtn)
|
|
179
|
+
el.append(copy, actions)
|
|
180
|
+
return { el, title, sub, retryBtn, reloadBtn }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
184
|
+
const platform = process.platform
|
|
185
|
+
const isWin = platform === 'win32'
|
|
186
|
+
// macOS: 12px (was 28). The DSH web UI's own top toolbar starts ~16px
|
|
187
|
+
// from the window top and does NOT consume dsh-desktop-titlebar-inset,
|
|
188
|
+
// so a 28px strip swallowed the upper half of every toolbar control —
|
|
189
|
+
// clicks had to aim low. 12px stays above that padding (drag-only band)
|
|
190
|
+
// while better-sidebar keeps yielding 28px for the traffic lights.
|
|
191
|
+
const dragHeight = isWin ? 32 : 12
|
|
192
|
+
// Width reserved for native window controls (Win caption buttons / mac
|
|
193
|
+
// traffic lights live at the top-right / top-left).
|
|
194
|
+
const rightReserve = isWin ? 138 : 80
|
|
195
|
+
const leftReserve = isWin ? 0 : 80
|
|
196
|
+
|
|
197
|
+
const strip = document.createElement('div')
|
|
198
|
+
strip.id = 'dsh-clean-shell-drag'
|
|
199
|
+
strip.style.cssText = `
|
|
200
|
+
position: fixed;
|
|
201
|
+
top: 0;
|
|
202
|
+
left: ${leftReserve}px;
|
|
203
|
+
right: ${rightReserve}px;
|
|
204
|
+
height: ${dragHeight}px;
|
|
205
|
+
-webkit-app-region: drag;
|
|
206
|
+
z-index: 2147483647;
|
|
207
|
+
`
|
|
208
|
+
document.body.appendChild(strip)
|
|
209
|
+
|
|
210
|
+
connUi = buildConnUi()
|
|
211
|
+
// Clear the drag band so the banner is clickable, then render any state
|
|
212
|
+
// pushed before the DOM was ready.
|
|
213
|
+
connUi.el.style.top = `${dragHeight + 10}px`
|
|
214
|
+
document.body.appendChild(connUi.el)
|
|
215
|
+
applyConnState()
|
|
216
|
+
})
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Preload for the progress window — exposes a sandbox-safe listener for
|
|
3
|
-
* status updates pushed from the main process.
|
|
4
|
-
*/
|
|
5
|
-
const { contextBridge, ipcRenderer } = require('electron')
|
|
6
|
-
|
|
7
|
-
contextBridge.exposeInMainWorld('progressAPI', {
|
|
8
|
-
onSet: (cb) => {
|
|
9
|
-
ipcRenderer.on('progress:set', (_event, data) => cb(data))
|
|
10
|
-
},
|
|
11
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* Preload for the progress window — exposes a sandbox-safe listener for
|
|
3
|
+
* status updates pushed from the main process.
|
|
4
|
+
*/
|
|
5
|
+
const { contextBridge, ipcRenderer } = require('electron')
|
|
6
|
+
|
|
7
|
+
contextBridge.exposeInMainWorld('progressAPI', {
|
|
8
|
+
onSet: (cb) => {
|
|
9
|
+
ipcRenderer.on('progress:set', (_event, data) => cb(data))
|
|
10
|
+
},
|
|
11
|
+
})
|
package/electron/progress.html
CHANGED
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'" />
|
|
6
|
-
<title>后端操作</title>
|
|
7
|
-
<style>
|
|
8
|
-
html, body {
|
|
9
|
-
margin: 0;
|
|
10
|
-
height: 100%;
|
|
11
|
-
background: #10131A;
|
|
12
|
-
color: #9aa3b2;
|
|
13
|
-
font-family: system-ui, "Segoe UI", "Microsoft YaHei", sans-serif;
|
|
14
|
-
-webkit-user-select: none;
|
|
15
|
-
user-select: none;
|
|
16
|
-
overflow: hidden;
|
|
17
|
-
}
|
|
18
|
-
.wrap {
|
|
19
|
-
height: 100%;
|
|
20
|
-
box-sizing: border-box;
|
|
21
|
-
padding: 18px 22px;
|
|
22
|
-
display: flex;
|
|
23
|
-
flex-direction: column;
|
|
24
|
-
gap: 12px;
|
|
25
|
-
}
|
|
26
|
-
.title {
|
|
27
|
-
font-size: 14px;
|
|
28
|
-
font-weight: 600;
|
|
29
|
-
color: #e8eaf0;
|
|
30
|
-
}
|
|
31
|
-
.status {
|
|
32
|
-
display: flex;
|
|
33
|
-
align-items: center;
|
|
34
|
-
gap: 10px;
|
|
35
|
-
font-size: 13px;
|
|
36
|
-
min-height: 18px;
|
|
37
|
-
}
|
|
38
|
-
.dot {
|
|
39
|
-
width: 14px;
|
|
40
|
-
height: 14px;
|
|
41
|
-
flex: none;
|
|
42
|
-
border-radius: 50%;
|
|
43
|
-
border: 2px solid #2c3644;
|
|
44
|
-
border-top-color: #7f858f;
|
|
45
|
-
animation: spin 0.8s linear infinite;
|
|
46
|
-
}
|
|
47
|
-
.dot.ok {
|
|
48
|
-
border-color: #3d8b6a;
|
|
49
|
-
animation: none;
|
|
50
|
-
}
|
|
51
|
-
.dot.error {
|
|
52
|
-
border-color: #c0564d;
|
|
53
|
-
animation: none;
|
|
54
|
-
}
|
|
55
|
-
.msg { line-height: 1.4; }
|
|
56
|
-
.msg.error { color: #e09090; }
|
|
57
|
-
.msg.ok { color: #8fbfa4; }
|
|
58
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
59
|
-
</style>
|
|
60
|
-
</head>
|
|
61
|
-
<body>
|
|
62
|
-
<div class="wrap">
|
|
63
|
-
<div class="title" id="title">后端操作</div>
|
|
64
|
-
<div class="status">
|
|
65
|
-
<div class="dot" id="dot"></div>
|
|
66
|
-
<div class="msg" id="msg">请稍候…</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
<script>
|
|
70
|
-
window.progressAPI.onSet((data) => {
|
|
71
|
-
if (data.title) document.getElementById('title').textContent = data.title
|
|
72
|
-
const msg = document.getElementById('msg')
|
|
73
|
-
msg.textContent = data.message || ''
|
|
74
|
-
msg.className = 'msg' + (data.state === 'error' ? ' error' : data.state === 'ok' ? ' ok' : '')
|
|
75
|
-
document.getElementById('dot').className = 'dot' + (data.state === 'error' ? ' error' : data.state === 'ok' ? ' ok' : '')
|
|
76
|
-
})
|
|
77
|
-
</script>
|
|
78
|
-
</body>
|
|
79
|
-
</html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'" />
|
|
6
|
+
<title>后端操作</title>
|
|
7
|
+
<style>
|
|
8
|
+
html, body {
|
|
9
|
+
margin: 0;
|
|
10
|
+
height: 100%;
|
|
11
|
+
background: #10131A;
|
|
12
|
+
color: #9aa3b2;
|
|
13
|
+
font-family: system-ui, "Segoe UI", "Microsoft YaHei", sans-serif;
|
|
14
|
+
-webkit-user-select: none;
|
|
15
|
+
user-select: none;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
}
|
|
18
|
+
.wrap {
|
|
19
|
+
height: 100%;
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
padding: 18px 22px;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
gap: 12px;
|
|
25
|
+
}
|
|
26
|
+
.title {
|
|
27
|
+
font-size: 14px;
|
|
28
|
+
font-weight: 600;
|
|
29
|
+
color: #e8eaf0;
|
|
30
|
+
}
|
|
31
|
+
.status {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: 10px;
|
|
35
|
+
font-size: 13px;
|
|
36
|
+
min-height: 18px;
|
|
37
|
+
}
|
|
38
|
+
.dot {
|
|
39
|
+
width: 14px;
|
|
40
|
+
height: 14px;
|
|
41
|
+
flex: none;
|
|
42
|
+
border-radius: 50%;
|
|
43
|
+
border: 2px solid #2c3644;
|
|
44
|
+
border-top-color: #7f858f;
|
|
45
|
+
animation: spin 0.8s linear infinite;
|
|
46
|
+
}
|
|
47
|
+
.dot.ok {
|
|
48
|
+
border-color: #3d8b6a;
|
|
49
|
+
animation: none;
|
|
50
|
+
}
|
|
51
|
+
.dot.error {
|
|
52
|
+
border-color: #c0564d;
|
|
53
|
+
animation: none;
|
|
54
|
+
}
|
|
55
|
+
.msg { line-height: 1.4; }
|
|
56
|
+
.msg.error { color: #e09090; }
|
|
57
|
+
.msg.ok { color: #8fbfa4; }
|
|
58
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
59
|
+
</style>
|
|
60
|
+
</head>
|
|
61
|
+
<body>
|
|
62
|
+
<div class="wrap">
|
|
63
|
+
<div class="title" id="title">后端操作</div>
|
|
64
|
+
<div class="status">
|
|
65
|
+
<div class="dot" id="dot"></div>
|
|
66
|
+
<div class="msg" id="msg">请稍候…</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<script>
|
|
70
|
+
window.progressAPI.onSet((data) => {
|
|
71
|
+
if (data.title) document.getElementById('title').textContent = data.title
|
|
72
|
+
const msg = document.getElementById('msg')
|
|
73
|
+
msg.textContent = data.message || ''
|
|
74
|
+
msg.className = 'msg' + (data.state === 'error' ? ' error' : data.state === 'ok' ? ' ok' : '')
|
|
75
|
+
document.getElementById('dot').className = 'dot' + (data.state === 'error' ? ' error' : data.state === 'ok' ? ' ok' : '')
|
|
76
|
+
})
|
|
77
|
+
</script>
|
|
78
|
+
</body>
|
|
79
|
+
</html>
|
package/electron/progress.js
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Small always-on-top progress window for backend operations
|
|
3
|
-
* (start / restart / stop). Single instance; reused across calls.
|
|
4
|
-
*
|
|
5
|
-
* States: 'busy' (spinner) | 'ok' | 'error' — the caller decides when to
|
|
6
|
-
* close it (auto-close timers live in the caller).
|
|
7
|
-
*/
|
|
8
|
-
import { BrowserWindow } from 'electron'
|
|
9
|
-
import { fileURLToPath } from 'node:url'
|
|
10
|
-
|
|
11
|
-
const PRELOAD = fileURLToPath(new URL('./progress-preload.js', import.meta.url))
|
|
12
|
-
const PAGE = fileURLToPath(new URL('./progress.html', import.meta.url))
|
|
13
|
-
|
|
14
|
-
let win = null
|
|
15
|
-
|
|
16
|
-
/** Show (or update) the progress window. */
|
|
17
|
-
export function showProgress({ title, message, state = 'busy' }) {
|
|
18
|
-
if (!win || win.isDestroyed()) {
|
|
19
|
-
win = new BrowserWindow({
|
|
20
|
-
width: 420,
|
|
21
|
-
height: 128,
|
|
22
|
-
frame: false,
|
|
23
|
-
resizable: false,
|
|
24
|
-
movable: true,
|
|
25
|
-
alwaysOnTop: true,
|
|
26
|
-
skipTaskbar: true,
|
|
27
|
-
show: false,
|
|
28
|
-
backgroundColor: '#10131A',
|
|
29
|
-
webPreferences: {
|
|
30
|
-
preload: PRELOAD,
|
|
31
|
-
contextIsolation: true,
|
|
32
|
-
nodeIntegration: false,
|
|
33
|
-
sandbox: true,
|
|
34
|
-
},
|
|
35
|
-
})
|
|
36
|
-
win.loadFile(PAGE)
|
|
37
|
-
win.once('ready-to-show', () => win.show())
|
|
38
|
-
win.on('closed', () => {
|
|
39
|
-
win = null
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
setProgress({ title, message, state })
|
|
43
|
-
return win
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Update the currently visible progress window (no-op if hidden). */
|
|
47
|
-
export function setProgress({ title, message, state }) {
|
|
48
|
-
if (win && !win.isDestroyed()) {
|
|
49
|
-
win.webContents.send('progress:set', { title, message, state })
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Close the progress window if open. */
|
|
54
|
-
export function closeProgress() {
|
|
55
|
-
if (win && !win.isDestroyed()) win.close()
|
|
56
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Small always-on-top progress window for backend operations
|
|
3
|
+
* (start / restart / stop). Single instance; reused across calls.
|
|
4
|
+
*
|
|
5
|
+
* States: 'busy' (spinner) | 'ok' | 'error' — the caller decides when to
|
|
6
|
+
* close it (auto-close timers live in the caller).
|
|
7
|
+
*/
|
|
8
|
+
import { BrowserWindow } from 'electron'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const PRELOAD = fileURLToPath(new URL('./progress-preload.js', import.meta.url))
|
|
12
|
+
const PAGE = fileURLToPath(new URL('./progress.html', import.meta.url))
|
|
13
|
+
|
|
14
|
+
let win = null
|
|
15
|
+
|
|
16
|
+
/** Show (or update) the progress window. */
|
|
17
|
+
export function showProgress({ title, message, state = 'busy' }) {
|
|
18
|
+
if (!win || win.isDestroyed()) {
|
|
19
|
+
win = new BrowserWindow({
|
|
20
|
+
width: 420,
|
|
21
|
+
height: 128,
|
|
22
|
+
frame: false,
|
|
23
|
+
resizable: false,
|
|
24
|
+
movable: true,
|
|
25
|
+
alwaysOnTop: true,
|
|
26
|
+
skipTaskbar: true,
|
|
27
|
+
show: false,
|
|
28
|
+
backgroundColor: '#10131A',
|
|
29
|
+
webPreferences: {
|
|
30
|
+
preload: PRELOAD,
|
|
31
|
+
contextIsolation: true,
|
|
32
|
+
nodeIntegration: false,
|
|
33
|
+
sandbox: true,
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
win.loadFile(PAGE)
|
|
37
|
+
win.once('ready-to-show', () => win.show())
|
|
38
|
+
win.on('closed', () => {
|
|
39
|
+
win = null
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
setProgress({ title, message, state })
|
|
43
|
+
return win
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Update the currently visible progress window (no-op if hidden). */
|
|
47
|
+
export function setProgress({ title, message, state }) {
|
|
48
|
+
if (win && !win.isDestroyed()) {
|
|
49
|
+
win.webContents.send('progress:set', { title, message, state })
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Close the progress window if open. */
|
|
54
|
+
export function closeProgress() {
|
|
55
|
+
if (win && !win.isDestroyed()) win.close()
|
|
56
|
+
}
|