dsh-clean-desktop-shell 0.1.10 → 0.1.12

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/electron/main.js CHANGED
@@ -1,187 +1,203 @@
1
- /**
2
- * dsh-clean-desktop-shell — Electron main process entry.
3
- *
4
- * Shell/core decoupling:
5
- * - default target: http://127.0.0.1:3080 (existing web profile, zero migration)
6
- * - configurable remote target (settings file)
7
- * - local backend auto-start on launch; full manual control from the tray
8
- *
9
- * The main window is a pure shell — all backend controls live in the tray.
10
- */
11
- import { app, BrowserWindow, dialog, nativeImage } from 'electron'
12
- import { fileURLToPath } from 'node:url'
13
- import { createMainWindow, reloadWindow } from './window.js'
14
- import { createTray, refreshTrayMenu } from './tray.js'
15
- import { loadConfig, saveConfig, DEFAULT_TARGET_URL } from './config.js'
16
- import { detect } from './service.js'
17
- import { setupAutoUpdater } from './update.js'
18
- import { shortcutSupported, hasDesktopShortcut, createDesktopShortcut, ensureStartMenuShortcut } from './shortcut.js'
19
- import { APP_USER_MODEL_ID } from './aumid.js'
20
- import { setupCrashGuard } from './crashGuard.js'
21
-
22
- const isMac = process.platform === 'darwin'
23
-
24
- // Last-resort error capture (userData/shell-crash.log) — install before
25
- // anything else so even early startup failures leave a trace.
26
- setupCrashGuard()
27
-
28
- // Windows: pin the AppUserModelId so the taskbar shows our whale icon
29
- // instead of the generic Electron icon. Plugin mode uses a distinct ID
30
- // (see aumid.js) so Windows re-reads the icon instead of serving a
31
- // stale per-AUMID cached one.
32
- if (process.platform === 'win32') {
33
- app.setAppUserModelId(APP_USER_MODEL_ID)
34
- }
35
-
36
- // macOS: a bare runtime has no .app bundle (no icon resource), so set the
37
- // Dock icon at runtime. Unlike Windows there is no per-AUMID taskbar cache
38
- // here — app.dock.setIcon applies directly. Packaged builds already carry
39
- // the icon in their bundle, so skip those.
40
- if (isMac && !app.isPackaged) {
41
- try {
42
- const dockIcon = nativeImage.createFromPath(
43
- fileURLToPath(new URL('../build/icon.png', import.meta.url)),
44
- )
45
- if (!dockIcon.isEmpty()) app.dock.setIcon(dockIcon)
46
- } catch {
47
- // non-fatal: keep the default icon
48
- }
49
- }
50
-
51
- // Uniform userData across both distribution branches (installer vs
52
- // plugin-market), so config (target URL, backend path, ...) is shared
53
- // no matter how the shell was launched.
54
- app.setName('DSH Clean Desktop Shell')
55
-
56
- /** Single instance: a second launch just focuses the existing window. */
57
- const gotLock = app.requestSingleInstanceLock()
58
- if (!gotLock) {
59
- app.quit()
60
- } else {
61
- let mainWindow = null
62
- let tray = null
63
-
64
- app.on('second-instance', () => {
65
- if (mainWindow) {
66
- if (mainWindow.isMinimized()) mainWindow.restore()
67
- mainWindow.focus()
68
- }
69
- })
70
-
71
- async function createWindow() {
72
- const config = loadConfig()
73
- const target = config.targetUrl || DEFAULT_TARGET_URL
74
-
75
- mainWindow = createMainWindow({ target })
76
- mainWindow.on('closed', () => {
77
- mainWindow = null
78
- })
79
- mainWindow.on('close', (event) => {
80
- // Close hides to tray unless we are actually quitting. Read the
81
- // config fresh so a runtime change to closeToTray takes effect.
82
- if (!app.isQuitting && loadConfig().closeToTray !== false) {
83
- event.preventDefault()
84
- mainWindow?.hide()
85
- }
86
- })
87
-
88
- return mainWindow
89
- }
90
-
91
- /**
92
- * Launch-time backend detection. Detects whether a local dsh web is
93
- * already running so the tray and window reflect the real state, but
94
- * never auto-starts it — starting is a manual action (tray menu or the
95
- * offline screen button) so it cannot fight an explicit "stop".
96
- */
97
- async function bootstrapBackend() {
98
- const config = loadConfig()
99
- const target = config.targetUrl || DEFAULT_TARGET_URL
100
- // Remote targets are the user's own business — never probe local.
101
- // Origin comparison (not string equality) tolerates trailing slashes
102
- // and an explicitly written default.
103
- if (new URL(target).origin !== new URL(DEFAULT_TARGET_URL).origin) return
104
- await detect()
105
- // Reflect the post-detect state in the tray menu.
106
- refreshTrayMenu()
107
- }
108
-
109
- /**
110
- * First-run desktop shortcut prompt (Windows packaged apps only).
111
- * Asks once; the tray "create desktop shortcut" item stays available
112
- * forever, so saying no is never a dead end.
113
- */
114
- async function ensureShortcut() {
115
- if (!shortcutSupported() || loadConfig().shortcutAsked) return
116
- saveConfig({ ...loadConfig(), shortcutAsked: true })
117
- if (await hasDesktopShortcut()) return
118
- const choice = dialog.showMessageBoxSync({
119
- type: 'question',
120
- title: '创建桌面快捷方式?',
121
- message: '是否在桌面创建「DSH Clean Desktop Shell」快捷方式?',
122
- detail: '选择「跳过」也不影响使用——之后可随时在托盘右键菜单中一键添加。',
123
- buttons: ['创建', '跳过'],
124
- defaultId: 0,
125
- cancelId: 1,
126
- })
127
- if (choice === 0) {
128
- const ok = await createDesktopShortcut()
129
- if (!ok) {
130
- dialog.showErrorBox('创建快捷方式失败', '无法在桌面创建快捷方式。可稍后在托盘右键菜单中重试。')
131
- }
132
- }
133
- }
134
-
135
- app.whenReady().then(async () => {
136
- await createWindow()
137
- tray = createTray({
138
- onShow: () => {
139
- if (!mainWindow) return createWindow()
140
- mainWindow.show()
141
- mainWindow.focus()
142
- },
143
- onReload: () => {
144
- if (mainWindow && !mainWindow.isDestroyed()) {
145
- reloadWindow(mainWindow, loadConfig().targetUrl || DEFAULT_TARGET_URL)
146
- }
147
- },
148
- onQuit: () => {
149
- app.isQuitting = true
150
- app.quit()
151
- },
152
- })
153
-
154
- // Backend bootstrap in background (never delays the window).
155
- bootstrapBackend().catch(() => {})
156
-
157
- // Windows: wire the auto-updater (downloads new installers silently).
158
- setupAutoUpdater()
159
-
160
- // Windows: ensure the AUMID-carrying Start-menu shortcut exists so the
161
- // taskbar button shows our icon (see shortcut.js). Best-effort.
162
- try {
163
- ensureStartMenuShortcut()
164
- } catch {
165
- // non-fatal
166
- }
167
-
168
- // First run: offer a desktop shortcut (never nags twice).
169
- ensureShortcut().catch(() => {})
170
-
171
- app.on('activate', () => {
172
- if (BrowserWindow.getAllWindows().length === 0) createWindow()
173
- else mainWindow?.show()
174
- })
175
- })
176
-
177
- app.on('window-all-closed', () => {
178
- // Keep the app alive in the tray (like a normal desktop utility).
179
- if (!isMac && !app.isQuitting) {
180
- // do not quit — tray keeps running
181
- }
182
- })
183
-
184
- app.on('before-quit', () => {
185
- app.isQuitting = true
186
- })
187
- }
1
+ /**
2
+ * dsh-clean-desktop-shell — Electron main process entry.
3
+ *
4
+ * Shell/core decoupling:
5
+ * - default target: http://127.0.0.1:3080 (existing web profile, zero migration)
6
+ * - configurable remote target (settings file)
7
+ * - local backend auto-start on launch; full manual control from the tray
8
+ *
9
+ * The main window is a pure shell — all backend controls live in the tray.
10
+ */
11
+ import { app, BrowserWindow, dialog, nativeImage } from 'electron'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { createMainWindow, reloadWindow } from './window.js'
14
+ import { createTray, refreshTrayMenu } from './tray.js'
15
+ import { loadConfig, saveConfig, DEFAULT_TARGET_URL } from './config.js'
16
+ import { detect, getAuthenticatedUrl, getStatus } from './service.js'
17
+ import { setupAutoUpdater } from './update.js'
18
+ import { shortcutSupported, hasDesktopShortcut, createDesktopShortcut, ensureStartMenuShortcut } from './shortcut.js'
19
+ import { APP_USER_MODEL_ID } from './aumid.js'
20
+ import { setupCrashGuard } from './crashGuard.js'
21
+
22
+ const isMac = process.platform === 'darwin'
23
+
24
+ // Last-resort error capture (userData/shell-crash.log) — install before
25
+ // anything else so even early startup failures leave a trace.
26
+ setupCrashGuard()
27
+
28
+ // Windows: pin the AppUserModelId so the taskbar shows our whale icon
29
+ // instead of the generic Electron icon. Plugin mode uses a distinct ID
30
+ // (see aumid.js) so Windows re-reads the icon instead of serving a
31
+ // stale per-AUMID cached one.
32
+ if (process.platform === 'win32') {
33
+ app.setAppUserModelId(APP_USER_MODEL_ID)
34
+ }
35
+
36
+ // macOS: a bare runtime has no .app bundle (no icon resource), so set the
37
+ // Dock icon at runtime. Unlike Windows there is no per-AUMID taskbar cache
38
+ // here — app.dock.setIcon applies directly. Packaged builds already carry
39
+ // the icon in their bundle, so skip those.
40
+ if (isMac && !app.isPackaged) {
41
+ try {
42
+ const dockIcon = nativeImage.createFromPath(
43
+ fileURLToPath(new URL('../build/icon.png', import.meta.url)),
44
+ )
45
+ if (!dockIcon.isEmpty()) app.dock.setIcon(dockIcon)
46
+ } catch {
47
+ // non-fatal: keep the default icon
48
+ }
49
+ }
50
+
51
+ // Uniform userData across both distribution branches (installer vs
52
+ // plugin-market), so config (target URL, backend path, ...) is shared
53
+ // no matter how the shell was launched.
54
+ app.setName('DSH Clean Desktop Shell')
55
+
56
+ /** Single instance: a second launch just focuses the existing window. */
57
+ const gotLock = app.requestSingleInstanceLock()
58
+ if (!gotLock) {
59
+ app.quit()
60
+ } else {
61
+ let mainWindow = null
62
+ let tray = null
63
+
64
+ app.on('second-instance', () => {
65
+ if (mainWindow) {
66
+ if (mainWindow.isMinimized()) mainWindow.restore()
67
+ mainWindow.focus()
68
+ }
69
+ })
70
+
71
+ async function createWindow() {
72
+ const config = loadConfig()
73
+ const target = config.targetUrl || DEFAULT_TARGET_URL
74
+ // In plugin mode the host half (same dsh web process) supplies the real
75
+ // per-process launch URL through DSH_WEB_LAUNCH_URL. In tray-start mode
76
+ // service.start() will surface a fresh one through status changes.
77
+ // Only apply BrowserAuth bootstrap to the local default; remote targets
78
+ // remain the user's own externally-managed surface.
79
+ let isLocalDefault = false
80
+ try {
81
+ isLocalDefault = new URL(target).origin === new URL(DEFAULT_TARGET_URL).origin
82
+ } catch {
83
+ // A malformed configured target must not break window creation, and it
84
+ // must never receive the local launch credential — stay non-local.
85
+ }
86
+ const launchUrl = isLocalDefault ? (getStatus().launchUrl || null) : null
87
+
88
+ mainWindow = createMainWindow({ target, launchUrl })
89
+ mainWindow.on('closed', () => {
90
+ mainWindow = null
91
+ })
92
+ mainWindow.on('close', (event) => {
93
+ // Close hides to tray unless we are actually quitting. Read the
94
+ // config fresh so a runtime change to closeToTray takes effect.
95
+ if (!app.isQuitting && loadConfig().closeToTray !== false) {
96
+ event.preventDefault()
97
+ mainWindow?.hide()
98
+ }
99
+ })
100
+
101
+ return mainWindow
102
+ }
103
+
104
+ /**
105
+ * Launch-time backend detection. Detects whether a local dsh web is
106
+ * already running so the tray and window reflect the real state, but
107
+ * never auto-starts it — starting is a manual action (tray menu or the
108
+ * offline screen button) so it cannot fight an explicit "stop".
109
+ */
110
+ async function bootstrapBackend() {
111
+ const config = loadConfig()
112
+ const target = config.targetUrl || DEFAULT_TARGET_URL
113
+ // Remote targets are the user's own business — never probe local.
114
+ // Origin comparison (not string equality) tolerates trailing slashes
115
+ // and an explicitly written default.
116
+ if (new URL(target).origin !== new URL(DEFAULT_TARGET_URL).origin) return
117
+ await detect()
118
+ // Reflect the post-detect state in the tray menu.
119
+ refreshTrayMenu()
120
+ }
121
+
122
+ /**
123
+ * First-run desktop shortcut prompt (Windows packaged apps only).
124
+ * Asks once; the tray "create desktop shortcut" item stays available
125
+ * forever, so saying no is never a dead end.
126
+ */
127
+ async function ensureShortcut() {
128
+ if (!shortcutSupported() || loadConfig().shortcutAsked) return
129
+ saveConfig({ ...loadConfig(), shortcutAsked: true })
130
+ if (await hasDesktopShortcut()) return
131
+ const choice = dialog.showMessageBoxSync({
132
+ type: 'question',
133
+ title: '创建桌面快捷方式?',
134
+ message: '是否在桌面创建「DSH Clean Desktop Shell」快捷方式?',
135
+ detail: '选择「跳过」也不影响使用——之后可随时在托盘右键菜单中一键添加。',
136
+ buttons: ['创建', '跳过'],
137
+ defaultId: 0,
138
+ cancelId: 1,
139
+ })
140
+ if (choice === 0) {
141
+ const ok = await createDesktopShortcut()
142
+ if (!ok) {
143
+ dialog.showErrorBox('创建快捷方式失败', '无法在桌面创建快捷方式。可稍后在托盘右键菜单中重试。')
144
+ }
145
+ }
146
+ }
147
+
148
+ app.whenReady().then(async () => {
149
+ await createWindow()
150
+ tray = createTray({
151
+ onShow: () => {
152
+ if (!mainWindow) return createWindow()
153
+ mainWindow.show()
154
+ mainWindow.focus()
155
+ },
156
+ onReload: () => {
157
+ if (mainWindow && !mainWindow.isDestroyed()) {
158
+ // Prefer the live launch-token URL from the last shell-started
159
+ // backend; a manual refresh is exactly when a stale config target
160
+ // (or a bare pre-0.1.2 loopback URL) would 401.
161
+ reloadWindow(mainWindow, getAuthenticatedUrl() || loadConfig().targetUrl || DEFAULT_TARGET_URL)
162
+ }
163
+ },
164
+ onQuit: () => {
165
+ app.isQuitting = true
166
+ app.quit()
167
+ },
168
+ })
169
+
170
+ // Backend bootstrap in background (never delays the window).
171
+ bootstrapBackend().catch(() => {})
172
+
173
+ // Windows: wire the auto-updater (downloads new installers silently).
174
+ setupAutoUpdater()
175
+
176
+ // Windows: ensure the AUMID-carrying Start-menu shortcut exists so the
177
+ // taskbar button shows our icon (see shortcut.js). Best-effort.
178
+ try {
179
+ ensureStartMenuShortcut()
180
+ } catch {
181
+ // non-fatal
182
+ }
183
+
184
+ // First run: offer a desktop shortcut (never nags twice).
185
+ ensureShortcut().catch(() => {})
186
+
187
+ app.on('activate', () => {
188
+ if (BrowserWindow.getAllWindows().length === 0) createWindow()
189
+ else mainWindow?.show()
190
+ })
191
+ })
192
+
193
+ app.on('window-all-closed', () => {
194
+ // Keep the app alive in the tray (like a normal desktop utility).
195
+ if (!isMac && !app.isQuitting) {
196
+ // do not quit — tray keeps running
197
+ }
198
+ })
199
+
200
+ app.on('before-quit', () => {
201
+ app.isQuitting = true
202
+ })
203
+ }
@@ -1,49 +1,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
- 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
+ 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,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
+ })