dsh-folia 4.15.2 → 4.15.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
@@ -18504,7 +18504,7 @@ window.__ModuleLoader__.load({
18504
18504
  className: "we-picker__btn", type: "button",
18505
18505
  disabled: Boolean(busy),
18506
18506
  onClick: () => load(true),
18507
- }, weT("刷新")),
18507
+ }, busyIsStructural ? weT("下载中…(可继续切换皮肤)") : weT("刷新")),
18508
18508
  ));
18509
18509
 
18510
18510
  // 皮肤中心在不在,决定这段提示说什么:
@@ -18568,6 +18568,14 @@ window.__ModuleLoader__.load({
18568
18568
  },
18569
18569
  }, note ? note.text : "\u00a0"));
18570
18570
 
18571
+ // ⚠️ 「正在下载/安装」**不能**把整块面板锁住:实测用户点「安装」后想切到另一套皮肤,
18572
+ // 结果所有「应用」都灰着,必须等 zip 下完 —— 下载是长任务,切换应该与它互不阻塞。
18573
+ // 所以把 busy 拆成两条互不相干的通道:
18574
+ // · 结构操作(install / remove / repair):改动磁盘目录,串行,期间只锁**结构按钮**
18575
+ // · 切换(activate):只写 active,幂等且"最后动作优先",期间只锁**该卡片的「应用」**
18576
+ const busyIsStructural = /^(install|remove|repair):/.test(String(busy || ""));
18577
+ const busyIsActivating = /^activate:/.test(String(busy || ""));
18578
+
18571
18579
  const installed = (state && state.installed) || [];
18572
18580
  const activeId = (state && state.active) || "";
18573
18581
  // 「皮肤中心记着哪套」≠「屏上现在是哪套」:壁纸在台上时皮肤会 withhold 自己,
