dsh-folia 4.10.0 → 4.12.1
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/cordis.patch.yml +28 -11
- package/lib/client.js +148 -18
- package/lib/routes/skins.js +95 -0
- package/package.json +2 -1
package/cordis.patch.yml
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
# dsh-plugin-folia bundle patch
|
|
1
|
+
# dsh-plugin-folia bundle patch.
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
# subpackage rows: about-notice / mascot-avatar / media-bridge / fonts / fx /
|
|
5
|
-
# parallax / pet-link). Those seven now live in this package under
|
|
6
|
-
# `lib/components/` and are mounted from `apply()` in `lib/index.js`, so the row
|
|
7
|
-
# set is exactly one row: this bundle.
|
|
3
|
+
# Row set: this bundle + the skin centre it brings along.
|
|
8
4
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
5
|
+
# ── 为什么这里有第二条 row ──────────────────────────────────────────────────
|
|
6
|
+
# 我们不再自己实现皮肤渲染:皮肤包(`skin.json` + `skin.css` + 可选 `hooks.mjs`)由
|
|
7
|
+
# `@linxin666/dsh-client-ui-skin-center` 加载与启用,我们的「助手皮肤」页签只负责把站点 zip
|
|
8
|
+
# 解到它读的那个目录(`$DSH_HOME/skins`)。所以它必须是**装得上的**:本包把它列为
|
|
9
|
+
# `dependencies`,并在下面插一条 row —— 用户装 dsh-folia 就同时得到两件套,不用再单独装。
|
|
10
|
+
#
|
|
11
|
+
# ── 重复 row 为什么不会打起来(实测依据,不是猜测)────────────────────────
|
|
12
|
+
# 1) 宿主半边:它的 `apply` 走 `mountOnce('@linxin666/dsh-client-ui-skin-center', …)`,
|
|
13
|
+
# 以 `globalThis[Symbol.for('dsh-web.mounted-plugins')]` 记账 ⇒ 第二次挂载是空操作,
|
|
14
|
+
# 不会重复注册 webserver 路由。
|
|
15
|
+
# 2) 客户端半边:它的设置分区注册包在 `try { ctx.slots.register(...) } catch { return () => {} }`,
|
|
16
|
+
# 重复注册被吞掉 ⇒ 不会出现两张「皮肤」设置页。
|
|
17
|
+
# 于是「用户已有 dsh-web-all(它也插了一条 row)」与「只用我们」两种情形都成立。
|
|
18
|
+
#
|
|
19
|
+
# 版本:依赖写 `^0.4.8`(本仓实测过的版本线);它若大改 v1alpha1 契约,我们跟着升级即可。
|
|
20
|
+
# 判据:test/verify-skin-center-bundle.mjs 会同时钉住「声明了依赖」与「插了唯一 id 的 row」。
|
|
14
21
|
#
|
|
15
22
|
# A patch replaces the targeted row's whole `config`, so only additive `insert`
|
|
16
23
|
# rows appear here (we never override an existing shipped row).
|
|
@@ -18,3 +25,13 @@
|
|
|
18
25
|
- insert:
|
|
19
26
|
- id: folia
|
|
20
27
|
name: 'dsh-folia'
|
|
28
|
+
# 皮肤中心:独立 npm 包(BSD-3-Clause),本包作为依赖携带。
|
|
29
|
+
#
|
|
30
|
+
# ⚠️ row id **不能**以 `folia-` 开头:`lib/subpackage-boot.js#subpackageSpecifiers` 把
|
|
31
|
+
# 「id 以 `folia-` 开头的行」当作**我们自己的子包行**做启动自检(解析不到就抛,因为宿主
|
|
32
|
+
# 对行级失败是静默的)。给第三方行起 `folia-skin-center` 这种 id 会被误判成"我们的子包"
|
|
33
|
+
# ⇒ 自检去仓库 node_modules 里解析它,开发树里没有这个依赖就直接抛,整个 apply 失败。
|
|
34
|
+
# 所以这里用不撞前缀的 `skin-center`(也与 web-all 的 `web-ui-skin-center`、
|
|
35
|
+
# 官方包的 `ui-skin-center` 都不同名)。
|
|
36
|
+
- id: skin-center
|
|
37
|
+
name: '@linxin666/dsh-client-ui-skin-center'
|
package/lib/client.js
CHANGED
|
@@ -801,6 +801,31 @@ window.__ModuleLoader__.load({
|
|
|
801
801
|
"下载地址不在白名单里": "The download host is not allow-listed",
|
|
802
802
|
"站点被关闭了(DSH_FOLIA_SITE=off)": "The site is disabled (DSH_FOLIA_SITE=off)",
|
|
803
803
|
"这个文件不是 zip": "This file is not a zip",
|
|
804
|
+
|
|
805
|
+
// ── 助手皮肤:皮肤中心缺失时的引导(4.10.1)──────────────────────────
|
|
806
|
+
"皮肤中心没装:它是**独立插件** @linxin666/dsh-client-ui-skin-center(不在 dsh-web-all 里,那个只是聚合包)。装它才能启用皮肤:": "The skin centre is not installed: it is its own plugin, @linxin666/dsh-client-ui-skin-center (dsh-web-all is only an aggregate). Install it to enable skins:",
|
|
807
|
+
"已复制安装命令": "Install command copied",
|
|
808
|
+
"复制失败,请手动选中上面的命令": "Copy failed — select the command above manually",
|
|
809
|
+
"复制命令": "Copy command",
|
|
810
|
+
"先装皮肤中心,否则皮肤装好也没法启用": "Install the skin centre first, or the skin cannot be enabled",
|
|
811
|
+
|
|
812
|
+
// ── 助手皮肤:页签内直接切换(4.12.0)────────────────────────────────
|
|
813
|
+
" · 使用中:{name}": " · active: {name}",
|
|
814
|
+
"✓ ": "✓ ",
|
|
815
|
+
"正在恢复…": "Restoring…",
|
|
816
|
+
"恢复默认外观": "Restore default look",
|
|
817
|
+
"应用": "Apply",
|
|
818
|
+
"切换中…": "Switching…",
|
|
819
|
+
"使用中": "Active",
|
|
820
|
+
"正在使用,先切到别的再卸": "Currently active — switch to another skin before removing",
|
|
821
|
+
"已切换到:{name}": "Switched to: {name}",
|
|
822
|
+
"已恢复官方默认外观": "Restored the default look",
|
|
823
|
+
"这个皮肤不在本机(先安装)": "This skin is not installed locally — install it first",
|
|
824
|
+
"装皮肤中心后才能切换皮肤(见下面的安装命令)": "The skin centre is required to switch skins (install command below)",
|
|
825
|
+
"皮肤 id 不合法": "Invalid skin id",
|
|
826
|
+
"已安装:{name} —— 点「应用」就能切换": "Installed: {name} — press Apply to switch",
|
|
827
|
+
"本页签可以直接切换皮肤:装完点「应用」即可,无需打开设置页。皮肤包落在皮肤中心目录,两边读同一份。":
|
|
828
|
+
"This tab switches skins directly: install, then press Apply — no need to open the settings page. Packs land in the skin centre's folder and both sides read the same files.",
|
|
804
829
|
};
|
|
805
830
|
|
|
806
831
|
// ── 内联模块:src/i18n-copy-picker.js —— 英文词表域:壁纸选择器与快捷面板(picker-* · quick-panel · sidebar-right · theme-follow)(构建期注入;勿手改本段)──
|
|
@@ -18272,6 +18297,16 @@ window.__ModuleLoader__.load({
|
|
|
18272
18297
|
return (v / 1048576).toFixed(1) + " MB";
|
|
18273
18298
|
}
|
|
18274
18299
|
|
|
18300
|
+
/**
|
|
18301
|
+
* 皮肤 id 的归一键:站点上的 slug 是**全站唯一**的,撞名时上传工具会加 `skin-` 前缀
|
|
18302
|
+
* (实测:blue-throated-bee-eater 同时是桌宠 id ⇒ 站上叫 skin-blue-throated-bee-eater),
|
|
18303
|
+
* 而本机目录名用的是 skin.json 里的原始 id。两边比对必须先归一,否则"已安装"会显示成未安装。
|
|
18304
|
+
*/
|
|
18305
|
+
function normSkinId(raw) {
|
|
18306
|
+
const s = String(raw || "").trim().toLowerCase();
|
|
18307
|
+
return s.startsWith("skin-") ? s.slice(5) : s;
|
|
18308
|
+
}
|
|
18309
|
+
|
|
18275
18310
|
function SkinsTabPanel() {
|
|
18276
18311
|
const [state, setState] = React.useState(null);
|
|
18277
18312
|
const [busy, setBusy] = React.useState("");
|
|
@@ -18282,6 +18317,14 @@ window.__ModuleLoader__.load({
|
|
|
18282
18317
|
const resp = await apiJson("/skins/list" + (force ? "?refresh=1" : ""), { parse: "always" });
|
|
18283
18318
|
const doc = (resp && resp.data) || null;
|
|
18284
18319
|
if (resp && resp.ok && doc && doc.ok) {
|
|
18320
|
+
// 顺带问一下当前在用哪套皮肤(皮肤中心没装时它会 409,忽略即可 —— 页签本来就会提示"缺插件")
|
|
18321
|
+
if (doc.skinCenter === "present") {
|
|
18322
|
+
try {
|
|
18323
|
+
const a = await apiJson("/skins/active", { parse: "always" });
|
|
18324
|
+
const ad = (a && a.data) || null;
|
|
18325
|
+
if (a && a.ok && ad && ad.ok) doc.active = ad.active || "";
|
|
18326
|
+
} catch { /* 读不到就显示"无使用中" */ }
|
|
18327
|
+
}
|
|
18285
18328
|
setState(doc);
|
|
18286
18329
|
if (doc.siteError && doc.siteError !== "site-off") {
|
|
18287
18330
|
setNote({ ok: false, text: weT("站点清单读不到:{msg}", { msg: String(doc.siteError) }) });
|
|
@@ -18297,8 +18340,10 @@ window.__ModuleLoader__.load({
|
|
|
18297
18340
|
React.useEffect(() => { load(false); }, [load]);
|
|
18298
18341
|
|
|
18299
18342
|
const act = async (kind, payload, label) => {
|
|
18343
|
+
// 结构性操作才锁整块 UI;`activate` 是幂等的、而且要让人能连点换皮肤,所以不锁。
|
|
18300
18344
|
const structural = kind === "install" || kind === "remove";
|
|
18301
18345
|
if (structural) setBusy(kind + ":" + ((payload && payload.id) || ""));
|
|
18346
|
+
if (kind === "activate") setBusy("activate:" + ((payload && payload.id) || ""));
|
|
18302
18347
|
setNote(null);
|
|
18303
18348
|
try {
|
|
18304
18349
|
const resp = await apiPostJson("/skins/" + kind, payload || {}, { parse: "always" });
|
|
@@ -18309,10 +18354,23 @@ window.__ModuleLoader__.load({
|
|
|
18309
18354
|
ok: true,
|
|
18310
18355
|
text: r.exists
|
|
18311
18356
|
? weT("已安装过:{name}", { name: label || "" })
|
|
18312
|
-
: weT("已安装:{name} ——
|
|
18357
|
+
: weT("已安装:{name} —— 点「应用」就能切换", { name: label || "" }),
|
|
18313
18358
|
});
|
|
18314
18359
|
}
|
|
18315
18360
|
if (kind === "remove") setNote({ ok: true, text: weT("已卸载:{name}", { name: label || "" }) });
|
|
18361
|
+
if (kind === "activate") {
|
|
18362
|
+
// 两条收敛路径:① 广播皮肤中心自己监听的 `dsh-skin-applied`(立即就地切换);
|
|
18363
|
+
// ② 它另外每 2 秒轮询 /api/skin-center/v2/active(事件丢了也会在 2 秒内跟上)。
|
|
18364
|
+
try {
|
|
18365
|
+
window.dispatchEvent(new CustomEvent("dsh-skin-applied", { detail: { id: (payload && payload.id) || null } }));
|
|
18366
|
+
} catch { /* 老环境没有 CustomEvent 也无所谓,②会兜住 */ }
|
|
18367
|
+
setNote({
|
|
18368
|
+
ok: true,
|
|
18369
|
+
text: payload && payload.id
|
|
18370
|
+
? weT("已切换到:{name}", { name: label || "" })
|
|
18371
|
+
: weT("已恢复官方默认外观"),
|
|
18372
|
+
});
|
|
18373
|
+
}
|
|
18316
18374
|
await load(true);
|
|
18317
18375
|
} else {
|
|
18318
18376
|
const code = (r && r.error) || "failed";
|
|
@@ -18323,6 +18381,9 @@ window.__ModuleLoader__.load({
|
|
|
18323
18381
|
'host-not-allowed': weT("下载地址不在白名单里"),
|
|
18324
18382
|
'site-off': weT("站点被关闭了(DSH_FOLIA_SITE=off)"),
|
|
18325
18383
|
'not-a-zip': weT("这个文件不是 zip"),
|
|
18384
|
+
'not-installed': weT("这个皮肤不在本机(先安装)"),
|
|
18385
|
+
'skin-center-missing': weT("装皮肤中心后才能切换皮肤(见下面的安装命令)"),
|
|
18386
|
+
'bad-id': weT("皮肤 id 不合法"),
|
|
18326
18387
|
};
|
|
18327
18388
|
setNote({ ok: false, text: map[code] || weT("操作失败:{code}", { code }) });
|
|
18328
18389
|
}
|
|
@@ -18343,11 +18404,33 @@ window.__ModuleLoader__.load({
|
|
|
18343
18404
|
}, weT("刷新")),
|
|
18344
18405
|
));
|
|
18345
18406
|
|
|
18346
|
-
|
|
18347
|
-
|
|
18348
|
-
|
|
18349
|
-
|
|
18350
|
-
|
|
18407
|
+
// 皮肤中心在不在,决定这段提示说什么:
|
|
18408
|
+
// 在 → "装完去皮肤中心点它"(本页签只负责落盘)
|
|
18409
|
+
// 不在 → 明说它是个**独立插件**(不在 dsh-web-all 里),给一条可复制的安装命令
|
|
18410
|
+
// unknown(探测失败)→ 保守地按"在"讲,不吓唬用户
|
|
18411
|
+
const skinCenter = (state && state.skinCenter) || "unknown";
|
|
18412
|
+
if (skinCenter === "absent") {
|
|
18413
|
+
children.push(React.createElement("div", {
|
|
18414
|
+
key: "hint-absent",
|
|
18415
|
+
className: "we-picker__hint",
|
|
18416
|
+
style: { margin: "6px 0 4px", color: "#ffd08a", lineHeight: "1.6" },
|
|
18417
|
+
}, weT("皮肤中心没装:它是**独立插件** @linxin666/dsh-client-ui-skin-center(不在 dsh-web-all 里,那个只是聚合包)。装它才能启用皮肤:")));
|
|
18418
|
+
children.push(React.createElement("div", { key: "cmd", style: { display: "flex", gap: "8px", alignItems: "center", margin: "0 0 6px" } },
|
|
18419
|
+
React.createElement("code", {
|
|
18420
|
+
style: { fontSize: "11px", padding: "3px 8px", borderRadius: "6px", background: "rgba(255,255,255,0.08)" },
|
|
18421
|
+
}, "dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center"),
|
|
18422
|
+
React.createElement("button", {
|
|
18423
|
+
className: "we-picker__btn", type: "button",
|
|
18424
|
+
onClick: () => { try { navigator.clipboard.writeText("dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center"); setNote({ ok: true, text: weT("已复制安装命令") }); } catch { setNote({ ok: false, text: weT("复制失败,请手动选中上面的命令") }); } },
|
|
18425
|
+
}, weT("复制命令")),
|
|
18426
|
+
));
|
|
18427
|
+
} else {
|
|
18428
|
+
children.push(React.createElement("div", {
|
|
18429
|
+
key: "hint",
|
|
18430
|
+
className: "we-picker__hint",
|
|
18431
|
+
style: { margin: "6px 0 4px", opacity: 0.8 },
|
|
18432
|
+
}, weT("本页签可以直接切换皮肤:装完点「应用」即可,无需打开设置页。皮肤包落在皮肤中心目录,两边读同一份。")));
|
|
18433
|
+
}
|
|
18351
18434
|
|
|
18352
18435
|
// 提示行常驻占位(与开机动画/桌宠页签同一条纪律:不因 note 出现/消失让面板跳高度)
|
|
18353
18436
|
children.push(React.createElement("div", {
|
|
@@ -18361,25 +18444,56 @@ window.__ModuleLoader__.load({
|
|
|
18361
18444
|
}, note ? note.text : "\u00a0"));
|
|
18362
18445
|
|
|
18363
18446
|
const installed = (state && state.installed) || [];
|
|
18447
|
+
const activeId = (state && state.active) || "";
|
|
18448
|
+
/** 这个 id 是不是"正在用"的那套(两边都归一后比,站点 slug 可能带 skin- 前缀)。 */
|
|
18449
|
+
const isActiveSkin = (id) => Boolean(activeId) && normSkinId(activeId) === normSkinId(id);
|
|
18364
18450
|
if (state) {
|
|
18365
18451
|
children.push(React.createElement("div", { key: "installed-head", className: "we-picker__hint" },
|
|
18366
|
-
weT("本机已装({n})", { n: installed.length })
|
|
18452
|
+
weT("本机已装({n})", { n: installed.length }),
|
|
18453
|
+
activeId ? weT(" · 使用中:{name}", { name: activeId }) : ""));
|
|
18454
|
+
// 「恢复默认」:清空选择、回到官方外观。只有确实有皮肤在用的时候才有意义。
|
|
18455
|
+
if (activeId) {
|
|
18456
|
+
children.push(React.createElement("button", {
|
|
18457
|
+
key: "clear",
|
|
18458
|
+
className: "we-picker__btn", type: "button",
|
|
18459
|
+
style: { alignSelf: "flex-start", margin: "0 0 6px" },
|
|
18460
|
+
disabled: Boolean(busy),
|
|
18461
|
+
onClick: () => act("activate", { id: "" }, ""),
|
|
18462
|
+
}, busy === "activate:" ? weT("正在恢复…") : weT("恢复默认外观")));
|
|
18463
|
+
}
|
|
18367
18464
|
children.push(React.createElement("div", { key: "installed", style: SKIN_STYLE.grid },
|
|
18368
18465
|
installed.length === 0
|
|
18369
18466
|
? [React.createElement("span", { key: "none", className: "we-picker__hint" }, weT("还没有安装任何皮肤 —— 从下面的站点清单里挑一个。"))]
|
|
18370
|
-
: installed.map((s) =>
|
|
18371
|
-
|
|
18467
|
+
: installed.map((s) => {
|
|
18468
|
+
const isActive = isActiveSkin(s.id);
|
|
18469
|
+
const busyThis = busy === "activate:" + s.id;
|
|
18470
|
+
return React.createElement("div", {
|
|
18471
|
+
key: s.id,
|
|
18472
|
+
style: isActive ? { ...SKIN_STYLE.card, borderColor: "rgba(120,220,150,0.75)" } : SKIN_STYLE.card,
|
|
18473
|
+
},
|
|
18474
|
+
React.createElement("div", { style: { ...SKIN_STYLE.title, color: s.accent || undefined } },
|
|
18475
|
+
(isActive ? weT("✓ ") : "") + (s.name || s.id)),
|
|
18372
18476
|
React.createElement("div", { style: SKIN_STYLE.meta },
|
|
18373
18477
|
[s.version ? "v" + s.version : "", skinFmtBytes(s.bytes), s.hasHooks ? weT("含动效钩子") : ""]
|
|
18374
18478
|
.filter(Boolean).join(" · ")),
|
|
18375
18479
|
React.createElement("div", { style: { display: "flex", gap: "6px", alignItems: "center" } },
|
|
18480
|
+
// 「应用」= 写皮肤中心的 active + 广播 `dsh-skin-applied`,就地切换(不刷新页面)。
|
|
18481
|
+
isActive
|
|
18482
|
+
? React.createElement("span", { style: { ...SKIN_STYLE.badge, background: "rgba(60,160,90,0.55)" } }, weT("使用中"))
|
|
18483
|
+
: React.createElement("button", {
|
|
18484
|
+
className: "we-picker__btn", type: "button",
|
|
18485
|
+
disabled: Boolean(busy),
|
|
18486
|
+
onClick: () => act("activate", { id: s.id }, s.name),
|
|
18487
|
+
}, busyThis ? weT("切换中…") : weT("应用")),
|
|
18376
18488
|
React.createElement("button", {
|
|
18377
18489
|
className: "we-picker__btn", type: "button",
|
|
18378
|
-
disabled: Boolean(busy),
|
|
18490
|
+
disabled: Boolean(busy) || isActive,
|
|
18491
|
+
title: isActive ? weT("正在使用,先切到别的再卸") : undefined,
|
|
18379
18492
|
onClick: () => act("remove", { id: s.id }, s.name),
|
|
18380
18493
|
}, busy === "remove:" + s.id ? weT("卸载中…") : weT("卸载")),
|
|
18381
18494
|
),
|
|
18382
|
-
)
|
|
18495
|
+
);
|
|
18496
|
+
}),
|
|
18383
18497
|
));
|
|
18384
18498
|
}
|
|
18385
18499
|
|
|
@@ -18400,13 +18514,29 @@ window.__ModuleLoader__.load({
|
|
|
18400
18514
|
React.createElement("div", { style: SKIN_STYLE.title, title: s.title }, s.title),
|
|
18401
18515
|
React.createElement("div", { style: SKIN_STYLE.meta },
|
|
18402
18516
|
[skinFmtBytes(s.bytes), (s.tags || []).includes("动态") ? weT("动态") : ""].filter(Boolean).join(" · ")),
|
|
18403
|
-
|
|
18404
|
-
|
|
18405
|
-
|
|
18406
|
-
|
|
18407
|
-
|
|
18408
|
-
|
|
18409
|
-
|
|
18517
|
+
React.createElement("div", { style: { display: "flex", gap: "6px", alignItems: "center", flexWrap: "wrap" } },
|
|
18518
|
+
s.installed
|
|
18519
|
+
// 已装的站点卡片也要能**直接应用**:用户是照着站点缩略图挑的,
|
|
18520
|
+
// 让他再滚回上面的「本机已装」区去点一次是多余的一步(实测反馈)。
|
|
18521
|
+
// 站点 slug 与本机目录名可能差一个 `skin-` 前缀 ⇒ 用归一后的比较。
|
|
18522
|
+
? [
|
|
18523
|
+
isActiveSkin(s.id)
|
|
18524
|
+
? React.createElement("span", { key: "on", style: { ...SKIN_STYLE.badge, background: "rgba(60,160,90,0.55)" } }, weT("使用中"))
|
|
18525
|
+
: React.createElement("button", {
|
|
18526
|
+
key: "apply",
|
|
18527
|
+
className: "we-picker__btn", type: "button",
|
|
18528
|
+
disabled: Boolean(busy),
|
|
18529
|
+
onClick: () => act("activate", { id: normSkinId(s.id) }, s.title),
|
|
18530
|
+
}, busy === "activate:" + normSkinId(s.id) ? weT("切换中…") : weT("应用")),
|
|
18531
|
+
React.createElement("span", { key: "done", style: SKIN_STYLE.badge }, weT("已安装")),
|
|
18532
|
+
]
|
|
18533
|
+
: React.createElement("button", {
|
|
18534
|
+
className: "we-picker__btn", type: "button",
|
|
18535
|
+
disabled: Boolean(busy),
|
|
18536
|
+
title: skinCenter === "absent" ? weT("先装皮肤中心,否则皮肤装好也没法启用") : undefined,
|
|
18537
|
+
onClick: () => act("install", { id: s.id, url: s.url }, s.title),
|
|
18538
|
+
}, busyThis ? weT("安装中…") : weT("安装")),
|
|
18539
|
+
),
|
|
18410
18540
|
);
|
|
18411
18541
|
}),
|
|
18412
18542
|
));
|
package/lib/routes/skins.js
CHANGED
|
@@ -49,6 +49,30 @@ function siteBase() {
|
|
|
49
49
|
return base.replace(/\/+$/, '');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 皮肤中心是否在线。
|
|
54
|
+
*
|
|
55
|
+
* 为什么由宿主探而不是前端探:两个插件跑在同一个 webServer 上,用请求自带的 Host 就能问到自己 ——
|
|
56
|
+
* 前端也能问,但宿主这一侧拿到的结果与"装没装"一一对应,而且不受浏览器缓存/跨源策略影响。
|
|
57
|
+
* 判据用**它自己的 catalog 路由**:该路由不需要鉴权(实测 200),404 就是没装。
|
|
58
|
+
*/
|
|
59
|
+
const skinCenterProbe = { at: 0, state: 'unknown' };
|
|
60
|
+
async function probeSkinCenter(hostHeader, force) {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
if (!force && skinCenterProbe.at && now - skinCenterProbe.at < 30000) return skinCenterProbe.state;
|
|
63
|
+
let state = 'unknown';
|
|
64
|
+
if (hostHeader) {
|
|
65
|
+
try {
|
|
66
|
+
const r = await fetch('http://' + hostHeader + '/api/skin-center/v2/catalog', { signal: AbortSignal.timeout(4000) });
|
|
67
|
+
state = r.status === 404 ? 'absent' : (r.ok ? 'present' : 'present');
|
|
68
|
+
// 401/403 也说明插件在(只是该路由被别的版本加了鉴权);只有 404 才代表没装。
|
|
69
|
+
} catch { state = 'unknown'; }
|
|
70
|
+
}
|
|
71
|
+
skinCenterProbe.at = now;
|
|
72
|
+
skinCenterProbe.state = state;
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
75
|
+
|
|
52
76
|
/** 站点列表缓存(30 秒):面板打开与"刷新"共用,避免每次都打站点。 */
|
|
53
77
|
const siteCache = { at: 0, items: [], error: null };
|
|
54
78
|
const SITE_TTL_MS = 30000;
|
|
@@ -238,6 +262,7 @@ export function registerSkinsRoutes(webServer, c) {
|
|
|
238
262
|
const force = /[?&]refresh=1/.test(req.url || '');
|
|
239
263
|
const site = await fetchSiteSkins(force);
|
|
240
264
|
const installed = listInstalled();
|
|
265
|
+
const skinCenter = await probeSkinCenter(req.headers && req.headers.host, force);
|
|
241
266
|
// ⚠️ 站点 slug 与本机皮肤 id **不总是同一个字符串**:站点上 slug 是全站唯一的,
|
|
242
267
|
// 撞名时上传工具会加 `skin-` 前缀(实测 blue-throated-bee-eater 同时是桌宠 id)。
|
|
243
268
|
// 所以比对要归一:两边都去掉可选的 `skin-` 前缀再比,否则会显示成"未安装"。
|
|
@@ -248,6 +273,7 @@ export function registerSkinsRoutes(webServer, c) {
|
|
|
248
273
|
}
|
|
249
274
|
sendJson(res, 200, {
|
|
250
275
|
ok: true,
|
|
276
|
+
skinCenter,
|
|
251
277
|
siteBase: siteBase(),
|
|
252
278
|
siteError: site.error,
|
|
253
279
|
installed,
|
|
@@ -365,4 +391,73 @@ export function registerSkinsRoutes(webServer, c) {
|
|
|
365
391
|
sendJson(res, 200, { ok: true, id });
|
|
366
392
|
},
|
|
367
393
|
}));
|
|
394
|
+
|
|
395
|
+
/* ④ 应用:把"选中的皮肤"写进皮肤中心的 active 端点 —— **不自己实现渲染**。
|
|
396
|
+
*
|
|
397
|
+
* 为什么这样接:皮肤中心的客户端有两条收敛路径(读它的源码得到,不是猜):
|
|
398
|
+
* · `window` 上的 `dsh-skin-applied` 事件(`detail: { id }`)⇒ 立即就地切换;
|
|
399
|
+
* · 每 2000ms 轮询 `/api/skin-center/v2/active` ⇒ 事件丢了也会在 2 秒内收敛。
|
|
400
|
+
* 所以这里只写"选择",客户端再补一个事件把切换做成瞬时的。皮肤中心没装时明确 409,
|
|
401
|
+
* 而不是让用户点了没反应。
|
|
402
|
+
*/
|
|
403
|
+
disposers.push(webServer.register({
|
|
404
|
+
kind: 'exact',
|
|
405
|
+
path: `${base}/skins/activate`,
|
|
406
|
+
handler: async (req, res) => {
|
|
407
|
+
if ((req.method || 'GET').toUpperCase() !== 'POST') { sendJson(res, 405, { ok: false, error: 'method-not-allowed' }); return; }
|
|
408
|
+
const host = req.headers && req.headers.host;
|
|
409
|
+
const state = await probeSkinCenter(host, true);
|
|
410
|
+
if (state === 'absent') { sendJson(res, 409, { ok: false, error: 'skin-center-missing' }); return; }
|
|
411
|
+
const body = await readJsonBody(req);
|
|
412
|
+
// 允许清空(回到官方默认):id 传空串 / null。
|
|
413
|
+
const raw = body && body.id;
|
|
414
|
+
const requested = (raw === null || raw === undefined || String(raw) === '') ? null : safeSkinId(raw);
|
|
415
|
+
if (raw !== null && raw !== undefined && String(raw) !== '' && !requested) {
|
|
416
|
+
sendJson(res, 400, { ok: false, error: 'bad-id' });
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// 只允许切到**本机真实存在**的皮肤:否则皮肤中心会尝试渲染一个不存在的目录。
|
|
420
|
+
if (requested && !existsSync(join(skinsRoot(), requested))) {
|
|
421
|
+
sendJson(res, 404, { ok: false, error: 'not-installed' });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const r = await fetch('http://' + host + '/api/skin-center/v2/active', {
|
|
426
|
+
method: 'POST',
|
|
427
|
+
headers: { 'content-type': 'application/json' },
|
|
428
|
+
body: JSON.stringify({ active: requested }),
|
|
429
|
+
signal: AbortSignal.timeout(6000),
|
|
430
|
+
});
|
|
431
|
+
const text = await r.text();
|
|
432
|
+
let doc = null;
|
|
433
|
+
try { doc = JSON.parse(text); } catch { /* 非 JSON */ }
|
|
434
|
+
if (!r.ok) {
|
|
435
|
+
sendJson(res, 502, { ok: false, error: 'skin-center-' + r.status, detail: String(text).slice(0, 160) });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
sendJson(res, 200, { ok: true, active: requested, skinCenter: doc && doc.active });
|
|
439
|
+
} catch (e) {
|
|
440
|
+
sendJson(res, 502, { ok: false, error: e && e.name === 'TimeoutError' ? 'timeout' : 'network' });
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
}));
|
|
444
|
+
|
|
445
|
+
/* ⑤ 当前皮肤(给面板显示"使用中",读的是皮肤中心自己记的选择) */
|
|
446
|
+
disposers.push(webServer.register({
|
|
447
|
+
kind: 'exact',
|
|
448
|
+
path: `${base}/skins/active`,
|
|
449
|
+
handler: async (req, res) => {
|
|
450
|
+
const host = req.headers && req.headers.host;
|
|
451
|
+
const state = await probeSkinCenter(host, true);
|
|
452
|
+
if (state === 'absent') { sendJson(res, 409, { ok: false, error: 'skin-center-missing' }); return; }
|
|
453
|
+
try {
|
|
454
|
+
const r = await fetch('http://' + host + '/api/skin-center/v2/active', { signal: AbortSignal.timeout(5000) });
|
|
455
|
+
const doc = await r.json().catch(() => null);
|
|
456
|
+
if (!r.ok) { sendJson(res, 502, { ok: false, error: 'skin-center-' + r.status }); return; }
|
|
457
|
+
sendJson(res, 200, { ok: true, active: (doc && doc.active) || null });
|
|
458
|
+
} catch (e) {
|
|
459
|
+
sendJson(res, 502, { ok: false, error: e && e.name === 'TimeoutError' ? 'timeout' : 'network' });
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
}));
|
|
368
463
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-folia",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.12.1",
|
|
4
4
|
"description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -193,6 +193,7 @@
|
|
|
193
193
|
}
|
|
194
194
|
},
|
|
195
195
|
"dependencies": {
|
|
196
|
+
"@linxin666/dsh-client-ui-skin-center": "^0.4.8",
|
|
196
197
|
"webwallgl": "^3.0.0",
|
|
197
198
|
"x11": "^4.3.1"
|
|
198
199
|
},
|