@wjm200509/dsh-plugin-version-watch 0.0.0-stage → 0.1.3
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 -0
- package/README.md +48 -3
- package/icon.svg +6 -0
- package/lib/client.js +613 -0
- package/lib/index.js +657 -0
- package/lib/lockfile.js +100 -0
- package/lib/spec.js +175 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +52 -4
- package/patch.yml +6 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-plugin-version-watch",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
const React = react;
|
|
9
|
+
|
|
10
|
+
/** Stable Cordis plugin name. */
|
|
11
|
+
const name = "version-watch";
|
|
12
|
+
/** The Settings seat and the locale service this page contributes to. */
|
|
13
|
+
const inject = ["slots", "locale"];
|
|
14
|
+
/** The Settings section this page occupies. */
|
|
15
|
+
const SECTION_ID = "plugin-updates";
|
|
16
|
+
/** Locale namespace of the page copy. */
|
|
17
|
+
const NS = "pluginVersionWatch";
|
|
18
|
+
/** How often an open page re-reads the Host check. */
|
|
19
|
+
const AUTO_CHECK_MS = 15 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
/** Host routes registered by this bundle's Host half. */
|
|
22
|
+
const STATUS_ROUTE = "api/version-watch/status";
|
|
23
|
+
const UPDATE_ROUTE = "api/version-watch/update";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Page stylesheet, themed through the shell's alias tokens. The card and
|
|
27
|
+
* row geometry follows the Settings pages the shell ships.
|
|
28
|
+
*/
|
|
29
|
+
const CSS = [
|
|
30
|
+
".dsh-vw-page{max-width:720px;display:flex;flex-direction:column;gap:16px;color:var(--dsw-alias-label-primary)}",
|
|
31
|
+
".dsh-vw-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;flex-wrap:wrap}",
|
|
32
|
+
".dsh-vw-actions{display:flex;align-items:center;gap:8px;flex:none}",
|
|
33
|
+
".dsh-vw-title{margin:0;font-size:16px;font-weight:500;line-height:24px}",
|
|
34
|
+
".dsh-vw-intro{margin:4px 0 0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}",
|
|
35
|
+
".dsh-vw-btn{box-sizing:border-box;height:32px;padding:0 14px;border-radius:8px;font:inherit;font-size:13px;line-height:1.5;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:6px;border:.5px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);transition:background .15s,border-color .15s;white-space:nowrap}",
|
|
36
|
+
".dsh-vw-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}",
|
|
37
|
+
".dsh-vw-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}",
|
|
38
|
+
".dsh-vw-btn:disabled{opacity:.5;cursor:default}",
|
|
39
|
+
".dsh-vw-btn.primary{border-color:transparent;background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary));color:var(--dsw-alias-label-primary-foreground,var(--dsw-alias-bg-overlay))}",
|
|
40
|
+
".dsh-vw-btn.primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary)))}",
|
|
41
|
+
".dsh-vw-meta{display:flex;flex-wrap:wrap;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
42
|
+
".dsh-vw-card{border:.5px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);display:flex;flex-direction:column;overflow:hidden}",
|
|
43
|
+
".dsh-vw-row{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:14px 16px}",
|
|
44
|
+
".dsh-vw-row+.dsh-vw-row{border-top:.5px solid var(--dsw-alias-border-l1)}",
|
|
45
|
+
".dsh-vw-info{min-width:0;display:flex;flex-direction:column;gap:4px}",
|
|
46
|
+
".dsh-vw-rowtitle{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
47
|
+
".dsh-vw-name{font-size:14px;font-weight:500;line-height:22px;word-break:break-all}",
|
|
48
|
+
".dsh-vw-version{font-size:12px;line-height:18px;padding:0 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-mono,ui-monospace,monospace)}",
|
|
49
|
+
".dsh-vw-off{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}",
|
|
50
|
+
".dsh-vw-detail{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);word-break:break-word}",
|
|
51
|
+
".dsh-vw-source{color:var(--dsw-alias-label-tertiary)}",
|
|
52
|
+
".dsh-vw-state{font-size:12px;line-height:18px}",
|
|
53
|
+
".dsh-vw-state.available{color:var(--dsw-alias-state-warn-primary)}",
|
|
54
|
+
".dsh-vw-state.current{color:var(--dsw-alias-state-success-primary)}",
|
|
55
|
+
".dsh-vw-state.failed{color:var(--dsw-alias-state-error-primary)}",
|
|
56
|
+
".dsh-vw-state.unsupported,.dsh-vw-state.unknown{color:var(--dsw-alias-label-tertiary)}",
|
|
57
|
+
".dsh-vw-result{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap}",
|
|
58
|
+
".dsh-vw-result.ok{color:var(--dsw-alias-state-success-primary)}",
|
|
59
|
+
".dsh-vw-result.error{color:var(--dsw-alias-state-error-primary)}",
|
|
60
|
+
".dsh-vw-action{flex:none;display:flex;align-items:center;padding-top:1px}",
|
|
61
|
+
".dsh-vw-empty{border:.5px dashed var(--dsw-alias-border-l2);border-radius:12px;padding:24px 16px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}",
|
|
62
|
+
".dsh-vw-error{border:.5px solid var(--dsw-alias-state-error-primary);border-radius:8px;padding:8px 12px;font-size:12px;line-height:18px;color:var(--dsw-alias-state-error-primary);word-break:break-word}",
|
|
63
|
+
".dsh-vw-icon{display:block;flex:none}",
|
|
64
|
+
].join("");
|
|
65
|
+
|
|
66
|
+
/** Page copy in the two baseline locales. */
|
|
67
|
+
const DICT = {
|
|
68
|
+
zh: {
|
|
69
|
+
navLabel: "插件更新",
|
|
70
|
+
title: "插件更新",
|
|
71
|
+
intro: "监控已安装插件的版本:Git 来源比较远端最新提交,npm 来源检查新版本;点击“更新”即热重载,通常无需重启。",
|
|
72
|
+
refresh: "检查更新",
|
|
73
|
+
refreshing: "检查中…",
|
|
74
|
+
autoNote: "打开本页时自动检查,并每 15 分钟复查一次。",
|
|
75
|
+
lastChecked: "上次检查",
|
|
76
|
+
summaryLoading: "正在检查已安装插件…",
|
|
77
|
+
summaryAvailable: "个插件有可用更新",
|
|
78
|
+
summaryCurrent: "全部为最新",
|
|
79
|
+
summaryFailed: "个检查失败",
|
|
80
|
+
emptyTitle: "没有可监控的插件",
|
|
81
|
+
emptyBody: "当前 profile 没有已安装的插件依赖。",
|
|
82
|
+
notProfile: "未能读取 profile 目录,无法比较 Git 提交。",
|
|
83
|
+
enabled: "已启用",
|
|
84
|
+
disabled: "已停用",
|
|
85
|
+
sourceRegistry: "npm 注册表",
|
|
86
|
+
sourceGit: "Git 仓库",
|
|
87
|
+
sourceLocal: "本地目录",
|
|
88
|
+
sourceTarball: "压缩包",
|
|
89
|
+
sourceUnknown: "未知来源",
|
|
90
|
+
statusChecking: "检查中…",
|
|
91
|
+
statusCurrent: "已是最新",
|
|
92
|
+
statusAvailableGit: "有新提交",
|
|
93
|
+
statusAvailableRegistry: "有新版本",
|
|
94
|
+
statusUnknown: "暂时不可读",
|
|
95
|
+
statusFailed: "检查失败",
|
|
96
|
+
statusUnsupported: "不支持自动更新",
|
|
97
|
+
reasonNoCommit: "锁文件未记录提交",
|
|
98
|
+
reasonSemver: "按版本范围安装",
|
|
99
|
+
reasonNotUpdatable: "非注册表安装",
|
|
100
|
+
reasonUnavailable: "插件文件正在更新,请稍后",
|
|
101
|
+
installedLabel: "当前",
|
|
102
|
+
latestLabel: "最新",
|
|
103
|
+
commitLabel: "提交",
|
|
104
|
+
update: "更新",
|
|
105
|
+
updating: "更新中…",
|
|
106
|
+
updatedLive: "已更新,新版本已实时生效,无需重启。",
|
|
107
|
+
updatedRestart: "已更新,但需要重启 DSH 才能生效",
|
|
108
|
+
upToDate: "已是最新,无需更新。",
|
|
109
|
+
updateFailed: "更新失败",
|
|
110
|
+
updateUnsupported: "该插件的来源不支持自动更新。",
|
|
111
|
+
errInstallFailed: "旧版本已卸载,但重装失败;插件当前不可用,请到 Plugins 页面重新安装。",
|
|
112
|
+
errRemovalFailed: "卸载旧版本失败,插件保持原状。",
|
|
113
|
+
errRemote: "无法连接远端仓库,请检查网络后重试。",
|
|
114
|
+
errMissing: "没有找到该插件的依赖记录。",
|
|
115
|
+
errBusy: "已有更新正在进行,请稍后再试。",
|
|
116
|
+
loadFailed: "读取插件列表失败",
|
|
117
|
+
retry: "重试",
|
|
118
|
+
forceCheck: "全部重新检查",
|
|
119
|
+
},
|
|
120
|
+
en: {
|
|
121
|
+
navLabel: "Plugin updates",
|
|
122
|
+
title: "Plugin updates",
|
|
123
|
+
intro: "Monitors the versions of installed plugins: Git sources are compared with the newest remote commit, npm sources with published versions. Update reloads them live, usually without a restart.",
|
|
124
|
+
refresh: "Check for updates",
|
|
125
|
+
refreshing: "Checking…",
|
|
126
|
+
autoNote: "Checks when this page opens and again every 15 minutes.",
|
|
127
|
+
lastChecked: "Last check",
|
|
128
|
+
summaryLoading: "Checking installed plugins…",
|
|
129
|
+
summaryAvailable: "plugin(s) can be updated",
|
|
130
|
+
summaryCurrent: "Everything is up to date",
|
|
131
|
+
summaryFailed: "check(s) failed",
|
|
132
|
+
emptyTitle: "No plugins to watch",
|
|
133
|
+
emptyBody: "This profile has no installed plugin dependencies.",
|
|
134
|
+
notProfile: "The profile directory could not be read, so Git commits cannot be compared.",
|
|
135
|
+
enabled: "Enabled",
|
|
136
|
+
disabled: "Disabled",
|
|
137
|
+
sourceRegistry: "npm registry",
|
|
138
|
+
sourceGit: "Git repository",
|
|
139
|
+
sourceLocal: "Local directory",
|
|
140
|
+
sourceTarball: "Tarball",
|
|
141
|
+
sourceUnknown: "Unknown source",
|
|
142
|
+
statusChecking: "Checking…",
|
|
143
|
+
statusCurrent: "Up to date",
|
|
144
|
+
statusAvailableGit: "New commits",
|
|
145
|
+
statusAvailableRegistry: "New version",
|
|
146
|
+
statusUnknown: "Temporarily unavailable",
|
|
147
|
+
statusFailed: "Check failed",
|
|
148
|
+
statusUnsupported: "Not auto-updatable",
|
|
149
|
+
reasonNoCommit: "the lockfile records no commit",
|
|
150
|
+
reasonSemver: "installed from a version range",
|
|
151
|
+
reasonNotUpdatable: "not installed from a registry",
|
|
152
|
+
reasonUnavailable: "plugin files are being updated, retry shortly",
|
|
153
|
+
installedLabel: "Current",
|
|
154
|
+
latestLabel: "Latest",
|
|
155
|
+
commitLabel: "Commit",
|
|
156
|
+
update: "Update",
|
|
157
|
+
updating: "Updating…",
|
|
158
|
+
updatedLive: "Updated and live — no restart needed.",
|
|
159
|
+
updatedRestart: "Updated, but DSH must restart to apply it",
|
|
160
|
+
upToDate: "Already up to date.",
|
|
161
|
+
updateFailed: "Update failed",
|
|
162
|
+
updateUnsupported: "This plugin's source cannot be updated automatically.",
|
|
163
|
+
errInstallFailed: "The old version was removed but the reinstall failed; the plugin is unavailable now. Reinstall it from the Plugins page.",
|
|
164
|
+
errRemovalFailed: "Removing the old version failed; the plugin was left as it was.",
|
|
165
|
+
errRemote: "The remote repository could not be reached. Check the network and retry.",
|
|
166
|
+
errMissing: "No dependency record was found for this plugin.",
|
|
167
|
+
errBusy: "Another update is already running; try again shortly.",
|
|
168
|
+
loadFailed: "Could not read the plugin list",
|
|
169
|
+
retry: "Retry",
|
|
170
|
+
forceCheck: "Re-check everything",
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Shorten a commit for display.
|
|
176
|
+
* @param {string | null | undefined} commit - object name.
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
function shortCommit(commit) {
|
|
180
|
+
return typeof commit === "string" && /^[0-9a-f]{40}/i.test(commit) ? commit.slice(0, 7) : (commit ?? "—");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The localized display title of one installed plugin.
|
|
185
|
+
* @param {object} plugin - Host row.
|
|
186
|
+
* @param {(text: unknown) => string} resolveText - locale text resolver.
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
function pluginTitle(plugin, resolveText) {
|
|
190
|
+
const title = plugin?.title;
|
|
191
|
+
if (typeof title === "string" && title !== "") return title;
|
|
192
|
+
if (title !== null && typeof title === "object") {
|
|
193
|
+
const resolved = resolveText(title);
|
|
194
|
+
if (typeof resolved === "string" && resolved !== "") return resolved;
|
|
195
|
+
}
|
|
196
|
+
return plugin?.name ?? "—";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The source label for one plugin.
|
|
201
|
+
* @param {object} plugin - Host row.
|
|
202
|
+
* @param {(key: string) => string} t - translate.
|
|
203
|
+
* @returns {string}
|
|
204
|
+
*/
|
|
205
|
+
function sourceLabel(plugin, t) {
|
|
206
|
+
switch (plugin?.kind) {
|
|
207
|
+
case "registry": return t("sourceRegistry");
|
|
208
|
+
case "git": return t("sourceGit");
|
|
209
|
+
case "local": return t("sourceLocal");
|
|
210
|
+
case "tarball": return t("sourceTarball");
|
|
211
|
+
default: return t("sourceUnknown");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The check state line of one plugin.
|
|
217
|
+
* @param {object} plugin - Host row.
|
|
218
|
+
* @param {(key: string) => string} t - translate.
|
|
219
|
+
* @returns {string}
|
|
220
|
+
*/
|
|
221
|
+
function stateText(plugin, t) {
|
|
222
|
+
const check = plugin?.check ?? {};
|
|
223
|
+
if (check.status === "current") return t("statusCurrent");
|
|
224
|
+
if (check.status === "available") return plugin?.kind === "registry" ? t("statusAvailableRegistry") : t("statusAvailableGit");
|
|
225
|
+
if (check.status === "unknown") {
|
|
226
|
+
const reason = check.reason === "no-installed-commit"
|
|
227
|
+
? t("reasonNoCommit")
|
|
228
|
+
: check.reason === "unavailable"
|
|
229
|
+
? t("reasonUnavailable")
|
|
230
|
+
: "";
|
|
231
|
+
return reason === "" ? t("statusUnknown") : `${t("statusUnknown")} · ${reason}`;
|
|
232
|
+
}
|
|
233
|
+
if (check.status === "failed") return check.reason ? `${t("statusFailed")} · ${check.reason}` : t("statusFailed");
|
|
234
|
+
if (check.status === "unsupported") {
|
|
235
|
+
const reason = check.reason === "semver-range" ? t("reasonSemver") : check.reason === "not-updatable" ? t("reasonNotUpdatable") : "";
|
|
236
|
+
return reason === "" ? t("statusUnsupported") : `${t("statusUnsupported")} · ${reason}`;
|
|
237
|
+
}
|
|
238
|
+
return t("statusChecking");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The version/commit detail line of one plugin.
|
|
243
|
+
* @param {object} plugin - Host row.
|
|
244
|
+
* @param {(key: string) => string} t - translate.
|
|
245
|
+
* @returns {string}
|
|
246
|
+
*/
|
|
247
|
+
function detailText(plugin, t) {
|
|
248
|
+
const check = plugin?.check ?? {};
|
|
249
|
+
if (plugin?.kind === "registry") {
|
|
250
|
+
const current = plugin?.version ?? "—";
|
|
251
|
+
const latest = check.latestVersion ?? "—";
|
|
252
|
+
return `${t("installedLabel")} ${current} · ${t("latestLabel")} ${latest}`;
|
|
253
|
+
}
|
|
254
|
+
const parts = [];
|
|
255
|
+
if (check.installedCommit !== null && check.installedCommit !== undefined) {
|
|
256
|
+
parts.push(`${t("commitLabel")} ${shortCommit(check.installedCommit)}`);
|
|
257
|
+
}
|
|
258
|
+
if (check.remoteCommit !== null && check.remoteCommit !== undefined && check.remoteCommit !== check.installedCommit) {
|
|
259
|
+
parts.push(`→ ${shortCommit(check.remoteCommit)}`);
|
|
260
|
+
}
|
|
261
|
+
if (check.remoteVersion !== null && check.remoteVersion !== undefined && check.remoteVersion !== plugin?.version) {
|
|
262
|
+
parts.push(`${t("latestLabel")} ${check.remoteVersion}`);
|
|
263
|
+
}
|
|
264
|
+
if (parts.length === 0) parts.push(sourceLabel(plugin, t));
|
|
265
|
+
return parts.join(" · ");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* One line summarizing the loaded page.
|
|
270
|
+
* @param {{ plugins?: object[] } | null} data - status response.
|
|
271
|
+
* @param {(key: string) => string} t - translate.
|
|
272
|
+
* @returns {string}
|
|
273
|
+
*/
|
|
274
|
+
function summaryOf(data, t) {
|
|
275
|
+
const plugins = data?.plugins ?? [];
|
|
276
|
+
const available = plugins.filter((plugin) => plugin?.check?.status === "available").length;
|
|
277
|
+
const failed = plugins.filter((plugin) => plugin?.check?.status === "failed").length;
|
|
278
|
+
const parts = [];
|
|
279
|
+
if (plugins.length === 0) return t("summaryCurrent");
|
|
280
|
+
if (available > 0) parts.push(`${available} ${t("summaryAvailable")}`);
|
|
281
|
+
else parts.push(t("summaryCurrent"));
|
|
282
|
+
if (failed > 0) parts.push(`${failed} ${t("summaryFailed")}`);
|
|
283
|
+
return parts.join(" · ");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The inline outcome of one row's last update attempt.
|
|
288
|
+
* @param {{ phase: string, answer?: any, error?: string } | undefined} entry - result.
|
|
289
|
+
* @param {(key: string) => string} t - translate.
|
|
290
|
+
* @returns {{ text: string, tone: "ok" | "error" | null } | null}
|
|
291
|
+
*/
|
|
292
|
+
function resultText(entry, t) {
|
|
293
|
+
if (entry === undefined || entry === null) return null;
|
|
294
|
+
if (entry.phase === "updating") return { text: t("updating"), tone: null };
|
|
295
|
+
if (entry.phase === "failed") return { text: `${t("updateFailed")} · ${entry.error ?? ""}`, tone: "error" };
|
|
296
|
+
const answer = entry.answer;
|
|
297
|
+
if (answer === null || typeof answer !== "object") return null;
|
|
298
|
+
if (answer.ok === true) {
|
|
299
|
+
if (answer.status === "updated") return { text: t("updatedLive"), tone: "ok" };
|
|
300
|
+
if (answer.status === "restart-required") {
|
|
301
|
+
const reason = typeof answer.restartReason === "string" && answer.restartReason !== "" ? ` (${answer.restartReason})` : "";
|
|
302
|
+
return { text: `${t("updatedRestart")}${reason}.`, tone: "ok" };
|
|
303
|
+
}
|
|
304
|
+
return { text: t("upToDate"), tone: "ok" };
|
|
305
|
+
}
|
|
306
|
+
const code = answer.error?.code;
|
|
307
|
+
const detail = typeof answer.error?.detail === "string" && answer.error.detail !== "" ? ` · ${answer.error.detail}` : "";
|
|
308
|
+
if (code === "install-failed") {
|
|
309
|
+
const spec = typeof answer.source === "string" && answer.source !== "" ? ` (${answer.source})` : "";
|
|
310
|
+
return { text: `${t("errInstallFailed")}${spec}${detail}`, tone: "error" };
|
|
311
|
+
}
|
|
312
|
+
if (code === "removal-failed") return { text: `${t("errRemovalFailed")}${detail}`, tone: "error" };
|
|
313
|
+
if (code === "remote-unreachable") return { text: `${t("errRemote")}${detail}`, tone: "error" };
|
|
314
|
+
if (code === "no-dependency" || code === "no-bundle") return { text: t("errMissing"), tone: "error" };
|
|
315
|
+
if (code === "busy") return { text: t("errBusy"), tone: "error" };
|
|
316
|
+
if (code === "unsupported-source" || code === "semver-range") return { text: t("updateUnsupported"), tone: "error" };
|
|
317
|
+
return { text: `${t("updateFailed")}${detail}`, tone: "error" };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Fire one JSON request to this bundle's Host routes.
|
|
322
|
+
* @param {string} path - route path relative to the application base.
|
|
323
|
+
* @param {RequestInit} [init] - fetch options.
|
|
324
|
+
* @returns {Promise<any>}
|
|
325
|
+
*/
|
|
326
|
+
async function requestJson(path, init) {
|
|
327
|
+
const response = await fetch(new URL(path, document.baseURI), {
|
|
328
|
+
cache: "no-store",
|
|
329
|
+
credentials: "same-origin",
|
|
330
|
+
...init,
|
|
331
|
+
});
|
|
332
|
+
const body = await response.json().catch(() => null);
|
|
333
|
+
if (!response.ok) {
|
|
334
|
+
const detail = body?.error?.detail ?? body?.error?.code ?? `HTTP ${response.status}`;
|
|
335
|
+
throw new Error(String(detail));
|
|
336
|
+
}
|
|
337
|
+
return body;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Settings navigation glyph: a downward update arrow over a tray. */
|
|
341
|
+
function WatchIcon(props) {
|
|
342
|
+
const size = Number.isFinite(props?.size) && props.size > 0 ? props.size : 20;
|
|
343
|
+
const active = props?.active === true;
|
|
344
|
+
return React.createElement(
|
|
345
|
+
"svg",
|
|
346
|
+
{
|
|
347
|
+
className: "dsh-vw-icon",
|
|
348
|
+
width: size,
|
|
349
|
+
height: size,
|
|
350
|
+
viewBox: "0 0 24 24",
|
|
351
|
+
"aria-hidden": true,
|
|
352
|
+
style: { color: active ? "var(--dsw-alias-brand-primary)" : "var(--dsw-alias-label-secondary)" },
|
|
353
|
+
},
|
|
354
|
+
React.createElement("path", {
|
|
355
|
+
d: "M12 3.5v10.5",
|
|
356
|
+
fill: "none",
|
|
357
|
+
stroke: "currentColor",
|
|
358
|
+
strokeWidth: 2,
|
|
359
|
+
strokeLinecap: "round",
|
|
360
|
+
}),
|
|
361
|
+
React.createElement("path", {
|
|
362
|
+
d: "M7.5 10.5 12 15l4.5-4.5",
|
|
363
|
+
fill: "none",
|
|
364
|
+
stroke: "currentColor",
|
|
365
|
+
strokeWidth: 2,
|
|
366
|
+
strokeLinecap: "round",
|
|
367
|
+
strokeLinejoin: "round",
|
|
368
|
+
}),
|
|
369
|
+
React.createElement("path", {
|
|
370
|
+
d: "M4.5 18.5v1a1.5 1.5 0 0 0 1.5 1.5h12a1.5 1.5 0 0 0 1.5-1.5v-1",
|
|
371
|
+
fill: "none",
|
|
372
|
+
stroke: "currentColor",
|
|
373
|
+
strokeWidth: 2,
|
|
374
|
+
strokeLinecap: "round",
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Presentational page; the stateful wrapper below owns its data.
|
|
381
|
+
* @param {object} props - page props.
|
|
382
|
+
* @returns {object}
|
|
383
|
+
*/
|
|
384
|
+
function UpdatesView(props) {
|
|
385
|
+
const t = props.t;
|
|
386
|
+
const data = props.data;
|
|
387
|
+
const plugins = data?.plugins ?? [];
|
|
388
|
+
const checking = props.phase === "checking";
|
|
389
|
+
const busy = props.busy ?? null;
|
|
390
|
+
const rows = plugins.map((plugin) => {
|
|
391
|
+
const check = plugin.check ?? {};
|
|
392
|
+
const entry = props.results?.[plugin.name];
|
|
393
|
+
const outcome = resultText(entry, t);
|
|
394
|
+
const canUpdate = check.status === "available";
|
|
395
|
+
const updating = entry?.phase === "updating" || busy === plugin.name;
|
|
396
|
+
const target = plugin.kind === "registry"
|
|
397
|
+
? check.latestVersion
|
|
398
|
+
: typeof check.remoteVersion === "string" && check.remoteVersion !== plugin.version
|
|
399
|
+
? check.remoteVersion
|
|
400
|
+
: shortCommit(check.remoteCommit);
|
|
401
|
+
const button = React.createElement(
|
|
402
|
+
"button",
|
|
403
|
+
{
|
|
404
|
+
type: "button",
|
|
405
|
+
className: canUpdate ? "dsh-vw-btn primary" : "dsh-vw-btn",
|
|
406
|
+
disabled: !canUpdate || busy !== null || checking,
|
|
407
|
+
onClick: canUpdate ? () => props.onUpdate?.(plugin) : undefined,
|
|
408
|
+
},
|
|
409
|
+
updating ? t("updating") : canUpdate ? (target ? `${t("update")} ${target}` : t("update")) : stateText(plugin, t),
|
|
410
|
+
);
|
|
411
|
+
return React.createElement(
|
|
412
|
+
"div",
|
|
413
|
+
{ className: "dsh-vw-row", key: plugin.name },
|
|
414
|
+
React.createElement(
|
|
415
|
+
"div",
|
|
416
|
+
{ className: "dsh-vw-info" },
|
|
417
|
+
React.createElement(
|
|
418
|
+
"div",
|
|
419
|
+
{ className: "dsh-vw-rowtitle" },
|
|
420
|
+
React.createElement("span", { className: "dsh-vw-name" }, pluginTitle(plugin, props.resolveText ?? ((text) => text))),
|
|
421
|
+
React.createElement("span", { className: "dsh-vw-version" }, plugin.version ?? "—"),
|
|
422
|
+
typeof plugin.enabled === "boolean"
|
|
423
|
+
? React.createElement("span", { className: "dsh-vw-off" }, plugin.enabled ? t("enabled") : t("disabled"))
|
|
424
|
+
: null,
|
|
425
|
+
React.createElement("span", { className: `dsh-vw-state ${check.status ?? "unknown"}` }, stateText(plugin, t)),
|
|
426
|
+
),
|
|
427
|
+
React.createElement(
|
|
428
|
+
"div",
|
|
429
|
+
{ className: "dsh-vw-detail" },
|
|
430
|
+
React.createElement("span", { className: "dsh-vw-source" }, `${sourceLabel(plugin, t)} · ${plugin.source ?? plugin.name}`),
|
|
431
|
+
),
|
|
432
|
+
React.createElement("div", { className: "dsh-vw-detail" }, detailText(plugin, t)),
|
|
433
|
+
outcome === null
|
|
434
|
+
? null
|
|
435
|
+
: React.createElement("div", { className: `dsh-vw-result ${outcome.tone ?? ""}` }, outcome.text),
|
|
436
|
+
),
|
|
437
|
+
React.createElement("div", { className: "dsh-vw-action" }, button),
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const body = props.phase === "failed" && data === null
|
|
442
|
+
? React.createElement(
|
|
443
|
+
"div",
|
|
444
|
+
{ className: "dsh-vw-error" },
|
|
445
|
+
`${t("loadFailed")}: ${props.error ?? ""}`,
|
|
446
|
+
)
|
|
447
|
+
: rows.length === 0
|
|
448
|
+
? React.createElement(
|
|
449
|
+
"div",
|
|
450
|
+
{ className: "dsh-vw-empty" },
|
|
451
|
+
React.createElement("div", { className: "dsh-vw-name" }, t("emptyTitle")),
|
|
452
|
+
React.createElement("div", null, data === null ? t("summaryLoading") : t("emptyBody")),
|
|
453
|
+
)
|
|
454
|
+
: React.createElement("div", { className: "dsh-vw-card" }, rows);
|
|
455
|
+
|
|
456
|
+
const meta = [];
|
|
457
|
+
if (data === null) {
|
|
458
|
+
if (checking) meta.push(t("summaryLoading"));
|
|
459
|
+
} else {
|
|
460
|
+
if (checking) meta.push(t("summaryLoading"));
|
|
461
|
+
else meta.push(summaryOf(data, t));
|
|
462
|
+
}
|
|
463
|
+
if (data?.checkedAt) meta.push(`${t("lastChecked")} ${new Date(data.checkedAt).toLocaleTimeString()}`);
|
|
464
|
+
if (data?.profileKnown === false) meta.push(t("notProfile"));
|
|
465
|
+
meta.push(t("autoNote"));
|
|
466
|
+
|
|
467
|
+
return React.createElement(
|
|
468
|
+
"div",
|
|
469
|
+
{ className: "dsh-vw-page" },
|
|
470
|
+
React.createElement(
|
|
471
|
+
"div",
|
|
472
|
+
{ className: "dsh-vw-head" },
|
|
473
|
+
React.createElement(
|
|
474
|
+
"div",
|
|
475
|
+
null,
|
|
476
|
+
React.createElement("h1", { className: "dsh-vw-title" }, t("title")),
|
|
477
|
+
React.createElement("p", { className: "dsh-vw-intro" }, t("intro")),
|
|
478
|
+
),
|
|
479
|
+
React.createElement(
|
|
480
|
+
"div",
|
|
481
|
+
{ className: "dsh-vw-actions" },
|
|
482
|
+
React.createElement(
|
|
483
|
+
"button",
|
|
484
|
+
{
|
|
485
|
+
type: "button",
|
|
486
|
+
className: "dsh-vw-btn",
|
|
487
|
+
disabled: checking || busy !== null,
|
|
488
|
+
onClick: () => props.onRefresh?.(true),
|
|
489
|
+
},
|
|
490
|
+
checking ? t("refreshing") : t("forceCheck"),
|
|
491
|
+
),
|
|
492
|
+
),
|
|
493
|
+
),
|
|
494
|
+
React.createElement("div", { className: "dsh-vw-meta" }, meta.join(" · ")),
|
|
495
|
+
body,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Stateful Settings page: reads the Host check and performs updates.
|
|
501
|
+
* @param {{ t: (key: string) => string }} props - translate function from `apply`.
|
|
502
|
+
* @returns {object}
|
|
503
|
+
*/
|
|
504
|
+
function UpdatesPage(props) {
|
|
505
|
+
const t = props.t;
|
|
506
|
+
const [data, setData] = React.useState(null);
|
|
507
|
+
const [phase, setPhase] = React.useState("checking");
|
|
508
|
+
const [error, setError] = React.useState(null);
|
|
509
|
+
const [results, setResults] = React.useState({});
|
|
510
|
+
const [busy, setBusy] = React.useState(null);
|
|
511
|
+
const reads = React.useRef(0);
|
|
512
|
+
|
|
513
|
+
const load = React.useCallback(async (force) => {
|
|
514
|
+
const read = ++reads.current;
|
|
515
|
+
setPhase("checking");
|
|
516
|
+
try {
|
|
517
|
+
const answer = await requestJson(force ? `${STATUS_ROUTE}?refresh=1` : STATUS_ROUTE);
|
|
518
|
+
if (read !== reads.current) return;
|
|
519
|
+
setData(answer);
|
|
520
|
+
setError(null);
|
|
521
|
+
setPhase("ready");
|
|
522
|
+
} catch (failure) {
|
|
523
|
+
if (read !== reads.current) return;
|
|
524
|
+
setError(String(failure?.message ?? failure));
|
|
525
|
+
setPhase("failed");
|
|
526
|
+
}
|
|
527
|
+
}, []);
|
|
528
|
+
|
|
529
|
+
React.useEffect(() => {
|
|
530
|
+
load(false);
|
|
531
|
+
const timer = setInterval(() => load(false), AUTO_CHECK_MS);
|
|
532
|
+
return () => clearInterval(timer);
|
|
533
|
+
}, [load]);
|
|
534
|
+
|
|
535
|
+
const update = React.useCallback(async (plugin) => {
|
|
536
|
+
setBusy(plugin.name);
|
|
537
|
+
setResults((previous) => ({ ...previous, [plugin.name]: { phase: "updating" } }));
|
|
538
|
+
try {
|
|
539
|
+
const answer = await requestJson(UPDATE_ROUTE, {
|
|
540
|
+
method: "POST",
|
|
541
|
+
headers: { "content-type": "application/json" },
|
|
542
|
+
body: JSON.stringify({ name: plugin.name }),
|
|
543
|
+
});
|
|
544
|
+
setResults((previous) => ({ ...previous, [plugin.name]: { phase: "done", answer } }));
|
|
545
|
+
} catch (failure) {
|
|
546
|
+
setResults((previous) => ({
|
|
547
|
+
...previous,
|
|
548
|
+
[plugin.name]: { phase: "failed", error: String(failure?.message ?? failure) },
|
|
549
|
+
}));
|
|
550
|
+
} finally {
|
|
551
|
+
setBusy(null);
|
|
552
|
+
await load(true);
|
|
553
|
+
// A reinstall can still be recomposing when its answer arrives;
|
|
554
|
+
// read once more so the updated row is not left out of the list.
|
|
555
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
556
|
+
await load(true);
|
|
557
|
+
}
|
|
558
|
+
}, [load]);
|
|
559
|
+
|
|
560
|
+
return React.createElement(UpdatesView, {
|
|
561
|
+
t,
|
|
562
|
+
data,
|
|
563
|
+
phase,
|
|
564
|
+
error,
|
|
565
|
+
results,
|
|
566
|
+
busy,
|
|
567
|
+
onRefresh: load,
|
|
568
|
+
onUpdate: update,
|
|
569
|
+
resolveText: props.resolveText,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Register the Settings section and its navigation glyph.
|
|
575
|
+
* @param {object} ctx - Client plugin context.
|
|
576
|
+
*/
|
|
577
|
+
function apply(ctx) {
|
|
578
|
+
ctx.effect(() => ctx.locale.register(NS, DICT), "version-watch: dictionary");
|
|
579
|
+
const t = ctx.locale.bind(NS);
|
|
580
|
+
const resolveText = (text) => ctx.locale.resolveText(text);
|
|
581
|
+
ctx.effect(() => {
|
|
582
|
+
const tag = document.createElement("style");
|
|
583
|
+
tag.textContent = CSS;
|
|
584
|
+
document.head.append(tag);
|
|
585
|
+
return () => tag.remove();
|
|
586
|
+
}, "version-watch: styles");
|
|
587
|
+
ctx.slots.inject("settings.section", () =>
|
|
588
|
+
ctx.slots.register(
|
|
589
|
+
{ name: "settings.section", id: SECTION_ID, order: 35, label: () => t("navLabel") },
|
|
590
|
+
() => React.createElement(UpdatesPage, { t, resolveText }),
|
|
591
|
+
),
|
|
592
|
+
);
|
|
593
|
+
ctx.slots.inject("settings.section.icon", () =>
|
|
594
|
+
ctx.slots.register({ name: "settings.section.icon", id: SECTION_ID }, (owner) =>
|
|
595
|
+
React.createElement(WatchIcon, { size: owner?.size ?? 20, active: owner?.active === true }),
|
|
596
|
+
),
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
exports.apply = apply;
|
|
601
|
+
exports.inject = inject;
|
|
602
|
+
exports.name = name;
|
|
603
|
+
exports.UpdatesPage = UpdatesPage;
|
|
604
|
+
exports.UpdatesView = UpdatesView;
|
|
605
|
+
exports.summaryOf = summaryOf;
|
|
606
|
+
exports.stateText = stateText;
|
|
607
|
+
exports.detailText = detailText;
|
|
608
|
+
exports.resultText = resultText;
|
|
609
|
+
exports.DICT = DICT;
|
|
610
|
+
exports.WatchIcon = WatchIcon;
|
|
611
|
+
return module.exports;
|
|
612
|
+
},
|
|
613
|
+
});
|