dsh-clean-desktop-shell 0.1.0 → 0.1.2

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,48 +1,46 @@
1
- /**
2
- * Shell config persistence (userData/config.json).
3
- *
4
- * Fields:
5
- * - targetUrl: default 'http://127.0.0.1:3080' — set to any remote
6
- * DSH address to run the shell as a pure window.
7
- * - autoStartService: default true — only applies to the local default URL.
8
- * - closeToTray: default true.
9
- * - windowMode: 'advanced' (mica/vibrancy) | 'compatibility'.
10
- * - autoLaunch: open at login.
11
- */
12
- import { app } from 'electron'
13
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
14
- import { dirname, join } from 'node:path'
15
-
16
- const DEFAULTS = {
17
- targetUrl: 'http://127.0.0.1:3080',
18
- autoStartService: true,
19
- closeToTray: true,
20
- windowMode: 'advanced',
21
- autoLaunch: false,
22
- backendPath: null,
23
- }
24
-
25
- let cached = null
26
-
27
- function configPath() {
28
- return join(app.getPath('userData'), 'config.json')
29
- }
30
-
31
- export function loadConfig() {
32
- if (cached) return cached
33
- try {
34
- const raw = readFileSync(configPath(), 'utf8')
35
- cached = { ...DEFAULTS, ...JSON.parse(raw) }
36
- } catch {
37
- cached = { ...DEFAULTS }
38
- }
39
- return cached
40
- }
41
-
42
- export function saveConfig(next) {
43
- cached = { ...DEFAULTS, ...next }
44
- const file = configPath()
45
- mkdirSync(dirname(file), { recursive: true })
46
- writeFileSync(file, JSON.stringify(cached, null, 2), 'utf8')
47
- return cached
48
- }
1
+ /**
2
+ * Shell config persistence (userData/config.json).
3
+ *
4
+ * Fields:
5
+ * - targetUrl: default 'http://127.0.0.1:3080' — set to any remote
6
+ * DSH address to run the shell as a pure window.
7
+ * - closeToTray: default true.
8
+ * - windowMode: 'advanced' (mica/vibrancy) | 'compatibility'.
9
+ * - autoLaunch: open at login.
10
+ */
11
+ import { app } from 'electron'
12
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
13
+ import { dirname, join } from 'node:path'
14
+
15
+ const DEFAULTS = {
16
+ targetUrl: 'http://127.0.0.1:3080',
17
+ closeToTray: true,
18
+ windowMode: 'advanced',
19
+ autoLaunch: false,
20
+ backendPath: null,
21
+ }
22
+
23
+ let cached = null
24
+
25
+ function configPath() {
26
+ return join(app.getPath('userData'), 'config.json')
27
+ }
28
+
29
+ export function loadConfig() {
30
+ if (cached) return cached
31
+ try {
32
+ const raw = readFileSync(configPath(), 'utf8')
33
+ cached = { ...DEFAULTS, ...JSON.parse(raw) }
34
+ } catch {
35
+ cached = { ...DEFAULTS }
36
+ }
37
+ return cached
38
+ }
39
+
40
+ export function saveConfig(next) {
41
+ cached = { ...DEFAULTS, ...next }
42
+ const file = configPath()
43
+ mkdirSync(dirname(file), { recursive: true })
44
+ writeFileSync(file, JSON.stringify(cached, null, 2), 'utf8')
45
+ return cached
46
+ }
@@ -0,0 +1,113 @@
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
+ height: 100%;
10
+ margin: 0;
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
+ }
17
+ .wrap {
18
+ height: 100%;
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ justify-content: center;
23
+ gap: 10px;
24
+ }
25
+ .whale {
26
+ width: 72px;
27
+ height: 72px;
28
+ border-radius: 18px;
29
+ background: #1b2330;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ font-size: 22px;
34
+ font-weight: 700;
35
+ letter-spacing: 0.5px;
36
+ color: #cdd3de;
37
+ filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.45));
38
+ }
39
+ h2 {
40
+ margin: 0;
41
+ font-size: 17px;
42
+ font-weight: 600;
43
+ color: #e8eaf0;
44
+ }
45
+ p {
46
+ margin: 0;
47
+ font-size: 13px;
48
+ }
49
+ button {
50
+ padding: 9px 22px;
51
+ font-size: 13px;
52
+ color: #e8eaf0;
53
+ background: #232d3d;
54
+ border: 1px solid #33405a;
55
+ border-radius: 8px;
56
+ cursor: pointer;
57
+ }
58
+ button:hover { background: #2b3750; }
59
+ button:active { background: #202a3c; }
60
+ .actions {
61
+ display: flex;
62
+ gap: 10px;
63
+ margin-top: 4px;
64
+ }
65
+ .folder-btn {
66
+ margin-top: 2px;
67
+ padding: 4px 8px;
68
+ font-size: 12px;
69
+ color: #6b7686;
70
+ background: none;
71
+ border: none;
72
+ border-radius: 6px;
73
+ text-decoration: underline;
74
+ }
75
+ .folder-btn:hover { color: #9aa3b2; background: none; }
76
+ .hint {
77
+ font-size: 12px;
78
+ opacity: 0.55;
79
+ }
80
+ </style>
81
+ </head>
82
+ <body>
83
+ <div class="wrap">
84
+ <div class="whale">DSH</div>
85
+ <h2>后端未连接</h2>
86
+ <p>请启动 dsh 后端,或使用下方按钮</p>
87
+ <button id="retry">重新加载</button>
88
+ <div class="actions">
89
+ <button id="start">启动后端</button>
90
+ <button id="detect">自动探测后端</button>
91
+ </div>
92
+ <button id="folder" class="folder-btn">设置后端安装文件夹</button>
93
+ <p class="hint">后端启动后本窗口会自动刷新</p>
94
+ </div>
95
+ <script>
96
+ // Injected by preload (sandbox-safe). Falls back to a plain reload.
97
+ var api = window.shellAPI || {};
98
+ document.getElementById('retry').addEventListener('click', function () {
99
+ if (api.reload) api.reload();
100
+ else location.reload();
101
+ });
102
+ var startBtn = document.getElementById('start');
103
+ if (api.startBackend) startBtn.addEventListener('click', api.startBackend);
104
+ else startBtn.style.display = 'none';
105
+ var detectBtn = document.getElementById('detect');
106
+ if (api.detectBackend) detectBtn.addEventListener('click', api.detectBackend);
107
+ else detectBtn.style.display = 'none';
108
+ var folderBtn = document.getElementById('folder');
109
+ if (api.chooseBackendFolder) folderBtn.addEventListener('click', api.chooseBackendFolder);
110
+ else folderBtn.style.display = 'none';
111
+ </script>
112
+ </body>
113
+ </html>
package/electron/main.js CHANGED
@@ -9,10 +9,11 @@
9
9
  * The main window is a pure shell — all backend controls live in the tray.
10
10
  */
11
11
  import { app, BrowserWindow } from 'electron'
12
- import { createMainWindow } from './window.js'
12
+ import { createMainWindow, reloadWindow } from './window.js'
13
13
  import { createTray, refreshTrayMenu } from './tray.js'
14
14
  import { loadConfig, saveConfig } from './config.js'
15
- import { detect, start } from './service.js'
15
+ import { detect } from './service.js'
16
+ import { setupAutoUpdater } from './update.js'
16
17
 
17
18
  const isMac = process.platform === 'darwin'
18
19
 
@@ -56,22 +57,18 @@ if (!gotLock) {
56
57
  return mainWindow
57
58
  }
58
59
 
59
- /** Launch-time backend bootstrap: detect, and auto-start when local. */
60
+ /**
61
+ * Launch-time backend detection. Detects whether a local dsh web is
62
+ * already running so the tray and window reflect the real state, but
63
+ * never auto-starts it — starting is a manual action (tray menu or the
64
+ * offline screen button) so it cannot fight an explicit "stop".
65
+ */
60
66
  async function bootstrapBackend() {
61
67
  const config = loadConfig()
62
68
  const target = config.targetUrl || 'http://127.0.0.1:3080'
63
- const isLocalDefault = target === 'http://127.0.0.1:3080'
64
- if (!isLocalDefault || config.autoStartService === false) return
65
-
66
- const url = await detect()
67
- if (!url) {
68
- // Not running — try to start it, but never block window opening.
69
- try {
70
- await start({ backendPath: config.backendPath })
71
- } catch {
72
- // Backend unavailable; window still opens, tray shows the error.
73
- }
74
- }
69
+ // Remote targets are the user's own business — never probe local.
70
+ if (target !== 'http://127.0.0.1:3080') return
71
+ await detect()
75
72
  // Reflect the post-detect state in the tray menu.
76
73
  refreshTrayMenu()
77
74
  }
@@ -89,6 +86,11 @@ if (!gotLock) {
89
86
  saveConfig({ ...config, autoLaunch: enabled })
90
87
  app.setLoginItemSettings({ openAtLogin: enabled })
91
88
  },
89
+ onReload: () => {
90
+ if (mainWindow && !mainWindow.isDestroyed()) {
91
+ reloadWindow(mainWindow, loadConfig().targetUrl || 'http://127.0.0.1:3080')
92
+ }
93
+ },
92
94
  onQuit: () => {
93
95
  app.isQuitting = true
94
96
  app.quit()
@@ -98,6 +100,9 @@ if (!gotLock) {
98
100
  // Backend bootstrap in background (never delays the window).
99
101
  bootstrapBackend().catch(() => {})
100
102
 
103
+ // Windows: wire the auto-updater (downloads new installers silently).
104
+ setupAutoUpdater()
105
+
101
106
  app.on('activate', () => {
102
107
  if (BrowserWindow.getAllWindows().length === 0) createWindow()
103
108
  else mainWindow?.show()
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Preload: makes the frameless window draggable.
2
+ * Preload: makes the frameless window draggable + exposes the minimal
3
+ * shell API to the loaded page (sandbox-safe contextBridge).
3
4
  *
4
5
  * A frameless window has no title bar, so dragging is provided by a
5
6
  * `-webkit-app-region: drag` strip along the top of the loaded page.
@@ -12,6 +13,18 @@
12
13
  * DSH top bar area. Overlap is acceptable: the strip is click-through for
13
14
  * everything except dragging (app-region drag areas swallow mouse events).
14
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
+
15
28
  window.addEventListener('DOMContentLoaded', () => {
16
29
  const platform = process.platform
17
30
  const isWin = platform === 'win32'
@@ -0,0 +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
+ })
@@ -0,0 +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>
@@ -0,0 +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
+ }