dsh-version-check 0.0.0-stage → 0.3.0

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/lib/client.js ADDED
@@ -0,0 +1,977 @@
1
+ // src/client.ts
2
+ window.__ModuleLoader__.load({
3
+ id: "dsh-version-check",
4
+ factory(require) {
5
+ const React = require("react");
6
+ const h = React.createElement;
7
+ const NS = "dsh-version-check";
8
+ const STATE_PATH = "/__dsh-version-check/state";
9
+ const CHECK_PATH = "/__dsh-version-check/check";
10
+ const UPDATE_PATH = "/__dsh-version-check/update";
11
+ const VERSIONS_PATH = "/__dsh-version-check/versions";
12
+ const ROLLBACK_PATH = "/__dsh-version-check/rollback";
13
+ const RESTART_PATH = "/__dsh-version-check/restart";
14
+ const PREFERENCES_PATH = "/__dsh-version-check/preferences";
15
+ const IDLE_POLL_MS = 3e4;
16
+ const ACTIVE_POLL_MS = 1e3;
17
+ const ACTIVE_PHASES = /* @__PURE__ */ new Set(["installing", "restarting"]);
18
+ const inject = ["slots", "locale"];
19
+ const zh = {
20
+ "badge.label": "DSH 版本",
21
+ "badge.checking": "检查中",
22
+ "badge.updateAvailable": "有新版本",
23
+ "badge.installing": "更新中",
24
+ "badge.restartRequired": "待重启",
25
+ "badge.error": "检查失败",
26
+ "panel.title": "DSH 版本",
27
+ "panel.current": "当前版本",
28
+ "panel.latest": "最新版本",
29
+ "panel.channel": "发布渠道",
30
+ "panel.lastChecked": "上次检查",
31
+ "panel.never": "尚未检查",
32
+ "panel.justNow": "刚刚",
33
+ "panel.minutesAgo": "{count} 分钟前",
34
+ "panel.hoursAgo": "{count} 小时前",
35
+ "panel.daysAgo": "{count} 天前",
36
+ "panel.status.upToDate": "已是最新版本",
37
+ "panel.status.notChecked": "尚未检查",
38
+ "panel.status.updateAvailable": "有新版本可用",
39
+ "panel.status.checking": "正在检查更新",
40
+ "panel.status.installing": "正在安装 {version}",
41
+ "panel.status.restarting": "正在重启服务",
42
+ "panel.status.restartRequired": "已安装 {version},需要重启 DSH 生效",
43
+ "panel.status.updated": "已更新到 {version}",
44
+ "panel.status.error": "检查失败",
45
+ "panel.action.check": "检查更新",
46
+ "panel.action.update": "更新并重启",
47
+ "panel.action.restart": "重启服务",
48
+ "panel.action.reload": "刷新页面",
49
+ "panel.note.restart": "更新会重启 DSH 服务,正在进行的会话会中断",
50
+ "panel.note.manualRestart": "当前部署无法自动重启,请手动重启 dsh 服务以应用新版本",
51
+ "panel.note.autoOff": "自动检查已关闭,仍可手动检查最新版本;重新开启后才能更新",
52
+ "panel.note.registry": "来源 npm registry",
53
+ "settings.nav": "版本更新",
54
+ "settings.description": "控制 DSH 自身更新检查的开关、自动检查间隔与发布渠道,也可以把主程序回滚到更早的已发布版本",
55
+ "settings.check.title": "检查更新",
56
+ "settings.check.desc": "关闭后不再定时检查,也不启用更新功能;仍可在弹层里手动检查最新版本",
57
+ "settings.interval.title": "自动检查间隔",
58
+ "settings.interval.desc": "两次自动检查之间的时间,范围 1 到 10080 分钟",
59
+ "settings.interval.unit": "分钟",
60
+ "settings.interval.invalid": "请输入 1 到 10080 之间的整数分钟",
61
+ "settings.channel.title": "发布渠道",
62
+ "settings.channel.desc": "自动跟随当前安装版本的前置标识,也可以固定为某个 npm dist-tag",
63
+ "settings.channel.auto": "自动",
64
+ "settings.save.saving": "正在保存",
65
+ "settings.save.saved": "已保存",
66
+ "settings.loading": "正在读取设置",
67
+ "settings.unavailable": "当前运行的 Host 版本不提供设置接口,请更新插件后重试",
68
+ "settings.rollback.title": "回滚 DSH 版本",
69
+ "settings.rollback.desc": "回滚会重启服务并中断正在进行的会话",
70
+ "settings.rollback.loading": "正在读取已发布版本",
71
+ "settings.rollback.error": "读取已发布版本失败:{message}",
72
+ "settings.rollback.retry": "重新读取",
73
+ "settings.rollback.pick": "选择版本",
74
+ "settings.rollback.pickTitle": "选择要回滚的版本",
75
+ "settings.rollback.empty": "没有比当前版本更旧的已发布版本",
76
+ "settings.rollback.expand": "显示全部 {count} 个版本",
77
+ "settings.rollback.collapse": "收起",
78
+ "settings.rollback.action": "回滚",
79
+ "settings.rollback.confirm": "回滚到 {version} 会重启 DSH 服务,正在进行的会话会中断。",
80
+ "settings.rollback.confirmAction": "确认回滚",
81
+ "settings.rollback.cancel": "取消",
82
+ "settings.rollback.installing": "正在安装 {version}",
83
+ "settings.rollback.restarting": "正在重启服务",
84
+ "settings.rollback.restartRequired": "已安装 {version},需要手动重启 dsh 服务才能生效"
85
+ };
86
+ const en = {
87
+ "badge.label": "DSH version",
88
+ "badge.checking": "Checking",
89
+ "badge.updateAvailable": "Update available",
90
+ "badge.installing": "Updating",
91
+ "badge.restartRequired": "Restart required",
92
+ "badge.error": "Check failed",
93
+ "panel.title": "DSH version",
94
+ "panel.current": "Current version",
95
+ "panel.latest": "Latest version",
96
+ "panel.channel": "Channel",
97
+ "panel.lastChecked": "Last checked",
98
+ "panel.never": "Never checked",
99
+ "panel.justNow": "Just now",
100
+ "panel.minutesAgo": "{count} min ago",
101
+ "panel.hoursAgo": "{count} hr ago",
102
+ "panel.daysAgo": "{count} days ago",
103
+ "panel.status.upToDate": "Up to date",
104
+ "panel.status.notChecked": "Not checked yet",
105
+ "panel.status.updateAvailable": "Update available",
106
+ "panel.status.checking": "Checking for updates",
107
+ "panel.status.installing": "Installing {version}",
108
+ "panel.status.restarting": "Restarting service",
109
+ "panel.status.restartRequired": "Installed {version}, restart DSH to apply",
110
+ "panel.status.updated": "Updated to {version}",
111
+ "panel.status.error": "Check failed",
112
+ "panel.action.check": "Check for updates",
113
+ "panel.action.update": "Update and restart",
114
+ "panel.action.restart": "Restart service",
115
+ "panel.action.reload": "Reload page",
116
+ "panel.note.restart": "Updating restarts the DSH service and interrupts running sessions",
117
+ "panel.note.manualRestart": "This deployment cannot restart automatically; restart dsh manually to apply the new version",
118
+ "panel.note.autoOff": "Automatic checking is off; you can still check manually, but updating requires re-enabling it",
119
+ "panel.note.registry": "Source: npm registry",
120
+ "settings.nav": "Version updates",
121
+ "settings.description": "Control DSH update checking, its automatic interval, and the release channel; you can also roll the main program back to an earlier published version",
122
+ "settings.check.title": "Check for updates",
123
+ "settings.check.desc": "When off, no scheduled checks and no updates; you can still check manually from the popover",
124
+ "settings.interval.title": "Auto-check interval",
125
+ "settings.interval.desc": "Time between automatic checks, from 1 to 10080 minutes",
126
+ "settings.interval.unit": "min",
127
+ "settings.interval.invalid": "Enter an integer number of minutes between 1 and 10080",
128
+ "settings.channel.title": "Release channel",
129
+ "settings.channel.desc": "Auto follows the installed version prerelease channel, or pin a npm dist-tag",
130
+ "settings.channel.auto": "Auto",
131
+ "settings.save.saving": "Saving",
132
+ "settings.save.saved": "Saved",
133
+ "settings.loading": "Loading settings",
134
+ "settings.unavailable": "The running Host does not expose the settings API; update the plugin and retry",
135
+ "settings.rollback.title": "Roll back DSH version",
136
+ "settings.rollback.desc": "Rolling back restarts the service and interrupts running sessions",
137
+ "settings.rollback.loading": "Loading published versions",
138
+ "settings.rollback.error": "Failed to load published versions: {message}",
139
+ "settings.rollback.retry": "Reload",
140
+ "settings.rollback.pick": "Choose a version",
141
+ "settings.rollback.pickTitle": "Choose a version to roll back to",
142
+ "settings.rollback.empty": "No published version older than the installed one",
143
+ "settings.rollback.expand": "Show all {count} versions",
144
+ "settings.rollback.collapse": "Show fewer",
145
+ "settings.rollback.action": "Roll back",
146
+ "settings.rollback.confirm": "Rolling back to {version} restarts the DSH service and interrupts running sessions.",
147
+ "settings.rollback.confirmAction": "Confirm rollback",
148
+ "settings.rollback.cancel": "Cancel",
149
+ "settings.rollback.installing": "Installing {version}",
150
+ "settings.rollback.restarting": "Restarting service",
151
+ "settings.rollback.restartRequired": "Installed {version}; restart dsh manually to apply"
152
+ };
153
+ function describeError(failure) {
154
+ return failure instanceof Error ? failure.message : String(failure);
155
+ }
156
+ function createVersionStore() {
157
+ const listeners = /* @__PURE__ */ new Set();
158
+ let view = Object.freeze({
159
+ snapshot: null,
160
+ offline: false,
161
+ actionError: null,
162
+ busy: null,
163
+ lastAction: null,
164
+ pendingTarget: null
165
+ });
166
+ let timer = null;
167
+ let running = false;
168
+ const publish = (patch) => {
169
+ view = Object.freeze({
170
+ ...view,
171
+ ...patch
172
+ });
173
+ for (const listener of [...listeners]) listener();
174
+ };
175
+ const subscribe = (listener) => {
176
+ listeners.add(listener);
177
+ return () => {
178
+ listeners.delete(listener);
179
+ };
180
+ };
181
+ const getView = () => view;
182
+ const readState = async () => {
183
+ try {
184
+ const response = await fetch(STATE_PATH, {
185
+ method: "GET",
186
+ headers: { accept: "application/json" },
187
+ cache: "no-store"
188
+ });
189
+ if (!response.ok) throw new Error(`GET ${STATE_PATH} 返回 HTTP ${response.status}`);
190
+ const snapshot = await response.json();
191
+ publish({
192
+ snapshot,
193
+ offline: false
194
+ });
195
+ } catch {
196
+ publish({ offline: true });
197
+ }
198
+ };
199
+ const pollDelay = () => {
200
+ if (view.busy !== null) return ACTIVE_POLL_MS;
201
+ if (view.offline) return IDLE_POLL_MS;
202
+ const snapshot = view.snapshot;
203
+ const phase = snapshot === null ? null : snapshot.phase;
204
+ if (phase !== null && ACTIVE_PHASES.has(phase)) return ACTIVE_POLL_MS;
205
+ if (snapshot !== null && snapshot.settings !== void 0 && snapshot.settings.checkEnabled === false) return IDLE_POLL_MS;
206
+ if (snapshot === null) return 3e3;
207
+ if (snapshot.latest === null && snapshot.lastError === null) return 3e3;
208
+ return IDLE_POLL_MS;
209
+ };
210
+ const schedule = () => {
211
+ if (!running) return;
212
+ if (timer !== null) clearTimeout(timer);
213
+ timer = setTimeout(() => {
214
+ timer = null;
215
+ readState().then(schedule);
216
+ }, pollDelay());
217
+ };
218
+ const start = () => {
219
+ if (running) return;
220
+ running = true;
221
+ readState().then(schedule);
222
+ };
223
+ const stop = () => {
224
+ running = false;
225
+ if (timer !== null) {
226
+ clearTimeout(timer);
227
+ timer = null;
228
+ }
229
+ };
230
+ const sendAction = async (path, body) => {
231
+ let response;
232
+ try {
233
+ response = await fetch(path, {
234
+ method: "POST",
235
+ headers: {
236
+ accept: "application/json",
237
+ "content-type": "application/json"
238
+ },
239
+ body: JSON.stringify(body === null ? {} : body)
240
+ });
241
+ } catch (failure) {
242
+ publish({ offline: true });
243
+ throw failure;
244
+ }
245
+ const text = await response.text();
246
+ const payload = text === "" ? null : JSON.parse(text);
247
+ if (!response.ok) {
248
+ const errorPayload = payload;
249
+ const message = errorPayload !== null && errorPayload.error !== void 0 && errorPayload.error !== null ? errorPayload.error.message : `POST ${path} 返回 HTTP ${response.status}`;
250
+ throw new Error(message);
251
+ }
252
+ return payload;
253
+ };
254
+ const runAction = async (action, task) => {
255
+ if (view.busy !== null) return;
256
+ publish({
257
+ busy: action,
258
+ lastAction: action,
259
+ actionError: null
260
+ });
261
+ try {
262
+ const snapshot = await task();
263
+ publish({
264
+ snapshot,
265
+ offline: false,
266
+ busy: null,
267
+ actionError: null
268
+ });
269
+ } catch (failure) {
270
+ publish({
271
+ busy: null,
272
+ actionError: describeError(failure)
273
+ });
274
+ }
275
+ schedule();
276
+ };
277
+ const checkNow = () => {
278
+ const phase = view.snapshot === null ? null : view.snapshot.phase;
279
+ if (phase !== null && ACTIVE_PHASES.has(phase)) return Promise.resolve();
280
+ return runAction("check", () => sendAction(CHECK_PATH, null));
281
+ };
282
+ const startUpdate = (version) => {
283
+ publish({ pendingTarget: version });
284
+ return runAction("update", () => sendAction(UPDATE_PATH, {
285
+ confirm: true,
286
+ version
287
+ }));
288
+ };
289
+ const rollbackTo = (version) => runAction("rollback", () => sendAction(ROLLBACK_PATH, {
290
+ confirm: true,
291
+ version
292
+ }));
293
+ const restartService = () => runAction("restart", () => sendAction(RESTART_PATH, { confirm: true }));
294
+ const savePreferences = (patch) => runAction("preferences", () => sendAction(PREFERENCES_PATH, patch));
295
+ return {
296
+ subscribe,
297
+ getView,
298
+ start,
299
+ stop,
300
+ checkNow,
301
+ startUpdate,
302
+ rollbackTo,
303
+ restartService,
304
+ savePreferences
305
+ };
306
+ }
307
+ function VersionIcon({ size }) {
308
+ return h("svg", {
309
+ className: "dvc-icon",
310
+ width: size,
311
+ height: size,
312
+ viewBox: "0 0 24 24",
313
+ "aria-hidden": true,
314
+ focusable: false
315
+ }, h("path", {
316
+ d: "M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z",
317
+ fill: "currentColor"
318
+ }));
319
+ }
320
+ function formatLastChecked(t, lastCheckedAt) {
321
+ if (lastCheckedAt === null || lastCheckedAt === void 0) return t("panel.never");
322
+ const elapsed = Date.now() - lastCheckedAt;
323
+ const minutes = Math.floor(Math.max(0, elapsed) / 6e4);
324
+ if (minutes < 1) return t("panel.justNow");
325
+ if (minutes < 60) return t("panel.minutesAgo", { count: minutes });
326
+ const hours = Math.floor(minutes / 60);
327
+ if (hours < 24) return t("panel.hoursAgo", { count: hours });
328
+ return t("panel.daysAgo", { count: Math.floor(hours / 24) });
329
+ }
330
+ function VersionBadge({ wide, t, store }) {
331
+ const view = React.useSyncExternalStore(store.subscribe, store.getView);
332
+ const [open, setOpen] = React.useState(false);
333
+ const [anchor, setAnchor] = React.useState(null);
334
+ const rootRef = React.useRef(null);
335
+ const snapshot = view.snapshot;
336
+ const phase = snapshot === null ? null : snapshot.phase;
337
+ const installedVersion = snapshot === null ? null : snapshot.installedVersion;
338
+ const latestVersion = snapshot === null || snapshot.latest === null ? null : snapshot.latest.version;
339
+ const updateAvailable = snapshot !== null && snapshot.updateAvailable === true;
340
+ const restartMode = snapshot === null ? null : snapshot.restartMode;
341
+ const target = snapshot !== null && snapshot.target !== null ? snapshot.target : view.pendingTarget !== null ? view.pendingTarget : latestVersion;
342
+ const installing = phase === "installing";
343
+ const restarting = phase === "restarting";
344
+ const restartRequired = phase === "restart-required";
345
+ const checking = view.busy === "check" || phase === "checking";
346
+ const updatedTo = view.pendingTarget !== null && snapshot !== null && snapshot.bootVersion === view.pendingTarget && snapshot.updateAvailable === false ? view.pendingTarget : null;
347
+ const hostError = snapshot === null || snapshot.lastError === null ? null : snapshot.lastError.message;
348
+ const checkEnabled = snapshot === null || snapshot.settings === void 0 ? true : snapshot.settings.checkEnabled !== false;
349
+ const paused = snapshot !== null && !checkEnabled;
350
+ const actionError = view.actionError !== null && (view.lastAction === "check" || view.lastAction === "update" || view.lastAction === "restart") ? view.actionError : null;
351
+ const errorText = actionError !== null ? actionError : hostError !== null ? hostError : snapshot === null && view.offline ? t("panel.status.error") : null;
352
+ React.useLayoutEffect(() => {
353
+ if (!open) return;
354
+ const place = () => {
355
+ const root = rootRef.current;
356
+ if (root === null) return;
357
+ const rect = root.getBoundingClientRect();
358
+ setAnchor({
359
+ left: rect.left,
360
+ bottom: window.innerHeight - rect.top + 8
361
+ });
362
+ };
363
+ place();
364
+ window.addEventListener("resize", place);
365
+ return () => {
366
+ window.removeEventListener("resize", place);
367
+ };
368
+ }, [open, wide]);
369
+ React.useEffect(() => {
370
+ if (!open) return;
371
+ const onPointerDown = (event) => {
372
+ const root = rootRef.current;
373
+ if (root !== null && root.contains(event.target)) return;
374
+ setOpen(false);
375
+ };
376
+ const onKeyDown = (event) => {
377
+ if (event.key === "Escape") setOpen(false);
378
+ };
379
+ document.addEventListener("pointerdown", onPointerDown);
380
+ document.addEventListener("keydown", onKeyDown);
381
+ return () => {
382
+ document.removeEventListener("pointerdown", onPointerDown);
383
+ document.removeEventListener("keydown", onKeyDown);
384
+ };
385
+ }, [open]);
386
+ let badgeText;
387
+ let badgeTone = null;
388
+ let badgeDot = false;
389
+ if (snapshot === null) {
390
+ if (view.offline) {
391
+ badgeText = t("badge.error");
392
+ badgeTone = "error";
393
+ } else badgeText = t("badge.checking");
394
+ } else if (checking) badgeText = t("badge.checking");
395
+ else if (installing || restarting || view.busy === "update") badgeText = t("badge.installing");
396
+ else if (restartRequired) {
397
+ badgeText = t("badge.restartRequired");
398
+ badgeTone = "update";
399
+ } else if (paused) badgeText = installedVersion !== null ? installedVersion : t("badge.checking");
400
+ else if (updateAvailable) {
401
+ badgeText = t("badge.updateAvailable");
402
+ badgeTone = "update";
403
+ badgeDot = true;
404
+ } else badgeText = installedVersion !== null ? installedVersion : t("badge.checking");
405
+ let pillText;
406
+ let pillTone = null;
407
+ if (snapshot === null) {
408
+ pillText = view.offline ? t("panel.status.error") : t("panel.status.checking");
409
+ if (view.offline) pillTone = "error";
410
+ } else if (updatedTo !== null) {
411
+ pillText = t("panel.status.updated", { version: updatedTo });
412
+ pillTone = "update";
413
+ } else if (checking) pillText = t("panel.status.checking");
414
+ else if (installing) pillText = t("panel.status.installing", { version: target === null ? "" : target });
415
+ else if (restarting) pillText = t("panel.status.restarting");
416
+ else if (restartRequired) {
417
+ pillText = t("panel.status.restartRequired", { version: installedVersion === null ? "" : installedVersion });
418
+ pillTone = "update";
419
+ } else if (errorText !== null) {
420
+ pillText = t("panel.status.error");
421
+ pillTone = "error";
422
+ } else if (updateAvailable) {
423
+ pillText = t("panel.status.updateAvailable");
424
+ pillTone = "update";
425
+ } else if (latestVersion === null) pillText = t("panel.status.notChecked");
426
+ else pillText = t("panel.status.upToDate");
427
+ const bodyChildren = [];
428
+ bodyChildren.push(h("div", {
429
+ className: "dvc-version-row",
430
+ key: "version"
431
+ }, h("span", { className: "dvc-version-block" }, h("span", { className: "dvc-version-label" }, t("panel.current")), h("span", { className: "dvc-version" }, installedVersion !== null ? installedVersion : "--")), h("span", {
432
+ className: "dvc-pill",
433
+ "data-tone": pillTone === null ? void 0 : pillTone
434
+ }, pillText)));
435
+ bodyChildren.push(h("div", {
436
+ className: "dvc-divider",
437
+ key: "divider-top"
438
+ }));
439
+ bodyChildren.push(h("div", {
440
+ className: "dvc-row",
441
+ key: "latest"
442
+ }, h("span", { className: "dvc-row-label" }, t("panel.latest")), h("span", { className: "dvc-row-value" }, latestVersion !== null ? latestVersion : "--")));
443
+ bodyChildren.push(h("div", {
444
+ className: "dvc-row",
445
+ key: "channel"
446
+ }, h("span", { className: "dvc-row-label" }, t("panel.channel")), h("span", { className: "dvc-row-value" }, snapshot !== null && snapshot.channel !== null ? snapshot.channel : "--")));
447
+ bodyChildren.push(h("div", {
448
+ className: "dvc-row",
449
+ key: "checked"
450
+ }, h("span", { className: "dvc-row-label" }, t("panel.lastChecked")), h("span", { className: "dvc-row-value" }, formatLastChecked(t, snapshot === null ? null : snapshot.lastCheckedAt))));
451
+ bodyChildren.push(h("div", {
452
+ className: "dvc-divider",
453
+ key: "divider-bottom"
454
+ }));
455
+ if (errorText !== null) bodyChildren.push(h("p", {
456
+ className: "dvc-error",
457
+ key: "error",
458
+ role: "alert"
459
+ }, errorText));
460
+ if (installing || restarting) {
461
+ const progressText = installing ? t("panel.status.installing", { version: target === null ? "" : target }) : t("panel.status.restarting");
462
+ bodyChildren.push(h("p", {
463
+ className: "dvc-progress",
464
+ key: "progress"
465
+ }, progressText));
466
+ const installOutput = snapshot === null || snapshot.installOutput === null ? null : snapshot.installOutput;
467
+ if (installOutput !== null) bodyChildren.push(h("p", {
468
+ className: "dvc-output",
469
+ key: "output"
470
+ }, installOutput));
471
+ }
472
+ const actionButtons = [];
473
+ if (updatedTo !== null) actionButtons.push(h("button", {
474
+ key: "reload",
475
+ type: "button",
476
+ className: "dvc-button",
477
+ "data-primary": true,
478
+ onClick: () => {
479
+ window.location.reload();
480
+ }
481
+ }, t("panel.action.reload")));
482
+ else {
483
+ const actionsDisabled = view.busy !== null || installing || restarting;
484
+ actionButtons.push(h("button", {
485
+ key: "check",
486
+ type: "button",
487
+ className: "dvc-button",
488
+ disabled: actionsDisabled,
489
+ onClick: () => {
490
+ store.checkNow();
491
+ }
492
+ }, t("panel.action.check")));
493
+ if (restartRequired && restartMode === "systemd") actionButtons.push(h("button", {
494
+ key: "restart",
495
+ type: "button",
496
+ className: "dvc-button",
497
+ "data-primary": true,
498
+ disabled: actionsDisabled,
499
+ onClick: () => {
500
+ store.restartService();
501
+ }
502
+ }, t("panel.action.restart")));
503
+ else if (updateAvailable && checkEnabled && latestVersion !== null && !installing && !restarting) actionButtons.push(h("button", {
504
+ key: "update",
505
+ type: "button",
506
+ className: "dvc-button",
507
+ "data-primary": true,
508
+ disabled: actionsDisabled,
509
+ onClick: () => {
510
+ store.startUpdate(latestVersion);
511
+ }
512
+ }, t("panel.action.update")));
513
+ }
514
+ if (actionButtons.length > 0) bodyChildren.push(h("div", {
515
+ className: "dvc-actions",
516
+ key: "actions"
517
+ }, actionButtons));
518
+ if (paused && !restartRequired) {
519
+ bodyChildren.push(h("p", {
520
+ className: "dvc-note",
521
+ key: "note-auto-off"
522
+ }, t("panel.note.autoOff")));
523
+ bodyChildren.push(h("p", {
524
+ className: "dvc-note",
525
+ key: "note-registry"
526
+ }, t("panel.note.registry")));
527
+ } else {
528
+ bodyChildren.push(h("p", {
529
+ className: "dvc-note",
530
+ key: "note-restart"
531
+ }, restartRequired && restartMode === "manual" ? t("panel.note.manualRestart") : t("panel.note.restart")));
532
+ bodyChildren.push(h("p", {
533
+ className: "dvc-note",
534
+ key: "note-registry"
535
+ }, t("panel.note.registry")));
536
+ }
537
+ const panel = open && anchor !== null ? h("section", {
538
+ key: "panel",
539
+ className: "dvc-panel",
540
+ style: anchor,
541
+ role: "dialog",
542
+ "aria-label": t("panel.title")
543
+ }, h("header", { className: "dvc-header" }, h("span", { className: "dvc-title" }, t("panel.title"))), h("div", { className: "dvc-body" }, bodyChildren)) : null;
544
+ const ariaLabel = `${t("badge.label")} ${badgeText}`;
545
+ return h("div", {
546
+ className: wide ? "dvc-layer" : "dvc-layer dvc-rail",
547
+ ref: rootRef
548
+ }, panel, h("button", {
549
+ type: "button",
550
+ className: "dvc-badge",
551
+ "aria-label": ariaLabel,
552
+ "aria-haspopup": "dialog",
553
+ "aria-expanded": open,
554
+ "data-active": open ? true : void 0,
555
+ onClick: () => {
556
+ setOpen((value) => !value);
557
+ }
558
+ }, h(VersionIcon, { size: wide ? 16 : 18 }), wide ? [h("span", {
559
+ className: "dvc-label",
560
+ key: "label"
561
+ }, t("badge.label")), h("span", {
562
+ className: "dvc-value",
563
+ key: "value",
564
+ "data-tone": badgeTone === null ? void 0 : badgeTone
565
+ }, badgeDot ? h("span", {
566
+ className: "dvc-dot",
567
+ key: "dot",
568
+ "aria-hidden": true
569
+ }) : null, badgeText)] : null, wide ? null : badgeDot ? h("span", {
570
+ className: "dvc-rail-dot",
571
+ key: "dot",
572
+ "aria-hidden": true
573
+ }) : null));
574
+ }
575
+ const INTERVAL_MIN = 1;
576
+ const INTERVAL_MAX = 10080;
577
+ const CHANNEL_OPTIONS = [
578
+ "auto",
579
+ "latest",
580
+ "next",
581
+ "alpha"
582
+ ];
583
+ const DEFAULT_VISIBLE_ROLLBACK_VERSIONS = 5;
584
+ const ROLLBACK_POPOVER_WIDTH = 320;
585
+ const ROLLBACK_POPOVER_HEIGHT = 360;
586
+ function VersionCheckSettings({ t, store }) {
587
+ const view = React.useSyncExternalStore(store.subscribe, store.getView);
588
+ const snapshot = view.snapshot;
589
+ const settings = snapshot === null || snapshot.settings === void 0 ? null : snapshot.settings;
590
+ const [intervalDraft, setIntervalDraft] = React.useState("");
591
+ const [intervalError, setIntervalError] = React.useState(false);
592
+ const [saved, setSaved] = React.useState(false);
593
+ const savedTimer = React.useRef(null);
594
+ const intervalValue = settings === null ? null : settings.checkIntervalMinutes;
595
+ React.useEffect(() => {
596
+ if (intervalValue === null) return;
597
+ setIntervalDraft(String(intervalValue));
598
+ setIntervalError(false);
599
+ }, [intervalValue]);
600
+ React.useEffect(() => () => {
601
+ if (savedTimer.current !== null) {
602
+ clearTimeout(savedTimer.current);
603
+ savedTimer.current = null;
604
+ }
605
+ }, []);
606
+ const busy = view.busy === "preferences";
607
+ const controlsDisabled = settings === null || view.busy !== null;
608
+ const markSaved = () => {
609
+ setSaved(true);
610
+ if (savedTimer.current !== null) clearTimeout(savedTimer.current);
611
+ savedTimer.current = setTimeout(() => {
612
+ savedTimer.current = null;
613
+ setSaved(false);
614
+ }, 2e3);
615
+ };
616
+ const save = async (patch, silent = false) => {
617
+ setSaved(false);
618
+ await store.savePreferences(patch);
619
+ const next = store.getView();
620
+ if (!silent && next.actionError === null && next.busy === null) markSaved();
621
+ };
622
+ const toggleCheck = () => {
623
+ if (settings === null || controlsDisabled) return;
624
+ save({ checkEnabled: !settings.checkEnabled }, true);
625
+ };
626
+ const commitInterval = () => {
627
+ if (settings === null || controlsDisabled) return;
628
+ const text = intervalDraft.trim();
629
+ const value = Number(text);
630
+ if (!/^\d+$/.test(text) || value < INTERVAL_MIN || value > INTERVAL_MAX) {
631
+ setIntervalError(true);
632
+ return;
633
+ }
634
+ setIntervalError(false);
635
+ if (value === settings.checkIntervalMinutes) return;
636
+ save({ checkIntervalMinutes: value });
637
+ };
638
+ const pickChannel = (event) => {
639
+ if (settings === null || controlsDisabled) return;
640
+ save({ channel: event.currentTarget.value });
641
+ };
642
+ let feedback = "";
643
+ let feedbackTone;
644
+ if (view.actionError !== null && view.lastAction === "preferences") {
645
+ feedback = view.actionError;
646
+ feedbackTone = "error";
647
+ } else if (busy) feedback = t("settings.save.saving");
648
+ else if (saved) feedback = t("settings.save.saved");
649
+ const rows = [];
650
+ if (settings === null) rows.push(h("div", {
651
+ className: "dvc-settings-row",
652
+ key: "loading"
653
+ }, h("span", { className: "dvc-settings-rowDesc" }, snapshot === null ? t("settings.loading") : t("settings.unavailable"))));
654
+ else {
655
+ rows.push(h("div", {
656
+ className: "dvc-settings-row",
657
+ key: "check"
658
+ }, h("div", { className: "dvc-settings-rowText" }, h("span", { className: "dvc-settings-rowTitle" }, t("settings.check.title")), h("span", { className: "dvc-settings-rowDesc" }, t("settings.check.desc"))), h("button", {
659
+ type: "button",
660
+ role: "switch",
661
+ className: "dvc-switch",
662
+ "aria-checked": settings.checkEnabled ? "true" : "false",
663
+ "aria-label": t("settings.check.title"),
664
+ disabled: controlsDisabled,
665
+ onClick: toggleCheck
666
+ }, h("span", { className: "dvc-switchThumb" }))));
667
+ rows.push(h("div", {
668
+ className: "dvc-settings-row",
669
+ key: "interval"
670
+ }, h("div", { className: "dvc-settings-rowText" }, h("span", { className: "dvc-settings-rowTitle" }, t("settings.interval.title")), h("span", { className: "dvc-settings-rowDesc" }, t("settings.interval.desc"))), h("div", { className: "dvc-settings-control" }, h("input", {
671
+ className: "dvc-settings-input",
672
+ type: "number",
673
+ inputMode: "numeric",
674
+ min: INTERVAL_MIN,
675
+ max: INTERVAL_MAX,
676
+ step: 1,
677
+ value: intervalDraft,
678
+ "aria-label": t("settings.interval.title"),
679
+ "aria-invalid": intervalError ? "true" : void 0,
680
+ disabled: controlsDisabled,
681
+ onChange: (event) => {
682
+ setIntervalError(false);
683
+ setIntervalDraft(event.currentTarget.value);
684
+ },
685
+ onBlur: commitInterval,
686
+ onKeyDown: (event) => {
687
+ if (event.key !== "Enter") return;
688
+ event.preventDefault();
689
+ commitInterval();
690
+ }
691
+ }), h("span", { className: "dvc-settings-unit" }, t("settings.interval.unit")))));
692
+ if (intervalError) rows.push(h("p", {
693
+ className: "dvc-settings-feedback",
694
+ key: "interval-error",
695
+ role: "alert",
696
+ "data-tone": "error"
697
+ }, t("settings.interval.invalid")));
698
+ const channels = CHANNEL_OPTIONS.includes(settings.channel) ? CHANNEL_OPTIONS : [...CHANNEL_OPTIONS, settings.channel];
699
+ rows.push(h("div", {
700
+ className: "dvc-settings-row",
701
+ key: "channel"
702
+ }, h("div", { className: "dvc-settings-rowText" }, h("span", { className: "dvc-settings-rowTitle" }, t("settings.channel.title")), h("span", { className: "dvc-settings-rowDesc" }, t("settings.channel.desc"))), h("select", {
703
+ className: "dvc-settings-select",
704
+ value: settings.channel,
705
+ "aria-label": t("settings.channel.title"),
706
+ disabled: controlsDisabled,
707
+ onChange: pickChannel
708
+ }, channels.map((channel) => h("option", {
709
+ value: channel,
710
+ key: channel
711
+ }, channel === "auto" ? t("settings.channel.auto") : channel)))));
712
+ }
713
+ return h("div", { className: "dvc-settings" }, h("p", { className: "dvc-settings-blurb" }, t("settings.description")), rows, feedback === "" ? null : h("p", {
714
+ className: "dvc-settings-feedback",
715
+ key: "feedback",
716
+ role: "status",
717
+ "data-tone": feedbackTone === void 0 ? void 0 : feedbackTone
718
+ }, feedback), h(RollbackSection, {
719
+ t,
720
+ store,
721
+ key: "rollback"
722
+ }));
723
+ }
724
+ /** 把 ISO 时间格式化为本地年月日。 */
725
+ function formatPublishDate(value) {
726
+ if (value === null) return "";
727
+ const date = new Date(value);
728
+ if (Number.isNaN(date.getTime())) return "";
729
+ const month = String(date.getMonth() + 1).padStart(2, "0");
730
+ const day = String(date.getDate()).padStart(2, "0");
731
+ return `${date.getFullYear()}-${month}-${day}`;
732
+ }
733
+ function RollbackSection({ t, store }) {
734
+ const view = React.useSyncExternalStore(store.subscribe, store.getView);
735
+ const snapshot = view.snapshot;
736
+ const bootVersion = snapshot === null ? null : snapshot.bootVersion;
737
+ const installedVersion = snapshot === null ? null : snapshot.installedVersion;
738
+ const phase = snapshot === null ? null : snapshot.phase;
739
+ const target = snapshot === null ? null : snapshot.target;
740
+ const [open, setOpen] = React.useState(false);
741
+ const [anchor, setAnchor] = React.useState(null);
742
+ const triggerRef = React.useRef(null);
743
+ const popoverRef = React.useRef(null);
744
+ const [status, setStatus] = React.useState("loading");
745
+ const [versions, setVersions] = React.useState([]);
746
+ const [loadError, setLoadError] = React.useState(null);
747
+ const [pending, setPending] = React.useState(null);
748
+ const [reloadToken, setReloadToken] = React.useState(0);
749
+ const [expanded, setExpanded] = React.useState(false);
750
+ React.useEffect(() => {
751
+ if (!open) return;
752
+ let cancelled = false;
753
+ setStatus("loading");
754
+ setLoadError(null);
755
+ setPending(null);
756
+ setExpanded(false);
757
+ (async () => {
758
+ try {
759
+ const response = await fetch(VERSIONS_PATH, {
760
+ method: "GET",
761
+ headers: { accept: "application/json" },
762
+ cache: "no-store"
763
+ });
764
+ const text = await response.text();
765
+ const payload = text === "" ? null : JSON.parse(text);
766
+ if (!response.ok) {
767
+ const failure = payload;
768
+ const message = failure !== null && failure.error !== void 0 && failure.error !== null ? failure.error.message : `GET ${VERSIONS_PATH} 返回 HTTP ${response.status}`;
769
+ throw new Error(message);
770
+ }
771
+ if (cancelled) return;
772
+ const parsed = payload;
773
+ setVersions(Array.isArray(parsed.versions) ? parsed.versions : []);
774
+ setStatus("ready");
775
+ } catch (failure) {
776
+ if (cancelled) return;
777
+ setLoadError(describeError(failure));
778
+ setStatus("error");
779
+ }
780
+ })();
781
+ return () => {
782
+ cancelled = true;
783
+ };
784
+ }, [open, reloadToken]);
785
+ React.useLayoutEffect(() => {
786
+ if (!open) return;
787
+ const place = () => {
788
+ const trigger = triggerRef.current;
789
+ if (trigger === null) return;
790
+ const rect = trigger.getBoundingClientRect();
791
+ const left = Math.max(8, Math.min(rect.right - ROLLBACK_POPOVER_WIDTH, window.innerWidth - ROLLBACK_POPOVER_WIDTH - 8));
792
+ const below = rect.bottom + 6;
793
+ const top = below + ROLLBACK_POPOVER_HEIGHT <= window.innerHeight - 8 ? below : Math.max(8, rect.top - ROLLBACK_POPOVER_HEIGHT - 6);
794
+ setAnchor({
795
+ top,
796
+ left
797
+ });
798
+ };
799
+ place();
800
+ window.addEventListener("resize", place);
801
+ document.addEventListener("scroll", place, true);
802
+ return () => {
803
+ window.removeEventListener("resize", place);
804
+ document.removeEventListener("scroll", place, true);
805
+ };
806
+ }, [open]);
807
+ React.useEffect(() => {
808
+ if (!open) return;
809
+ const onPointerDown = (event) => {
810
+ const targetNode = event.target;
811
+ if (targetNode === null) return;
812
+ if (popoverRef.current !== null && popoverRef.current.contains(targetNode)) return;
813
+ if (triggerRef.current !== null && triggerRef.current.contains(targetNode)) return;
814
+ setOpen(false);
815
+ };
816
+ const onKeyDown = (event) => {
817
+ if (event.key === "Escape") setOpen(false);
818
+ };
819
+ document.addEventListener("pointerdown", onPointerDown);
820
+ document.addEventListener("keydown", onKeyDown);
821
+ return () => {
822
+ document.removeEventListener("pointerdown", onPointerDown);
823
+ document.removeEventListener("keydown", onKeyDown);
824
+ };
825
+ }, [open]);
826
+ const busy = view.busy !== null || phase === "installing" || phase === "restarting";
827
+ const hostError = snapshot === null || snapshot.lastError === null ? null : snapshot.lastError.message;
828
+ const actionError = view.actionError !== null && view.lastAction === "rollback" ? view.actionError : null;
829
+ let progressText = null;
830
+ if (phase === "installing") progressText = t("settings.rollback.installing", { version: target === null ? "" : target });
831
+ else if (phase === "restarting") progressText = t("settings.rollback.restarting");
832
+ else if (phase === "restart-required" && installedVersion !== bootVersion) progressText = t("settings.rollback.restartRequired", { version: installedVersion === null ? "" : installedVersion });
833
+ const visibleVersions = expanded ? versions : versions.slice(0, DEFAULT_VISIBLE_ROLLBACK_VERSIONS);
834
+ const content = [];
835
+ if (status === "loading") content.push(h("p", {
836
+ className: "dvc-settings-feedback",
837
+ key: "loading"
838
+ }, t("settings.rollback.loading")));
839
+ else if (status === "error") {
840
+ content.push(h("p", {
841
+ className: "dvc-settings-feedback",
842
+ key: "load-error",
843
+ role: "alert",
844
+ "data-tone": "error"
845
+ }, t("settings.rollback.error", { message: loadError === null ? "" : loadError })));
846
+ content.push(h("button", {
847
+ type: "button",
848
+ className: "dvc-button",
849
+ key: "retry",
850
+ disabled: busy,
851
+ onClick: () => {
852
+ setReloadToken((value) => value + 1);
853
+ }
854
+ }, t("settings.rollback.retry")));
855
+ } else if (versions.length === 0) content.push(h("p", {
856
+ className: "dvc-settings-feedback",
857
+ key: "empty"
858
+ }, t("settings.rollback.empty")));
859
+ else if (pending !== null) {
860
+ content.push(h("p", {
861
+ className: "dvc-rollback-confirmText",
862
+ key: "confirm-text"
863
+ }, t("settings.rollback.confirm", { version: pending })));
864
+ content.push(h("div", {
865
+ className: "dvc-rollback-confirmActions",
866
+ key: "confirm-actions"
867
+ }, h("button", {
868
+ type: "button",
869
+ className: "dvc-button",
870
+ onClick: () => {
871
+ setPending(null);
872
+ }
873
+ }, t("settings.rollback.cancel")), h("button", {
874
+ type: "button",
875
+ className: "dvc-button",
876
+ "data-primary": true,
877
+ disabled: busy,
878
+ onClick: () => {
879
+ const version = pending;
880
+ setPending(null);
881
+ setOpen(false);
882
+ store.rollbackTo(version);
883
+ }
884
+ }, t("settings.rollback.confirmAction"))));
885
+ } else content.push(h("div", {
886
+ className: "dvc-rollback-list",
887
+ key: "list"
888
+ }, visibleVersions.map((entry) => h("div", {
889
+ className: "dvc-rollback-row",
890
+ key: entry.version
891
+ }, h("span", { className: "dvc-rollback-version" }, entry.version), h("span", { className: "dvc-rollback-date" }, formatPublishDate(entry.publishedAt)), h("button", {
892
+ type: "button",
893
+ className: "dvc-button",
894
+ disabled: busy,
895
+ onClick: () => {
896
+ setPending(entry.version);
897
+ }
898
+ }, t("settings.rollback.action"))))));
899
+ const toggle = status === "ready" && pending === null && versions.length > DEFAULT_VISIBLE_ROLLBACK_VERSIONS ? h("button", {
900
+ type: "button",
901
+ className: "dvc-button dvc-rollback-toggle",
902
+ key: "toggle",
903
+ disabled: busy,
904
+ onClick: () => {
905
+ setExpanded((value) => !value);
906
+ }
907
+ }, expanded ? t("settings.rollback.collapse") : t("settings.rollback.expand", { count: versions.length })) : null;
908
+ const notices = [];
909
+ if (actionError !== null) notices.push(h("p", {
910
+ className: "dvc-error",
911
+ key: "action-error",
912
+ role: "alert"
913
+ }, actionError));
914
+ else if (hostError !== null && phase === "error") notices.push(h("p", {
915
+ className: "dvc-error",
916
+ key: "host-error",
917
+ role: "alert"
918
+ }, hostError));
919
+ if (progressText !== null) notices.push(h("p", {
920
+ className: "dvc-note",
921
+ key: "progress"
922
+ }, progressText));
923
+ const popover = open && anchor !== null ? h("section", {
924
+ key: "popover",
925
+ className: "dvc-rollback-popover",
926
+ style: anchor,
927
+ role: "dialog",
928
+ "aria-label": t("settings.rollback.pickTitle"),
929
+ ref: popoverRef
930
+ }, h("header", { className: "dvc-rollback-popoverHeader" }, h("span", { className: "dvc-rollback-popoverTitle" }, t("settings.rollback.pickTitle"))), h("div", { className: "dvc-rollback-popoverBody" }, content), toggle === null ? null : h("div", { className: "dvc-rollback-popoverFooter" }, toggle)) : null;
931
+ return h("div", { className: "dvc-settings-section" }, h("div", { className: "dvc-rollback-header" }, h("span", { className: "dvc-settings-rowTitle" }, t("settings.rollback.title")), h("div", { className: "dvc-rollback-picker" }, h("button", {
932
+ ref: triggerRef,
933
+ type: "button",
934
+ className: "dvc-button dvc-rollback-pick",
935
+ "aria-haspopup": "dialog",
936
+ "aria-expanded": open,
937
+ disabled: busy,
938
+ onClick: () => {
939
+ setOpen((value) => !value);
940
+ }
941
+ }, t("settings.rollback.pick")), popover)), h("p", { className: "dvc-settings-rowDesc" }, t("settings.rollback.desc")), notices, h("p", { className: "dvc-note" }, t("panel.note.registry")));
942
+ }
943
+ function apply(ctx) {
944
+ ctx.effect(() => ctx.locale.register(NS, {
945
+ zh,
946
+ en
947
+ }), "dsh-version-check: dictionaries");
948
+ const t = ctx.locale.bind(NS);
949
+ const store = createVersionStore();
950
+ ctx.effect(() => {
951
+ store.start();
952
+ return () => {
953
+ store.stop();
954
+ };
955
+ }, "dsh-version-check: state polling");
956
+ ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
957
+ name: "sidebar.footer.action",
958
+ id: "dsh-version-check",
959
+ order: 1e3,
960
+ locale: NS,
961
+ inject: () => ({ store })
962
+ }, VersionBadge));
963
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
964
+ name: "settings.section",
965
+ id: "dsh-version-check",
966
+ order: 55,
967
+ label: () => t("settings.nav"),
968
+ locale: NS,
969
+ inject: () => ({ store })
970
+ }, VersionCheckSettings));
971
+ }
972
+ return {
973
+ inject,
974
+ apply
975
+ };
976
+ }
977
+ });