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.
- package/LICENSE +21 -21
- package/README.en.md +168 -91
- package/README.md +161 -90
- package/cordis.patch.yml +4 -4
- package/electron/config.js +46 -48
- package/electron/error.html +113 -0
- package/electron/main.js +20 -15
- package/electron/preload.js +14 -1
- package/electron/progress-preload.js +11 -0
- package/electron/progress.html +79 -0
- package/electron/progress.js +56 -0
- package/electron/service.js +181 -47
- package/electron/tray.js +103 -48
- package/electron/update.js +201 -74
- package/electron/window.js +179 -5
- package/lib/client.js +10 -10
- package/lib/index.js +13 -13
- package/package.json +11 -3
- package/scripts/build.mjs +12 -12
- package/scripts/gen-icons.mjs +70 -70
- package/src/client/client.js +10 -10
- package/src/host/index.js +13 -13
- package/version.txt +1 -1
package/electron/config.js
CHANGED
|
@@ -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
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
cached = { ...DEFAULTS
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
64
|
-
if (
|
|
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()
|
package/electron/preload.js
CHANGED
|
@@ -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
|
+
}
|