dsh-folia 4.16.1 → 4.17.0
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 +94 -4
- package/lib/routes/downloads.js +6 -37
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -18955,10 +18955,40 @@ window.__ModuleLoader__.load({
|
|
|
18955
18955
|
* 整区面板:这个页签的内容**整个**由功能自己的组件渲染(而不是 quick-panel 内的通用列表)。
|
|
18956
18956
|
* 没登记的页签走 quick-panel 自己的通用布局(壁纸列表 / 外观节 / 播放节)。
|
|
18957
18957
|
*/
|
|
18958
|
+
/**
|
|
18959
|
+
* 整区面板的**守卫包装**:任何一个页签在渲染期抛错,只让这一页显示错误信息,
|
|
18960
|
+
* 不许把整个面板带走。
|
|
18961
|
+
*
|
|
18962
|
+
* 实测故障:站点数据里出现一条字段形状异常的条目,渲染期抛错 ⇒ React 丢弃整棵树 ⇒
|
|
18963
|
+
* 整个「壁纸」窗口一片空白,用户只看到"界面消失了",换版本也一样(故障在数据)。
|
|
18964
|
+
* 单卡错离在 quick-panel 里做;这里补"整页错离",两层缺一就会重演那种空白。
|
|
18965
|
+
*/
|
|
18966
|
+
function guardTabPanel(name, Panel) {
|
|
18967
|
+
if (typeof Panel !== "function") return Panel;
|
|
18968
|
+
function GuardedTabPanel(props) {
|
|
18969
|
+
try {
|
|
18970
|
+
return Panel(props);
|
|
18971
|
+
} catch (e) {
|
|
18972
|
+
const why = (e && (e.message || String(e))) || "unknown";
|
|
18973
|
+
try { console.warn("[folia] 页签渲染失败:" + name, why); } catch { /* ignore */ }
|
|
18974
|
+
return React.createElement("div", {
|
|
18975
|
+
className: "we-picker",
|
|
18976
|
+
style: { padding: "12px", fontSize: "13px", lineHeight: 1.6, color: "#ff9a9a" },
|
|
18977
|
+
},
|
|
18978
|
+
React.createElement("div", null, weT("这一页加载失败:{name}", { name })),
|
|
18979
|
+
React.createElement("div", { style: { opacity: 0.8, marginTop: "4px", fontSize: "12px" } }, String(why).slice(0, 240)),
|
|
18980
|
+
React.createElement("div", { style: { opacity: 0.6, marginTop: "6px", fontSize: "12px" } },
|
|
18981
|
+
weT("其它页签仍可用;把这行原因发给作者即可定位。")));
|
|
18982
|
+
}
|
|
18983
|
+
}
|
|
18984
|
+
GuardedTabPanel.displayName = "Guarded(" + name + ")";
|
|
18985
|
+
return GuardedTabPanel;
|
|
18986
|
+
}
|
|
18987
|
+
|
|
18958
18988
|
const WE_FEATURE_PANELS = {
|
|
18959
|
-
boot: BootTabPanel,
|
|
18960
|
-
pets: PetsTabPanel,
|
|
18961
|
-
skins: SkinsTabPanel,
|
|
18989
|
+
boot: guardTabPanel("boot", BootTabPanel),
|
|
18990
|
+
pets: guardTabPanel("pets", PetsTabPanel),
|
|
18991
|
+
skins: guardTabPanel("skins", SkinsTabPanel),
|
|
18962
18992
|
};
|
|
18963
18993
|
|
|
18964
18994
|
/**
|
|
@@ -19316,6 +19346,63 @@ window.__ModuleLoader__.load({
|
|
|
19316
19346
|
return shortSide > 0 && shortSide < 1280;
|
|
19317
19347
|
}
|
|
19318
19348
|
|
|
19349
|
+
/**
|
|
19350
|
+
* 把一条清单数据**归一**成渲染层可以无条件相信的形状。
|
|
19351
|
+
*
|
|
19352
|
+
* 为什么在渲染前统一做,而不是每处判空:站点清单来自外部(154+ 条、每天都在变),
|
|
19353
|
+
* 字段形状出现一次意外(`tags` 是字符串、`preview` 是对象、`bytes` 是 "1.2 MB")就会让
|
|
19354
|
+
* 卡片渲染期抛错。判空散落在十几个位置必然漏,归一只有一处。
|
|
19355
|
+
*/
|
|
19356
|
+
function qpNormalizeItem(raw) {
|
|
19357
|
+
const w = (raw && typeof raw === "object") ? raw : {};
|
|
19358
|
+
const str = (v) => (typeof v === "string" && v ? v : null);
|
|
19359
|
+
const num = (v) => {
|
|
19360
|
+
const n = Number(v);
|
|
19361
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
19362
|
+
};
|
|
19363
|
+
let tags = w.tags;
|
|
19364
|
+
if (typeof tags === "string") tags = tags.split(/[,,\s]+/).filter(Boolean);
|
|
19365
|
+
if (!Array.isArray(tags)) tags = [];
|
|
19366
|
+
tags = tags.filter((t) => typeof t === "string");
|
|
19367
|
+
const type = str(w.type);
|
|
19368
|
+
return {
|
|
19369
|
+
...w,
|
|
19370
|
+
id: (typeof w.id === "string" || typeof w.id === "number") ? w.id : String(w.id || ""),
|
|
19371
|
+
title: str(w.title) || String(w.id || ""),
|
|
19372
|
+
type: (type === "video" || type === "image" || type === "scene" || type === "package") ? type : "image",
|
|
19373
|
+
preview: str(w.preview),
|
|
19374
|
+
thumb: str(w.thumb),
|
|
19375
|
+
media: str(w.media),
|
|
19376
|
+
mediaProxy: str(w.mediaProxy),
|
|
19377
|
+
bytes: num(w.bytes),
|
|
19378
|
+
width: num(w.width),
|
|
19379
|
+
height: num(w.height),
|
|
19380
|
+
duration: num(w.duration),
|
|
19381
|
+
tags,
|
|
19382
|
+
rating: str(w.rating) || "unknown",
|
|
19383
|
+
};
|
|
19384
|
+
}
|
|
19385
|
+
|
|
19386
|
+
/**
|
|
19387
|
+
* 渲染一张卡,**任何异常都只影响这一张**。
|
|
19388
|
+
* 失败时渲染一张"不可用"占位卡(带原因),而不是抛出把整棵树带走。
|
|
19389
|
+
*/
|
|
19390
|
+
function qpRenderCardGuarded(raw, sel, cardsView) {
|
|
19391
|
+
const w = qpNormalizeItem(raw);
|
|
19392
|
+
try {
|
|
19393
|
+
return cardsView ? qpRenderCard(w, sel) : qpRenderRow(w, sel);
|
|
19394
|
+
} catch (e) {
|
|
19395
|
+
const why = (e && (e.message || String(e))) || "unknown";
|
|
19396
|
+
try { console.warn("[folia] 卡片渲染失败,已跳过:", w && w.id, why); } catch { /* ignore */ }
|
|
19397
|
+
return React.createElement("div", {
|
|
19398
|
+
key: "bad-" + String((w && w.id) || Math.random()),
|
|
19399
|
+
className: "we-qp__card we-qp__card--broken",
|
|
19400
|
+
title: why,
|
|
19401
|
+
style: { opacity: 0.5, padding: "8px", fontSize: "12px", lineHeight: 1.4 },
|
|
19402
|
+
}, weT("这条无法显示(数据异常)"), React.createElement("br"), String((w && w.id) || ""), " · ", why.slice(0, 80));
|
|
19403
|
+
}
|
|
19404
|
+
}
|
|
19405
|
+
|
|
19319
19406
|
function qpRenderCard(w, sel) {
|
|
19320
19407
|
// ── 卡:缩略图网格形态(视图切换的「卡片」档;点击语义与列表行一致)──
|
|
19321
19408
|
return React.createElement("div", {
|
|
@@ -19642,7 +19729,10 @@ window.__ModuleLoader__.load({
|
|
|
19642
19729
|
rows.length
|
|
19643
19730
|
? React.createElement(React.Fragment, null,
|
|
19644
19731
|
vTopH > 0 ? qpRenderSpacer("we-qp-vtop", vTopH) : null,
|
|
19645
|
-
|
|
19732
|
+
// **单卡错离**:一条数据坏掉只丢那一张卡,不许带走整片列表。
|
|
19733
|
+
// 实测故障:站点清单里出现字段形状异常(tags 非数组 / preview 非字符串 / media 缺失)后,
|
|
19734
|
+
// 卡片渲染期抛错 ⇒ React 丢弃整棵树 ⇒ 整个面板一片空白且没有任何提示。
|
|
19735
|
+
rows.map((w) => qpRenderCardGuarded(w, sel, cardsView)),
|
|
19646
19736
|
vBtmH > 0 ? qpRenderSpacer("we-qp-vbtm", vBtmH) : null,
|
|
19647
19737
|
)
|
|
19648
19738
|
: React.createElement(React.Fragment, null,
|
package/lib/routes/downloads.js
CHANGED
|
@@ -15,18 +15,11 @@
|
|
|
15
15
|
* 注入契约:`disposers` / `base` / `serveFile` / `log`(队列由本模块自建单例)。
|
|
16
16
|
*/
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
|
-
import { existsSync, statSync } from 'node:fs';
|
|
19
18
|
import { join } from 'node:path';
|
|
20
19
|
import { homedir } from 'node:os';
|
|
21
20
|
import { createDownloadQueue } from '../download-queue.js';
|
|
22
21
|
export const DEPS = ['disposers', 'base', 'serveFile', 'log'];
|
|
23
22
|
|
|
24
|
-
/**
|
|
25
|
-
* 与 `lib/routes/site-media.js#EXT_OK` **必须一致**:那个模块按"扩展名在白名单里就保留、
|
|
26
|
-
* 否则用 `.bin`"算缓存文件名。这里查缓存时必须用同一套规则,否则永远命不中(=重复下载)。
|
|
27
|
-
*/
|
|
28
|
-
const SITE_MEDIA_EXT_OK = new Set(['.mp4', '.webm', '.mov', '.m4v', '.jpg', '.jpeg', '.png', '.webp', '.avif', '.gif']);
|
|
29
|
-
|
|
30
23
|
/** 本地库与状态文件的位置(与其它路由同一口径:DSH_HOME 或桌面端的 harness 目录)。 */
|
|
31
24
|
function dshHome() {
|
|
32
25
|
return process.env.DSH_HOME || join(process.env.APPDATA || homedir(), 'dsh-desktop', 'harness');
|
|
@@ -46,44 +39,20 @@ function libraryDest(kind, slug, ext) {
|
|
|
46
39
|
}
|
|
47
40
|
|
|
48
41
|
/**
|
|
49
|
-
* inventory
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* ① 下载队列落地(`<home>/library/<kind>/…`)⇒ `/folia/local-media/<id>`
|
|
53
|
-
* ② **site-media 缓存**(首次播放时边下边播留下的那份)⇒ 走已有的
|
|
54
|
-
* `/folia/site-media/<name>?u=…`,它命中缓存时直接 `serveFile`,**不触网**
|
|
55
|
-
*
|
|
56
|
-
* 为什么必须查②:站点壁纸第一次播放会经过 site-media 代理并落盘,如果这里只看①,
|
|
57
|
-
* 同一份字节会被**再下载一遍**(21 MB 的包在弱网上就是"点了很久才出来"的主因),
|
|
58
|
-
* 而且磁盘上存两份。
|
|
42
|
+
* inventory 用:这条站点资源**是否已经落到本地**,以及它的 job id。
|
|
43
|
+
* 命中 ⇒ inventory 把 `media` 指向 `/folia/local-media/<id>`(渲染只读本地)。
|
|
44
|
+
* 未命中 ⇒ 返回 null,调用方保持原来的远端/代理地址(用户点了会先入队下载)。
|
|
59
45
|
*/
|
|
60
46
|
export function localCopyFor(url, kind = 'wallpaper') {
|
|
61
47
|
try {
|
|
62
|
-
const
|
|
63
|
-
const u = new URL(raw);
|
|
48
|
+
const u = new URL(String(url));
|
|
64
49
|
const ext = (u.pathname.match(/\.([a-z0-9]{2,5})$/i) || [])[1] || 'bin';
|
|
65
50
|
const slug = u.pathname.split('/').filter(Boolean).pop() || 'item';
|
|
66
51
|
const dest = libraryDest(kind, slug, ext);
|
|
67
52
|
const q = getDownloadQueue();
|
|
68
53
|
const job = q.snapshot().find((j) => j.dest === dest);
|
|
69
|
-
if (job
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
// ② site-media 缓存:键算法必须与 lib/routes/site-media.js#keyFor **逐字一致**
|
|
73
|
-
// (`sha256(url)[:32] + 规范化扩展名`),否则永远命不中。
|
|
74
|
-
const key = createHash('sha256').update(raw).digest('hex').slice(0, 32)
|
|
75
|
-
+ (SITE_MEDIA_EXT_OK.has('.' + ext.toLowerCase()) ? '.' + ext.toLowerCase() : '.bin');
|
|
76
|
-
const cached = join(dshHome(), 'cache', 'site-media', key);
|
|
77
|
-
if (existsSync(cached)) {
|
|
78
|
-
const name = raw.split('/').pop() || 'media';
|
|
79
|
-
return {
|
|
80
|
-
id: key,
|
|
81
|
-
url: '/folia/site-media/' + encodeURIComponent(name) + '?u=' + encodeURIComponent(raw),
|
|
82
|
-
bytes: (() => { try { return statSync(cached).size; } catch { return 0; } })(),
|
|
83
|
-
source: 'site-media-cache',
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
return null;
|
|
54
|
+
if (!job || job.status !== 'done') return null;
|
|
55
|
+
return { id: job.id, url: `/folia/local-media/${job.id}`, bytes: job.total };
|
|
87
56
|
} catch { return null; }
|
|
88
57
|
}
|
|
89
58
|
|