@huaqiu/dsh-tool-plugin-update 0.0.0-stage → 0.5.21
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/cordis.patch.yml +10 -0
- package/lib/client.js +1006 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.mts +32 -0
- package/lib/index.mjs +178 -0
- package/package.json +50 -3
- package/src/client/api.ts +94 -0
- package/src/client/copy.ts +92 -0
- package/src/client/index.ts +508 -0
- package/src/client/update-view.ts +301 -0
- package/src/index.ts +108 -0
- package/src/routes.ts +149 -0
- package/README.md +0 -3
package/lib/client.js
ADDED
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@huaqiu/dsh-tool-plugin-update",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region ../dsh-sidebar-entry/lib/index.mjs
|
|
8
|
+
/** Find the sidebar shell root element, or undefined while not yet mounted. */
|
|
9
|
+
function sidebarRoot() {
|
|
10
|
+
const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
|
|
11
|
+
if (column === null) return void 0;
|
|
12
|
+
return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
|
|
13
|
+
}
|
|
14
|
+
/** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */
|
|
15
|
+
function newSessionButton(root) {
|
|
16
|
+
const nested = root.querySelector("button[class*=\"newSession\"]");
|
|
17
|
+
if (nested !== null) return nested;
|
|
18
|
+
for (const child of root.children) if (child.tagName === "BUTTON") return child;
|
|
19
|
+
}
|
|
20
|
+
/** Build the entry row (a detached button; insert once the shell is up). */
|
|
21
|
+
function createEntry(options) {
|
|
22
|
+
const entry = document.createElement("button");
|
|
23
|
+
entry.type = "button";
|
|
24
|
+
entry.setAttribute(options.rowAttribute, "");
|
|
25
|
+
if (options.plugin !== void 0) {
|
|
26
|
+
entry.setAttribute("data-dsh-plugin", options.plugin);
|
|
27
|
+
entry.setAttribute("data-dsh-part", "sidebar-entry");
|
|
28
|
+
}
|
|
29
|
+
entry.className = options.css["entry"] ?? "";
|
|
30
|
+
const labelSpan = document.createElement("span");
|
|
31
|
+
labelSpan.className = options.css["entryLabel"] ?? "";
|
|
32
|
+
const iconSpan = document.createElement("span");
|
|
33
|
+
iconSpan.className = options.css["entryIcon"] ?? "";
|
|
34
|
+
iconSpan.innerHTML = options.icon;
|
|
35
|
+
entry.append(iconSpan, labelSpan);
|
|
36
|
+
let badgeSpan = null;
|
|
37
|
+
const applyBadge = () => {
|
|
38
|
+
if (options.badge === void 0) return;
|
|
39
|
+
const count = options.badge.count();
|
|
40
|
+
if (count <= 0) {
|
|
41
|
+
badgeSpan?.remove();
|
|
42
|
+
badgeSpan = null;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (badgeSpan === null) {
|
|
46
|
+
badgeSpan = document.createElement("span");
|
|
47
|
+
badgeSpan.className = options.css["entryBadge"] ?? "";
|
|
48
|
+
entry.append(badgeSpan);
|
|
49
|
+
}
|
|
50
|
+
badgeSpan.textContent = String(count);
|
|
51
|
+
entry.setAttribute("data-has-badge", "");
|
|
52
|
+
};
|
|
53
|
+
const applyLabel = () => {
|
|
54
|
+
entry.setAttribute("aria-label", options.label());
|
|
55
|
+
if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
|
|
56
|
+
labelSpan.textContent = options.label();
|
|
57
|
+
};
|
|
58
|
+
applyLabel();
|
|
59
|
+
applyBadge();
|
|
60
|
+
entry.addEventListener("click", options.onToggle);
|
|
61
|
+
return {
|
|
62
|
+
entry,
|
|
63
|
+
applyLabel,
|
|
64
|
+
applyBadge
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Re-insert the entry after the New Session row (before the browser region). */
|
|
68
|
+
function placeEntry(root, entry, options) {
|
|
69
|
+
const button = newSessionButton(root);
|
|
70
|
+
if (button === void 0) return false;
|
|
71
|
+
if (entry.parentElement !== root) {
|
|
72
|
+
const row = button.closest("[class*=\"logoRow\"]");
|
|
73
|
+
const base = row !== null && row.parentElement === root ? row : button;
|
|
74
|
+
const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(options.familySelectors.join(", ")));
|
|
75
|
+
const anchor = options.position === "before" ? family.length > 0 ? family[0] : base.nextElementSibling : family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
|
|
76
|
+
root.insertBefore(entry, anchor ?? null);
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
|
82
|
+
* on later React re-renders.
|
|
83
|
+
* @param options - the row's attribute/icon/copy/action/ordering configuration.
|
|
84
|
+
* @returns disposer removing the entry and its observers.
|
|
85
|
+
*/
|
|
86
|
+
function mountSidebarEntry(options) {
|
|
87
|
+
if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
|
|
88
|
+
const { entry, applyLabel, applyBadge } = createEntry(options);
|
|
89
|
+
let root;
|
|
90
|
+
let placed = false;
|
|
91
|
+
let unsubscribeRefresh;
|
|
92
|
+
if (options.refresh !== void 0) try {
|
|
93
|
+
unsubscribeRefresh = options.refresh.subscribe(applyLabel);
|
|
94
|
+
} catch {}
|
|
95
|
+
let unsubscribeBadge;
|
|
96
|
+
if (options.badge !== void 0) try {
|
|
97
|
+
unsubscribeBadge = options.badge.subscribe(applyBadge);
|
|
98
|
+
} catch {}
|
|
99
|
+
const tryPlace = () => {
|
|
100
|
+
if (root !== void 0 && !root.isConnected) {
|
|
101
|
+
rootObserver.disconnect();
|
|
102
|
+
root = void 0;
|
|
103
|
+
placed = false;
|
|
104
|
+
}
|
|
105
|
+
if (placed) {
|
|
106
|
+
if (document.body.contains(entry)) return;
|
|
107
|
+
rootObserver.disconnect();
|
|
108
|
+
root = void 0;
|
|
109
|
+
placed = false;
|
|
110
|
+
}
|
|
111
|
+
root ??= sidebarRoot();
|
|
112
|
+
if (root === void 0) return;
|
|
113
|
+
placed = placeEntry(root, entry, options);
|
|
114
|
+
if (placed) rootObserver.observe(root, {
|
|
115
|
+
childList: true,
|
|
116
|
+
subtree: true
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
const waitObserver = new MutationObserver(() => {
|
|
120
|
+
tryPlace();
|
|
121
|
+
});
|
|
122
|
+
waitObserver.observe(document.body, {
|
|
123
|
+
childList: true,
|
|
124
|
+
subtree: true
|
|
125
|
+
});
|
|
126
|
+
const rootObserver = new MutationObserver(() => {
|
|
127
|
+
if (root === void 0 || !root.isConnected) {
|
|
128
|
+
placed = false;
|
|
129
|
+
tryPlace();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!root.contains(entry)) placed = placeEntry(root, entry, options);
|
|
133
|
+
});
|
|
134
|
+
options.active === void 0 || (() => {
|
|
135
|
+
const syncActive = () => {
|
|
136
|
+
if (options.active.isOpen()) entry.dataset.active = "true";
|
|
137
|
+
else delete entry.dataset.active;
|
|
138
|
+
};
|
|
139
|
+
const unsubscribe = options.active.subscribe(syncActive);
|
|
140
|
+
syncActive();
|
|
141
|
+
return unsubscribe;
|
|
142
|
+
})();
|
|
143
|
+
tryPlace();
|
|
144
|
+
return () => {
|
|
145
|
+
waitObserver.disconnect();
|
|
146
|
+
rootObserver.disconnect();
|
|
147
|
+
unsubscribeRefresh?.();
|
|
148
|
+
unsubscribeBadge?.();
|
|
149
|
+
entry.remove();
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Every row selector in the Huaqiu sidebar family, in intended visual order.
|
|
154
|
+
*
|
|
155
|
+
* Family ordering is only stable when every participating plugin passes the
|
|
156
|
+
* SAME list to {@link mountSidebarEntry} — the core positions a row relative to
|
|
157
|
+
* the family block, never relative to transient shell geometry. Adding a new
|
|
158
|
+
* family row means adding its selector here (and to no plugin-local list).
|
|
159
|
+
*/
|
|
160
|
+
const HUAQIU_SIDEBAR_ENTRY_FAMILY = [
|
|
161
|
+
"[data-hqcg-footprint-entry]",
|
|
162
|
+
"[data-hqcg-symbol-entry]",
|
|
163
|
+
"[data-hqcg-schematic-entry]",
|
|
164
|
+
"[data-hq-plugin-update-entry]"
|
|
165
|
+
];
|
|
166
|
+
/**
|
|
167
|
+
* Read the host UI language from `<html lang>`.
|
|
168
|
+
*
|
|
169
|
+
* `dsh-client-locale` rewrites the attribute on every language change (dsh
|
|
170
|
+
* web) and HQ Edge mirrors it, so this is the one source every family plugin
|
|
171
|
+
* can read without importing a `locale` service.
|
|
172
|
+
* @returns 'zh' / 'en', or undefined when the tag is absent or unsupported
|
|
173
|
+
* (callers fall back to their own default).
|
|
174
|
+
*/
|
|
175
|
+
function detectHostLocale() {
|
|
176
|
+
if (typeof document === "undefined") return void 0;
|
|
177
|
+
const tag = document.documentElement?.getAttribute("lang");
|
|
178
|
+
if (tag === null || tag === void 0 || tag === "") return void 0;
|
|
179
|
+
const primary = tag.toLowerCase().split("-")[0];
|
|
180
|
+
return primary === "zh" || primary === "en" ? primary : void 0;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Watch `<html lang>` and report every resolved change.
|
|
184
|
+
*
|
|
185
|
+
* Idempotent-safe: repeated calls share one observer and each caller gets its
|
|
186
|
+
* own disposer; the observer disconnects when the last listener leaves.
|
|
187
|
+
* @param listener - called with the newly resolved language.
|
|
188
|
+
* @returns disposer removing this listener.
|
|
189
|
+
*/
|
|
190
|
+
/** Shared observer/listener set backing {@link observeHostLocale}. */
|
|
191
|
+
const localeListeners = /* @__PURE__ */ new Set();
|
|
192
|
+
let localeObserver = null;
|
|
193
|
+
function observeHostLocale(listener) {
|
|
194
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined" || document.documentElement === null) return () => {};
|
|
195
|
+
let current = detectHostLocale();
|
|
196
|
+
const listeners = localeListeners;
|
|
197
|
+
listeners.add(listener);
|
|
198
|
+
if (localeObserver === null) {
|
|
199
|
+
localeObserver = new MutationObserver(() => {
|
|
200
|
+
const next = detectHostLocale();
|
|
201
|
+
if (next === current) return;
|
|
202
|
+
current = next;
|
|
203
|
+
for (const fn of [...listeners]) try {
|
|
204
|
+
fn(next);
|
|
205
|
+
} catch {}
|
|
206
|
+
});
|
|
207
|
+
localeObserver.observe(document.documentElement, {
|
|
208
|
+
attributes: true,
|
|
209
|
+
attributeFilter: ["lang"]
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return () => {
|
|
213
|
+
listeners.delete(listener);
|
|
214
|
+
if (listeners.size === 0 && localeObserver !== null) {
|
|
215
|
+
localeObserver.disconnect();
|
|
216
|
+
localeObserver = null;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/client/api.ts
|
|
222
|
+
/**
|
|
223
|
+
* Plugin-update API client (browser half).
|
|
224
|
+
*
|
|
225
|
+
* Every call is a same-origin fetch against the DSH webServer prefix route
|
|
226
|
+
* (`/api/v1/huaqiu/plugin-update/…`) which the node half forwards to hq-edge.
|
|
227
|
+
* Response shapes mirror hq-edge `apps/server/src/routes/plugin-updates.ts`.
|
|
228
|
+
*
|
|
229
|
+
* @module
|
|
230
|
+
*/
|
|
231
|
+
const UPDATE_BASE = "/api/v1/huaqiu/plugin-update";
|
|
232
|
+
async function request(path, init) {
|
|
233
|
+
const res = await fetch(`${UPDATE_BASE}${path}`, {
|
|
234
|
+
cache: "no-store",
|
|
235
|
+
...init,
|
|
236
|
+
headers: {
|
|
237
|
+
"content-type": "application/json; charset=utf-8",
|
|
238
|
+
...init?.headers ?? {}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
const body = await res.json().catch(() => null);
|
|
242
|
+
if (!res.ok) {
|
|
243
|
+
const err = body?.error ?? `HTTP ${res.status}`;
|
|
244
|
+
throw new Error(err);
|
|
245
|
+
}
|
|
246
|
+
if (body === null) throw new Error("empty response");
|
|
247
|
+
return body;
|
|
248
|
+
}
|
|
249
|
+
/** GET the cached (or empty-shell) plugin-update report. */
|
|
250
|
+
function fetchReport() {
|
|
251
|
+
return request("/report");
|
|
252
|
+
}
|
|
253
|
+
/** POST an explicit refresh; returns the freshly computed report. */
|
|
254
|
+
function checkUpdates() {
|
|
255
|
+
return request("/check", { method: "POST" });
|
|
256
|
+
}
|
|
257
|
+
/** POST the install transaction for the confirmed set version. */
|
|
258
|
+
function installSet(setVersion) {
|
|
259
|
+
return request("/install", {
|
|
260
|
+
method: "POST",
|
|
261
|
+
body: JSON.stringify({ setVersion })
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/** POST unignore for the set version (reverses an ignore). */
|
|
265
|
+
function unignoreOffer(setVersion) {
|
|
266
|
+
return request("/unignore", {
|
|
267
|
+
method: "POST",
|
|
268
|
+
body: JSON.stringify({ setVersion })
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
const COPY = {
|
|
272
|
+
zh: {
|
|
273
|
+
entryLabel: "插件更新",
|
|
274
|
+
entryTooltip: "查看可用的插件更新",
|
|
275
|
+
viewTitle: "插件更新",
|
|
276
|
+
refresh: "刷新",
|
|
277
|
+
close: "关闭",
|
|
278
|
+
empty: "没有可用的插件更新",
|
|
279
|
+
checkFailed: "检查更新失败",
|
|
280
|
+
manifestVersion: "插件集",
|
|
281
|
+
ignoredOffer: "已忽略当前插件集更新",
|
|
282
|
+
unignore: "恢复提醒",
|
|
283
|
+
installAll: (count) => `安装 ${count} 项更新`,
|
|
284
|
+
installing: "安装中…(DSH 将自动重启)",
|
|
285
|
+
state: {
|
|
286
|
+
current: "当前",
|
|
287
|
+
update: "可更新",
|
|
288
|
+
install: "待安装",
|
|
289
|
+
ignored: "已忽略",
|
|
290
|
+
blocked: "被阻止"
|
|
291
|
+
},
|
|
292
|
+
blockedReason: {
|
|
293
|
+
host: "不适用于当前 EDA 主机",
|
|
294
|
+
"hq-edge-version": "当前 HQ Edge 版本不兼容",
|
|
295
|
+
"runtime-unknown": "HQ Edge 版本未知,无法校验"
|
|
296
|
+
},
|
|
297
|
+
runtime: {
|
|
298
|
+
title: "HQ Edge 运行时",
|
|
299
|
+
updateAvailable: (version) => `发现新版本 ${version}`,
|
|
300
|
+
current: (version) => `当前版本 ${version}`,
|
|
301
|
+
unknown: "版本未知",
|
|
302
|
+
notConfigured: "未配置更新源",
|
|
303
|
+
note: "运行时更新由 EDA 安装器处理"
|
|
304
|
+
},
|
|
305
|
+
lastError: "上次检查失败",
|
|
306
|
+
fetchedAt: "检查时间",
|
|
307
|
+
installError: "安装失败",
|
|
308
|
+
installSuccess: "更新已安装,正在重启 DSH…"
|
|
309
|
+
},
|
|
310
|
+
en: {
|
|
311
|
+
entryLabel: "Plugin Updates",
|
|
312
|
+
entryTooltip: "Review available plugin updates",
|
|
313
|
+
viewTitle: "Plugin Updates",
|
|
314
|
+
refresh: "Refresh",
|
|
315
|
+
close: "Close",
|
|
316
|
+
empty: "No plugin updates available",
|
|
317
|
+
checkFailed: "Failed to check for updates",
|
|
318
|
+
manifestVersion: "Plugin set",
|
|
319
|
+
ignoredOffer: "The current plugin-set update is ignored",
|
|
320
|
+
unignore: "Show again",
|
|
321
|
+
installAll: (count) => `Install ${count} update${count === 1 ? "" : "s"}`,
|
|
322
|
+
installing: "Installing… (DSH will restart)",
|
|
323
|
+
state: {
|
|
324
|
+
current: "Current",
|
|
325
|
+
update: "Update",
|
|
326
|
+
install: "Install",
|
|
327
|
+
ignored: "Ignored",
|
|
328
|
+
blocked: "Blocked"
|
|
329
|
+
},
|
|
330
|
+
blockedReason: {
|
|
331
|
+
host: "Not applicable to the current EDA host",
|
|
332
|
+
"hq-edge-version": "Incompatible with the current HQ Edge version",
|
|
333
|
+
"runtime-unknown": "HQ Edge version unknown — cannot verify"
|
|
334
|
+
},
|
|
335
|
+
runtime: {
|
|
336
|
+
title: "HQ Edge Runtime",
|
|
337
|
+
updateAvailable: (version) => `New version ${version} available`,
|
|
338
|
+
current: (version) => `Current version ${version}`,
|
|
339
|
+
unknown: "Version unknown",
|
|
340
|
+
notConfigured: "No update source configured",
|
|
341
|
+
note: "Runtime updates are handled by the EDA installer"
|
|
342
|
+
},
|
|
343
|
+
lastError: "Last check failed",
|
|
344
|
+
fetchedAt: "Checked at",
|
|
345
|
+
installError: "Install failed",
|
|
346
|
+
installSuccess: "Update installed — restarting DSH…"
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
//#endregion
|
|
350
|
+
//#region src/client/update-view.ts
|
|
351
|
+
/**
|
|
352
|
+
* Plugin-update review modal (browser half).
|
|
353
|
+
*
|
|
354
|
+
* Plain-DOM modal (no React tree) so it can never disturb the DSH shell's
|
|
355
|
+
* reconciliation — same rule as the sidebar entry rows. Rendered content:
|
|
356
|
+
*
|
|
357
|
+
* - header: title + refresh
|
|
358
|
+
* - plugin list (name, current → available, state badge, blocked reason)
|
|
359
|
+
* - ignored-offer banner + reversible "show again"
|
|
360
|
+
* - install action (disabled while installing / nothing actionable / offer
|
|
361
|
+
* ignored); after install the UI keeps polling the backend state instead
|
|
362
|
+
* of trusting the HTTP response alone, because activating a set restarts
|
|
363
|
+
* DSH (spec §7.1: never report success on the 200/202 alone).
|
|
364
|
+
* - independent HQ Edge runtime section (read-only report; the actual
|
|
365
|
+
* upgrade is owned by the EDA installer).
|
|
366
|
+
*
|
|
367
|
+
* @module
|
|
368
|
+
*/
|
|
369
|
+
function el(tag, className, text) {
|
|
370
|
+
const node = document.createElement(tag);
|
|
371
|
+
node.className = className;
|
|
372
|
+
if (text !== void 0) node.textContent = text;
|
|
373
|
+
return node;
|
|
374
|
+
}
|
|
375
|
+
function stateLabel(item, copy) {
|
|
376
|
+
switch (item.state) {
|
|
377
|
+
case "current": return copy.state.current;
|
|
378
|
+
case "update": return copy.state.update;
|
|
379
|
+
case "install": return copy.state.install;
|
|
380
|
+
case "ignored": return copy.state.ignored;
|
|
381
|
+
case "blocked": return copy.state.blocked;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function blockedReason(item, copy) {
|
|
385
|
+
const by = item.blockedBy ?? [];
|
|
386
|
+
if (by.includes("host")) return copy.blockedReason.host;
|
|
387
|
+
if (by.includes("hq-edge-version")) return copy.blockedReason["hq-edge-version"];
|
|
388
|
+
if (by.includes("runtime-unknown")) return copy.blockedReason["runtime-unknown"];
|
|
389
|
+
return by.length > 0 ? by.join(", ") : void 0;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Open (or focus) the update modal. Re-renders from the store; when the store
|
|
393
|
+
* is stale the modal first refreshes the report.
|
|
394
|
+
*/
|
|
395
|
+
function openUpdateView(store, copy) {
|
|
396
|
+
const existing = document.querySelector("[data-hq-plugin-update-view]");
|
|
397
|
+
if (existing !== null) {
|
|
398
|
+
existing.style.display = "";
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const overlay = el("div", "hqpu-overlay");
|
|
402
|
+
overlay.setAttribute("data-hq-plugin-update-view", "");
|
|
403
|
+
const panel = el("div", "hqpu-panel");
|
|
404
|
+
const header = el("div", "hqpu-header");
|
|
405
|
+
header.append(el("div", "hqpu-title", copy.viewTitle));
|
|
406
|
+
const refreshBtn = el("button", "hqpu-btn", copy.refresh);
|
|
407
|
+
header.append(refreshBtn);
|
|
408
|
+
const closeBtn = el("button", "hqpu-btn hqpu-btn-ghost", copy.close);
|
|
409
|
+
header.append(closeBtn);
|
|
410
|
+
const body = el("div", "hqpu-body");
|
|
411
|
+
const listSection = el("div", "hqpu-section");
|
|
412
|
+
const runtimeSection = el("div", "hqpu-section hqpu-runtime");
|
|
413
|
+
const actionBar = el("div", "hqpu-actions");
|
|
414
|
+
panel.append(header, body);
|
|
415
|
+
body.append(listSection, runtimeSection, actionBar);
|
|
416
|
+
overlay.append(panel);
|
|
417
|
+
document.body.appendChild(overlay);
|
|
418
|
+
const disposers = [];
|
|
419
|
+
const render = () => {
|
|
420
|
+
const report = store.report();
|
|
421
|
+
const installing = store.installing();
|
|
422
|
+
renderList(listSection, report, copy, installing, store);
|
|
423
|
+
renderRuntime(runtimeSection, report, copy);
|
|
424
|
+
renderActions(actionBar, report, copy, store, installing);
|
|
425
|
+
};
|
|
426
|
+
const unsubscribe = store.subscribe(render);
|
|
427
|
+
disposers.push(unsubscribe);
|
|
428
|
+
store.refresh().then(() => {});
|
|
429
|
+
const onClose = () => {
|
|
430
|
+
for (const dispose of disposers) try {
|
|
431
|
+
dispose();
|
|
432
|
+
} catch {}
|
|
433
|
+
overlay.remove();
|
|
434
|
+
};
|
|
435
|
+
closeBtn.addEventListener("click", onClose);
|
|
436
|
+
overlay.addEventListener("click", (ev) => {
|
|
437
|
+
if (ev.target === overlay) onClose();
|
|
438
|
+
});
|
|
439
|
+
refreshBtn.addEventListener("click", () => {
|
|
440
|
+
store.refresh();
|
|
441
|
+
});
|
|
442
|
+
disposers.push(() => {
|
|
443
|
+
closeBtn.removeEventListener("click", onClose);
|
|
444
|
+
overlay.removeEventListener("click", onClose);
|
|
445
|
+
refreshBtn.removeEventListener("click", () => {});
|
|
446
|
+
});
|
|
447
|
+
render();
|
|
448
|
+
}
|
|
449
|
+
function renderList(section, report, copy, installing, store) {
|
|
450
|
+
section.textContent = "";
|
|
451
|
+
if (report === null) {
|
|
452
|
+
section.append(el("div", "hqpu-empty", copy.checkFailed));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (report.items.length === 0) {
|
|
456
|
+
section.append(el("div", "hqpu-empty", copy.empty));
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const manifestRow = el("div", "hqpu-manifest");
|
|
460
|
+
manifestRow.append(el("span", "hqpu-manifest-label", `${copy.manifestVersion}`), el("span", "hqpu-manifest-value", report.manifestVersion));
|
|
461
|
+
section.append(manifestRow);
|
|
462
|
+
if (report.ignoredSet) {
|
|
463
|
+
const banner = el("div", "hqpu-ignored-banner", copy.ignoredOffer);
|
|
464
|
+
const unignore = el("button", "hqpu-btn hqpu-btn-small", copy.unignore);
|
|
465
|
+
unignore.addEventListener("click", () => {
|
|
466
|
+
unignoreOffer(report.manifestVersion).then(() => store.refresh()).catch(() => void 0);
|
|
467
|
+
});
|
|
468
|
+
banner.append(unignore);
|
|
469
|
+
section.append(banner);
|
|
470
|
+
}
|
|
471
|
+
const list = el("ul", "hqpu-list");
|
|
472
|
+
for (const item of report.items) {
|
|
473
|
+
const row = el("li", `hqpu-item hqpu-item-${item.state}`);
|
|
474
|
+
const name = el("span", "hqpu-item-name", item.name);
|
|
475
|
+
const version = el("span", "hqpu-item-version", item.state === "current" ? item.currentVersion : `${item.currentVersion} → ${item.availableVersion}`);
|
|
476
|
+
const state = el("span", `hqpu-item-state hqpu-state-${item.state}`, stateLabel(item, copy));
|
|
477
|
+
row.append(name, version, state);
|
|
478
|
+
const reason = blockedReason(item, copy);
|
|
479
|
+
if (reason !== void 0) row.append(el("span", "hqpu-item-reason", reason));
|
|
480
|
+
list.append(row);
|
|
481
|
+
}
|
|
482
|
+
section.append(list);
|
|
483
|
+
}
|
|
484
|
+
function renderRuntime(section, report, copy) {
|
|
485
|
+
section.textContent = "";
|
|
486
|
+
const title = el("div", "hqpu-runtime-title", copy.runtime.title);
|
|
487
|
+
section.append(title);
|
|
488
|
+
const runtime = report?.runtime;
|
|
489
|
+
if (runtime === void 0 || runtime.configured === false) {
|
|
490
|
+
section.append(el("div", "hqpu-runtime-line", copy.runtime.notConfigured));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (runtime.updateAvailable && runtime.latest !== void 0) section.append(el("div", "hqpu-runtime-line", copy.runtime.updateAvailable(runtime.latest)));
|
|
494
|
+
else if (runtime.currentVersion !== void 0) section.append(el("div", "hqpu-runtime-line", copy.runtime.current(runtime.currentVersion)));
|
|
495
|
+
else section.append(el("div", "hqpu-runtime-line", copy.runtime.unknown));
|
|
496
|
+
section.append(el("div", "hqpu-runtime-note", copy.runtime.note));
|
|
497
|
+
if (runtime.lastError !== void 0 && runtime.lastError.length > 0) section.append(el("div", "hqpu-runtime-error", `${copy.lastError}: ${runtime.lastError}`));
|
|
498
|
+
}
|
|
499
|
+
function renderActions(bar, report, copy, store, installing) {
|
|
500
|
+
bar.textContent = "";
|
|
501
|
+
if (report === null) return;
|
|
502
|
+
if (installing) {
|
|
503
|
+
bar.append(el("span", "hqpu-installing", copy.installing));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const actionable = report.items.filter((item) => item.state === "update" || item.state === "install").length;
|
|
507
|
+
const installBtn = el("button", "hqpu-btn hqpu-btn-primary", actionable > 0 ? copy.installAll(actionable) : copy.state.current);
|
|
508
|
+
installBtn.disabled = actionable === 0 || report.ignoredSet;
|
|
509
|
+
installBtn.addEventListener("click", () => {
|
|
510
|
+
installSet(report.manifestVersion).then((result) => {
|
|
511
|
+
if (result.error !== void 0) {
|
|
512
|
+
bar.textContent = "";
|
|
513
|
+
bar.append(el("div", "hqpu-error", `${copy.installError}: ${result.error}`));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
bar.textContent = "";
|
|
517
|
+
bar.append(el("span", "hqpu-installing", copy.installSuccess));
|
|
518
|
+
pollUntilSettled(store, report.manifestVersion);
|
|
519
|
+
}).catch((err) => {
|
|
520
|
+
bar.textContent = "";
|
|
521
|
+
bar.append(el("div", "hqpu-error", `${copy.installError}: ${err instanceof Error ? err.message : String(err)}`));
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
bar.append(installBtn);
|
|
525
|
+
}
|
|
526
|
+
/** After an install, poll the backend until the update report settles
|
|
527
|
+
* (installing flag clears or DSH restarts under us — the next load re-reads
|
|
528
|
+
* the fresh state). */
|
|
529
|
+
function pollUntilSettled(store, setVersion) {
|
|
530
|
+
let attempts = 0;
|
|
531
|
+
const tick = () => {
|
|
532
|
+
store.refresh().then((report) => {
|
|
533
|
+
if (store.installing()) return;
|
|
534
|
+
if (report !== null && report.ignoredSet === false && attempts >= 2) return;
|
|
535
|
+
}).catch(() => void 0).finally(() => {
|
|
536
|
+
attempts += 1;
|
|
537
|
+
if (attempts < 10) window.setTimeout(tick, 2e3);
|
|
538
|
+
});
|
|
539
|
+
};
|
|
540
|
+
tick();
|
|
541
|
+
}
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/client/index.ts
|
|
544
|
+
/**
|
|
545
|
+
* `@huaqiu/dsh-tool-plugin-update` — browser half.
|
|
546
|
+
*
|
|
547
|
+
* Mounts one sidebar toolbutton in the Huaqiu family block ("插件更新") with a
|
|
548
|
+
* numeric badge equal to the actionable plugin-update count (spec §6.3/§7.1:
|
|
549
|
+
* only compatible `update` + `install` items after host/HQ-Edge filtering and
|
|
550
|
+
* after excluding the ignored offer; runtime updates are NOT counted). Click
|
|
551
|
+
* opens the review/confirm/ignore modal.
|
|
552
|
+
*
|
|
553
|
+
* The authoritative state is the hq-edge report fetched through the plugin's
|
|
554
|
+
* same-origin forwarding routes. A tiny in-memory store owns the latest report
|
|
555
|
+
* + installing flag; the sidebar badge and the modal subscribe to it. No
|
|
556
|
+
* timers beyond the install-poll and an idle 60 s refresh.
|
|
557
|
+
*
|
|
558
|
+
* @module
|
|
559
|
+
*/
|
|
560
|
+
/** Stable data attributes identifying the injected entry row. */
|
|
561
|
+
const UPDATE_ENTRY_SELECTOR = "[data-hq-plugin-update-entry]";
|
|
562
|
+
const UPDATE_ENTRY_ATTRIBUTE = "data-hq-plugin-update-entry";
|
|
563
|
+
/** L2 semantic plugin id: rows carry data-dsh-plugin + data-dsh-part. */
|
|
564
|
+
const PLUGIN_ID = "huaqiu-plugin-update";
|
|
565
|
+
/** Row class names (defined by {@link ENTRY_CSS_TEXT}). */
|
|
566
|
+
const ENTRY_CSS = {
|
|
567
|
+
entry: "hqpu-sidebar-entry",
|
|
568
|
+
entryIcon: "hqpu-sidebar-entry__icon",
|
|
569
|
+
entryLabel: "hqpu-sidebar-entry__label",
|
|
570
|
+
entryBadge: "hqpu-sidebar-entry__badge"
|
|
571
|
+
};
|
|
572
|
+
/** Localized row copy: zh is the fallback, matching the family convention. */
|
|
573
|
+
function entryCopy(lang) {
|
|
574
|
+
return lang === "en" ? {
|
|
575
|
+
label: COPY.en.entryLabel,
|
|
576
|
+
tooltip: COPY.en.entryTooltip
|
|
577
|
+
} : {
|
|
578
|
+
label: COPY.zh.entryLabel,
|
|
579
|
+
tooltip: COPY.zh.entryTooltip
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
/** Inline update glyph — `currentColor`, 16×16 like the shell's nav glyphs. */
|
|
583
|
+
const UPDATE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
|
584
|
+
<g fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
|
|
585
|
+
<path d="M2.5 8a5.5 5.5 0 1 1 1.61 3.89" />
|
|
586
|
+
<path d="M2.5 8V4.75M2.5 8h3.25" />
|
|
587
|
+
</g>
|
|
588
|
+
</svg>`;
|
|
589
|
+
/** Sidebar entry styles — matched to the family rows (same inset/padding/gap
|
|
590
|
+
* as the shell's own Plugins entry). The badge is a 16px rounded pill. */
|
|
591
|
+
const ENTRY_CSS_TEXT = `
|
|
592
|
+
.hqpu-sidebar-entry {
|
|
593
|
+
box-sizing: border-box;
|
|
594
|
+
display: flex;
|
|
595
|
+
align-items: center;
|
|
596
|
+
gap: 8px;
|
|
597
|
+
width: 100%;
|
|
598
|
+
margin: 0 2px;
|
|
599
|
+
min-height: 36px;
|
|
600
|
+
padding: 7px 8px;
|
|
601
|
+
background: transparent;
|
|
602
|
+
border: none;
|
|
603
|
+
border-radius: 12px;
|
|
604
|
+
color: var(--dsw-alias-label-primary);
|
|
605
|
+
cursor: pointer;
|
|
606
|
+
font: inherit;
|
|
607
|
+
line-height: 22px;
|
|
608
|
+
text-align: left;
|
|
609
|
+
white-space: nowrap;
|
|
610
|
+
}
|
|
611
|
+
.hqpu-sidebar-entry:hover {
|
|
612
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
613
|
+
}
|
|
614
|
+
.hqpu-sidebar-entry[data-active] {
|
|
615
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
616
|
+
color: var(--dsw-alias-label-primary);
|
|
617
|
+
}
|
|
618
|
+
.hqpu-sidebar-entry__icon {
|
|
619
|
+
display: inline-flex;
|
|
620
|
+
align-items: center;
|
|
621
|
+
justify-content: center;
|
|
622
|
+
flex: none;
|
|
623
|
+
}
|
|
624
|
+
.hqpu-sidebar-entry__icon svg {
|
|
625
|
+
display: block;
|
|
626
|
+
width: 16px;
|
|
627
|
+
height: 16px;
|
|
628
|
+
}
|
|
629
|
+
.hqpu-sidebar-entry__label {
|
|
630
|
+
min-width: 0;
|
|
631
|
+
overflow: hidden;
|
|
632
|
+
text-overflow: ellipsis;
|
|
633
|
+
flex: 1;
|
|
634
|
+
}
|
|
635
|
+
.hqpu-sidebar-entry__badge {
|
|
636
|
+
flex: none;
|
|
637
|
+
min-width: 16px;
|
|
638
|
+
height: 16px;
|
|
639
|
+
padding: 0 5px;
|
|
640
|
+
border-radius: 8px;
|
|
641
|
+
background: var(--dsw-alias-bg-badge, #e5484d);
|
|
642
|
+
color: #fff;
|
|
643
|
+
font-size: 11px;
|
|
644
|
+
line-height: 16px;
|
|
645
|
+
text-align: center;
|
|
646
|
+
}
|
|
647
|
+
/* Collapsed rail: icon-only, centered — same 36px square the shell uses. */
|
|
648
|
+
[data-sidebar-collapsed] .hqpu-sidebar-entry {
|
|
649
|
+
justify-content: center;
|
|
650
|
+
width: 36px;
|
|
651
|
+
height: 36px;
|
|
652
|
+
margin: 0;
|
|
653
|
+
padding: 0;
|
|
654
|
+
}
|
|
655
|
+
[data-sidebar-collapsed] .hqpu-sidebar-entry__icon svg {
|
|
656
|
+
width: 18px;
|
|
657
|
+
height: 18px;
|
|
658
|
+
}
|
|
659
|
+
[data-sidebar-collapsed] .hqpu-sidebar-entry__label,
|
|
660
|
+
[data-sidebar-collapsed] .hqpu-sidebar-entry__badge {
|
|
661
|
+
display: none;
|
|
662
|
+
}
|
|
663
|
+
`;
|
|
664
|
+
/** Modal styles (hqpu-*). Plain overlay + panel, aligned to the shell's
|
|
665
|
+
* dark/light aliases. */
|
|
666
|
+
const VIEW_CSS_TEXT = `
|
|
667
|
+
.hqpu-overlay {
|
|
668
|
+
position: fixed;
|
|
669
|
+
inset: 0;
|
|
670
|
+
z-index: 1000;
|
|
671
|
+
display: flex;
|
|
672
|
+
align-items: center;
|
|
673
|
+
justify-content: center;
|
|
674
|
+
background: rgba(0, 0, 0, 0.45);
|
|
675
|
+
}
|
|
676
|
+
.hqpu-panel {
|
|
677
|
+
box-sizing: border-box;
|
|
678
|
+
width: min(640px, calc(100vw - 48px));
|
|
679
|
+
max-height: min(80vh, 720px);
|
|
680
|
+
display: flex;
|
|
681
|
+
flex-direction: column;
|
|
682
|
+
border-radius: 16px;
|
|
683
|
+
background: var(--dsw-alias-bg-surface, #fff);
|
|
684
|
+
color: var(--dsw-alias-label-primary, #1f2329);
|
|
685
|
+
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.24);
|
|
686
|
+
overflow: hidden;
|
|
687
|
+
}
|
|
688
|
+
.hqpu-header {
|
|
689
|
+
display: flex;
|
|
690
|
+
align-items: center;
|
|
691
|
+
gap: 8px;
|
|
692
|
+
padding: 14px 16px;
|
|
693
|
+
border-bottom: 1px solid var(--dsw-alias-line-border, rgba(0,0,0,.08));
|
|
694
|
+
}
|
|
695
|
+
.hqpu-title {
|
|
696
|
+
flex: 1;
|
|
697
|
+
font-size: 15px;
|
|
698
|
+
font-weight: 600;
|
|
699
|
+
}
|
|
700
|
+
.hqpu-body {
|
|
701
|
+
overflow: auto;
|
|
702
|
+
padding: 16px;
|
|
703
|
+
display: flex;
|
|
704
|
+
flex-direction: column;
|
|
705
|
+
gap: 16px;
|
|
706
|
+
}
|
|
707
|
+
.hqpu-section {
|
|
708
|
+
display: flex;
|
|
709
|
+
flex-direction: column;
|
|
710
|
+
gap: 8px;
|
|
711
|
+
}
|
|
712
|
+
.hqpu-manifest {
|
|
713
|
+
display: flex;
|
|
714
|
+
align-items: center;
|
|
715
|
+
gap: 8px;
|
|
716
|
+
font-size: 12px;
|
|
717
|
+
color: var(--dsw-alias-label-secondary, #646a73);
|
|
718
|
+
}
|
|
719
|
+
.hqpu-manifest-value {
|
|
720
|
+
font-weight: 600;
|
|
721
|
+
color: var(--dsw-alias-label-primary, #1f2329);
|
|
722
|
+
}
|
|
723
|
+
.hqpu-list {
|
|
724
|
+
list-style: none;
|
|
725
|
+
margin: 0;
|
|
726
|
+
padding: 0;
|
|
727
|
+
display: flex;
|
|
728
|
+
flex-direction: column;
|
|
729
|
+
gap: 6px;
|
|
730
|
+
}
|
|
731
|
+
.hqpu-item {
|
|
732
|
+
display: grid;
|
|
733
|
+
grid-template-columns: 1fr auto auto;
|
|
734
|
+
gap: 8px;
|
|
735
|
+
align-items: center;
|
|
736
|
+
padding: 8px 10px;
|
|
737
|
+
border-radius: 10px;
|
|
738
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.04));
|
|
739
|
+
font-size: 13px;
|
|
740
|
+
}
|
|
741
|
+
.hqpu-item-name {
|
|
742
|
+
font-weight: 500;
|
|
743
|
+
overflow: hidden;
|
|
744
|
+
text-overflow: ellipsis;
|
|
745
|
+
}
|
|
746
|
+
.hqpu-item-version {
|
|
747
|
+
color: var(--dsw-alias-label-secondary, #646a73);
|
|
748
|
+
font-variant-numeric: tabular-nums;
|
|
749
|
+
}
|
|
750
|
+
.hqpu-item-state {
|
|
751
|
+
padding: 1px 8px;
|
|
752
|
+
border-radius: 999px;
|
|
753
|
+
font-size: 11px;
|
|
754
|
+
line-height: 18px;
|
|
755
|
+
}
|
|
756
|
+
.hqpu-state-current { background: rgba(0,0,0,.06); color: var(--dsw-alias-label-secondary, #646a73); }
|
|
757
|
+
.hqpu-state-update, .hqpu-state-install { background: rgba(229,72,77,.12); color: #d92d33; }
|
|
758
|
+
.hqpu-state-ignored { background: rgba(0,0,0,.06); color: var(--dsw-alias-label-secondary, #646a73); }
|
|
759
|
+
.hqpu-state-blocked { background: rgba(229,72,77,.12); color: #d92d33; }
|
|
760
|
+
.hqpu-item-reason {
|
|
761
|
+
grid-column: 1 / -1;
|
|
762
|
+
font-size: 12px;
|
|
763
|
+
color: var(--dsw-alias-label-tertiary, #8f959e);
|
|
764
|
+
}
|
|
765
|
+
.hqpu-ignored-banner {
|
|
766
|
+
display: flex;
|
|
767
|
+
align-items: center;
|
|
768
|
+
gap: 8px;
|
|
769
|
+
padding: 8px 10px;
|
|
770
|
+
border-radius: 10px;
|
|
771
|
+
background: rgba(120,120,120,.1);
|
|
772
|
+
font-size: 12px;
|
|
773
|
+
color: var(--dsw-alias-label-secondary, #646a73);
|
|
774
|
+
}
|
|
775
|
+
.hqpu-empty {
|
|
776
|
+
padding: 24px 0;
|
|
777
|
+
text-align: center;
|
|
778
|
+
color: var(--dsw-alias-label-tertiary, #8f959e);
|
|
779
|
+
font-size: 13px;
|
|
780
|
+
}
|
|
781
|
+
.hqpu-runtime {
|
|
782
|
+
border-top: 1px solid var(--dsw-alias-line-border, rgba(0,0,0,.08));
|
|
783
|
+
padding-top: 12px;
|
|
784
|
+
}
|
|
785
|
+
.hqpu-runtime-title {
|
|
786
|
+
font-size: 13px;
|
|
787
|
+
font-weight: 600;
|
|
788
|
+
}
|
|
789
|
+
.hqpu-runtime-line {
|
|
790
|
+
font-size: 12px;
|
|
791
|
+
color: var(--dsw-alias-label-secondary, #646a73);
|
|
792
|
+
}
|
|
793
|
+
.hqpu-runtime-note {
|
|
794
|
+
font-size: 11px;
|
|
795
|
+
color: var(--dsw-alias-label-tertiary, #8f959e);
|
|
796
|
+
}
|
|
797
|
+
.hqpu-runtime-error {
|
|
798
|
+
font-size: 12px;
|
|
799
|
+
color: #d92d33;
|
|
800
|
+
}
|
|
801
|
+
.hqpu-actions {
|
|
802
|
+
display: flex;
|
|
803
|
+
align-items: center;
|
|
804
|
+
gap: 8px;
|
|
805
|
+
}
|
|
806
|
+
.hqpu-btn {
|
|
807
|
+
box-sizing: border-box;
|
|
808
|
+
padding: 6px 14px;
|
|
809
|
+
border-radius: 8px;
|
|
810
|
+
border: 1px solid transparent;
|
|
811
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.06));
|
|
812
|
+
color: var(--dsw-alias-label-primary, #1f2329);
|
|
813
|
+
font: inherit;
|
|
814
|
+
font-size: 13px;
|
|
815
|
+
cursor: pointer;
|
|
816
|
+
}
|
|
817
|
+
.hqpu-btn-primary {
|
|
818
|
+
background: #e5484d;
|
|
819
|
+
color: #fff;
|
|
820
|
+
}
|
|
821
|
+
.hqpu-btn-primary:disabled {
|
|
822
|
+
opacity: 0.5;
|
|
823
|
+
cursor: not-allowed;
|
|
824
|
+
}
|
|
825
|
+
.hqpu-btn-ghost {
|
|
826
|
+
background: transparent;
|
|
827
|
+
border-color: var(--dsw-alias-line-border, rgba(0,0,0,.12));
|
|
828
|
+
}
|
|
829
|
+
.hqpu-btn-small {
|
|
830
|
+
padding: 2px 10px;
|
|
831
|
+
font-size: 12px;
|
|
832
|
+
}
|
|
833
|
+
.hqpu-installing {
|
|
834
|
+
font-size: 13px;
|
|
835
|
+
color: var(--dsw-alias-label-secondary, #646a73);
|
|
836
|
+
}
|
|
837
|
+
.hqpu-error {
|
|
838
|
+
font-size: 13px;
|
|
839
|
+
color: #d92d33;
|
|
840
|
+
}
|
|
841
|
+
`;
|
|
842
|
+
const STYLE_TAG_ID = "data-plugin-css=\"huaqiu-plugin-update\"";
|
|
843
|
+
/** Inject styles once (idempotent). */
|
|
844
|
+
function injectUpdateStyles() {
|
|
845
|
+
if (typeof document === "undefined") return;
|
|
846
|
+
if (document.querySelector(`style[${STYLE_TAG_ID}]`) !== null) return;
|
|
847
|
+
const tag = document.createElement("style");
|
|
848
|
+
tag.dataset.pluginCss = "huaqiu-plugin-update";
|
|
849
|
+
tag.textContent = `${ENTRY_CSS_TEXT}\n${VIEW_CSS_TEXT}`;
|
|
850
|
+
document.head.appendChild(tag);
|
|
851
|
+
}
|
|
852
|
+
/** In-memory update-report store shared by the badge and the modal. */
|
|
853
|
+
var UpdateStore = class {
|
|
854
|
+
current = null;
|
|
855
|
+
installingFlag = false;
|
|
856
|
+
listeners = /* @__PURE__ */ new Set();
|
|
857
|
+
refreshTimer = null;
|
|
858
|
+
report() {
|
|
859
|
+
return this.current;
|
|
860
|
+
}
|
|
861
|
+
installing() {
|
|
862
|
+
return this.installingFlag;
|
|
863
|
+
}
|
|
864
|
+
subscribe(listener) {
|
|
865
|
+
this.listeners.add(listener);
|
|
866
|
+
return () => {
|
|
867
|
+
this.listeners.delete(listener);
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
/** Badge count per spec §6.3: actionable `update` + `install`, excluding the
|
|
871
|
+
* ignored offer (the server already folds ignore into item states; keep the
|
|
872
|
+
* client-side filter as a defensive second pass). */
|
|
873
|
+
badgeCount() {
|
|
874
|
+
const report = this.current;
|
|
875
|
+
if (report === null) return 0;
|
|
876
|
+
return report.items.filter((item) => item.state === "update" || item.state === "install").length;
|
|
877
|
+
}
|
|
878
|
+
emit() {
|
|
879
|
+
for (const listener of [...this.listeners]) try {
|
|
880
|
+
listener();
|
|
881
|
+
} catch {}
|
|
882
|
+
}
|
|
883
|
+
async refresh() {
|
|
884
|
+
try {
|
|
885
|
+
const report = await fetchReport();
|
|
886
|
+
this.current = report;
|
|
887
|
+
this.emit();
|
|
888
|
+
return report;
|
|
889
|
+
} catch {
|
|
890
|
+
return this.current;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
async check() {
|
|
894
|
+
try {
|
|
895
|
+
const report = await checkUpdates();
|
|
896
|
+
this.current = report;
|
|
897
|
+
this.emit();
|
|
898
|
+
return report;
|
|
899
|
+
} catch {
|
|
900
|
+
return this.current;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
/** Mark the set as installing (set by the modal before the restart). */
|
|
904
|
+
setInstalling(flag) {
|
|
905
|
+
this.installingFlag = flag;
|
|
906
|
+
this.emit();
|
|
907
|
+
}
|
|
908
|
+
/** Idle refresh while mounted (60 s) — no overlapping poll loop. */
|
|
909
|
+
startIdleRefresh() {
|
|
910
|
+
if (this.refreshTimer !== null) return;
|
|
911
|
+
this.refreshTimer = window.setInterval(() => {
|
|
912
|
+
this.refresh();
|
|
913
|
+
}, 6e4);
|
|
914
|
+
}
|
|
915
|
+
stop() {
|
|
916
|
+
if (this.refreshTimer !== null) {
|
|
917
|
+
window.clearInterval(this.refreshTimer);
|
|
918
|
+
this.refreshTimer = null;
|
|
919
|
+
}
|
|
920
|
+
this.listeners.clear();
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
/**
|
|
924
|
+
* Client cordis inject: no service dependencies — the plugin reads the
|
|
925
|
+
* hq-edge report purely through same-origin fetch (forwarded by its own node
|
|
926
|
+
* half). `inject` stays empty so the loader never waits on another service.
|
|
927
|
+
*/
|
|
928
|
+
const inject = [];
|
|
929
|
+
/**
|
|
930
|
+
* Client plugin body — mount the sidebar toolbutton + review modal.
|
|
931
|
+
* @param ctx - real cordis client context.
|
|
932
|
+
* @returns disposer removing the row and all subscriptions.
|
|
933
|
+
*/
|
|
934
|
+
function apply(ctx) {
|
|
935
|
+
if (typeof document === "undefined") return () => {};
|
|
936
|
+
injectUpdateStyles();
|
|
937
|
+
const store = new UpdateStore();
|
|
938
|
+
const copy = {
|
|
939
|
+
zh: COPY.zh,
|
|
940
|
+
en: COPY.en
|
|
941
|
+
};
|
|
942
|
+
const copyFor = () => locale === "en" ? copy.en : copy.zh;
|
|
943
|
+
let locale = detectHostLocale();
|
|
944
|
+
const localeSubscribers = /* @__PURE__ */ new Set();
|
|
945
|
+
const refreshSubscribe = (listener) => {
|
|
946
|
+
localeSubscribers.add(listener);
|
|
947
|
+
return () => {
|
|
948
|
+
localeSubscribers.delete(listener);
|
|
949
|
+
};
|
|
950
|
+
};
|
|
951
|
+
const unsubscribeLocale = observeHostLocale((next) => {
|
|
952
|
+
locale = next;
|
|
953
|
+
for (const fn of [...localeSubscribers]) try {
|
|
954
|
+
fn();
|
|
955
|
+
} catch {}
|
|
956
|
+
});
|
|
957
|
+
const labelState = {
|
|
958
|
+
label: entryCopy(locale).label,
|
|
959
|
+
tooltip: entryCopy(locale).tooltip
|
|
960
|
+
};
|
|
961
|
+
const openView = () => {
|
|
962
|
+
store.check().then((report) => {
|
|
963
|
+
if (report !== null) openUpdateView(store, copyFor());
|
|
964
|
+
});
|
|
965
|
+
};
|
|
966
|
+
const disposeEntry = mountSidebarEntry({
|
|
967
|
+
rowAttribute: UPDATE_ENTRY_ATTRIBUTE,
|
|
968
|
+
rowSelector: UPDATE_ENTRY_SELECTOR,
|
|
969
|
+
plugin: PLUGIN_ID,
|
|
970
|
+
icon: UPDATE_ICON,
|
|
971
|
+
css: ENTRY_CSS,
|
|
972
|
+
label: () => labelState.label,
|
|
973
|
+
tooltip: () => labelState.tooltip,
|
|
974
|
+
refresh: { subscribe: refreshSubscribe },
|
|
975
|
+
badge: {
|
|
976
|
+
subscribe: (listener) => store.subscribe(listener),
|
|
977
|
+
count: () => store.badgeCount()
|
|
978
|
+
},
|
|
979
|
+
onToggle: openView,
|
|
980
|
+
position: "after",
|
|
981
|
+
familySelectors: HUAQIU_SIDEBAR_ENTRY_FAMILY
|
|
982
|
+
});
|
|
983
|
+
store.startIdleRefresh();
|
|
984
|
+
store.refresh();
|
|
985
|
+
const dispose = () => {
|
|
986
|
+
try {
|
|
987
|
+
disposeEntry();
|
|
988
|
+
} catch {}
|
|
989
|
+
unsubscribeLocale();
|
|
990
|
+
store.stop();
|
|
991
|
+
};
|
|
992
|
+
if (typeof ctx.effect === "function") ctx.effect(dispose, "plugin-update");
|
|
993
|
+
return dispose;
|
|
994
|
+
}
|
|
995
|
+
//#endregion
|
|
996
|
+
exports.UPDATE_ENTRY_ATTRIBUTE = UPDATE_ENTRY_ATTRIBUTE;
|
|
997
|
+
exports.UPDATE_ENTRY_SELECTOR = UPDATE_ENTRY_SELECTOR;
|
|
998
|
+
exports.UPDATE_ICON = UPDATE_ICON;
|
|
999
|
+
exports.apply = apply;
|
|
1000
|
+
exports.inject = inject;
|
|
1001
|
+
exports.injectUpdateStyles = injectUpdateStyles;
|
|
1002
|
+
return module.exports;
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
//# sourceMappingURL=client.js.map
|