@@ -18670,7 +18678,7 @@ window.__ModuleLoader__.load({
18670
18678
  ? React.createElement("span", { style: { ...SKIN_STYLE.badge, background: "rgba(60,160,90,0.55)" } }, weT("使用中"))
18671
18679
  : React.createElement("button", {
18672
18680
  className: "we-picker__btn", type: "button",
18673
- disabled: Boolean(busy) || !canApply || notListed,
18681
+ disabled: (busyIsActivating && busyThis) || !canApply || notListed,
18674
18682
  title: notListed ? notListedWhy : (applyBlockedReason || undefined),
18675
18683
  onClick: () => act("activate", { id: s.id }, s.name),
18676
18684
  }, busyThis ? weT("切换中…") : weT("应用")),
@@ -18679,14 +18687,14 @@ window.__ModuleLoader__.load({
18679
18687
  notListed
18680
18688
  ? React.createElement("button", {
18681
18689
  className: "we-picker__btn", type: "button",
18682
- disabled: Boolean(busy),
18690
+ disabled: busyIsStructural,
18683
18691
  title: notListedWhy,
18684
18692
  onClick: () => act("repair", { id: s.dirId || s.id }, s.name),
18685
18693
  }, busy === "repair:" + (s.dirId || s.id) ? weT("修复中…") : weT("修复清单"))
18686
18694
  : null,
18687
18695
  React.createElement("button", {
18688
18696
  className: "we-picker__btn", type: "button",
18689
- disabled: Boolean(busy) || isActive,
18697
+ disabled: busyIsStructural || isActive,
18690
18698
  title: isActive ? weT("正在使用,先切到别的再卸") : undefined,
18691
18699
  onClick: () => act("remove", { id: s.id }, s.name),
18692
18700
  }, busy === "remove:" + s.id ? weT("卸载中…") : weT("卸载")),
@@ -18727,7 +18735,7 @@ window.__ModuleLoader__.load({
18727
18735
  : React.createElement("button", {
18728
18736
  key: "apply",
18729
18737
  className: "we-picker__btn", type: "button",
18730
- disabled: Boolean(busy) || !canApply,
18738
+ disabled: (busyIsActivating && busy === "activate:" + localId) || !canApply,
18731
18739
  title: applyBlockedReason || undefined,
18732
18740
  onClick: () => act("activate", { id: localId }, s.title),
18733
18741
  }, busy === "activate:" + localId ? weT("切换中…") : weT("应用")),
@@ -18735,7 +18743,7 @@ window.__ModuleLoader__.load({
18735
18743
  ]
18736
18744
  : React.createElement("button", {
18737
18745
  className: "we-picker__btn", type: "button",
18738
- disabled: Boolean(busy),
18746
+ disabled: busyIsStructural || busy === "install:" + s.id,
18739
18747
  title: skinCenter === "absent" ? weT("先装皮肤中心,否则皮肤装好也没法启用") : undefined,
18740
18748
  onClick: () => act("install", { id: s.id, url: s.url }, s.title),
18741
18749
  }, busyThis ? weT("安装中…") : weT("安装")),
package/lib/features.js CHANGED
@@ -37,7 +37,7 @@ export const FEATURES = [
37
37
  role: '本机/上传/站点三源合并成壁纸库;着色、图层、播放与轮换由客户端接管',
38
38
  host: [
39
39
  'lib/routes/inventory.js',
40
- 'lib/routes/site-media.js',
40
+ 'lib/routes/site-media.js',
41
41
  'lib/routes/media-bytes.js',
42
42
  'lib/routes/media-derived.js',
43
43
  'lib/routes/scene-frame.js',
package/lib/index.js CHANGED
@@ -86,7 +86,7 @@ import { registerOcclusionRoutes } from './routes/occlusion.js';
86
86
  import { registerSceneFrameRoutes } from './routes/scene-frame.js';
87
87
  import { registerMediaDerivedRoutes } from './routes/media-derived.js';
88
88
  import { registerMediaBytesRoutes } from './routes/media-bytes.js';
89
- import { registerSiteMediaRoutes } from './routes/site-media.js';
89
+ import { registerSiteMediaRoutes } from './routes/site-media.js';
90
90
  import { registerWeAssetsRoutes } from './routes/we-assets.js';
91
91
  import { registerSceneMediaRoutes } from './routes/scene-media.js';
92
92
  import { registerSceneServeRoutes } from './routes/scene-serve.js';
@@ -330,7 +330,9 @@ export function apply(ctx) {
330
330
  // 为什么单开一族而不是塞进 /media:那条是**本机字节直出**(token → 磁盘),
331
331
  // 这条是"上游 → 磁盘 → 客户端"的三方转发,失败语义完全不同(一切失败 302 直连)。
332
332
  // ⚠️ 必须早于任何 `/site-*` 前缀族:本路由是 exact,不受顺序影响,但保持就近声明。
333
- registerSiteMediaRoutes(webServer, { disposers, base: BASE, serveFile, log });
333
+ registerSiteMediaRoutes(webServer, { disposers, base: BASE, serveFile, log });
334
+
335
+
334
336
 
335
337
  // 3c-3b/3c-5. 场景帧服务族路由(实时抓帧缓存 / 自定义画面)→ lib/routes/scene-frame.js。
336
338
  // 三条注册与它们的依赖都在那个文件里;这里只声明它的依赖。
@@ -92,7 +92,7 @@ export const FEATURE_ROUTE_FAMILIES = [
92
92
  export const INLINE_ROUTE_FAMILIES = [
93
93
  // 每条写「族 id + 门面里的注册函数名」:两者**不总同形**(presets → registerGlassPresetsRoutes),
94
94
  // 靠名字猜会在守卫里制造假红。迁移一个族 = 在 FEATURE_ROUTE_FAMILIES 加一条 + 删这里一行。
95
- { id: 'site-media', fn: 'registerSiteMediaRoutes' },
95
+ { id: 'site-media', fn: 'registerSiteMediaRoutes' },
96
96
  { id: 'media-bytes', fn: 'registerMediaBytesRoutes' },
97
97
  { id: 'media-derived', fn: 'registerMediaDerivedRoutes' },
98
98
  { id: 'scene-frame', fn: 'registerSceneFrameRoutes' },
@@ -276,6 +276,47 @@ export function registerSiteMediaRoutes(webServer, c) {
276
276
  }
277
277
  }
278
278
 
279
+ // ── 计数与统计:别的机器"慢"到底慢在哪,必须有可读的数字,而不是靠感觉 ──────────
280
+ // hits/misses 区分"没缓存所以每次都要下"与"缓存生效但网络慢";
281
+ // bytes/ms 给出每次都真实吞吐;writable 说明那台机器的缓存目录到底能不能写。
282
+ const stats = { hits: 0, misses: 0, bytes: 0, ms: 0, failures: 0, since: Date.now() };
283
+
284
+ disposers.push(webServer.register({
285
+ kind: 'exact',
286
+ path: `${BASE}/site-media-stats`,
287
+ handler: (req, res) => {
288
+ let total = 0;
289
+ let files = 0;
290
+ try {
291
+ const dir = cacheRoot();
292
+ for (const name of readdirSync(dir)) {
293
+ if (name.endsWith('.part') || name.endsWith('.part.json')) continue;
294
+ try { total += statSync(join(dir, name)).size; files += 1; } catch { /* ignore */ }
295
+ }
296
+ } catch { /* 目录不存在即 0 */ }
297
+ const dirRoot = cacheRoot();
298
+ const body = JSON.stringify({
299
+ ok: true,
300
+ root: dirRoot,
301
+ writable: ensureCache(dirRoot),
302
+ files,
303
+ bytesOnDisk: total,
304
+ capBytes: capBytes(),
305
+ hits: stats.hits,
306
+ misses: stats.misses,
307
+ failures: stats.failures,
308
+ avgMBps: stats.ms > 0 ? Number(((stats.bytes / 1048576) / (stats.ms / 1000)).toFixed(2)) : null,
309
+ uptimeSec: Math.round((Date.now() - stats.since) / 1000),
310
+ });
311
+ try {
312
+ res.statusCode = 200;
313
+ res.setHeader('content-type', 'application/json; charset=utf-8');
314
+ res.setHeader('cache-control', 'no-store');
315
+ res.end(body);
316
+ } catch { /* ignore */ }
317
+ },
318
+ }));
319
+
279
320
  disposers.push(webServer.register({
280
321
  kind: 'prefix',
281
322
  // 路径尾部只是给客户端留一个"像文件名"的段(客户端按 `url.split('/').pop()` 取 token 做
@@ -301,6 +342,7 @@ export function registerSiteMediaRoutes(webServer, c) {
301
342
  const abs = join(dir, key);
302
343
 
303
344
  if (ensureCache(dir) && existsSync(abs)) {
345
+ stats.hits += 1;
304
346
  touch(abs);
305
347
  // 与 miss 对称的可观测头:页签/诊断据此区分"网络慢"与"我们没有缓存"。
306
348
  try { res.setHeader('x-folia-cache', 'hit'); } catch { /* ignore */ }
@@ -317,7 +359,13 @@ export function registerSiteMediaRoutes(webServer, c) {
317
359
  redirect(res, u);
318
360
  return;
319
361
  }
320
- const task = download(u, key, req, res, headOnly);
362
+ stats.misses += 1;
363
+ const t0 = Date.now();
364
+ const task = download(u, key, req, res, headOnly).then((ok) => {
365
+ stats.ms += Date.now() - t0;
366
+ if (ok) { try { stats.bytes += statSync(abs).size; } catch { /* ignore */ } }
367
+ return ok;
368
+ }).catch((e) => { stats.failures += 1; throw e; });
321
369
  inflight.set(key, task);
322
370
  let ok = false;
323
371
  try { ok = await task; } finally { inflight.delete(key); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-folia",
3
- "version": "4.15.2",
3
+ "version": "4.15.4",
4
4
  "description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
5
5
  "type": "module",
6
6
  "engines": {
@@ -42,6 +42,7 @@
42
42
  "files": [
43
43
  "icon.png",
44
44
  "lib/index.js",
45
+ "lib/client.js",
45
46
  "lib/host-boot.js",
46
47
  "lib/log.js",
47
48
  "lib/notice.js",
@@ -57,7 +58,6 @@
57
58
  "lib/glass-presets/",
58
59
  "lib/about/",
59
60
  "lib/component-visibility.js",
60
- "lib/client.js",
61
61
  "lib/http-body.js",
62
62
  "lib/json-response.js",
63
63
  "lib/pkg-read.js",