dsh-app-updater 1.3.2 → 1.3.4

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 CHANGED
@@ -1,1460 +1,1491 @@
1
- window.__ModuleLoader__.load({
2
- id: "dsh-app-updater",
3
- factory: (require) => {
4
- var module = { exports: {} };
5
- var exports = module.exports;
6
- const react = require("react");
7
- const h = react.createElement;
8
-
9
- /* ------------------------------------------------------------------ *
10
- * 常量
11
- * ------------------------------------------------------------------ */
12
-
13
- const STATE_URL = "/dsh-app-updater/state.json";
14
- const SCAN_URL = "/dsh-app-updater/scan";
15
- const UPDATE_URL = "/dsh-app-updater/update";
16
- const CANCEL_URL = "/dsh-app-updater/cancel";
17
- const OPEN_DOWNLOADS_URL = "/dsh-app-updater/open-downloads";
18
- const SETTINGS_URL = "/dsh-app-updater/settings";
19
-
20
- const POLL_BUSY_MS = 900; // 扫描 / 更新进行中
21
- const POLL_IDLE_MS = 5000; // 空闲
22
- const LOG_LIMIT = 100;
23
-
24
- /* 本地记住的界面偏好(localStorage 不可用时静默降级为内存默认值) */
25
- const STORE_COLS_KEY = "dsh-appupd-cols-v1";
26
- const STORE_SETTINGS_OPEN_KEY = "dsh-appupd-settings-open";
27
-
28
- /* 列宽:默认给足,应用名不再被压到看不全;每列可手动拖拽调整 */
29
- const DEFAULT_COLS = { name: 260, version: 96, available: 96, state: 108 };
30
- const RESIZABLE_COLS = ["name", "version", "available", "state"];
31
- const COL_LABELS = { name: "应用", version: "已装版本", available: "可升级到", state: "状态" };
32
- const CHECK_COL_PX = 28;
33
- const OP_COL_PX = 62;
34
- const MIN_COL_PX = 48;
35
- const MAX_COL_PX = 640;
36
-
37
- /* 设置项 → 中文标签与说明。
38
- `only` 限定平台:跨平台的开关不写,平台专属的开关只有对应系统才显示,
39
- 避免 macOS 上出现「自动提权安装」这种 Windows 概念。 */
40
- const TOGGLES = [
41
- {
42
- key: "keepInstallers",
43
- label: "保留安装包",
44
- hint: "关闭后,安装完成即删除该应用刚下载的安装包",
45
- },
46
- {
47
- key: "dryRun",
48
- label: "演练模式",
49
- hint: "只从官网下载完整安装包,不执行安装",
50
- warn: true,
51
- },
52
- {
53
- key: "matchSystemArch",
54
- label: "按本机架构安装包",
55
- hint: "按当前系统与芯片(Windows ARM64 / x64、Apple Silicon / Intel)挑选对应安装包",
56
- },
57
- {
58
- key: "autoElevate",
59
- label: "自动提权安装",
60
- hint: "需要管理员的安装包(MSI、写入 Program Files)改由管理员上下文执行;UAC 开启时每次安装会弹一次确认",
61
- only: "windows",
62
- },
63
- {
64
- key: "hashFallbackUpgrade",
65
- label: "下载失败时回退 winget",
66
- hint: "官网包哈希与目录记录不一致时,改用 winget 安装同源全量包",
67
- only: "windows",
68
- },
69
- {
70
- key: "includeStoreApps",
71
- label: "包含应用商店应用",
72
- hint: "商店 / MSIX 应用只列出,更新仍由 Microsoft Store 负责",
73
- only: "windows",
74
- },
75
- {
76
- key: "includeRegistryOnly",
77
- label: "包含目录未收录应用",
78
- hint: "用注册表补齐 winget 目录未收录的软件(只列出,无法自动更新)",
79
- only: "windows",
80
- },
81
- {
82
- key: "includeStoreApps",
83
- label: "包含 App Store 应用",
84
- hint: "App Store 应用只列出,升级仍由 App Store 负责",
85
- only: "macos",
86
- },
87
- {
88
- key: "includeUnmanagedApps",
89
- label: "包含非 Homebrew 应用",
90
- hint: "把拖 DMG / 手动安装的应用也列出来(只列出,无法自动更新)",
91
- only: "macos",
92
- },
93
- ];
94
-
95
- /** 当前平台该显示的开关(未知平台按 Windows 显示,保持兼容)。 */
96
- function togglesFor(family) {
97
- const fam = family === "macos" ? "macos" : "windows";
98
- return TOGGLES.filter((t) => !t.only || t.only === fam);
99
- }
100
-
101
- const REASON_TEXT = {
102
- "not-in-catalog": "软件目录未收录",
103
- "not-managed": "非 Homebrew 管理",
104
- "unknown-version": "版本未知",
105
- "system-component": "系统组件",
106
- pinned: "版本被固定",
107
- "store-managed": "由应用商店管理",
108
- };
109
-
110
- const SOURCE_TEXT = {
111
- winget: "winget",
112
- msstore: "msstore",
113
- registry: "registry",
114
- homebrew: "homebrew",
115
- appstore: "App Store",
116
- app: "应用",
117
- };
118
-
119
- /** 引擎显示名 */
120
- const ENGINE_TEXT = {
121
- winget: "winget",
122
- homebrew: "Homebrew",
123
- none: "无可用更新引擎",
124
- };
125
-
126
- /* ------------------------------------------------------------------ *
127
- * 样式(宿主 CSS 变量 + fallback,全部 appupd- 前缀)
128
- * ------------------------------------------------------------------ */
129
-
130
- const CSS = [
131
- ".appupd-root{display:flex;flex-direction:column;gap:10px;font-size:13px;color:inherit;min-width:0;}",
132
-
133
- /* 工具条 */
134
- ".appupd-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
135
- ".appupd-title{font-size:14px;font-weight:600;margin-right:auto;color:var(--dsw-alias-label-primary,#f3f4f6);}",
136
- ".appupd-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
137
- ".appupd-btn{font-family:inherit;font-size:12px;line-height:1.5;padding:4px 12px;border-radius:6px;border:1px solid var(--dsw-alias-border-l1,#4b5563);background:var(--dsw-alias-bg-layer-2,#374151);color:var(--dsw-alias-label-primary,#f3f4f6);cursor:pointer;white-space:nowrap;transition:border-color .15s ease,background .15s ease;}",
138
- ".appupd-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#4f8cff);}",
139
- ".appupd-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:1px;}",
140
- ".appupd-btn:disabled{opacity:.45;cursor:default;}",
141
- ".appupd-btn-primary{background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary,#4f8cff));border-color:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary,#4f8cff));color:var(--dsw-alias-label-primary-foreground,#fff);font-weight:600;}",
142
- ".appupd-btn-quiet{background:transparent;}",
143
-
144
- /* 状态行 */
145
- ".appupd-status{display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary,#9ca3af);min-height:18px;}",
146
- ".appupd-status-strong{color:var(--dsw-alias-label-primary,#f3f4f6);}",
147
- ".appupd-err{color:#f87171;}",
148
- ".appupd-sep{opacity:.45;}",
149
-
150
- /* 进度 */
151
- ".appupd-progress{display:flex;flex-direction:column;gap:6px;border:1px solid var(--dsw-alias-border-l1,#4b5563);border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2,rgba(55,65,81,.35));}",
152
- ".appupd-progress-head{display:flex;align-items:center;gap:8px;font-size:12px;min-width:0;color:var(--dsw-alias-label-primary,#f3f4f6);}",
153
- ".appupd-progress-count{font-variant-numeric:tabular-nums;font-weight:600;flex:none;}",
154
- ".appupd-progress-name{color:var(--dsw-alias-label-secondary,#9ca3af);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}",
155
- ".appupd-progress-track{height:6px;border-radius:3px;background:rgba(128,128,128,.28);overflow:hidden;}",
156
- ".appupd-progress-fill{height:100%;border-radius:3px;background:var(--dsw-alias-brand-primary,#4f8cff);transition:width .25s ease;}",
157
-
158
- /* 筛选行 */
159
- ".appupd-filter{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
160
- ".appupd-search{font-family:inherit;font-size:12px;padding:4px 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l1,#4b5563);background:transparent;color:inherit;flex:1 1 200px;max-width:320px;min-width:140px;}",
161
- ".appupd-search::placeholder{color:var(--dsw-alias-label-secondary,#9ca3af);}",
162
- ".appupd-search:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:1px;}",
163
- ".appupd-seg{display:inline-flex;border:1px solid var(--dsw-alias-border-l1,#4b5563);border-radius:6px;overflow:hidden;}",
164
- ".appupd-seg-btn{font-family:inherit;font-size:12px;padding:4px 10px;background:transparent;color:var(--dsw-alias-label-secondary,#9ca3af);border:0;border-right:1px solid var(--dsw-alias-border-l1,#4b5563);cursor:pointer;white-space:nowrap;}",
165
- ".appupd-seg-btn:last-child{border-right:0;}",
166
- ".appupd-seg-btn:hover{color:var(--dsw-alias-label-primary,#f3f4f6);}",
167
- ".appupd-seg-btn-on{background:var(--dsw-alias-bg-layer-2,#374151);color:var(--dsw-alias-label-primary,#f3f4f6);font-weight:600;}",
168
- ".appupd-count{font-size:12px;color:var(--dsw-alias-label-secondary,#9ca3af);margin-left:auto;white-space:nowrap;}",
169
-
170
- /* 表格 */
171
- ".appupd-tablewrap{max-height:min(52vh,520px);overflow-y:auto;overflow-x:auto;border:1px solid var(--dsw-alias-border-l1,#4b5563);border-radius:8px;scrollbar-width:thin;scrollbar-color:rgba(148,163,184,.45) transparent;}",
172
- ".appupd-tablewrap::-webkit-scrollbar{width:8px;height:8px;}",
173
- ".appupd-tablewrap::-webkit-scrollbar-thumb{background:rgba(148,163,184,.4);border-radius:4px;}",
174
- ".appupd-tablewrap::-webkit-scrollbar-track{background:transparent;}",
175
- ".appupd-table{width:100%;border-collapse:separate;border-spacing:0;font-size:12px;table-layout:fixed;}",
176
- ".appupd-table thead th{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-2,#374151);color:var(--dsw-alias-label-secondary,#9ca3af);font-weight:600;font-size:11px;text-align:left;padding:5px 8px;border-bottom:1px solid var(--dsw-alias-border-l1,#4b5563);white-space:nowrap;}",
177
- ".appupd-table tbody td{padding:3px 8px;height:30px;border-bottom:1px solid rgba(128,128,128,.16);vertical-align:middle;overflow:hidden;}",
178
- ".appupd-table tbody tr:last-child td{border-bottom:0;}",
179
- ".appupd-table tbody tr:hover td{background:rgba(128,128,128,.08);}",
180
- ".appupd-th{position:relative;}",
181
- ".appupd-col-check{padding-left:10px !important;padding-right:0 !important;}",
182
- ".appupd-col-ver,.appupd-col-op{white-space:nowrap;}",
183
- ".appupd-col-op{text-align:right;padding-right:10px !important;}",
184
- ".appupd-grip{position:absolute;top:0;right:-3px;width:7px;height:100%;cursor:col-resize;z-index:2;background:transparent;}",
185
- ".appupd-grip::after{content:'';position:absolute;top:22%;right:3px;width:1px;height:56%;background:var(--dsw-alias-border-l1,#4b5563);}",
186
- ".appupd-grip:hover::after,.appupd-grip-on::after{background:var(--dsw-alias-brand-primary,#4f8cff);width:2px;}",
187
- ".appupd-cb{margin:0;accent-color:var(--dsw-alias-brand-primary,#4f8cff);cursor:pointer;vertical-align:middle;}",
188
- ".appupd-cb:disabled{cursor:default;opacity:.45;}",
189
- ".appupd-nameline{display:flex;align-items:center;gap:6px;min-width:0;}",
190
- ".appupd-name{font-size:12px;color:inherit;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}",
191
- ".appupd-pub{font-size:11px;line-height:1.25;color:var(--dsw-alias-label-secondary,#9ca3af);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
192
- ".appupd-ver{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#9ca3af);}",
193
- ".appupd-ver-new{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-brand-primary,#4f8cff);}",
194
- ".appupd-muted{color:var(--dsw-alias-label-secondary,#9ca3af);}",
195
- ".appupd-badge{display:inline-block;font-size:10px;line-height:15px;padding:0 5px;border-radius:9px;border:1px solid var(--dsw-alias-border-l1,#4b5563);color:var(--dsw-alias-label-secondary,#9ca3af);white-space:nowrap;flex:none;}",
196
- ".appupd-badge-winget{border-color:rgba(79,140,255,.55);color:var(--dsw-alias-brand-primary,#4f8cff);}",
197
- ".appupd-badge-msstore{border-color:rgba(52,211,153,.55);color:#34d399;}",
198
- ".appupd-badge-registry{border-color:rgba(148,163,184,.5);}",
199
- ".appupd-badge-homebrew{border-color:rgba(251,191,36,.55);color:#fbbf24;}",
200
- ".appupd-badge-appstore{border-color:rgba(52,211,153,.55);color:#34d399;}",
201
- ".appupd-badge-app{border-color:rgba(148,163,184,.5);}",
202
- ".appupd-envline{font-size:12px;color:var(--dsw-alias-label-primary,#f3f4f6);word-break:break-word;}",
203
- ".appupd-state-new{font-weight:600;color:var(--dsw-alias-brand-primary,#4f8cff);white-space:nowrap;}",
204
- ".appupd-state-ok{color:#34d399;white-space:nowrap;}",
205
- ".appupd-state-none{color:var(--dsw-alias-label-secondary,#9ca3af);white-space:nowrap;}",
206
- ".appupd-reason{font-size:10px;line-height:1.2;color:var(--dsw-alias-label-secondary,#9ca3af);max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
207
- ".appupd-rowbtn{font-family:inherit;font-size:11px;padding:2px 10px;border-radius:5px;border:1px solid var(--dsw-alias-border-l1,#4b5563);background:transparent;color:var(--dsw-alias-label-primary,#f3f4f6);cursor:pointer;white-space:nowrap;}",
208
- ".appupd-rowbtn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#4f8cff);color:var(--dsw-alias-brand-primary,#4f8cff);}",
209
- ".appupd-rowbtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:1px;}",
210
- ".appupd-rowbtn:disabled{opacity:.4;cursor:default;}",
211
- ".appupd-empty-cell{padding:24px 12px !important;text-align:center;color:var(--dsw-alias-label-secondary,#9ca3af);}",
212
-
213
- /* 空态 */
214
- ".appupd-empty{padding:26px 12px;text-align:center;font-size:12px;color:var(--dsw-alias-label-secondary,#9ca3af);border:1px dashed var(--dsw-alias-border-l1,#4b5563);border-radius:8px;}",
215
-
216
- /* 日志 */
217
- ".appupd-log{border:1px solid var(--dsw-alias-border-l1,#4b5563);border-radius:8px;overflow:hidden;}",
218
- ".appupd-log-head{display:flex;align-items:center;gap:6px;width:100%;padding:6px 10px;background:transparent;border:0;font-family:inherit;font-size:12px;color:var(--dsw-alias-label-secondary,#9ca3af);cursor:pointer;text-align:left;}",
219
- ".appupd-log-head:hover{color:var(--dsw-alias-label-primary,#f3f4f6);}",
220
- ".appupd-log-head:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:-2px;}",
221
- ".appupd-log-caret{display:inline-block;width:10px;flex:none;opacity:.8;}",
222
- ".appupd-log-body{max-height:200px;overflow-y:auto;padding:2px 10px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:1.65;scrollbar-width:thin;scrollbar-color:rgba(148,163,184,.45) transparent;}",
223
- ".appupd-log-line{display:flex;gap:8px;white-space:pre-wrap;word-break:break-word;}",
224
- ".appupd-log-time{flex:none;color:var(--dsw-alias-label-secondary,#9ca3af);opacity:.75;}",
225
- ".appupd-lv-error{color:#f87171;}",
226
- ".appupd-lv-warn{color:#fbbf24;}",
227
- ".appupd-lv-success{color:#34d399;}",
228
- ".appupd-lv-info{color:var(--dsw-alias-label-secondary,#9ca3af);}",
229
-
230
- /* 演练模式警示条 */
231
- ".appupd-banner{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:8px;border:1px solid rgba(251,191,36,.5);background:rgba(251,191,36,.12);color:#fbbf24;font-size:12px;}",
232
- /* 兼容模式(仅回环放行)警示条:默认关闭,开启时用红色提醒 */
233
- ".appupd-banner-warn{border-color:rgba(239,68,68,.55);background:rgba(239,68,68,.12);color:#f87171;}",
234
-
235
- /* 插件设置面板 */
236
- ".appupd-panel{border:1px solid var(--dsw-alias-border-l1,#4b5563);border-radius:8px;overflow:hidden;}",
237
- ".appupd-panel-head{display:flex;align-items:center;gap:6px;width:100%;padding:6px 10px;background:transparent;border:0;font-family:inherit;font-size:12px;color:var(--dsw-alias-label-secondary,#9ca3af);cursor:pointer;text-align:left;}",
238
- ".appupd-panel-head:hover{color:var(--dsw-alias-label-primary,#f3f4f6);}",
239
- ".appupd-panel-head:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:-2px;}",
240
- ".appupd-panel-body{display:flex;flex-direction:column;gap:9px;padding:2px 10px 10px;border-top:1px solid rgba(128,128,128,.16);}",
241
- ".appupd-field{display:flex;align-items:flex-start;gap:10px;}",
242
- ".appupd-field-label{flex:0 0 152px;padding-top:3px;font-size:12px;color:var(--dsw-alias-label-primary,#f3f4f6);}",
243
- ".appupd-field-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:3px;}",
244
- ".appupd-field-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;}",
245
- ".appupd-hint{font-size:11px;line-height:1.35;color:var(--dsw-alias-label-secondary,#9ca3af);}",
246
- ".appupd-input{font-family:inherit;font-size:12px;padding:4px 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l1,#4b5563);background:transparent;color:inherit;flex:1 1 200px;min-width:140px;}",
247
- ".appupd-input::placeholder{color:var(--dsw-alias-label-secondary,#9ca3af);}",
248
- ".appupd-input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:1px;}",
249
- ".appupd-switch{position:relative;flex:none;width:36px;height:20px;margin-top:2px;border-radius:10px;border:1px solid var(--dsw-alias-border-l1,#4b5563);background:rgba(128,128,128,.35);cursor:pointer;padding:0;transition:background .16s ease,border-color .16s ease;}",
250
- ".appupd-switch::after{content:'';position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.3);transition:left .16s ease;}",
251
- ".appupd-switch-on{background:var(--dsw-alias-brand-primary,#4f8cff);border-color:var(--dsw-alias-brand-primary,#4f8cff);}",
252
- ".appupd-switch-on::after{left:18px;}",
253
- ".appupd-switch:disabled{opacity:.45;cursor:default;}",
254
- ".appupd-switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:2px;}",
255
-
256
- /* 降低动效 */
257
- "@media (prefers-reduced-motion: reduce){.appupd-btn,.appupd-progress-fill,.appupd-rowbtn,.appupd-seg-btn,.appupd-switch,.appupd-switch::after{transition:none !important;}}",
258
- ].join("");
259
-
260
- /* ------------------------------------------------------------------ *
261
- * 工具函数
262
- * ------------------------------------------------------------------ */
263
-
264
- function errText(e) {
265
- if (!e) return "未知错误";
266
- if (typeof e === "string") return e;
267
- const m = e.message || e.error || "";
268
- return String(m || e);
269
- }
270
-
271
- async function getJson(url) {
272
- const r = await fetch(url, { cache: "no-store", credentials: "same-origin" });
273
- if (!r.ok) throw new Error("HTTP " + r.status);
274
- return r.json();
275
- }
276
-
277
- async function postJson(url, body) {
278
- const r = await fetch(url, {
279
- method: "POST",
280
- cache: "no-store",
281
- credentials: "same-origin",
282
- headers: { "content-type": "application/json" },
283
- body: JSON.stringify(body || {}),
284
- });
285
- let j = null;
286
- try {
287
- j = await r.json();
288
- } catch (e) {
289
- j = null;
290
- }
291
- if (!r.ok) throw new Error((j && (j.error || j.message)) || "HTTP " + r.status);
292
- if (j && j.ok === false) throw new Error(j.error || "请求被拒绝");
293
- return j;
294
- }
295
-
296
- function pad2(n) {
297
- return n < 10 ? "0" + n : String(n);
298
- }
299
-
300
- function fmtClock(ts) {
301
- const d = new Date(typeof ts === "number" && ts > 0 ? ts : Date.now());
302
- return pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
303
- }
304
-
305
- function fmtRel(ts, now) {
306
- if (typeof ts !== "number" || !isFinite(ts) || ts <= 0) return "从未";
307
- const diff = Math.max(0, (now || Date.now()) - ts);
308
- if (diff < 5000) return "刚刚";
309
- if (diff < 60000) return Math.floor(diff / 1000) + " 秒前";
310
- if (diff < 3600000) return Math.floor(diff / 60000) + " 分钟前";
311
- if (diff < 86400000) return Math.floor(diff / 3600000) + " 小时前";
312
- return Math.floor(diff / 86400000) + " 天前";
313
- }
314
-
315
- function reasonText(app) {
316
- const r = app && app.reason;
317
- if (!r) return "";
318
- return REASON_TEXT[r] || String(r);
319
- }
320
-
321
- /** 引擎状态文案:winget 1.29.380 / Homebrew 4.5.0 / 无可用更新引擎 */
322
- function engineTextOf(engine) {
323
- if (!engine || !engine.id) return "引擎状态未知";
324
- const name = ENGINE_TEXT[engine.id] || engine.id;
325
- if (engine.available === false) return name + " 不可用";
326
- return engine.version ? name + " " + engine.version : name + " 可用";
327
- }
328
-
329
- /* ---------------- 本地偏好(列宽 / 面板展开) ---------------- */
330
-
331
- /** localStorage 包装:不可用(隐私模式、无 window)时静默降级为默认值。 */
332
- function readStore(key, fallback) {
333
- try {
334
- if (typeof window === "undefined" || !window.localStorage) return fallback;
335
- const v = window.localStorage.getItem(key);
336
- return v === null ? fallback : v;
337
- } catch (e) {
338
- return fallback;
339
- }
340
- }
341
-
342
- function writeStore(key, value) {
343
- try {
344
- if (typeof window === "undefined" || !window.localStorage) return;
345
- window.localStorage.setItem(key, String(value));
346
- } catch (e) {
347
- /* 存不下就只当记不住偏好,不影响功能 */
348
- }
349
- }
350
-
351
- function clampWidth(px) {
352
- const n = Math.round(Number(px) || 0);
353
- if (!isFinite(n)) return MIN_COL_PX;
354
- return Math.max(MIN_COL_PX, Math.min(MAX_COL_PX, n));
355
- }
356
-
357
- function loadCols() {
358
- const out = Object.assign({}, DEFAULT_COLS);
359
- const raw = readStore(STORE_COLS_KEY, "");
360
- if (!raw) return out;
361
- try {
362
- const parsed = JSON.parse(raw);
363
- if (parsed && typeof parsed === "object") {
364
- for (const key of RESIZABLE_COLS) {
365
- if (typeof parsed[key] === "number") out[key] = clampWidth(parsed[key]);
366
- }
367
- }
368
- } catch (e) {
369
- /* 存的内容坏了就用默认值 */
370
- }
371
- return out;
372
- }
373
-
374
- function saveCols(cols) {
375
- try {
376
- const slim = {};
377
- for (const key of RESIZABLE_COLS) slim[key] = clampWidth(cols[key]);
378
- writeStore(STORE_COLS_KEY, JSON.stringify(slim));
379
- } catch (e) {
380
- /* 忽略 */
381
- }
382
- }
383
-
384
- /** 下载目录校验:留空 = 使用默认目录;否则必须是绝对路径(盘符或 UNC)。 */
385
- const DRIVE_ABS_RE = /^[A-Za-z]:[\\/]/;
386
- const UNC_ABS_RE = /^\\\\[^\\]/;
387
-
388
- function isAbsoluteDir(p) {
389
- const v = String(p == null ? "" : p).trim();
390
- if (!v) return true;
391
- return DRIVE_ABS_RE.test(v) || UNC_ABS_RE.test(v);
392
- }
393
-
394
- function toggleLabel(key) {
395
- for (const t of TOGGLES) {
396
- if (t.key === key) return t.label;
397
- }
398
- return key;
399
- }
400
-
401
- /* ------------------------------------------------------------------ *
402
- * 设置页
403
- * ------------------------------------------------------------------ */
404
-
405
- function AppUpdaterSection() {
406
- const [state, setState] = react.useState(null);
407
- const [loadError, setLoadError] = react.useState(null);
408
- const [localLog, setLocalLog] = react.useState([]);
409
- const [query, setQuery] = react.useState("");
410
- const [mode, setMode] = react.useState("all");
411
- const [selected, setSelected] = react.useState({});
412
- const [busy, setBusy] = react.useState({ scan: false, update: false, cancel: false, open: false });
413
- const [logOpen, setLogOpen] = react.useState(false);
414
- const [now, setNow] = react.useState(Date.now());
415
-
416
- const phaseRef = react.useRef("idle");
417
- const inFlightRef = react.useRef(false);
418
- const pollFailRef = react.useRef(false);
419
-
420
- /* 列宽(可拖拽调整、本地记住) */
421
- const [cols, setCols] = react.useState(loadCols);
422
- const [resizingCol, setResizingCol] = react.useState(null);
423
- const colsRef = react.useRef(cols);
424
- const dragRef = react.useRef(null);
425
- const dragCleanupRef = react.useRef(null);
426
-
427
- /* 设置面板 */
428
- const [settingsOpen, setSettingsOpen] = react.useState(
429
- () => readStore(STORE_SETTINGS_OPEN_KEY, "1") === "1"
430
- );
431
- const [dirDraft, setDirDraft] = react.useState("");
432
- const [dirTouched, setDirTouched] = react.useState(false);
433
- const [settingsBusy, setSettingsBusy] = react.useState(false);
434
- const [settingsError, setSettingsError] = react.useState(null);
435
-
436
- const pushLog = react.useCallback((level, text) => {
437
- setLocalLog((prev) => {
438
- const next = prev.concat([{ at: Date.now(), level: level, text: String(text) }]);
439
- return next.length > LOG_LIMIT ? next.slice(next.length - LOG_LIMIT) : next;
440
- });
441
- }, []);
442
-
443
- const refresh = react.useCallback(async () => {
444
- if (inFlightRef.current) return;
445
- inFlightRef.current = true;
446
- try {
447
- const j = await getJson(STATE_URL);
448
- if (j && typeof j === "object") {
449
- setState(j);
450
- setLoadError(null);
451
- pollFailRef.current = false;
452
- }
453
- } catch (e) {
454
- setLoadError(errText(e));
455
- if (!pollFailRef.current) {
456
- pollFailRef.current = true;
457
- pushLog("error", "读取状态失败:" + errText(e));
458
- }
459
- } finally {
460
- inFlightRef.current = false;
461
- }
462
- }, [pushLog]);
463
-
464
- /* 让轮询循环读到最新的 phase,而不必重建定时器 */
465
- react.useEffect(() => {
466
- phaseRef.current = (state && state.phase) || "idle";
467
- }, [state]);
468
-
469
- /* 相对时间每 10s 刷新一次 */
470
- react.useEffect(() => {
471
- const id = setInterval(() => setNow(Date.now()), 10000);
472
- return () => clearInterval(id);
473
- }, []);
474
-
475
- /* 轮询:忙 900ms / 空闲 5000ms;页面不可见时暂停 */
476
- react.useEffect(() => {
477
- let timer = null;
478
- let disposed = false;
479
-
480
- const clearTimer = () => {
481
- if (timer !== null) {
482
- clearTimeout(timer);
483
- timer = null;
484
- }
485
- };
486
- const isVisible = () =>
487
- typeof document === "undefined" || document.visibilityState === "visible";
488
- const schedule = () => {
489
- clearTimer();
490
- if (disposed) return;
491
- timer = setTimeout(tick, phaseRef.current !== "idle" ? POLL_BUSY_MS : POLL_IDLE_MS);
492
- };
493
- const tick = async () => {
494
- if (disposed) return;
495
- if (!isVisible()) {
496
- clearTimer();
497
- return;
498
- }
499
- await refresh();
500
- schedule();
501
- };
502
- const onVisibility = () => {
503
- if (disposed) return;
504
- if (isVisible()) {
505
- clearTimer();
506
- tick();
507
- } else {
508
- clearTimer();
509
- }
510
- };
511
-
512
- tick();
513
- if (typeof document !== "undefined") {
514
- document.addEventListener("visibilitychange", onVisibility);
515
- }
516
- return () => {
517
- disposed = true;
518
- clearTimer();
519
- if (typeof document !== "undefined") {
520
- document.removeEventListener("visibilitychange", onVisibility);
521
- }
522
- };
523
- }, [refresh]);
524
-
525
- /* ---------------- 列宽:拖拽调整 + 本地记忆 ---------------- */
526
-
527
- react.useEffect(() => {
528
- colsRef.current = cols;
529
- }, [cols]);
530
-
531
- /* 组件卸载时,若还在拖拽则摘掉 document 上的监听 */
532
- react.useEffect(() => {
533
- return () => {
534
- if (dragCleanupRef.current) dragCleanupRef.current();
535
- dragCleanupRef.current = null;
536
- dragRef.current = null;
537
- };
538
- }, []);
539
-
540
- const applyDrag = (clientX) => {
541
- const drag = dragRef.current;
542
- if (!drag) return;
543
- const width = clampWidth(drag.startWidth + (clientX - drag.startX));
544
- setCols((prev) => (prev[drag.key] === width ? prev : Object.assign({}, prev, { [drag.key]: width })));
545
- };
546
-
547
- const onColResizeStart = (key, e) => {
548
- if (!e || (e.button !== undefined && e.button !== 0)) return;
549
- if (typeof document === "undefined") return;
550
- e.preventDefault();
551
- e.stopPropagation();
552
- if (dragCleanupRef.current) dragCleanupRef.current();
553
-
554
- dragRef.current = { key: key, startX: e.clientX, startWidth: colsRef.current[key] || DEFAULT_COLS[key] };
555
- setResizingCol(key);
556
-
557
- const onMove = (ev) => applyDrag(ev.clientX);
558
- const finish = () => {
559
- document.removeEventListener("mousemove", onMove);
560
- document.removeEventListener("mouseup", finish);
561
- dragRef.current = null;
562
- dragCleanupRef.current = null;
563
- setResizingCol(null);
564
- saveCols(colsRef.current);
565
- };
566
- const cleanup = () => {
567
- document.removeEventListener("mousemove", onMove);
568
- document.removeEventListener("mouseup", finish);
569
- };
570
- dragCleanupRef.current = cleanup;
571
- document.addEventListener("mousemove", onMove);
572
- document.addEventListener("mouseup", finish);
573
- };
574
-
575
- const onColReset = (key, e) => {
576
- if (e) {
577
- e.preventDefault();
578
- e.stopPropagation();
579
- }
580
- setCols((prev) => {
581
- const next = Object.assign({}, prev, { [key]: DEFAULT_COLS[key] });
582
- saveCols(next);
583
- return next;
584
- });
585
- };
586
-
587
- /* ---------------- 设置面板 ---------------- */
588
-
589
- /* 宿主返回的生效目录:只在用户没动过输入框时同步到草稿 */
590
- react.useEffect(() => {
591
- if (dirTouched) return;
592
- const s = (state && state.settings) || null;
593
- setDirDraft(s && typeof s.downloadDir === "string" ? s.downloadDir : "");
594
- }, [state, dirTouched]);
595
-
596
- const toggleSettingsPanel = () => {
597
- setSettingsOpen((v) => {
598
- const next = !v;
599
- writeStore(STORE_SETTINGS_OPEN_KEY, next ? "1" : "0");
600
- return next;
601
- });
602
- };
603
-
604
- const saveSettings = react.useCallback(
605
- async (patch, okText) => {
606
- setSettingsBusy(true);
607
- setSettingsError(null);
608
- try {
609
- const r = await postJson(SETTINGS_URL, { confirm: true, settings: patch });
610
- if (okText) pushLog("success", okText);
611
- await refresh();
612
- return true;
613
- } catch (e) {
614
- const msg = errText(e);
615
- setSettingsError(msg);
616
- pushLog("error", "保存设置失败:" + msg);
617
- return false;
618
- } finally {
619
- setSettingsBusy(false);
620
- }
621
- },
622
- [pushLog, refresh]
623
- );
624
-
625
- const toggleSetting = (key) => {
626
- const s = (state && state.settings) || {};
627
- const next = !s[key];
628
- saveSettings({ [key]: next }, (toggleLabel(key) || key) + (next ? " 已开启" : " 已关闭"));
629
- };
630
-
631
- const saveDir = async () => {
632
- const value = String(dirDraft || "").trim();
633
- if (!isAbsoluteDir(value)) {
634
- setSettingsError("下载目录必须是绝对路径(如 D:\\Apps)或 UNC 路径(\\\\server\\share)");
635
- return;
636
- }
637
- const ok = await saveSettings({ downloadDir: value }, value ? "下载目录已保存" : "已恢复默认下载目录");
638
- if (ok) setDirTouched(false);
639
- };
640
-
641
- const resetDir = () => {
642
- setDirDraft("");
643
- setDirTouched(false);
644
- saveSettings({ downloadDir: "" }, "已恢复默认下载目录");
645
- };
646
-
647
- /* ---------------- 派生数据 ---------------- */
648
-
649
- const phase = (state && state.phase) || "idle";
650
- const scanning = phase === "scanning";
651
- const updating = phase === "updating";
652
- const apps = state && state.apps ? state.apps : null;
653
- const items = apps && Array.isArray(apps.items) ? apps.items : null;
654
- const updateInfo = (state && state.update) || {};
655
- const winget = (state && state.winget) || null;
656
- const platform = state && state.platform;
657
-
658
- /* 运行环境(系统 / 架构)与更新引擎 */
659
- const env = (state && state.env) || null;
660
- const engine = (state && state.engine) || null;
661
- const caps = (state && state.capabilities) || null;
662
- const envSummary =
663
- (state && state.envSummary) ||
664
- (env ? [env.osName || env.platform, env.archLabel].filter(Boolean).join(" · ") : "环境未知");
665
- const engineText = !state ? "" : engineTextOf(engine);
666
- /* 平台族:设置面板与空状态文案都要用,必须在下面的组件定义之前算出来 */
667
- const family = (env && env.family) || (platform === "darwin" ? "macos" : "windows");
668
-
669
- const filtered = react.useMemo(() => {
670
- if (!items) return [];
671
- const q = query.trim().toLowerCase();
672
- const out = [];
673
- for (let i = 0; i < items.length; i++) {
674
- const a = items[i];
675
- if (!a) continue;
676
- const canUpdate = !!(a.hasUpdate && a.updateSupported);
677
- if (mode === "updatable" && !canUpdate) continue;
678
- if (mode === "unsupported" && a.updateSupported) continue;
679
- if (q) {
680
- const hay =
681
- String(a.name || "") +
682
- " " +
683
- String(a.publisher || "") +
684
- " " +
685
- String(a.id || "");
686
- if (hay.toLowerCase().indexOf(q) < 0) continue;
687
- }
688
- out.push(a);
689
- }
690
- return out;
691
- }, [items, query, mode]);
692
-
693
- const selectableIds = react.useMemo(() => {
694
- const out = [];
695
- for (let i = 0; i < filtered.length; i++) {
696
- const a = filtered[i];
697
- if (a && a.hasUpdate && a.updateSupported && a.id) out.push(a.id);
698
- }
699
- return out;
700
- }, [filtered]);
701
-
702
- const selectedCount = react.useMemo(() => {
703
- let n = 0;
704
- for (let i = 0; i < selectableIds.length; i++) {
705
- if (selected[selectableIds[i]]) n++;
706
- }
707
- return n;
708
- }, [selectableIds, selected]);
709
-
710
- const allSelected = selectableIds.length > 0 && selectedCount === selectableIds.length;
711
-
712
- const total = apps && apps.stats && typeof apps.stats.total === "number" ? apps.stats.total : items ? items.length : 0;
713
- const upgradable =
714
- apps && apps.stats && typeof apps.stats.upgradable === "number"
715
- ? apps.stats.upgradable
716
- : selectableIds.length;
717
-
718
- const mergedLog = react.useMemo(() => {
719
- const server = state && Array.isArray(state.log) ? state.log : [];
720
- const all = server.concat(localLog);
721
- all.sort((a, b) => (Number(a && a.at) || 0) - (Number(b && b.at) || 0));
722
- return all.slice(all.length > LOG_LIMIT ? all.length - LOG_LIMIT : 0);
723
- }, [state, localLog]);
724
-
725
- /* ---------------- 动作 ---------------- */
726
-
727
- const doScan = async () => {
728
- if (busy.scan || phase !== "idle") return;
729
- setBusy((b) => ({ ...b, scan: true }));
730
- try {
731
- await postJson(SCAN_URL, { confirm: true, force: true });
732
- pushLog("info", "已开始扫描本机应用…");
733
- await refresh();
734
- } catch (e) {
735
- pushLog("error", "启动扫描失败:" + errText(e));
736
- } finally {
737
- setBusy((b) => ({ ...b, scan: false }));
738
- }
739
- };
740
-
741
- const runUpdate = async (ids, label) => {
742
- const list = (ids || []).filter(Boolean);
743
- if (!list.length) return;
744
- setBusy((b) => ({ ...b, update: true }));
745
- try {
746
- const r = await postJson(UPDATE_URL, { confirm: true, ids: list, mode: "silent" });
747
- pushLog(
748
- "info",
749
- "已提交 " + ((r && r.queued) || list.length) + " 个更新任务" + (label ? "(" + label + ")" : "")
750
- );
751
- setSelected({});
752
- await refresh();
753
- } catch (e) {
754
- pushLog("error", "启动更新失败:" + errText(e));
755
- } finally {
756
- setBusy((b) => ({ ...b, update: false }));
757
- }
758
- };
759
-
760
- const doCancel = async () => {
761
- if (busy.cancel) return;
762
- setBusy((b) => ({ ...b, cancel: true }));
763
- try {
764
- await postJson(CANCEL_URL, { confirm: true });
765
- pushLog("warn", "已请求取消更新");
766
- await refresh();
767
- } catch (e) {
768
- pushLog("error", "取消失败:" + errText(e));
769
- } finally {
770
- setBusy((b) => ({ ...b, cancel: false }));
771
- }
772
- };
773
-
774
- const doOpenDownloads = async () => {
775
- if (busy.open) return;
776
- setBusy((b) => ({ ...b, open: true }));
777
- try {
778
- const r = await postJson(OPEN_DOWNLOADS_URL, { confirm: true });
779
- pushLog("success", "已打开下载目录" + (r && r.path ? ":" + r.path : ""));
780
- } catch (e) {
781
- pushLog("error", "打开下载目录失败:" + errText(e));
782
- } finally {
783
- setBusy((b) => ({ ...b, open: false }));
784
- }
785
- };
786
-
787
- const toggleOne = (id, enabled) => {
788
- if (!enabled) return;
789
- setSelected((prev) => {
790
- const next = { ...prev };
791
- if (next[id]) delete next[id];
792
- else next[id] = true;
793
- return next;
794
- });
795
- };
796
-
797
- const toggleAll = () => {
798
- if (!selectableIds.length) return;
799
- setSelected((prev) => {
800
- if (allSelected) {
801
- const next = { ...prev };
802
- for (let i = 0; i < selectableIds.length; i++) delete next[selectableIds[i]];
803
- return next;
804
- }
805
- const next = { ...prev };
806
- for (let i = 0; i < selectableIds.length; i++) next[selectableIds[i]] = true;
807
- return next;
808
- });
809
- };
810
-
811
- const updateAll = () => runUpdate(selectableIds, "当前筛选");
812
- const updateSelected = () => {
813
- const ids = selectableIds.filter((id) => selected[id]);
814
- return runUpdate(ids, "已选 " + ids.length + " 项");
815
- };
816
-
817
- /* ---------------- 渲染片段 ---------------- */
818
-
819
- const btn = (label, onClick, opts) => {
820
- const o = opts || {};
821
- return h(
822
- "button",
823
- {
824
- type: "button",
825
- className:
826
- "appupd-btn" +
827
- (o.primary ? " appupd-btn-primary" : "") +
828
- (o.quiet ? " appupd-btn-quiet" : ""),
829
- onClick: onClick,
830
- disabled: !!o.disabled,
831
- title: o.title || undefined,
832
- },
833
- label
834
- );
835
- };
836
-
837
- const statusCell = (a) => {
838
- if (a.hasUpdate && a.updateSupported) {
839
- return h("span", { className: "appupd-state-new" }, "有新版本");
840
- }
841
- if (a.hasUpdate && !a.updateSupported) {
842
- const r = reasonText(a);
843
- return h(
844
- "span",
845
- { className: "appupd-state-none", title: r ? "无法更新:" + r : "无法更新" },
846
- "未收录"
847
- );
848
- }
849
- return h("span", { className: "appupd-state-ok" }, "已是最新");
850
- };
851
-
852
- const rows = filtered.map((a) => {
853
- const key = a.id || a.name;
854
- const canUpdate = !!(a.hasUpdate && a.updateSupported);
855
- const checked = canUpdate && !!selected[key];
856
- const reason = canUpdate ? "" : reasonText(a);
857
- const rowBusy = phase !== "idle" || busy.update;
858
- return h(
859
- "tr",
860
- { key: key, className: "appupd-row" },
861
- h(
862
- "td",
863
- { className: "appupd-col-check" },
864
- h("input", {
865
- type: "checkbox",
866
- className: "appupd-cb",
867
- checked: checked,
868
- disabled: !canUpdate,
869
- onChange: () => toggleOne(key, canUpdate),
870
- "aria-label": "选择 " + String(a.name || key),
871
- title: reason || "选择以批量更新",
872
- })
873
- ),
874
- h(
875
- "td",
876
- null,
877
- h(
878
- "div",
879
- { className: "appupd-nameline" },
880
- h(
881
- "span",
882
- {
883
- className: "appupd-name",
884
- title: String(a.name || key) + (a.id ? "\n" + a.id : ""),
885
- },
886
- a.name || key
887
- ),
888
- h(
889
- "span",
890
- { className: "appupd-badge appupd-badge-" + String(a.source || "registry") },
891
- SOURCE_TEXT[a.source] || a.source || "未知"
892
- )
893
- ),
894
- a.publisher && a.publisher !== a.name
895
- ? h("div", { className: "appupd-pub", title: a.publisher }, a.publisher)
896
- : null
897
- ),
898
- h("td", { className: "appupd-col-ver" }, h("span", { className: "appupd-ver" }, a.version || "—")),
899
- h(
900
- "td",
901
- { className: "appupd-col-ver" },
902
- a.hasUpdate && a.available
903
- ? h("span", { className: "appupd-ver-new" }, a.available)
904
- : h("span", { className: "appupd-muted" }, "—")
905
- ),
906
- h(
907
- "td",
908
- { className: "appupd-col-state" },
909
- statusCell(a),
910
- reason ? h("div", { className: "appupd-reason", title: reason }, reason) : null
911
- ),
912
- h(
913
- "td",
914
- { className: "appupd-col-op" },
915
- h(
916
- "button",
917
- {
918
- type: "button",
919
- className: "appupd-rowbtn",
920
- disabled: !canUpdate || rowBusy,
921
- onClick: () => runUpdate([key], a.name),
922
- title: !canUpdate
923
- ? reason
924
- : rowBusy
925
- ? "更新进行中,请稍候"
926
- : "更新到 " + (a.available || "最新版"),
927
- },
928
- "更新"
929
- )
930
- )
931
- );
932
- });
933
-
934
- const emptyBody = () => {
935
- if (!state) return h("div", { className: "appupd-empty" }, "正在读取状态…");
936
- if (!items) {
937
- return h(
938
- "div",
939
- { className: "appupd-empty" },
940
- "还没有检测过,点右上角『刷新检测』开始"
941
- );
942
- }
943
- if (!items.length) {
944
- // 分平台给出可执行的下一步,而不是一句「未检测到任何已安装应用」
945
- return h(
946
- "div",
947
- { className: "appupd-empty" },
948
- family === "macos"
949
- ? "未在应用目录里找到应用。若确认本机有应用,请看看下面的运行日志里「枚举应用目录」的记录;" +
950
- "应用装在非标准位置时本插件看不到它。"
951
- : "未检测到任何已安装应用"
952
- );
953
- }
954
- return h("div", { className: "appupd-empty" }, "没有匹配的应用");
955
- };
956
-
957
- const body = !items || !filtered.length ? emptyBody() : null;
958
-
959
- /* ---------------- 设置面板 / 演练提示 ---------------- */
960
-
961
- const settings = (state && state.settings) || {};
962
- const dryRunOn = Boolean(settings.dryRun);
963
- const visibleToggles = togglesFor(family);
964
- const effectiveDir =
965
- (state && state.downloadDir) ||
966
- (family === "macos" ? "~/Downloads/DSH App Updater" : "%USERPROFILE%\\Downloads\\DSH App Updater");
967
-
968
- const dryRunBanner = dryRunOn
969
- ? h(
970
- "div",
971
- { className: "appupd-banner", role: "status" },
972
- h("span", null, "演练模式已开启:只从官网下载全量安装包,不会真正安装。")
973
- )
974
- : null;
975
-
976
- /* 兼容模式:只有用户显式开启(插件配置 / 环境变量)才会出现,默认不显示 */
977
- const admission = (state && state.admission) || null;
978
- const legacyOn = Boolean(admission && admission.allowLegacyLoopback);
979
- const legacyBanner = legacyOn
980
- ? h(
981
- "div",
982
- { className: "appupd-banner appupd-banner-warn", role: "alert" },
983
- h(
984
- "span",
985
- null,
986
- "兼容模式已开启:没有连接凭据的本机回环请求会被放行(旧宿主没有 connection 准入时才会用到)。非必要请关闭,见 README 的「安全」一节。"
987
- )
988
- )
989
- : null;
990
-
991
- const switchEl = (key) => {
992
- const on = Boolean(settings[key]);
993
- return h("button", {
994
- type: "button",
995
- className: "appupd-switch" + (on ? " appupd-switch-on" : ""),
996
- role: "switch",
997
- "aria-checked": on,
998
- "aria-label": toggleLabel(key),
999
- disabled: settingsBusy || !state,
1000
- onClick: () => toggleSetting(key),
1001
- title: on ? "点击关闭" : "点击开启",
1002
- });
1003
- };
1004
-
1005
- const settingsPanel = h(
1006
- "div",
1007
- { className: "appupd-panel" },
1008
- h(
1009
- "button",
1010
- {
1011
- type: "button",
1012
- className: "appupd-panel-head",
1013
- onClick: toggleSettingsPanel,
1014
- "aria-expanded": settingsOpen,
1015
- },
1016
- h("span", { className: "appupd-log-caret" }, settingsOpen ? "▾" : "▸"),
1017
- "插件设置"
1018
- ),
1019
- settingsOpen
1020
- ? h(
1021
- "div",
1022
- { className: "appupd-panel-body" },
1023
- h(
1024
- "div",
1025
- { className: "appupd-field" },
1026
- h("div", { className: "appupd-field-label" }, "运行环境"),
1027
- h(
1028
- "div",
1029
- { className: "appupd-field-main" },
1030
- h(
1031
- "div",
1032
- { className: "appupd-field-row" },
1033
- h("span", { className: "appupd-envline" }, envSummary + (engineText ? " · " + engineText : ""))
1034
- ),
1035
- env && env.cpuModel
1036
- ? h(
1037
- "div",
1038
- { className: "appupd-hint" },
1039
- env.cpuModel + (env.cpuCount ? " · " + env.cpuCount + " 线程" : "")
1040
- )
1041
- : null
1042
- )
1043
- ),
1044
- caps && !caps.scan && caps.note
1045
- ? h("div", { className: "appupd-err", style: { fontSize: 12 } }, caps.note)
1046
- : null,
1047
- h(
1048
- "div",
1049
- { className: "appupd-field" },
1050
- h("div", { className: "appupd-field-label" }, "安装包下载目录"),
1051
- h(
1052
- "div",
1053
- { className: "appupd-field-main" },
1054
- h(
1055
- "div",
1056
- { className: "appupd-field-row" },
1057
- h("input", {
1058
- className: "appupd-input",
1059
- type: "text",
1060
- value: dirDraft,
1061
- placeholder: effectiveDir,
1062
- onChange: (e) => {
1063
- setDirTouched(true);
1064
- setSettingsError(null);
1065
- setDirDraft(e.target.value);
1066
- },
1067
- onKeyDown: (e) => {
1068
- if (e.key === "Enter") saveDir();
1069
- },
1070
- "aria-label": "安装包下载目录",
1071
- }),
1072
- h(
1073
- "button",
1074
- { type: "button", className: "appupd-rowbtn", disabled: settingsBusy, onClick: saveDir },
1075
- settingsBusy ? "保存中…" : "保存"
1076
- ),
1077
- h(
1078
- "button",
1079
- { type: "button", className: "appupd-rowbtn", disabled: settingsBusy, onClick: resetDir },
1080
- "恢复默认"
1081
- )
1082
- ),
1083
- h(
1084
- "div",
1085
- { className: "appupd-hint" },
1086
- "留空 = 默认目录(" + effectiveDir + ");从官网下载的完整安装包与对应清单一并保存在该目录"
1087
- )
1088
- )
1089
- ),
1090
- ...visibleToggles.map((t) =>
1091
- h(
1092
- "div",
1093
- { className: "appupd-field", key: t.key },
1094
- h("div", { className: "appupd-field-label" }, t.label),
1095
- h(
1096
- "div",
1097
- { className: "appupd-field-main" },
1098
- h(
1099
- "div",
1100
- { className: "appupd-field-row" },
1101
- switchEl(t.key),
1102
- h("span", { className: "appupd-hint" }, t.hint)
1103
- )
1104
- )
1105
- )
1106
- ),
1107
- settingsError ? h("div", { className: "appupd-err", style: { fontSize: 12 } }, settingsError) : null
1108
- )
1109
- : null
1110
- );
1111
-
1112
- /* 表格:总宽 = 各列固定宽度之和,超出容器就横向滚动(列宽即所见) */
1113
- const colsWidth = CHECK_COL_PX + cols.name + cols.version + cols.available + cols.state + OP_COL_PX;
1114
-
1115
- const grip = (key) =>
1116
- h("span", {
1117
- className: "appupd-grip" + (resizingCol === key ? " appupd-grip-on" : ""),
1118
- onMouseDown: (e) => onColResizeStart(key, e),
1119
- onDoubleClick: (e) => onColReset(key, e),
1120
- title:
1121
- "拖动调整「" + COL_LABELS[key] + "」列宽(当前 " + cols[key] + "px);双击恢复默认",
1122
- "aria-hidden": "true",
1123
- });
1124
-
1125
- const progress =
1126
- updating || updateInfo.running
1127
- ? (() => {
1128
- const t = Number(updateInfo.total) || 0;
1129
- const d = Number(updateInfo.done) || 0;
1130
- const pct = t > 0 ? Math.max(0, Math.min(100, Math.round((d / t) * 100))) : 0;
1131
- return h(
1132
- "div",
1133
- { className: "appupd-progress" },
1134
- h(
1135
- "div",
1136
- { className: "appupd-progress-head" },
1137
- h("span", { className: "appupd-progress-count" }, d + "/" + t),
1138
- h(
1139
- "span",
1140
- { className: "appupd-progress-name", title: updateInfo.current || "" },
1141
- updateInfo.current ? "正在处理 " + updateInfo.current : "正在准备…"
1142
- ),
1143
- h("span", { className: "appupd-progress-count" }, pct + "%")
1144
- ),
1145
- h(
1146
- "div",
1147
- { className: "appupd-progress-track" },
1148
- h("div", { className: "appupd-progress-fill", style: { width: pct + "%" } })
1149
- ),
1150
- updateInfo.error
1151
- ? h("div", { className: "appupd-err", style: { fontSize: 12 } }, "更新出错:" + updateInfo.error)
1152
- : null
1153
- );
1154
- })()
1155
- : null;
1156
-
1157
- const statusRow = h(
1158
- "div",
1159
- { className: "appupd-status" },
1160
- !state
1161
- ? h("span", null, loadError ? h("span", { className: "appupd-err" }, "无法连接宿主:" + loadError) : "正在读取状态…")
1162
- : h(
1163
- "span",
1164
- engine && engine.available === false ? { className: "appupd-err" } : null,
1165
- engineText
1166
- ),
1167
- state ? h("span", { className: "appupd-sep" }, "·") : null,
1168
- state
1169
- ? h(
1170
- "span",
1171
- { className: "appupd-status-strong", title: "操作系统与 CPU 架构" },
1172
- envSummary
1173
- )
1174
- : null,
1175
- state ? h("span", { className: "appupd-sep" }, "·") : null,
1176
- state
1177
- ? h("span", null, "上次扫描:" + fmtRel(apps && apps.scannedAt, now))
1178
- : null,
1179
- state ? h("span", { className: "appupd-sep" }, "·") : null,
1180
- state
1181
- ? h(
1182
- "span",
1183
- { className: "appupd-status-strong" },
1184
- "共 " + total + " 个应用 · " + upgradable + " 个可更新"
1185
- )
1186
- : null,
1187
- scanning ? h("span", null, "扫描中…") : null,
1188
- caps && caps.note ? h("span", { className: "appupd-err" }, caps.note) : null,
1189
- loadError && state ? h("span", { className: "appupd-err" }, "(状态刷新失败:" + loadError + ")") : null
1190
- );
1191
-
1192
- const logLines = mergedLog.map((l, i) => {
1193
- const level = l && l.level ? String(l.level) : "info";
1194
- const cls =
1195
- level === "error"
1196
- ? "appupd-lv-error"
1197
- : level === "warn"
1198
- ? "appupd-lv-warn"
1199
- : level === "success"
1200
- ? "appupd-lv-success"
1201
- : "appupd-lv-info";
1202
- return h(
1203
- "div",
1204
- { key: i, className: "appupd-log-line" },
1205
- h("span", { className: "appupd-log-time" }, fmtClock(l && l.at)),
1206
- h("span", { className: cls }, (l && l.text) || "")
1207
- );
1208
- });
1209
-
1210
- return h(
1211
- "div",
1212
- { className: "appupd-root" },
1213
- /* 1. 工具条 */
1214
- h(
1215
- "div",
1216
- { className: "appupd-bar" },
1217
- h("div", { className: "appupd-title" }, "应用更新"),
1218
- h(
1219
- "div",
1220
- { className: "appupd-actions" },
1221
- btn(busy.scan || scanning ? "检测中…" : "刷新检测", doScan, {
1222
- primary: true,
1223
- disabled: busy.scan || phase !== "idle",
1224
- title: phase !== "idle" ? "当前有任务在执行" : "扫描本机已安装应用并检查更新",
1225
- }),
1226
- selectedCount > 0
1227
- ? btn("更新所选 (" + selectedCount + ")", updateSelected, {
1228
- disabled: phase !== "idle" || busy.update,
1229
- title: "仅更新勾选的应用",
1230
- })
1231
- : null,
1232
- btn("全部更新", updateAll, {
1233
- disabled: phase !== "idle" || busy.update || selectableIds.length === 0,
1234
- title:
1235
- selectableIds.length === 0
1236
- ? "当前筛选下没有可更新的应用"
1237
- : "更新当前筛选下 " + selectableIds.length + " 个应用",
1238
- }),
1239
- btn(busy.cancel ? "取消中…" : "取消", doCancel, {
1240
- quiet: true,
1241
- disabled: !updating || busy.cancel,
1242
- title: updating ? "取消尚未开始的更新" : "当前没有进行中的更新",
1243
- }),
1244
- btn("打开下载目录", doOpenDownloads, {
1245
- quiet: true,
1246
- disabled: busy.open,
1247
- title:
1248
- (state && state.settings && state.settings.downloadDir) || "在资源管理器中打开安装包下载目录",
1249
- })
1250
- )
1251
- ),
1252
-
1253
- /* 2. 状态行 */
1254
- statusRow,
1255
-
1256
- /* 3. 演练模式提示 */
1257
- dryRunBanner,
1258
-
1259
- /* 3b. 兼容模式(仅回环放行)提示:默认不出现 */
1260
- legacyBanner,
1261
-
1262
- /* 4. 进度 */
1263
- progress,
1264
-
1265
- /* 5. 插件设置 */
1266
- settingsPanel,
1267
-
1268
- /* 6. 筛选行 */
1269
- items && items.length
1270
- ? h(
1271
- "div",
1272
- { className: "appupd-filter" },
1273
- h("input", {
1274
- type: "search",
1275
- className: "appupd-search",
1276
- placeholder: "搜索应用名 / 发布者 / ID",
1277
- value: query,
1278
- onChange: (e) => setQuery(e.target.value),
1279
- "aria-label": "搜索应用",
1280
- }),
1281
- h(
1282
- "div",
1283
- { className: "appupd-seg", role: "group", "aria-label": "筛选" },
1284
- h(
1285
- "button",
1286
- {
1287
- type: "button",
1288
- className: "appupd-seg-btn" + (mode === "all" ? " appupd-seg-btn-on" : ""),
1289
- onClick: () => setMode("all"),
1290
- "aria-pressed": mode === "all",
1291
- },
1292
- "全部"
1293
- ),
1294
- h(
1295
- "button",
1296
- {
1297
- type: "button",
1298
- className: "appupd-seg-btn" + (mode === "updatable" ? " appupd-seg-btn-on" : ""),
1299
- onClick: () => setMode("updatable"),
1300
- "aria-pressed": mode === "updatable",
1301
- },
1302
- "仅可更新"
1303
- ),
1304
- h(
1305
- "button",
1306
- {
1307
- type: "button",
1308
- className: "appupd-seg-btn" + (mode === "unsupported" ? " appupd-seg-btn-on" : ""),
1309
- onClick: () => setMode("unsupported"),
1310
- "aria-pressed": mode === "unsupported",
1311
- },
1312
- "未收录"
1313
- )
1314
- ),
1315
- h(
1316
- "span",
1317
- { className: "appupd-count" },
1318
- "筛选结果 " + filtered.length + " / " + items.length
1319
- )
1320
- )
1321
- : null,
1322
-
1323
- /* 7. 表格 */
1324
- body
1325
- ? body
1326
- : h(
1327
- "div",
1328
- { className: "appupd-tablewrap" },
1329
- h(
1330
- "table",
1331
- {
1332
- className: "appupd-table",
1333
- style: {
1334
- width: colsWidth + "px",
1335
- minWidth: "100%",
1336
- },
1337
- },
1338
- h(
1339
- "colgroup",
1340
- null,
1341
- h("col", { style: { width: CHECK_COL_PX + "px" } }),
1342
- h("col", { style: { width: cols.name + "px" } }),
1343
- h("col", { style: { width: cols.version + "px" } }),
1344
- h("col", { style: { width: cols.available + "px" } }),
1345
- h("col", { style: { width: cols.state + "px" } }),
1346
- h("col", { style: { width: OP_COL_PX + "px" } })
1347
- ),
1348
- h(
1349
- "thead",
1350
- null,
1351
- h(
1352
- "tr",
1353
- null,
1354
- h(
1355
- "th",
1356
- { className: "appupd-col-check" },
1357
- h("input", {
1358
- type: "checkbox",
1359
- className: "appupd-cb",
1360
- checked: allSelected,
1361
- disabled: selectableIds.length === 0,
1362
- onChange: toggleAll,
1363
- "aria-label": "全选可更新的应用",
1364
- title:
1365
- selectableIds.length === 0
1366
- ? "当前筛选下没有可更新的应用"
1367
- : allSelected
1368
- ? "全不选"
1369
- : "全选",
1370
- ref: (el) => {
1371
- if (el) el.indeterminate = !allSelected && selectedCount > 0;
1372
- },
1373
- })
1374
- ),
1375
- h("th", { className: "appupd-th" }, "应用", grip("name")),
1376
- h("th", { className: "appupd-th appupd-col-ver" }, "已装版本", grip("version")),
1377
- h("th", { className: "appupd-th appupd-col-ver" }, "可升级到", grip("available")),
1378
- h("th", { className: "appupd-th appupd-col-state" }, "状态", grip("state")),
1379
- h("th", { className: "appupd-col-op" }, "操作")
1380
- )
1381
- ),
1382
- h(
1383
- "tbody",
1384
- null,
1385
- rows.length
1386
- ? rows
1387
- : h(
1388
- "tr",
1389
- null,
1390
- h("td", { colSpan: 6, className: "appupd-empty-cell" }, "没有匹配的应用")
1391
- )
1392
- )
1393
- )
1394
- ),
1395
-
1396
- /* 6. 日志 */
1397
- h(
1398
- "div",
1399
- { className: "appupd-log" },
1400
- h(
1401
- "button",
1402
- {
1403
- type: "button",
1404
- className: "appupd-log-head",
1405
- onClick: () => setLogOpen((v) => !v),
1406
- "aria-expanded": logOpen,
1407
- },
1408
- h("span", { className: "appupd-log-caret" }, logOpen ? "▾" : "▸"),
1409
- h("span", null, "运行日志(最近 " + mergedLog.length + " 条)")
1410
- ),
1411
- logOpen
1412
- ? h(
1413
- "div",
1414
- { className: "appupd-log-body" },
1415
- logLines.length ? logLines : h("div", { className: "appupd-lv-info" }, "暂无日志")
1416
- )
1417
- : null
1418
- )
1419
- );
1420
- }
1421
-
1422
- /* ------------------------------------------------------------------ *
1423
- * 插件入口
1424
- * ------------------------------------------------------------------ */
1425
-
1426
- function apply(ctx) {
1427
- ctx.effect(() => {
1428
- if (typeof document === "undefined") return;
1429
- const prev = document.querySelector('style[data-plugin-css="dsh-app-updater"]');
1430
- if (prev && prev.parentNode) prev.parentNode.removeChild(prev);
1431
- const tag = document.createElement("style");
1432
- tag.dataset.plugin = "dsh-app-updater";
1433
- tag.dataset.pluginCss = "dsh-app-updater";
1434
- tag.textContent = CSS;
1435
- document.head.appendChild(tag);
1436
- return () => {
1437
- if (tag.parentNode) tag.parentNode.removeChild(tag);
1438
- };
1439
- }, "dsh-app-updater: styles");
1440
-
1441
- ctx.inject(["slots", "locale"], (scope) => {
1442
- scope.slots.inject("settings.section", () =>
1443
- scope.slots.register(
1444
- {
1445
- name: "settings.section",
1446
- id: "dsh-app-updater",
1447
- order: 42,
1448
- label: () => "应用更新",
1449
- },
1450
- AppUpdaterSection
1451
- )
1452
- );
1453
- });
1454
- }
1455
-
1456
- exports.apply = apply;
1457
- exports.inject = ["slots", "locale"];
1458
- return module.exports;
1459
- },
1460
- });
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-app-updater",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ const react = require("react");
7
+ const h = react.createElement;
8
+
9
+ /* ------------------------------------------------------------------ *
10
+ * 常量
11
+ * ------------------------------------------------------------------ */
12
+
13
+ const STATE_URL = "/dsh-app-updater/state.json";
14
+ const SCAN_URL = "/dsh-app-updater/scan";
15
+ const UPDATE_URL = "/dsh-app-updater/update";
16
+ const CANCEL_URL = "/dsh-app-updater/cancel";
17
+ const OPEN_DOWNLOADS_URL = "/dsh-app-updater/open-downloads";
18
+ const SETTINGS_URL = "/dsh-app-updater/settings";
19
+
20
+ const POLL_BUSY_MS = 900; // 扫描 / 更新进行中
21
+ const POLL_IDLE_MS = 5000; // 空闲
22
+ const LOG_LIMIT = 100;
23
+
24
+ /* 本地记住的界面偏好(localStorage 不可用时静默降级为内存默认值) */
25
+ const STORE_COLS_KEY = "dsh-appupd-cols-v1";
26
+ const STORE_SETTINGS_OPEN_KEY = "dsh-appupd-settings-open";
27
+
28
+ /* 列宽:默认给足,应用名不再被压到看不全;每列可手动拖拽调整 */
29
+ const DEFAULT_COLS = { name: 260, version: 96, available: 96, state: 108 };
30
+ const RESIZABLE_COLS = ["name", "version", "available", "state"];
31
+ const COL_LABELS = { name: "应用", version: "已装版本", available: "可升级到", state: "状态" };
32
+ const CHECK_COL_PX = 28;
33
+ const OP_COL_PX = 62;
34
+ const MIN_COL_PX = 48;
35
+ const MAX_COL_PX = 640;
36
+
37
+ /* 设置项 → 中文标签与说明。
38
+ `only` 限定平台:跨平台的开关不写,平台专属的开关只有对应系统才显示,
39
+ 避免 macOS 上出现「自动提权安装」这种 Windows 概念。 */
40
+ const TOGGLES = [
41
+ {
42
+ key: "keepInstallers",
43
+ label: "保留安装包",
44
+ hint: "关闭后,安装完成即删除该应用刚下载的安装包",
45
+ },
46
+ {
47
+ key: "dryRun",
48
+ label: "演练模式",
49
+ hint: "只从官网下载完整安装包,不执行安装",
50
+ warn: true,
51
+ },
52
+ {
53
+ key: "matchSystemArch",
54
+ label: "按本机架构安装包",
55
+ hint: "按当前系统与芯片(Windows ARM64 / x64、Apple Silicon / Intel)挑选对应安装包",
56
+ },
57
+ {
58
+ key: "autoElevate",
59
+ label: "自动提权安装",
60
+ hint: "需要管理员的安装包(MSI、写入 Program Files)改由管理员上下文执行;UAC 开启时每次安装会弹一次确认",
61
+ only: "windows",
62
+ },
63
+ {
64
+ key: "hashFallbackUpgrade",
65
+ label: "下载失败时回退 winget",
66
+ hint: "官网包哈希与目录记录不一致时,改用 winget 安装同源全量包",
67
+ only: "windows",
68
+ },
69
+ {
70
+ key: "includeStoreApps",
71
+ label: "包含应用商店应用",
72
+ hint: "商店 / MSIX 应用只列出,更新仍由 Microsoft Store 负责",
73
+ only: "windows",
74
+ },
75
+ {
76
+ key: "includeRegistryOnly",
77
+ label: "包含目录未收录应用",
78
+ hint: "用注册表补齐 winget 目录未收录的软件(只列出,无法自动更新)",
79
+ only: "windows",
80
+ },
81
+ {
82
+ key: "includeStoreApps",
83
+ label: "包含 App Store 应用",
84
+ hint: "App Store 应用只列出,升级仍由 App Store 负责",
85
+ only: "macos",
86
+ },
87
+ {
88
+ key: "includeUnmanagedApps",
89
+ label: "包含非 Homebrew 应用",
90
+ hint: "把拖 DMG / 手动安装的应用也列出来(只列出,无法自动更新)",
91
+ only: "macos",
92
+ },
93
+ ];
94
+
95
+ /** 当前平台该显示的开关(未知平台按 Windows 显示,保持兼容)。 */
96
+ function togglesFor(family) {
97
+ const fam = family === "macos" ? "macos" : "windows";
98
+ return TOGGLES.filter((t) => !t.only || t.only === fam);
99
+ }
100
+
101
+ const REASON_TEXT = {
102
+ "not-in-catalog": "软件目录未收录",
103
+ "not-managed": "非 Homebrew 管理",
104
+ "unknown-version": "版本未知",
105
+ "system-component": "系统组件",
106
+ pinned: "版本被固定",
107
+ "store-managed": "由应用商店管理",
108
+ };
109
+
110
+ const SOURCE_TEXT = {
111
+ winget: "winget",
112
+ msstore: "msstore",
113
+ registry: "registry",
114
+ homebrew: "homebrew",
115
+ appstore: "App Store",
116
+ app: "应用",
117
+ };
118
+
119
+ /** 引擎显示名 */
120
+ const ENGINE_TEXT = {
121
+ winget: "winget",
122
+ homebrew: "Homebrew",
123
+ none: "无可用更新引擎",
124
+ };
125
+
126
+ /* ------------------------------------------------------------------ *
127
+ * 样式(宿主 CSS 变量 + fallback,全部 appupd- 前缀)
128
+ * ------------------------------------------------------------------ */
129
+
130
+ const CSS = [
131
+ ".appupd-root{display:flex;flex-direction:column;gap:10px;font-size:13px;color:inherit;min-width:0;}",
132
+
133
+ /* 工具条 */
134
+ ".appupd-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
135
+ ".appupd-title{font-size:14px;font-weight:600;margin-right:auto;color:var(--dsw-alias-label-primary,#f3f4f6);}",
136
+ ".appupd-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
137
+ ".appupd-btn{font-family:inherit;font-size:12px;line-height:1.5;padding:4px 12px;border-radius:6px;border:1px solid var(--dsw-alias-border-l1,#4b5563);background:var(--dsw-alias-bg-layer-2,#374151);color:var(--dsw-alias-label-primary,#f3f4f6);cursor:pointer;white-space:nowrap;transition:border-color .15s ease,background .15s ease;}",
138
+ ".appupd-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#4f8cff);}",
139
+ ".appupd-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:1px;}",
140
+ ".appupd-btn:disabled{opacity:.45;cursor:default;}",
141
+ ".appupd-btn-primary{background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary,#4f8cff));border-color:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary,#4f8cff));color:var(--dsw-alias-label-primary-foreground,#fff);font-weight:600;}",
142
+ ".appupd-btn-quiet{background:transparent;}",
143
+
144
+ /* 状态行 */
145
+ ".appupd-status{display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary,#9ca3af);min-height:18px;}",
146
+ ".appupd-status-strong{color:var(--dsw-alias-label-primary,#f3f4f6);}",
147
+ ".appupd-err{color:#f87171;}",
148
+ ".appupd-sep{opacity:.45;}",
149
+
150
+ /* 进度 */
151
+ ".appupd-progress{display:flex;flex-direction:column;gap:6px;border:1px solid var(--dsw-alias-border-l1,#4b5563);border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2,rgba(55,65,81,.35));}",
152
+ ".appupd-progress-head{display:flex;align-items:center;gap:8px;font-size:12px;min-width:0;color:var(--dsw-alias-label-primary,#f3f4f6);}",
153
+ ".appupd-progress-count{font-variant-numeric:tabular-nums;font-weight:600;flex:none;}",
154
+ ".appupd-progress-name{color:var(--dsw-alias-label-secondary,#9ca3af);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}",
155
+ ".appupd-progress-track{height:6px;border-radius:3px;background:rgba(128,128,128,.28);overflow:hidden;}",
156
+ ".appupd-progress-fill{height:100%;border-radius:3px;background:var(--dsw-alias-brand-primary,#4f8cff);transition:width .25s ease;}",
157
+
158
+ /* 筛选行 */
159
+ ".appupd-filter{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
160
+ ".appupd-search{font-family:inherit;font-size:12px;padding:4px 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l1,#4b5563);background:transparent;color:inherit;flex:1 1 200px;max-width:320px;min-width:140px;}",
161
+ ".appupd-search::placeholder{color:var(--dsw-alias-label-secondary,#9ca3af);}",
162
+ ".appupd-search:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:1px;}",
163
+ ".appupd-seg{display:inline-flex;border:1px solid var(--dsw-alias-border-l1,#4b5563);border-radius:6px;overflow:hidden;}",
164
+ ".appupd-seg-btn{font-family:inherit;font-size:12px;padding:4px 10px;background:transparent;color:var(--dsw-alias-label-secondary,#9ca3af);border:0;border-right:1px solid var(--dsw-alias-border-l1,#4b5563);cursor:pointer;white-space:nowrap;}",
165
+ ".appupd-seg-btn:last-child{border-right:0;}",
166
+ ".appupd-seg-btn:hover{color:var(--dsw-alias-label-primary,#f3f4f6);}",
167
+ ".appupd-seg-btn-on{background:var(--dsw-alias-bg-layer-2,#374151);color:var(--dsw-alias-label-primary,#f3f4f6);font-weight:600;}",
168
+ ".appupd-count{font-size:12px;color:var(--dsw-alias-label-secondary,#9ca3af);margin-left:auto;white-space:nowrap;}",
169
+
170
+ /* 表格 */
171
+ ".appupd-tablewrap{max-height:min(52vh,520px);overflow-y:auto;overflow-x:auto;border:1px solid var(--dsw-alias-border-l1,#4b5563);border-radius:8px;scrollbar-width:thin;scrollbar-color:rgba(148,163,184,.45) transparent;}",
172
+ ".appupd-tablewrap::-webkit-scrollbar{width:8px;height:8px;}",
173
+ ".appupd-tablewrap::-webkit-scrollbar-thumb{background:rgba(148,163,184,.4);border-radius:4px;}",
174
+ ".appupd-tablewrap::-webkit-scrollbar-track{background:transparent;}",
175
+ ".appupd-table{width:100%;border-collapse:separate;border-spacing:0;font-size:12px;table-layout:fixed;}",
176
+ ".appupd-table thead th{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-2,#374151);color:var(--dsw-alias-label-secondary,#9ca3af);font-weight:600;font-size:11px;text-align:left;padding:5px 8px;border-bottom:1px solid var(--dsw-alias-border-l1,#4b5563);white-space:nowrap;}",
177
+ ".appupd-table tbody td{padding:3px 8px;height:30px;border-bottom:1px solid rgba(128,128,128,.16);vertical-align:middle;overflow:hidden;}",
178
+ ".appupd-table tbody tr:last-child td{border-bottom:0;}",
179
+ ".appupd-table tbody tr:hover td{background:rgba(128,128,128,.08);}",
180
+ ".appupd-th{position:relative;}",
181
+ ".appupd-col-check{padding-left:10px !important;padding-right:0 !important;}",
182
+ ".appupd-col-ver,.appupd-col-op{white-space:nowrap;}",
183
+ ".appupd-col-op{text-align:right;padding-right:10px !important;}",
184
+ ".appupd-grip{position:absolute;top:0;right:-3px;width:7px;height:100%;cursor:col-resize;z-index:2;background:transparent;}",
185
+ ".appupd-grip::after{content:'';position:absolute;top:22%;right:3px;width:1px;height:56%;background:var(--dsw-alias-border-l1,#4b5563);}",
186
+ ".appupd-grip:hover::after,.appupd-grip-on::after{background:var(--dsw-alias-brand-primary,#4f8cff);width:2px;}",
187
+ ".appupd-cb{margin:0;accent-color:var(--dsw-alias-brand-primary,#4f8cff);cursor:pointer;vertical-align:middle;}",
188
+ ".appupd-cb:disabled{cursor:default;opacity:.45;}",
189
+ ".appupd-nameline{display:flex;align-items:center;gap:6px;min-width:0;}",
190
+ ".appupd-name{font-size:12px;color:inherit;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}",
191
+ ".appupd-pub{font-size:11px;line-height:1.25;color:var(--dsw-alias-label-secondary,#9ca3af);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
192
+ ".appupd-ver{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#9ca3af);}",
193
+ ".appupd-ver-new{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-brand-primary,#4f8cff);}",
194
+ ".appupd-muted{color:var(--dsw-alias-label-secondary,#9ca3af);}",
195
+ ".appupd-badge{display:inline-block;font-size:10px;line-height:15px;padding:0 5px;border-radius:9px;border:1px solid var(--dsw-alias-border-l1,#4b5563);color:var(--dsw-alias-label-secondary,#9ca3af);white-space:nowrap;flex:none;}",
196
+ ".appupd-badge-winget{border-color:rgba(79,140,255,.55);color:var(--dsw-alias-brand-primary,#4f8cff);}",
197
+ ".appupd-badge-msstore{border-color:rgba(52,211,153,.55);color:#34d399;}",
198
+ ".appupd-badge-registry{border-color:rgba(148,163,184,.5);}",
199
+ ".appupd-badge-homebrew{border-color:rgba(251,191,36,.55);color:#fbbf24;}",
200
+ ".appupd-badge-appstore{border-color:rgba(52,211,153,.55);color:#34d399;}",
201
+ ".appupd-badge-app{border-color:rgba(148,163,184,.5);}",
202
+ ".appupd-envline{font-size:12px;color:var(--dsw-alias-label-primary,#f3f4f6);word-break:break-word;}",
203
+ ".appupd-state-new{font-weight:600;color:var(--dsw-alias-brand-primary,#4f8cff);white-space:nowrap;}",
204
+ ".appupd-state-ok{color:#34d399;white-space:nowrap;}",
205
+ ".appupd-state-none{color:var(--dsw-alias-label-secondary,#9ca3af);white-space:nowrap;}",
206
+ ".appupd-reason{font-size:10px;line-height:1.2;color:var(--dsw-alias-label-secondary,#9ca3af);max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
207
+ ".appupd-rowbtn{font-family:inherit;font-size:11px;padding:2px 10px;border-radius:5px;border:1px solid var(--dsw-alias-border-l1,#4b5563);background:transparent;color:var(--dsw-alias-label-primary,#f3f4f6);cursor:pointer;white-space:nowrap;}",
208
+ ".appupd-rowbtn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#4f8cff);color:var(--dsw-alias-brand-primary,#4f8cff);}",
209
+ ".appupd-rowbtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:1px;}",
210
+ ".appupd-rowbtn:disabled{opacity:.4;cursor:default;}",
211
+ ".appupd-empty-cell{padding:24px 12px !important;text-align:center;color:var(--dsw-alias-label-secondary,#9ca3af);}",
212
+
213
+ /* 空态 */
214
+ ".appupd-empty{padding:26px 12px;text-align:center;font-size:12px;color:var(--dsw-alias-label-secondary,#9ca3af);border:1px dashed var(--dsw-alias-border-l1,#4b5563);border-radius:8px;}",
215
+
216
+ /* 日志 */
217
+ ".appupd-log{border:1px solid var(--dsw-alias-border-l1,#4b5563);border-radius:8px;overflow:hidden;}",
218
+ ".appupd-log-head{display:flex;align-items:center;gap:6px;width:100%;padding:6px 10px;background:transparent;border:0;font-family:inherit;font-size:12px;color:var(--dsw-alias-label-secondary,#9ca3af);cursor:pointer;text-align:left;}",
219
+ ".appupd-log-head:hover{color:var(--dsw-alias-label-primary,#f3f4f6);}",
220
+ ".appupd-log-head:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:-2px;}",
221
+ ".appupd-log-caret{display:inline-block;width:10px;flex:none;opacity:.8;}",
222
+ ".appupd-log-body{max-height:200px;overflow-y:auto;padding:2px 10px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:1.65;scrollbar-width:thin;scrollbar-color:rgba(148,163,184,.45) transparent;}",
223
+ ".appupd-log-line{display:flex;gap:8px;white-space:pre-wrap;word-break:break-word;}",
224
+ ".appupd-log-time{flex:none;color:var(--dsw-alias-label-secondary,#9ca3af);opacity:.75;}",
225
+ ".appupd-lv-error{color:#f87171;}",
226
+ ".appupd-lv-warn{color:#fbbf24;}",
227
+ ".appupd-lv-success{color:#34d399;}",
228
+ ".appupd-lv-info{color:var(--dsw-alias-label-secondary,#9ca3af);}",
229
+
230
+ /* 演练模式警示条 */
231
+ ".appupd-banner{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:8px;border:1px solid rgba(251,191,36,.5);background:rgba(251,191,36,.12);color:#fbbf24;font-size:12px;}",
232
+ /* 兼容模式(仅回环放行)警示条:默认关闭,开启时用红色提醒 */
233
+ ".appupd-banner-warn{border-color:rgba(239,68,68,.55);background:rgba(239,68,68,.12);color:#f87171;}",
234
+ ".appupd-warn-detail{margin-left:auto;font-size:11px;color:inherit;}",
235
+ ".appupd-warn-detail summary{cursor:pointer;}",
236
+ ".appupd-warn-detail ul{margin:4px 0 0;padding-left:16px;max-height:140px;overflow:auto;}",
237
+
238
+ /* 插件设置面板 */
239
+ ".appupd-panel{border:1px solid var(--dsw-alias-border-l1,#4b5563);border-radius:8px;overflow:hidden;}",
240
+ ".appupd-panel-head{display:flex;align-items:center;gap:6px;width:100%;padding:6px 10px;background:transparent;border:0;font-family:inherit;font-size:12px;color:var(--dsw-alias-label-secondary,#9ca3af);cursor:pointer;text-align:left;}",
241
+ ".appupd-panel-head:hover{color:var(--dsw-alias-label-primary,#f3f4f6);}",
242
+ ".appupd-panel-head:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:-2px;}",
243
+ ".appupd-panel-body{display:flex;flex-direction:column;gap:9px;padding:2px 10px 10px;border-top:1px solid rgba(128,128,128,.16);}",
244
+ ".appupd-field{display:flex;align-items:flex-start;gap:10px;}",
245
+ ".appupd-field-label{flex:0 0 152px;padding-top:3px;font-size:12px;color:var(--dsw-alias-label-primary,#f3f4f6);}",
246
+ ".appupd-field-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:3px;}",
247
+ ".appupd-field-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;}",
248
+ ".appupd-hint{font-size:11px;line-height:1.35;color:var(--dsw-alias-label-secondary,#9ca3af);}",
249
+ ".appupd-input{font-family:inherit;font-size:12px;padding:4px 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l1,#4b5563);background:transparent;color:inherit;flex:1 1 200px;min-width:140px;}",
250
+ ".appupd-input::placeholder{color:var(--dsw-alias-label-secondary,#9ca3af);}",
251
+ ".appupd-input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:1px;}",
252
+ ".appupd-switch{position:relative;flex:none;width:36px;height:20px;margin-top:2px;border-radius:10px;border:1px solid var(--dsw-alias-border-l1,#4b5563);background:rgba(128,128,128,.35);cursor:pointer;padding:0;transition:background .16s ease,border-color .16s ease;}",
253
+ ".appupd-switch::after{content:'';position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.3);transition:left .16s ease;}",
254
+ ".appupd-switch-on{background:var(--dsw-alias-brand-primary,#4f8cff);border-color:var(--dsw-alias-brand-primary,#4f8cff);}",
255
+ ".appupd-switch-on::after{left:18px;}",
256
+ ".appupd-switch:disabled{opacity:.45;cursor:default;}",
257
+ ".appupd-switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:2px;}",
258
+
259
+ /* 降低动效 */
260
+ "@media (prefers-reduced-motion: reduce){.appupd-btn,.appupd-progress-fill,.appupd-rowbtn,.appupd-seg-btn,.appupd-switch,.appupd-switch::after{transition:none !important;}}",
261
+ ].join("");
262
+
263
+ /* ------------------------------------------------------------------ *
264
+ * 工具函数
265
+ * ------------------------------------------------------------------ */
266
+
267
+ function errText(e) {
268
+ if (!e) return "未知错误";
269
+ if (typeof e === "string") return e;
270
+ const m = e.message || e.error || "";
271
+ return String(m || e);
272
+ }
273
+
274
+ async function getJson(url) {
275
+ const r = await fetch(url, { cache: "no-store", credentials: "same-origin" });
276
+ if (!r.ok) throw new Error("HTTP " + r.status);
277
+ return r.json();
278
+ }
279
+
280
+ async function postJson(url, body) {
281
+ const r = await fetch(url, {
282
+ method: "POST",
283
+ cache: "no-store",
284
+ credentials: "same-origin",
285
+ headers: { "content-type": "application/json" },
286
+ body: JSON.stringify(body || {}),
287
+ });
288
+ let j = null;
289
+ try {
290
+ j = await r.json();
291
+ } catch (e) {
292
+ j = null;
293
+ }
294
+ if (!r.ok) throw new Error((j && (j.error || j.message)) || "HTTP " + r.status);
295
+ if (j && j.ok === false) throw new Error(j.error || "请求被拒绝");
296
+ return j;
297
+ }
298
+
299
+ function pad2(n) {
300
+ return n < 10 ? "0" + n : String(n);
301
+ }
302
+
303
+ function fmtClock(ts) {
304
+ const d = new Date(typeof ts === "number" && ts > 0 ? ts : Date.now());
305
+ return pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
306
+ }
307
+
308
+ function fmtRel(ts, now) {
309
+ if (typeof ts !== "number" || !isFinite(ts) || ts <= 0) return "从未";
310
+ const diff = Math.max(0, (now || Date.now()) - ts);
311
+ if (diff < 5000) return "刚刚";
312
+ if (diff < 60000) return Math.floor(diff / 1000) + " 秒前";
313
+ if (diff < 3600000) return Math.floor(diff / 60000) + " 分钟前";
314
+ if (diff < 86400000) return Math.floor(diff / 3600000) + " 小时前";
315
+ return Math.floor(diff / 86400000) + " 天前";
316
+ }
317
+
318
+ function reasonText(app) {
319
+ const r = app && app.reason;
320
+ if (!r) return "";
321
+ return REASON_TEXT[r] || String(r);
322
+ }
323
+
324
+ /** 引擎状态文案:winget 1.29.380 / Homebrew 4.5.0 / 无可用更新引擎 */
325
+ function engineTextOf(engine) {
326
+ if (!engine || !engine.id) return "引擎状态未知";
327
+ const name = ENGINE_TEXT[engine.id] || engine.id;
328
+ if (engine.available === false) return name + " 不可用";
329
+ return engine.version ? name + " " + engine.version : name + " 可用";
330
+ }
331
+
332
+ /* ---------------- 本地偏好(列宽 / 面板展开) ---------------- */
333
+
334
+ /** localStorage 包装:不可用(隐私模式、无 window)时静默降级为默认值。 */
335
+ function readStore(key, fallback) {
336
+ try {
337
+ if (typeof window === "undefined" || !window.localStorage) return fallback;
338
+ const v = window.localStorage.getItem(key);
339
+ return v === null ? fallback : v;
340
+ } catch (e) {
341
+ return fallback;
342
+ }
343
+ }
344
+
345
+ function writeStore(key, value) {
346
+ try {
347
+ if (typeof window === "undefined" || !window.localStorage) return;
348
+ window.localStorage.setItem(key, String(value));
349
+ } catch (e) {
350
+ /* 存不下就只当记不住偏好,不影响功能 */
351
+ }
352
+ }
353
+
354
+ function clampWidth(px) {
355
+ const n = Math.round(Number(px) || 0);
356
+ if (!isFinite(n)) return MIN_COL_PX;
357
+ return Math.max(MIN_COL_PX, Math.min(MAX_COL_PX, n));
358
+ }
359
+
360
+ function loadCols() {
361
+ const out = Object.assign({}, DEFAULT_COLS);
362
+ const raw = readStore(STORE_COLS_KEY, "");
363
+ if (!raw) return out;
364
+ try {
365
+ const parsed = JSON.parse(raw);
366
+ if (parsed && typeof parsed === "object") {
367
+ for (const key of RESIZABLE_COLS) {
368
+ if (typeof parsed[key] === "number") out[key] = clampWidth(parsed[key]);
369
+ }
370
+ }
371
+ } catch (e) {
372
+ /* 存的内容坏了就用默认值 */
373
+ }
374
+ return out;
375
+ }
376
+
377
+ function saveCols(cols) {
378
+ try {
379
+ const slim = {};
380
+ for (const key of RESIZABLE_COLS) slim[key] = clampWidth(cols[key]);
381
+ writeStore(STORE_COLS_KEY, JSON.stringify(slim));
382
+ } catch (e) {
383
+ /* 忽略 */
384
+ }
385
+ }
386
+
387
+ /** 下载目录校验:留空 = 使用默认目录;否则必须是绝对路径(盘符或 UNC)。 */
388
+ const DRIVE_ABS_RE = /^[A-Za-z]:[\\/]/;
389
+ const UNC_ABS_RE = /^\\\\[^\\]/;
390
+
391
+ function isAbsoluteDir(p) {
392
+ const v = String(p == null ? "" : p).trim();
393
+ if (!v) return true;
394
+ return DRIVE_ABS_RE.test(v) || UNC_ABS_RE.test(v);
395
+ }
396
+
397
+ function toggleLabel(key) {
398
+ for (const t of TOGGLES) {
399
+ if (t.key === key) return t.label;
400
+ }
401
+ return key;
402
+ }
403
+
404
+ /* ------------------------------------------------------------------ *
405
+ * 设置页
406
+ * ------------------------------------------------------------------ */
407
+
408
+ function AppUpdaterSection() {
409
+ const [state, setState] = react.useState(null);
410
+ const [loadError, setLoadError] = react.useState(null);
411
+ const [localLog, setLocalLog] = react.useState([]);
412
+ const [query, setQuery] = react.useState("");
413
+ const [mode, setMode] = react.useState("all");
414
+ const [selected, setSelected] = react.useState({});
415
+ const [busy, setBusy] = react.useState({ scan: false, update: false, cancel: false, open: false });
416
+ const [logOpen, setLogOpen] = react.useState(false);
417
+ const [now, setNow] = react.useState(Date.now());
418
+
419
+ const phaseRef = react.useRef("idle");
420
+ const inFlightRef = react.useRef(false);
421
+ const pollFailRef = react.useRef(false);
422
+
423
+ /* 列宽(可拖拽调整、本地记住) */
424
+ const [cols, setCols] = react.useState(loadCols);
425
+ const [resizingCol, setResizingCol] = react.useState(null);
426
+ const colsRef = react.useRef(cols);
427
+ const dragRef = react.useRef(null);
428
+ const dragCleanupRef = react.useRef(null);
429
+
430
+ /* 设置面板 */
431
+ const [settingsOpen, setSettingsOpen] = react.useState(
432
+ () => readStore(STORE_SETTINGS_OPEN_KEY, "1") === "1"
433
+ );
434
+ const [dirDraft, setDirDraft] = react.useState("");
435
+ const [dirTouched, setDirTouched] = react.useState(false);
436
+ const [settingsBusy, setSettingsBusy] = react.useState(false);
437
+ const [settingsError, setSettingsError] = react.useState(null);
438
+
439
+ const pushLog = react.useCallback((level, text) => {
440
+ setLocalLog((prev) => {
441
+ const next = prev.concat([{ at: Date.now(), level: level, text: String(text) }]);
442
+ return next.length > LOG_LIMIT ? next.slice(next.length - LOG_LIMIT) : next;
443
+ });
444
+ }, []);
445
+
446
+ const refresh = react.useCallback(async () => {
447
+ if (inFlightRef.current) return;
448
+ inFlightRef.current = true;
449
+ try {
450
+ const j = await getJson(STATE_URL);
451
+ if (j && typeof j === "object") {
452
+ setState(j);
453
+ setLoadError(null);
454
+ pollFailRef.current = false;
455
+ }
456
+ } catch (e) {
457
+ setLoadError(errText(e));
458
+ if (!pollFailRef.current) {
459
+ pollFailRef.current = true;
460
+ pushLog("error", "读取状态失败:" + errText(e));
461
+ }
462
+ } finally {
463
+ inFlightRef.current = false;
464
+ }
465
+ }, [pushLog]);
466
+
467
+ /* 让轮询循环读到最新的 phase,而不必重建定时器 */
468
+ react.useEffect(() => {
469
+ phaseRef.current = (state && state.phase) || "idle";
470
+ }, [state]);
471
+
472
+ /* 相对时间每 10s 刷新一次 */
473
+ react.useEffect(() => {
474
+ const id = setInterval(() => setNow(Date.now()), 10000);
475
+ return () => clearInterval(id);
476
+ }, []);
477
+
478
+ /* 轮询:忙 900ms / 空闲 5000ms;页面不可见时暂停 */
479
+ react.useEffect(() => {
480
+ let timer = null;
481
+ let disposed = false;
482
+
483
+ const clearTimer = () => {
484
+ if (timer !== null) {
485
+ clearTimeout(timer);
486
+ timer = null;
487
+ }
488
+ };
489
+ const isVisible = () =>
490
+ typeof document === "undefined" || document.visibilityState === "visible";
491
+ const schedule = () => {
492
+ clearTimer();
493
+ if (disposed) return;
494
+ timer = setTimeout(tick, phaseRef.current !== "idle" ? POLL_BUSY_MS : POLL_IDLE_MS);
495
+ };
496
+ const tick = async () => {
497
+ if (disposed) return;
498
+ if (!isVisible()) {
499
+ clearTimer();
500
+ return;
501
+ }
502
+ await refresh();
503
+ schedule();
504
+ };
505
+ const onVisibility = () => {
506
+ if (disposed) return;
507
+ if (isVisible()) {
508
+ clearTimer();
509
+ tick();
510
+ } else {
511
+ clearTimer();
512
+ }
513
+ };
514
+
515
+ tick();
516
+ if (typeof document !== "undefined") {
517
+ document.addEventListener("visibilitychange", onVisibility);
518
+ }
519
+ return () => {
520
+ disposed = true;
521
+ clearTimer();
522
+ if (typeof document !== "undefined") {
523
+ document.removeEventListener("visibilitychange", onVisibility);
524
+ }
525
+ };
526
+ }, [refresh]);
527
+
528
+ /* ---------------- 列宽:拖拽调整 + 本地记忆 ---------------- */
529
+
530
+ react.useEffect(() => {
531
+ colsRef.current = cols;
532
+ }, [cols]);
533
+
534
+ /* 组件卸载时,若还在拖拽则摘掉 document 上的监听 */
535
+ react.useEffect(() => {
536
+ return () => {
537
+ if (dragCleanupRef.current) dragCleanupRef.current();
538
+ dragCleanupRef.current = null;
539
+ dragRef.current = null;
540
+ };
541
+ }, []);
542
+
543
+ const applyDrag = (clientX) => {
544
+ const drag = dragRef.current;
545
+ if (!drag) return;
546
+ const width = clampWidth(drag.startWidth + (clientX - drag.startX));
547
+ setCols((prev) => (prev[drag.key] === width ? prev : Object.assign({}, prev, { [drag.key]: width })));
548
+ };
549
+
550
+ const onColResizeStart = (key, e) => {
551
+ if (!e || (e.button !== undefined && e.button !== 0)) return;
552
+ if (typeof document === "undefined") return;
553
+ e.preventDefault();
554
+ e.stopPropagation();
555
+ if (dragCleanupRef.current) dragCleanupRef.current();
556
+
557
+ dragRef.current = { key: key, startX: e.clientX, startWidth: colsRef.current[key] || DEFAULT_COLS[key] };
558
+ setResizingCol(key);
559
+
560
+ const onMove = (ev) => applyDrag(ev.clientX);
561
+ const finish = () => {
562
+ document.removeEventListener("mousemove", onMove);
563
+ document.removeEventListener("mouseup", finish);
564
+ dragRef.current = null;
565
+ dragCleanupRef.current = null;
566
+ setResizingCol(null);
567
+ saveCols(colsRef.current);
568
+ };
569
+ const cleanup = () => {
570
+ document.removeEventListener("mousemove", onMove);
571
+ document.removeEventListener("mouseup", finish);
572
+ };
573
+ dragCleanupRef.current = cleanup;
574
+ document.addEventListener("mousemove", onMove);
575
+ document.addEventListener("mouseup", finish);
576
+ };
577
+
578
+ const onColReset = (key, e) => {
579
+ if (e) {
580
+ e.preventDefault();
581
+ e.stopPropagation();
582
+ }
583
+ setCols((prev) => {
584
+ const next = Object.assign({}, prev, { [key]: DEFAULT_COLS[key] });
585
+ saveCols(next);
586
+ return next;
587
+ });
588
+ };
589
+
590
+ /* ---------------- 设置面板 ---------------- */
591
+
592
+ /* 宿主返回的生效目录:只在用户没动过输入框时同步到草稿 */
593
+ react.useEffect(() => {
594
+ if (dirTouched) return;
595
+ const s = (state && state.settings) || null;
596
+ setDirDraft(s && typeof s.downloadDir === "string" ? s.downloadDir : "");
597
+ }, [state, dirTouched]);
598
+
599
+ const toggleSettingsPanel = () => {
600
+ setSettingsOpen((v) => {
601
+ const next = !v;
602
+ writeStore(STORE_SETTINGS_OPEN_KEY, next ? "1" : "0");
603
+ return next;
604
+ });
605
+ };
606
+
607
+ const saveSettings = react.useCallback(
608
+ async (patch, okText) => {
609
+ setSettingsBusy(true);
610
+ setSettingsError(null);
611
+ try {
612
+ const r = await postJson(SETTINGS_URL, { confirm: true, settings: patch });
613
+ if (okText) pushLog("success", okText);
614
+ await refresh();
615
+ return true;
616
+ } catch (e) {
617
+ const msg = errText(e);
618
+ setSettingsError(msg);
619
+ pushLog("error", "保存设置失败:" + msg);
620
+ return false;
621
+ } finally {
622
+ setSettingsBusy(false);
623
+ }
624
+ },
625
+ [pushLog, refresh]
626
+ );
627
+
628
+ const toggleSetting = (key) => {
629
+ const s = (state && state.settings) || {};
630
+ const next = !s[key];
631
+ saveSettings({ [key]: next }, (toggleLabel(key) || key) + (next ? " 已开启" : " 已关闭"));
632
+ };
633
+
634
+ const saveDir = async () => {
635
+ const value = String(dirDraft || "").trim();
636
+ if (!isAbsoluteDir(value)) {
637
+ setSettingsError("下载目录必须是绝对路径(如 D:\\Apps)或 UNC 路径(\\\\server\\share)");
638
+ return;
639
+ }
640
+ const ok = await saveSettings({ downloadDir: value }, value ? "下载目录已保存" : "已恢复默认下载目录");
641
+ if (ok) setDirTouched(false);
642
+ };
643
+
644
+ const resetDir = () => {
645
+ setDirDraft("");
646
+ setDirTouched(false);
647
+ saveSettings({ downloadDir: "" }, "已恢复默认下载目录");
648
+ };
649
+
650
+ /* ---------------- 派生数据 ---------------- */
651
+
652
+ const phase = (state && state.phase) || "idle";
653
+ const scanning = phase === "scanning";
654
+ const updating = phase === "updating";
655
+ const apps = state && state.apps ? state.apps : null;
656
+ const items = apps && Array.isArray(apps.items) ? apps.items : null;
657
+ const updateInfo = (state && state.update) || {};
658
+ const winget = (state && state.winget) || null;
659
+ const platform = state && state.platform;
660
+
661
+ /* 运行环境(系统 / 架构)与更新引擎 */
662
+ const env = (state && state.env) || null;
663
+ const engine = (state && state.engine) || null;
664
+ const caps = (state && state.capabilities) || null;
665
+ const envSummary =
666
+ (state && state.envSummary) ||
667
+ (env ? [env.osName || env.platform, env.archLabel].filter(Boolean).join(" · ") : "环境未知");
668
+ const engineText = !state ? "" : engineTextOf(engine);
669
+ /* 平台族:设置面板与空状态文案都要用,必须在下面的组件定义之前算出来 */
670
+ const family = (env && env.family) || (platform === "darwin" ? "macos" : "windows");
671
+
672
+ const filtered = react.useMemo(() => {
673
+ if (!items) return [];
674
+ const q = query.trim().toLowerCase();
675
+ const out = [];
676
+ for (let i = 0; i < items.length; i++) {
677
+ const a = items[i];
678
+ if (!a) continue;
679
+ const canUpdate = !!(a.hasUpdate && a.updateSupported);
680
+ if (mode === "updatable" && !canUpdate) continue;
681
+ if (mode === "unsupported" && a.updateSupported) continue;
682
+ if (q) {
683
+ const hay =
684
+ String(a.name || "") +
685
+ " " +
686
+ String(a.publisher || "") +
687
+ " " +
688
+ String(a.id || "");
689
+ if (hay.toLowerCase().indexOf(q) < 0) continue;
690
+ }
691
+ out.push(a);
692
+ }
693
+ return out;
694
+ }, [items, query, mode]);
695
+
696
+ const selectableIds = react.useMemo(() => {
697
+ const out = [];
698
+ for (let i = 0; i < filtered.length; i++) {
699
+ const a = filtered[i];
700
+ if (a && a.hasUpdate && a.updateSupported && a.id) out.push(a.id);
701
+ }
702
+ return out;
703
+ }, [filtered]);
704
+
705
+ const selectedCount = react.useMemo(() => {
706
+ let n = 0;
707
+ for (let i = 0; i < selectableIds.length; i++) {
708
+ if (selected[selectableIds[i]]) n++;
709
+ }
710
+ return n;
711
+ }, [selectableIds, selected]);
712
+
713
+ const allSelected = selectableIds.length > 0 && selectedCount === selectableIds.length;
714
+
715
+ const total = apps && apps.stats && typeof apps.stats.total === "number" ? apps.stats.total : items ? items.length : 0;
716
+ const upgradable =
717
+ apps && apps.stats && typeof apps.stats.upgradable === "number"
718
+ ? apps.stats.upgradable
719
+ : selectableIds.length;
720
+
721
+ const mergedLog = react.useMemo(() => {
722
+ const server = state && Array.isArray(state.log) ? state.log : [];
723
+ const all = server.concat(localLog);
724
+ all.sort((a, b) => (Number(a && a.at) || 0) - (Number(b && b.at) || 0));
725
+ return all.slice(all.length > LOG_LIMIT ? all.length - LOG_LIMIT : 0);
726
+ }, [state, localLog]);
727
+
728
+ /* ---------------- 动作 ---------------- */
729
+
730
+ const doScan = async () => {
731
+ if (busy.scan || phase !== "idle") return;
732
+ setBusy((b) => ({ ...b, scan: true }));
733
+ try {
734
+ await postJson(SCAN_URL, { confirm: true, force: true });
735
+ pushLog("info", "已开始扫描本机应用…");
736
+ await refresh();
737
+ } catch (e) {
738
+ pushLog("error", "启动扫描失败:" + errText(e));
739
+ } finally {
740
+ setBusy((b) => ({ ...b, scan: false }));
741
+ }
742
+ };
743
+
744
+ const runUpdate = async (ids, label) => {
745
+ const list = (ids || []).filter(Boolean);
746
+ if (!list.length) return;
747
+ setBusy((b) => ({ ...b, update: true }));
748
+ try {
749
+ const r = await postJson(UPDATE_URL, { confirm: true, ids: list, mode: "silent" });
750
+ pushLog(
751
+ "info",
752
+ "已提交 " + ((r && r.queued) || list.length) + " 个更新任务" + (label ? "(" + label + ")" : "")
753
+ );
754
+ setSelected({});
755
+ await refresh();
756
+ } catch (e) {
757
+ pushLog("error", "启动更新失败:" + errText(e));
758
+ } finally {
759
+ setBusy((b) => ({ ...b, update: false }));
760
+ }
761
+ };
762
+
763
+ const doCancel = async () => {
764
+ if (busy.cancel) return;
765
+ setBusy((b) => ({ ...b, cancel: true }));
766
+ try {
767
+ await postJson(CANCEL_URL, { confirm: true });
768
+ pushLog("warn", "已请求取消更新");
769
+ await refresh();
770
+ } catch (e) {
771
+ pushLog("error", "取消失败:" + errText(e));
772
+ } finally {
773
+ setBusy((b) => ({ ...b, cancel: false }));
774
+ }
775
+ };
776
+
777
+ const doOpenDownloads = async () => {
778
+ if (busy.open) return;
779
+ setBusy((b) => ({ ...b, open: true }));
780
+ try {
781
+ const r = await postJson(OPEN_DOWNLOADS_URL, { confirm: true });
782
+ pushLog("success", "已打开下载目录" + (r && r.path ? ":" + r.path : ""));
783
+ } catch (e) {
784
+ pushLog("error", "打开下载目录失败:" + errText(e));
785
+ } finally {
786
+ setBusy((b) => ({ ...b, open: false }));
787
+ }
788
+ };
789
+
790
+ const toggleOne = (id, enabled) => {
791
+ if (!enabled) return;
792
+ setSelected((prev) => {
793
+ const next = { ...prev };
794
+ if (next[id]) delete next[id];
795
+ else next[id] = true;
796
+ return next;
797
+ });
798
+ };
799
+
800
+ const toggleAll = () => {
801
+ if (!selectableIds.length) return;
802
+ setSelected((prev) => {
803
+ if (allSelected) {
804
+ const next = { ...prev };
805
+ for (let i = 0; i < selectableIds.length; i++) delete next[selectableIds[i]];
806
+ return next;
807
+ }
808
+ const next = { ...prev };
809
+ for (let i = 0; i < selectableIds.length; i++) next[selectableIds[i]] = true;
810
+ return next;
811
+ });
812
+ };
813
+
814
+ const updateAll = () => runUpdate(selectableIds, "当前筛选");
815
+ const updateSelected = () => {
816
+ const ids = selectableIds.filter((id) => selected[id]);
817
+ return runUpdate(ids, "已选 " + ids.length + " 项");
818
+ };
819
+
820
+ /* ---------------- 渲染片段 ---------------- */
821
+
822
+ const btn = (label, onClick, opts) => {
823
+ const o = opts || {};
824
+ return h(
825
+ "button",
826
+ {
827
+ type: "button",
828
+ className:
829
+ "appupd-btn" +
830
+ (o.primary ? " appupd-btn-primary" : "") +
831
+ (o.quiet ? " appupd-btn-quiet" : ""),
832
+ onClick: onClick,
833
+ disabled: !!o.disabled,
834
+ title: o.title || undefined,
835
+ },
836
+ label
837
+ );
838
+ };
839
+
840
+ const statusCell = (a) => {
841
+ if (a.hasUpdate && a.updateSupported) {
842
+ return h("span", { className: "appupd-state-new" }, "有新版本");
843
+ }
844
+ if (a.hasUpdate && !a.updateSupported) {
845
+ const r = reasonText(a);
846
+ return h(
847
+ "span",
848
+ { className: "appupd-state-none", title: r ? "无法更新:" + r : "无法更新" },
849
+ "未收录"
850
+ );
851
+ }
852
+ return h("span", { className: "appupd-state-ok" }, "已是最新");
853
+ };
854
+
855
+ const rows = filtered.map((a) => {
856
+ const key = a.id || a.name;
857
+ const canUpdate = !!(a.hasUpdate && a.updateSupported);
858
+ const checked = canUpdate && !!selected[key];
859
+ const reason = canUpdate ? "" : reasonText(a);
860
+ const rowBusy = phase !== "idle" || busy.update;
861
+ return h(
862
+ "tr",
863
+ { key: key, className: "appupd-row" },
864
+ h(
865
+ "td",
866
+ { className: "appupd-col-check" },
867
+ h("input", {
868
+ type: "checkbox",
869
+ className: "appupd-cb",
870
+ checked: checked,
871
+ disabled: !canUpdate,
872
+ onChange: () => toggleOne(key, canUpdate),
873
+ "aria-label": "选择 " + String(a.name || key),
874
+ title: reason || "选择以批量更新",
875
+ })
876
+ ),
877
+ h(
878
+ "td",
879
+ null,
880
+ h(
881
+ "div",
882
+ { className: "appupd-nameline" },
883
+ h(
884
+ "span",
885
+ {
886
+ className: "appupd-name",
887
+ title: String(a.name || key) + (a.id ? "\n" + a.id : ""),
888
+ },
889
+ a.name || key
890
+ ),
891
+ h(
892
+ "span",
893
+ { className: "appupd-badge appupd-badge-" + String(a.source || "registry") },
894
+ SOURCE_TEXT[a.source] || a.source || "未知"
895
+ )
896
+ ),
897
+ a.publisher && a.publisher !== a.name
898
+ ? h("div", { className: "appupd-pub", title: a.publisher }, a.publisher)
899
+ : null
900
+ ),
901
+ h("td", { className: "appupd-col-ver" }, h("span", { className: "appupd-ver" }, a.version || "—")),
902
+ h(
903
+ "td",
904
+ { className: "appupd-col-ver" },
905
+ a.hasUpdate && a.available
906
+ ? h("span", { className: "appupd-ver-new" }, a.available)
907
+ : h("span", { className: "appupd-muted" }, "—")
908
+ ),
909
+ h(
910
+ "td",
911
+ { className: "appupd-col-state" },
912
+ statusCell(a),
913
+ reason ? h("div", { className: "appupd-reason", title: reason }, reason) : null
914
+ ),
915
+ h(
916
+ "td",
917
+ { className: "appupd-col-op" },
918
+ h(
919
+ "button",
920
+ {
921
+ type: "button",
922
+ className: "appupd-rowbtn",
923
+ disabled: !canUpdate || rowBusy,
924
+ onClick: () => runUpdate([key], a.name),
925
+ title: !canUpdate
926
+ ? reason
927
+ : rowBusy
928
+ ? "更新进行中,请稍候"
929
+ : "更新到 " + (a.available || "最新版"),
930
+ },
931
+ "更新"
932
+ )
933
+ )
934
+ );
935
+ });
936
+
937
+ const emptyBody = () => {
938
+ if (!state) return h("div", { className: "appupd-empty" }, "正在读取状态…");
939
+ if (!items) {
940
+ return h(
941
+ "div",
942
+ { className: "appupd-empty" },
943
+ "还没有检测过,点右上角『刷新检测』开始"
944
+ );
945
+ }
946
+ if (!items.length) {
947
+ // 分平台给出可执行的下一步,而不是一句「未检测到任何已安装应用」
948
+ return h(
949
+ "div",
950
+ { className: "appupd-empty" },
951
+ family === "macos"
952
+ ? "未在应用目录里找到应用。若确认本机有应用,请看看下面的运行日志里「枚举应用目录」的记录;" +
953
+ "应用装在非标准位置时本插件看不到它。"
954
+ : "未检测到任何已安装应用"
955
+ );
956
+ }
957
+ return h("div", { className: "appupd-empty" }, "没有匹配的应用");
958
+ };
959
+
960
+ const body = !items || !filtered.length ? emptyBody() : null;
961
+
962
+ /* ---------------- 设置面板 / 演练提示 ---------------- */
963
+
964
+ const settings = (state && state.settings) || {};
965
+ const dryRunOn = Boolean(settings.dryRun);
966
+ const visibleToggles = togglesFor(family);
967
+ const effectiveDir =
968
+ (state && state.downloadDir) ||
969
+ (family === "macos" ? "~/Downloads/DSH App Updater" : "%USERPROFILE%\\Downloads\\DSH App Updater");
970
+
971
+ const dryRunBanner = dryRunOn
972
+ ? h(
973
+ "div",
974
+ { className: "appupd-banner", role: "status" },
975
+ h("span", null, "演练模式已开启:只从官网下载全量安装包,不会真正安装。")
976
+ )
977
+ : null;
978
+
979
+ /* 扫描警告:目录抓不到 / Homebrew 命令失败时,界面必须自己说出来,
980
+ 否则用户只会看到「0 个可更新」,误以为本机应用都最新 */
981
+ const scanWarnings = (state && state.scan && Array.isArray(state.scan.warnings) ? state.scan.warnings : [])
982
+ .filter((w) => typeof w === "string" && w);
983
+ /* 扫描整体失败(例如目录读不出来、宿主抛错)以前只写在 state 里,界面完全看不到,
984
+ 只剩一行「0 个应用」;这里连同 warnings 一起显示。 */
985
+ const scanError = state && state.scan && state.scan.error ? String(state.scan.error) : "";
986
+ const scanWarningBanner =
987
+ (scanError || scanWarnings.length) && !scanning
988
+ ? h(
989
+ "div",
990
+ { className: "appupd-banner appupd-banner-warn", role: "alert" },
991
+ h(
992
+ "span",
993
+ null,
994
+ scanError
995
+ ? `检测失败:${scanError}`
996
+ : `检测有 ${scanWarnings.length} 条警告:${scanWarnings[0]}`
997
+ ),
998
+ !scanError && scanWarnings.length > 1
999
+ ? h("details", { className: "appupd-warn-detail" }, h("summary", null, "查看全部"), h("ul", null, scanWarnings.map((w, i) => h("li", { key: i }, w))))
1000
+ : null
1001
+ )
1002
+ : null;
1003
+
1004
+ /* 兼容模式:只有用户显式开启(插件配置 / 环境变量)才会出现,默认不显示 */
1005
+ const admission = (state && state.admission) || null;
1006
+ const legacyOn = Boolean(admission && admission.allowLegacyLoopback);
1007
+ const legacyBanner = legacyOn
1008
+ ? h(
1009
+ "div",
1010
+ { className: "appupd-banner appupd-banner-warn", role: "alert" },
1011
+ h(
1012
+ "span",
1013
+ null,
1014
+ "兼容模式已开启:没有连接凭据的本机回环请求会被放行(旧宿主没有 connection 准入时才会用到)。非必要请关闭,见 README 的「安全」一节。"
1015
+ )
1016
+ )
1017
+ : null;
1018
+
1019
+ const switchEl = (key) => {
1020
+ const on = Boolean(settings[key]);
1021
+ return h("button", {
1022
+ type: "button",
1023
+ className: "appupd-switch" + (on ? " appupd-switch-on" : ""),
1024
+ role: "switch",
1025
+ "aria-checked": on,
1026
+ "aria-label": toggleLabel(key),
1027
+ disabled: settingsBusy || !state,
1028
+ onClick: () => toggleSetting(key),
1029
+ title: on ? "点击关闭" : "点击开启",
1030
+ });
1031
+ };
1032
+
1033
+ const settingsPanel = h(
1034
+ "div",
1035
+ { className: "appupd-panel" },
1036
+ h(
1037
+ "button",
1038
+ {
1039
+ type: "button",
1040
+ className: "appupd-panel-head",
1041
+ onClick: toggleSettingsPanel,
1042
+ "aria-expanded": settingsOpen,
1043
+ },
1044
+ h("span", { className: "appupd-log-caret" }, settingsOpen ? "▾" : "▸"),
1045
+ "插件设置"
1046
+ ),
1047
+ settingsOpen
1048
+ ? h(
1049
+ "div",
1050
+ { className: "appupd-panel-body" },
1051
+ h(
1052
+ "div",
1053
+ { className: "appupd-field" },
1054
+ h("div", { className: "appupd-field-label" }, "运行环境"),
1055
+ h(
1056
+ "div",
1057
+ { className: "appupd-field-main" },
1058
+ h(
1059
+ "div",
1060
+ { className: "appupd-field-row" },
1061
+ h("span", { className: "appupd-envline" }, envSummary + (engineText ? " · " + engineText : ""))
1062
+ ),
1063
+ env && env.cpuModel
1064
+ ? h(
1065
+ "div",
1066
+ { className: "appupd-hint" },
1067
+ env.cpuModel + (env.cpuCount ? " · " + env.cpuCount + " 线程" : "")
1068
+ )
1069
+ : null
1070
+ )
1071
+ ),
1072
+ caps && !caps.scan && caps.note
1073
+ ? h("div", { className: "appupd-err", style: { fontSize: 12 } }, caps.note)
1074
+ : null,
1075
+ h(
1076
+ "div",
1077
+ { className: "appupd-field" },
1078
+ h("div", { className: "appupd-field-label" }, "安装包下载目录"),
1079
+ h(
1080
+ "div",
1081
+ { className: "appupd-field-main" },
1082
+ h(
1083
+ "div",
1084
+ { className: "appupd-field-row" },
1085
+ h("input", {
1086
+ className: "appupd-input",
1087
+ type: "text",
1088
+ value: dirDraft,
1089
+ placeholder: effectiveDir,
1090
+ onChange: (e) => {
1091
+ setDirTouched(true);
1092
+ setSettingsError(null);
1093
+ setDirDraft(e.target.value);
1094
+ },
1095
+ onKeyDown: (e) => {
1096
+ if (e.key === "Enter") saveDir();
1097
+ },
1098
+ "aria-label": "安装包下载目录",
1099
+ }),
1100
+ h(
1101
+ "button",
1102
+ { type: "button", className: "appupd-rowbtn", disabled: settingsBusy, onClick: saveDir },
1103
+ settingsBusy ? "保存中…" : "保存"
1104
+ ),
1105
+ h(
1106
+ "button",
1107
+ { type: "button", className: "appupd-rowbtn", disabled: settingsBusy, onClick: resetDir },
1108
+ "恢复默认"
1109
+ )
1110
+ ),
1111
+ h(
1112
+ "div",
1113
+ { className: "appupd-hint" },
1114
+ "留空 = 默认目录(" + effectiveDir + ");从官网下载的完整安装包与对应清单一并保存在该目录"
1115
+ )
1116
+ )
1117
+ ),
1118
+ ...visibleToggles.map((t) =>
1119
+ h(
1120
+ "div",
1121
+ { className: "appupd-field", key: t.key },
1122
+ h("div", { className: "appupd-field-label" }, t.label),
1123
+ h(
1124
+ "div",
1125
+ { className: "appupd-field-main" },
1126
+ h(
1127
+ "div",
1128
+ { className: "appupd-field-row" },
1129
+ switchEl(t.key),
1130
+ h("span", { className: "appupd-hint" }, t.hint)
1131
+ )
1132
+ )
1133
+ )
1134
+ ),
1135
+ settingsError ? h("div", { className: "appupd-err", style: { fontSize: 12 } }, settingsError) : null
1136
+ )
1137
+ : null
1138
+ );
1139
+
1140
+ /* 表格:总宽 = 各列固定宽度之和,超出容器就横向滚动(列宽即所见) */
1141
+ const colsWidth = CHECK_COL_PX + cols.name + cols.version + cols.available + cols.state + OP_COL_PX;
1142
+
1143
+ const grip = (key) =>
1144
+ h("span", {
1145
+ className: "appupd-grip" + (resizingCol === key ? " appupd-grip-on" : ""),
1146
+ onMouseDown: (e) => onColResizeStart(key, e),
1147
+ onDoubleClick: (e) => onColReset(key, e),
1148
+ title:
1149
+ "拖动调整「" + COL_LABELS[key] + "」列宽(当前 " + cols[key] + "px);双击恢复默认",
1150
+ "aria-hidden": "true",
1151
+ });
1152
+
1153
+ const progress =
1154
+ updating || updateInfo.running
1155
+ ? (() => {
1156
+ const t = Number(updateInfo.total) || 0;
1157
+ const d = Number(updateInfo.done) || 0;
1158
+ const pct = t > 0 ? Math.max(0, Math.min(100, Math.round((d / t) * 100))) : 0;
1159
+ return h(
1160
+ "div",
1161
+ { className: "appupd-progress" },
1162
+ h(
1163
+ "div",
1164
+ { className: "appupd-progress-head" },
1165
+ h("span", { className: "appupd-progress-count" }, d + "/" + t),
1166
+ h(
1167
+ "span",
1168
+ { className: "appupd-progress-name", title: updateInfo.current || "" },
1169
+ updateInfo.current ? "正在处理 " + updateInfo.current : "正在准备…"
1170
+ ),
1171
+ h("span", { className: "appupd-progress-count" }, pct + "%")
1172
+ ),
1173
+ h(
1174
+ "div",
1175
+ { className: "appupd-progress-track" },
1176
+ h("div", { className: "appupd-progress-fill", style: { width: pct + "%" } })
1177
+ ),
1178
+ updateInfo.error
1179
+ ? h("div", { className: "appupd-err", style: { fontSize: 12 } }, "更新出错:" + updateInfo.error)
1180
+ : null
1181
+ );
1182
+ })()
1183
+ : null;
1184
+
1185
+ const statusRow = h(
1186
+ "div",
1187
+ { className: "appupd-status" },
1188
+ !state
1189
+ ? h("span", null, loadError ? h("span", { className: "appupd-err" }, "无法连接宿主:" + loadError) : "正在读取状态…")
1190
+ : h(
1191
+ "span",
1192
+ engine && engine.available === false ? { className: "appupd-err" } : null,
1193
+ engineText
1194
+ ),
1195
+ state ? h("span", { className: "appupd-sep" }, "·") : null,
1196
+ state
1197
+ ? h(
1198
+ "span",
1199
+ { className: "appupd-status-strong", title: "操作系统与 CPU 架构" },
1200
+ envSummary
1201
+ )
1202
+ : null,
1203
+ state ? h("span", { className: "appupd-sep" }, "·") : null,
1204
+ state
1205
+ ? h("span", null, "上次扫描:" + fmtRel(apps && apps.scannedAt, now))
1206
+ : null,
1207
+ state ? h("span", { className: "appupd-sep" }, "·") : null,
1208
+ state
1209
+ ? h(
1210
+ "span",
1211
+ { className: "appupd-status-strong" },
1212
+ "共 " + total + " 个应用 · " + upgradable + " 个可更新"
1213
+ )
1214
+ : null,
1215
+ scanning ? h("span", null, "扫描中…") : null,
1216
+ caps && caps.note ? h("span", { className: "appupd-err" }, caps.note) : null,
1217
+ loadError && state ? h("span", { className: "appupd-err" }, "(状态刷新失败:" + loadError + ")") : null
1218
+ );
1219
+
1220
+ const logLines = mergedLog.map((l, i) => {
1221
+ const level = l && l.level ? String(l.level) : "info";
1222
+ const cls =
1223
+ level === "error"
1224
+ ? "appupd-lv-error"
1225
+ : level === "warn"
1226
+ ? "appupd-lv-warn"
1227
+ : level === "success"
1228
+ ? "appupd-lv-success"
1229
+ : "appupd-lv-info";
1230
+ return h(
1231
+ "div",
1232
+ { key: i, className: "appupd-log-line" },
1233
+ h("span", { className: "appupd-log-time" }, fmtClock(l && l.at)),
1234
+ h("span", { className: cls }, (l && l.text) || "")
1235
+ );
1236
+ });
1237
+
1238
+ return h(
1239
+ "div",
1240
+ { className: "appupd-root" },
1241
+ /* 1. 工具条 */
1242
+ h(
1243
+ "div",
1244
+ { className: "appupd-bar" },
1245
+ h("div", { className: "appupd-title" }, "应用更新"),
1246
+ h(
1247
+ "div",
1248
+ { className: "appupd-actions" },
1249
+ btn(busy.scan || scanning ? "检测中…" : "刷新检测", doScan, {
1250
+ primary: true,
1251
+ disabled: busy.scan || phase !== "idle",
1252
+ title: phase !== "idle" ? "当前有任务在执行" : "扫描本机已安装应用并检查更新",
1253
+ }),
1254
+ selectedCount > 0
1255
+ ? btn("更新所选 (" + selectedCount + ")", updateSelected, {
1256
+ disabled: phase !== "idle" || busy.update,
1257
+ title: "仅更新勾选的应用",
1258
+ })
1259
+ : null,
1260
+ btn("全部更新", updateAll, {
1261
+ disabled: phase !== "idle" || busy.update || selectableIds.length === 0,
1262
+ title:
1263
+ selectableIds.length === 0
1264
+ ? "当前筛选下没有可更新的应用"
1265
+ : "更新当前筛选下 " + selectableIds.length + " 个应用",
1266
+ }),
1267
+ btn(busy.cancel ? "取消中…" : "取消", doCancel, {
1268
+ quiet: true,
1269
+ disabled: !updating || busy.cancel,
1270
+ title: updating ? "取消尚未开始的更新" : "当前没有进行中的更新",
1271
+ }),
1272
+ btn("打开下载目录", doOpenDownloads, {
1273
+ quiet: true,
1274
+ disabled: busy.open,
1275
+ title:
1276
+ (state && state.settings && state.settings.downloadDir) || "在资源管理器中打开安装包下载目录",
1277
+ })
1278
+ )
1279
+ ),
1280
+
1281
+ /* 2. 状态行 */
1282
+ statusRow,
1283
+
1284
+ /* 3. 演练模式提示 */
1285
+ dryRunBanner,
1286
+
1287
+ /* 3a. 扫描警告(目录抓取失败 / Homebrew 命令失败):必须让用户看见 */
1288
+ scanWarningBanner,
1289
+
1290
+ /* 3b. 兼容模式(仅回环放行)提示:默认不出现 */
1291
+ legacyBanner,
1292
+
1293
+ /* 4. 进度 */
1294
+ progress,
1295
+
1296
+ /* 5. 插件设置 */
1297
+ settingsPanel,
1298
+
1299
+ /* 6. 筛选行 */
1300
+ items && items.length
1301
+ ? h(
1302
+ "div",
1303
+ { className: "appupd-filter" },
1304
+ h("input", {
1305
+ type: "search",
1306
+ className: "appupd-search",
1307
+ placeholder: "搜索应用名 / 发布者 / ID",
1308
+ value: query,
1309
+ onChange: (e) => setQuery(e.target.value),
1310
+ "aria-label": "搜索应用",
1311
+ }),
1312
+ h(
1313
+ "div",
1314
+ { className: "appupd-seg", role: "group", "aria-label": "筛选" },
1315
+ h(
1316
+ "button",
1317
+ {
1318
+ type: "button",
1319
+ className: "appupd-seg-btn" + (mode === "all" ? " appupd-seg-btn-on" : ""),
1320
+ onClick: () => setMode("all"),
1321
+ "aria-pressed": mode === "all",
1322
+ },
1323
+ "全部"
1324
+ ),
1325
+ h(
1326
+ "button",
1327
+ {
1328
+ type: "button",
1329
+ className: "appupd-seg-btn" + (mode === "updatable" ? " appupd-seg-btn-on" : ""),
1330
+ onClick: () => setMode("updatable"),
1331
+ "aria-pressed": mode === "updatable",
1332
+ },
1333
+ "仅可更新"
1334
+ ),
1335
+ h(
1336
+ "button",
1337
+ {
1338
+ type: "button",
1339
+ className: "appupd-seg-btn" + (mode === "unsupported" ? " appupd-seg-btn-on" : ""),
1340
+ onClick: () => setMode("unsupported"),
1341
+ "aria-pressed": mode === "unsupported",
1342
+ },
1343
+ "未收录"
1344
+ )
1345
+ ),
1346
+ h(
1347
+ "span",
1348
+ { className: "appupd-count" },
1349
+ "筛选结果 " + filtered.length + " / " + items.length
1350
+ )
1351
+ )
1352
+ : null,
1353
+
1354
+ /* 7. 表格 */
1355
+ body
1356
+ ? body
1357
+ : h(
1358
+ "div",
1359
+ { className: "appupd-tablewrap" },
1360
+ h(
1361
+ "table",
1362
+ {
1363
+ className: "appupd-table",
1364
+ style: {
1365
+ width: colsWidth + "px",
1366
+ minWidth: "100%",
1367
+ },
1368
+ },
1369
+ h(
1370
+ "colgroup",
1371
+ null,
1372
+ h("col", { style: { width: CHECK_COL_PX + "px" } }),
1373
+ h("col", { style: { width: cols.name + "px" } }),
1374
+ h("col", { style: { width: cols.version + "px" } }),
1375
+ h("col", { style: { width: cols.available + "px" } }),
1376
+ h("col", { style: { width: cols.state + "px" } }),
1377
+ h("col", { style: { width: OP_COL_PX + "px" } })
1378
+ ),
1379
+ h(
1380
+ "thead",
1381
+ null,
1382
+ h(
1383
+ "tr",
1384
+ null,
1385
+ h(
1386
+ "th",
1387
+ { className: "appupd-col-check" },
1388
+ h("input", {
1389
+ type: "checkbox",
1390
+ className: "appupd-cb",
1391
+ checked: allSelected,
1392
+ disabled: selectableIds.length === 0,
1393
+ onChange: toggleAll,
1394
+ "aria-label": "全选可更新的应用",
1395
+ title:
1396
+ selectableIds.length === 0
1397
+ ? "当前筛选下没有可更新的应用"
1398
+ : allSelected
1399
+ ? "全不选"
1400
+ : "全选",
1401
+ ref: (el) => {
1402
+ if (el) el.indeterminate = !allSelected && selectedCount > 0;
1403
+ },
1404
+ })
1405
+ ),
1406
+ h("th", { className: "appupd-th" }, "应用", grip("name")),
1407
+ h("th", { className: "appupd-th appupd-col-ver" }, "已装版本", grip("version")),
1408
+ h("th", { className: "appupd-th appupd-col-ver" }, "可升级到", grip("available")),
1409
+ h("th", { className: "appupd-th appupd-col-state" }, "状态", grip("state")),
1410
+ h("th", { className: "appupd-col-op" }, "操作")
1411
+ )
1412
+ ),
1413
+ h(
1414
+ "tbody",
1415
+ null,
1416
+ rows.length
1417
+ ? rows
1418
+ : h(
1419
+ "tr",
1420
+ null,
1421
+ h("td", { colSpan: 6, className: "appupd-empty-cell" }, "没有匹配的应用")
1422
+ )
1423
+ )
1424
+ )
1425
+ ),
1426
+
1427
+ /* 6. 日志 */
1428
+ h(
1429
+ "div",
1430
+ { className: "appupd-log" },
1431
+ h(
1432
+ "button",
1433
+ {
1434
+ type: "button",
1435
+ className: "appupd-log-head",
1436
+ onClick: () => setLogOpen((v) => !v),
1437
+ "aria-expanded": logOpen,
1438
+ },
1439
+ h("span", { className: "appupd-log-caret" }, logOpen ? "▾" : "▸"),
1440
+ h("span", null, "运行日志(最近 " + mergedLog.length + " 条)")
1441
+ ),
1442
+ logOpen
1443
+ ? h(
1444
+ "div",
1445
+ { className: "appupd-log-body" },
1446
+ logLines.length ? logLines : h("div", { className: "appupd-lv-info" }, "暂无日志")
1447
+ )
1448
+ : null
1449
+ )
1450
+ );
1451
+ }
1452
+
1453
+ /* ------------------------------------------------------------------ *
1454
+ * 插件入口
1455
+ * ------------------------------------------------------------------ */
1456
+
1457
+ function apply(ctx) {
1458
+ ctx.effect(() => {
1459
+ if (typeof document === "undefined") return;
1460
+ const prev = document.querySelector('style[data-plugin-css="dsh-app-updater"]');
1461
+ if (prev && prev.parentNode) prev.parentNode.removeChild(prev);
1462
+ const tag = document.createElement("style");
1463
+ tag.dataset.plugin = "dsh-app-updater";
1464
+ tag.dataset.pluginCss = "dsh-app-updater";
1465
+ tag.textContent = CSS;
1466
+ document.head.appendChild(tag);
1467
+ return () => {
1468
+ if (tag.parentNode) tag.parentNode.removeChild(tag);
1469
+ };
1470
+ }, "dsh-app-updater: styles");
1471
+
1472
+ ctx.inject(["slots", "locale"], (scope) => {
1473
+ scope.slots.inject("settings.section", () =>
1474
+ scope.slots.register(
1475
+ {
1476
+ name: "settings.section",
1477
+ id: "dsh-app-updater",
1478
+ order: 42,
1479
+ label: () => "应用更新",
1480
+ },
1481
+ AppUpdaterSection
1482
+ )
1483
+ );
1484
+ });
1485
+ }
1486
+
1487
+ exports.apply = apply;
1488
+ exports.inject = ["slots", "locale"];
1489
+ return module.exports;
1490
+ },
1491
+ });