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.
@@ -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
- const { contextBridge, ipcRenderer } = require('electron')
17
-
18
- contextBridge.exposeInMainWorld('shellAPI', {
19
- // Ask the main process to (re)load the real target URL. Used by the
20
- // offline screen's retry button.
21
- reload: () => ipcRenderer.send('shell:reload'),
22
- // Offline-screen quick actions (mirror the tray backend controls).
23
- startBackend: () => ipcRenderer.send('shell:start-backend'),
24
- detectBackend: () => ipcRenderer.send('shell:detect-backend'),
25
- chooseBackendFolder: () => ipcRenderer.send('shell:choose-backend-folder'),
26
- })
27
-
28
- window.addEventListener('DOMContentLoaded', () => {
29
- const platform = process.platform
30
- const isWin = platform === 'win32'
31
- const dragHeight = isWin ? 32 : 28
32
- // Width reserved for native window controls (Win caption buttons / mac
33
- // traffic lights live at the top-right / top-left).
34
- const rightReserve = isWin ? 138 : 80
35
- const leftReserve = isWin ? 0 : 80
36
-
37
- const strip = document.createElement('div')
38
- strip.id = 'dsh-clean-shell-drag'
39
- strip.style.cssText = `
40
- position: fixed;
41
- top: 0;
42
- left: ${leftReserve}px;
43
- right: ${rightReserve}px;
44
- height: ${dragHeight}px;
45
- -webkit-app-region: drag;
46
- z-index: 2147483647;
47
- `
48
- document.body.appendChild(strip)
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
+ })
@@ -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>
@@ -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
+ }