dsh-folia 4.14.1 → 4.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -18309,6 +18309,9 @@ window.__ModuleLoader__.load({
18309
18309
  function SkinsTabPanel() {
18310
18310
  const [state, setState] = React.useState(null);
18311
18311
  const [busy, setBusy] = React.useState("");
18312
+ // 给"静默对账"用的只读镜像:定时器/可见性回调里不能读闭包里的旧 `busy`(会一直看到 ""),
18313
+ // 用一个 ref 跟住最新值,操作进行中就不打扰用户。
18314
+ const busyRef = React.useRef("");
18312
18315
  const [note, setNote] = React.useState(null);
18313
18316
 
18314
18317
  const load = React.useCallback(async (force) => {
@@ -18356,11 +18359,33 @@ window.__ModuleLoader__.load({
18356
18359
 
18357
18360
  React.useEffect(() => { load(false); }, [load]);
18358
18361
 
18362
+ // 与外部改动**保持同步**:皮肤可能在皮肤中心那边被删/装,也可能在另一个窗口里操作。
18363
+ // 之前只在挂载时拉一次 ⇒ 那边删了,这边还列着(点了才失败,看起来像"没同步")。
18364
+ // 三条腿都只做"重拉一次列表",不做别的:
18365
+ // ① 页签重新可见(switch 回来)② 窗口重新获得焦点 ③ 每 20 秒的轻量对账
18366
+ // 轻量对账只在**没有正在进行的操作**时跑,避免把用户刚点的"安装中…"状态冲掉。
18367
+ React.useEffect(() => {
18368
+ if (typeof document === "undefined" || typeof window === "undefined") return undefined;
18369
+ let timer = null;
18370
+ const refreshQuietly = () => { try { if (!busyRef.current) load(false); } catch { /* ignore */ } };
18371
+ const onVisible = () => { try { if (document.visibilityState === "visible") refreshQuietly(); } catch { /* ignore */ } };
18372
+ try { document.addEventListener("visibilitychange", onVisible); } catch { /* ignore */ }
18373
+ try { window.addEventListener("focus", refreshQuietly); } catch { /* ignore */ }
18374
+ try { timer = window.setInterval(refreshQuietly, 20000); } catch { timer = null; }
18375
+ return () => {
18376
+ try { document.removeEventListener("visibilitychange", onVisible); } catch { /* ignore */ }
18377
+ try { window.removeEventListener("focus", refreshQuietly); } catch { /* ignore */ }
18378
+ if (timer !== null) { try { window.clearInterval(timer); } catch { /* ignore */ } }
18379
+ };
18380
+ }, [load]);
18381
+
18359
18382
  const act = async (kind, payload, label) => {
18360
18383
  // 结构性操作才锁整块 UI;`activate` 是幂等的、而且要让人能连点换皮肤,所以不锁。
18361
18384
  const structural = kind === "install" || kind === "remove";
18362
- if (structural) setBusy(kind + ":" + ((payload && payload.id) || ""));
18363
- if (kind === "activate") setBusy("activate:" + ((payload && payload.id) || ""));
18385
+ const busyKey = structural
18386
+ ? kind + ":" + ((payload && payload.id) || "")
18387
+ : (kind === "activate" ? "activate:" + ((payload && payload.id) || "") : "");
18388
+ if (busyKey) { setBusy(busyKey); busyRef.current = busyKey; }
18364
18389
  setNote(null);
18365
18390
  try {
18366
18391
  if (kind === "activate") {
@@ -18454,6 +18479,7 @@ window.__ModuleLoader__.load({
18454
18479
  setNote({ ok: false, text: weT("请求失败:{msg}", { msg: (e && e.message) || weT("网络错误") }) });
18455
18480
  } finally {
18456
18481
  setBusy("");
18482
+ busyRef.current = "";
18457
18483
  }
18458
18484
  };
18459
18485
 
@@ -18572,9 +18598,11 @@ window.__ModuleLoader__.load({
18572
18598
  // 皮肤中心**没收录**这套(它有自己的 manifest 规则)⇒ 应用后它会回退到内置默认款,
18573
18599
  // 用户看到的是"点哪套都不变"。这是实测症状(别人机器上"只剩鲸鱼娘"),所以这里
18574
18600
  // 直接把原因与「应用」的禁用一起给出来,而不是让人一遍遍点。
18601
+ // ⚠️ 只有 `=== false` 才下结论:`null` 表示"问不到它的 catalog"(未知),
18602
+ // 未知绝不能禁用按钮 —— 那会变成"替我猜错了就不让我点"。
18575
18603
  const notListed = s.inCatalog === false;
18576
18604
  const notListedWhy = notListed
18577
- ? weT("皮肤中心没有收录这套(它的 manifest 规则更严:id 必须等于目录名、必须 v2、name/nameEn/author/stylesheet 齐全)")
18605
+ ? weT("皮肤中心仍未收录这套(它按 manifest 规则筛选:id 必须等于目录名、必须 v2、name/nameEn/author/stylesheet 齐全)。点「刷新」会强制它对账一次;若仍显示这条,说明确实被它拒收")
18578
18606
  : "";
18579
18607
  return React.createElement("div", {
18580
18608
  key: s.id,
@@ -181,8 +181,24 @@ async function fetchSkinCenterCatalog(hostHeader, force) {
181
181
  const siteCache = { at: 0, items: [], error: null };
182
182
  const SITE_TTL_MS = 30000;
183
183
 
184
- async function fetchSiteSkins(force) {
185
- const now = Date.now();
184
+ /**
185
+ * 给"本机已装"逐条附上**皮肤中心是否收录**。
186
+ *
187
+ * 比对口径要宽:它的 catalog 以 `manifest.id` 为键,而我们的条目同时有 `id`(manifest)与
188
+ * `dirId`(目录名),且历史上站点 slug 可能带 `skin-` 前缀 ⇒ 三种形态都试,任一命中即算收录。
189
+ * `catalog === null`(问不到)时给 `null`:**未知不等于未收录**,界面不能据此禁用「应用」。
190
+ */
191
+ function annotateCatalog(installed, catalog) {
192
+ if (!catalog) return installed.map((s) => ({ ...s, inCatalog: null, catalogWarnings: [] }));
193
+ return installed.map((s) => {
194
+ const hit = catalog.has(s.id) ? s.id
195
+ : catalog.has(s.dirId) ? s.dirId
196
+ : catalog.has(normSkinId(s.id)) ? normSkinId(s.id) : null;
197
+ return { ...s, inCatalog: Boolean(hit), catalogWarnings: hit ? catalog.get(hit) : [] };
198
+ });
199
+ }
200
+
201
+ async function fetchSiteSkins(force) { const now = Date.now();
186
202
  if (!force && siteCache.at && now - siteCache.at < SITE_TTL_MS) return siteCache;
187
203
  const base = siteBase();
188
204
  if (!base) { siteCache.at = now; siteCache.items = []; siteCache.error = 'site-off'; return siteCache; }
@@ -385,14 +401,18 @@ export function registerSkinsRoutes(webServer, c) {
385
401
  // 关键对账:**它收录了哪几套**。`null` = 问不到(不据此判红)。
386
402
  // 被它剔除的皮肤"能装但应用后画面不变"——它客户端会回退到内置默认款,
387
403
  // 用户看到的就是"点哪套都还是鲸鱼娘/blue-fantasy"(实测症状)。
388
- const catalog = skinCenter === 'present'
389
- ? await fetchSkinCenterCatalog(req.headers && req.headers.host, force)
390
- : null;
391
- const installed = installedBase.map((s) => {
392
- if (!catalog) return { ...s, inCatalog: null, catalogWarnings: [] };
393
- const hit = catalog.has(s.id) ? s.id : (catalog.has(s.dirId) ? s.dirId : null);
394
- return { ...s, inCatalog: Boolean(hit), catalogWarnings: hit ? catalog.get(hit) : [] };
395
- });
404
+ //
405
+ // ⚠️ 但"它没列出"**不能**只凭一次缓存就下定论:刚装完皮肤时,它自己的 catalog 可能还没
406
+ // 重扫(它按目录 mtime 做缓存),我们 30 秒的副本更可能比它更旧。实测:别人机器上刚装完
407
+ // 就被标成「皮肤中心未收录」,而打开它的设置页却是有的 —— 一次假红。
408
+ // 所以对账分两拍:先按缓存比,**出现"未收录"就强制重拉一次**再定论。
409
+ const hostHeader = req.headers && req.headers.host;
410
+ let catalog = skinCenter === 'present' ? await fetchSkinCenterCatalog(hostHeader, force) : null;
411
+ let installed = annotateCatalog(installedBase, catalog);
412
+ if (catalog && installed.some((s) => s.inCatalog === false)) {
413
+ catalog = await fetchSkinCenterCatalog(hostHeader, true);
414
+ installed = annotateCatalog(installedBase, catalog);
415
+ }
396
416
  // ⚠️ 站点 slug 与本机皮肤 id **不总是同一个字符串**:站点上 slug 是全站唯一的,
397
417
  // 撞名时上传工具会加 `skin-` 前缀(实测 blue-throated-bee-eater 同时是桌宠 id)。
398
418
  // 所以比对要归一:两边都去掉可选的 `skin-` 前缀再比,否则会显示成"未安装"。
@@ -524,11 +544,24 @@ export function registerSkinsRoutes(webServer, c) {
524
544
  const dir = resolveInside(root, id);
525
545
  if (!dir) { sendJson(res, 400, { ok: false, error: 'unsafe-id' }); return; }
526
546
  if (!existsSync(dir)) { sendJson(res, 404, { ok: false, error: 'not-installed' }); return; }
547
+ // 删之前先看它是不是"当前正在用的那套":如果是,必须**先把选择清掉**再删目录,
548
+ // 否则 active 指向一个不存在的 id ⇒ 皮肤中心回退到内置默认款,用户看到的是
549
+ // "皮肤没换成我选的那套"(实测症状)。清空走同一套兜底链路(HTTP→状态文件)。
550
+ let clearedActive = false;
551
+ try {
552
+ const statePath = activeStatePath();
553
+ let cur = null;
554
+ try { cur = (JSON.parse(readFileSync(statePath, 'utf8')) || {}).active || null; } catch { cur = null; }
555
+ if (cur && (cur === id || normSkinId(cur) === normSkinId(id))) {
556
+ writeActiveStateFile(null);
557
+ clearedActive = true;
558
+ }
559
+ } catch { /* 清不掉也不阻塞删除:目录照删,只是 active 可能悬空(页签会显示"恢复默认外观") */ }
527
560
  try { rmSync(dir, { recursive: true, force: true }); } catch (e) {
528
561
  sendJson(res, 500, { ok: false, error: 'remove-failed', detail: String(e && e.message).slice(0, 160) });
529
562
  return;
530
563
  }
531
- sendJson(res, 200, { ok: true, id });
564
+ sendJson(res, 200, { ok: true, id, clearedActive });
532
565
  },
533
566
  }));
534
567
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-folia",
3
- "version": "4.14.1",
3
+ "version": "4.14.3",
4
4
  "description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
5
5
  "type": "module",
6
6
  "engines": {