dsh-folia 2.6.0 → 2.7.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 +161 -10
- package/lib/routes/skins.js +34 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -16337,7 +16337,131 @@ window.__ModuleLoader__.load({
|
|
|
16337
16337
|
return { kind: pick.type };
|
|
16338
16338
|
}
|
|
16339
16339
|
|
|
16340
|
-
|
|
16340
|
+
/* ── 热切换运行时:作用域属性 + 六个装饰层 + hooks ───────────────────────────────
|
|
16341
|
+
*
|
|
16342
|
+
* 皮肤不是"一份 CSS"。它有三层,缺一层就"显示不全":
|
|
16343
|
+
* ① 样式:`stylesheet` + `patches`
|
|
16344
|
+
* ② 背景:`contributes.backgroundMedia`
|
|
16345
|
+
* ③ 作用域与装饰层:`html[data-dsh-skin="<id>"]` + 六个 `[data-dsh-skin-layer=...]`
|
|
16346
|
+
*
|
|
16347
|
+
* 第 ③ 项是"大部分皮肤看起来没生效"的真正原因 —— 65 套里有 **30 套**把规则整段挂在
|
|
16348
|
+
* `html[data-dsh-skin]` 下面,属性不设,那些规则一条都不匹配;另有 **28 套**带
|
|
16349
|
+
* `hooks.mjs`(粒子/交互),契约见皮肤中心 `contracts/hooks-api.d.ts`:
|
|
16350
|
+
* apply({ skinId, scopeAttr, assetBase, layers{...}, theme{get,subscribe}, onCleanup })
|
|
16351
|
+
* 所以这里补齐作用域属性、六个层,并按契约跑 hooks(失败只降级)。
|
|
16352
|
+
*/
|
|
16353
|
+
|
|
16354
|
+
const SKIN_LAYER_NAMES = ["background", "ambient", "bottom", "sidebar", "top", "foreground"];
|
|
16355
|
+
/** 本次激活注册的清理函数(切换皮肤时逐个跑掉,幂等)。 */
|
|
16356
|
+
let skinHotCleanups = [];
|
|
16357
|
+
|
|
16358
|
+
function skinThemeNow() {
|
|
16359
|
+
try {
|
|
16360
|
+
return (document.body && document.body.hasAttribute("data-ds-dark-theme")) ? "dark" : "light";
|
|
16361
|
+
} catch {
|
|
16362
|
+
return "light";
|
|
16363
|
+
}
|
|
16364
|
+
}
|
|
16365
|
+
|
|
16366
|
+
/** 建立/复用六个装饰层,返回契约里那个 `layers` 对象。 */
|
|
16367
|
+
function ensureSkinLayers() {
|
|
16368
|
+
const host = document.body || document.documentElement;
|
|
16369
|
+
const out = {};
|
|
16370
|
+
for (const name of SKIN_LAYER_NAMES) {
|
|
16371
|
+
let el = null;
|
|
16372
|
+
try { el = document.querySelector('[data-dsh-skin-layer="' + name + '"]'); } catch { el = null; }
|
|
16373
|
+
if (!el) {
|
|
16374
|
+
el = document.createElement("div");
|
|
16375
|
+
el.setAttribute("data-dsh-skin-layer", name);
|
|
16376
|
+
// 只给最必要的几何 + 不吃事件;层级交给皮肤自己的 CSS
|
|
16377
|
+
//(`background` 是契约里明写 z-index -2 的那层,其余留 auto)。
|
|
16378
|
+
el.style.cssText = "position:fixed;inset:0;pointer-events:none;"
|
|
16379
|
+
+ (name === "background" ? "z-index:-2;" : "");
|
|
16380
|
+
host.appendChild(el);
|
|
16381
|
+
}
|
|
16382
|
+
out[name] = el;
|
|
16383
|
+
}
|
|
16384
|
+
return out;
|
|
16385
|
+
}
|
|
16386
|
+
|
|
16387
|
+
/**
|
|
16388
|
+
* 按契约跑一套皮肤的 hooks。失败**只降级**(返回原因),绝不打断样式/背景。
|
|
16389
|
+
* @returns {Promise<{ ran: boolean, reason?: string }>}
|
|
16390
|
+
*/
|
|
16391
|
+
async function applySkinHooks(id) {
|
|
16392
|
+
const resp = await apiJson("/skins/manifest?id=" + encodeURIComponent(id));
|
|
16393
|
+
const doc = (resp && resp.data) || null;
|
|
16394
|
+
const rel = doc && doc.hooks;
|
|
16395
|
+
if (!rel) return { ran: false, reason: "no-hooks" };
|
|
16396
|
+
|
|
16397
|
+
const encoded = String(rel).split("/").map(encodeURIComponent).join("/");
|
|
16398
|
+
const r = await apiFetch("/skins/files/" + encodeURIComponent(id) + "/" + encoded, {
|
|
16399
|
+
parse: false,
|
|
16400
|
+
cache: "no-store",
|
|
16401
|
+
});
|
|
16402
|
+
if (!r || !r.ok || !r.response) return { ran: false, reason: "hooks-http-" + ((r && r.status) || 0) };
|
|
16403
|
+
const source = await r.response.text();
|
|
16404
|
+
if (!source || !source.trim()) return { ran: false, reason: "hooks-empty" };
|
|
16405
|
+
|
|
16406
|
+
const blobUrl = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
|
|
16407
|
+
try {
|
|
16408
|
+
const mod = await import(blobUrl);
|
|
16409
|
+
const factory = mod && typeof mod.default === "function" ? mod.default : null;
|
|
16410
|
+
if (!factory) return { ran: false, reason: "no-default-export" };
|
|
16411
|
+
const hooks = factory();
|
|
16412
|
+
if (!hooks || typeof hooks.apply !== "function") return { ran: false, reason: "no-apply" };
|
|
16413
|
+
|
|
16414
|
+
const layers = ensureSkinLayers();
|
|
16415
|
+
const unsubs = [];
|
|
16416
|
+
const ctx = {
|
|
16417
|
+
skinId: id,
|
|
16418
|
+
scopeAttr: id,
|
|
16419
|
+
assetBase: apiUrl("/skins/files/" + encodeURIComponent(id) + "/"),
|
|
16420
|
+
layers,
|
|
16421
|
+
theme: {
|
|
16422
|
+
get: skinThemeNow,
|
|
16423
|
+
subscribe(listener) {
|
|
16424
|
+
try {
|
|
16425
|
+
const obs = new MutationObserver(() => {
|
|
16426
|
+
try { listener(skinThemeNow()); } catch { /* 监听方出错不影响别人 */ }
|
|
16427
|
+
});
|
|
16428
|
+
obs.observe(document.body, { attributes: true, attributeFilter: ["data-ds-dark-theme"] });
|
|
16429
|
+
const off = () => { try { obs.disconnect(); } catch { /* ignore */ } };
|
|
16430
|
+
unsubs.push(off);
|
|
16431
|
+
return off;
|
|
16432
|
+
} catch {
|
|
16433
|
+
return () => {};
|
|
16434
|
+
}
|
|
16435
|
+
},
|
|
16436
|
+
},
|
|
16437
|
+
onCleanup(fn) { if (typeof fn === "function") skinHotCleanups.push(fn); },
|
|
16438
|
+
};
|
|
16439
|
+
hooks.apply(ctx);
|
|
16440
|
+
if (typeof hooks.dispose === "function") {
|
|
16441
|
+
skinHotCleanups.push(() => { try { hooks.dispose(); } catch { /* 幂等 */ } });
|
|
16442
|
+
}
|
|
16443
|
+
skinHotCleanups.push(() => { for (const off of unsubs) { try { off(); } catch { /* ignore */ } } });
|
|
16444
|
+
return { ran: true };
|
|
16445
|
+
} finally {
|
|
16446
|
+
setTimeout(() => { try { URL.revokeObjectURL(blobUrl); } catch { /* ignore */ } }, 10000);
|
|
16447
|
+
}
|
|
16448
|
+
}
|
|
16449
|
+
|
|
16450
|
+
/** 撤掉上一次热切换留下的东西(清理函数 + 注入样式)。作用域属性由下一次直接覆盖。 */
|
|
16451
|
+
function clearSkinHot() {
|
|
16452
|
+
for (const fn of skinHotCleanups.splice(0).reverse()) {
|
|
16453
|
+
try { fn(); } catch { /* 清理幂等,单个失败不影响其余 */ }
|
|
16454
|
+
}
|
|
16455
|
+
try {
|
|
16456
|
+
const el = document.getElementById("folia-skin-hot");
|
|
16457
|
+
if (el) el.remove();
|
|
16458
|
+
} catch { /* ignore */ }
|
|
16459
|
+
}
|
|
16460
|
+
|
|
16461
|
+
function SkinsTabPanel(props) {
|
|
16462
|
+
// 壁纸让位:挂载点(设置页 / 首页抽屉)各自把"清掉当前壁纸"的回调传进来 ——
|
|
16463
|
+
// 皮肤和壁纸都在铺背景,必须二选一。本组件不直接碰设置,只调用传进来的入口。
|
|
16464
|
+
const onClearWallpaper = props && props.onClearWallpaper;
|
|
16341
16465
|
const [list, setList] = React.useState([]);
|
|
16342
16466
|
const [active, setActive] = React.useState(null);
|
|
16343
16467
|
const [loading, setLoading] = React.useState(true);
|
|
@@ -16405,23 +16529,46 @@ window.__ModuleLoader__.load({
|
|
|
16405
16529
|
let info = null;
|
|
16406
16530
|
let applyErr = "";
|
|
16407
16531
|
let bgKind = "none";
|
|
16532
|
+
let hooksInfo = { ran: false, reason: "skipped" };
|
|
16408
16533
|
try {
|
|
16534
|
+
// ① 先清掉上一次热切换留下的清理函数与注入样式(避免两套皮肤叠加)
|
|
16535
|
+
clearSkinHot();
|
|
16536
|
+
// ② 作用域属性:30/65 套皮肤的规则整段挂在 html[data-dsh-skin="<id>"] 下,
|
|
16537
|
+
// 不设这个属性,那些规则一条都不匹配 —— 这是"显示不全"的最大来源。
|
|
16538
|
+
try { document.documentElement.setAttribute("data-dsh-skin", id); } catch { /* ignore */ }
|
|
16539
|
+
// ③ 六个装饰层(hooks 与部分皮肤 CSS 都要用)
|
|
16540
|
+
ensureSkinLayers();
|
|
16541
|
+
// ④ 样式、⑤ 背景、⑥ hooks
|
|
16409
16542
|
info = await applySkinCss(id);
|
|
16410
|
-
// 样式之后立刻搭背景层:没有它,凡是靠 backgroundMedia 的皮肤(水墨丹青、
|
|
16411
|
-
// 鲸鱼娘、深海女仆工坊…)都只会"变配色不变画面"。
|
|
16412
16543
|
const bg = await applySkinBackground(id);
|
|
16413
16544
|
bgKind = bg.kind;
|
|
16545
|
+
try {
|
|
16546
|
+
hooksInfo = await applySkinHooks(id);
|
|
16547
|
+
} catch (e) {
|
|
16548
|
+
hooksInfo = { ran: false, reason: String((e && e.message) || e) };
|
|
16549
|
+
}
|
|
16414
16550
|
} catch (e) {
|
|
16415
16551
|
applyErr = String((e && e.message) || e);
|
|
16416
16552
|
}
|
|
16553
|
+
// ⑦ 壁纸让位:皮肤和壁纸都在铺背景,必须互斥。清掉 `id` 之后壁纸层会自己撤下
|
|
16554
|
+
// (同时 `data-we-wallpaper` 标记消失,皮肤中心也不会再扣住皮肤)。
|
|
16555
|
+
let wallpaperCleared = false;
|
|
16556
|
+
try {
|
|
16557
|
+
if (typeof onClearWallpaper === "function") {
|
|
16558
|
+
await Promise.resolve(onClearWallpaper());
|
|
16559
|
+
wallpaperCleared = true;
|
|
16560
|
+
}
|
|
16561
|
+
} catch { /* 清壁纸失败不影响换肤本身 */ }
|
|
16417
16562
|
setNote({
|
|
16418
16563
|
kind: applyErr ? "err" : "ok",
|
|
16419
16564
|
text: applyErr
|
|
16420
16565
|
? weT("已切换为「") + label + weT("」,但样式注入失败:") + applyErr
|
|
16421
16566
|
: weT("已切换到「") + label + weT("」—— 样式 ")
|
|
16422
16567
|
+ Math.round(info.bytes / 1024) + " KB / " + info.rules + weT(" 条规则")
|
|
16423
|
-
+ (bgKind === "none" ? weT("
|
|
16424
|
-
|
|
16568
|
+
+ (bgKind === "none" ? weT(",无背景媒体") : weT(",背景:") + bgKind)
|
|
16569
|
+
+ (hooksInfo.ran ? weT(",动效已启动") : (hooksInfo.reason === "no-hooks" ? weT(",无动效") : weT(",动效未启动(") + hooksInfo.reason + ")"))
|
|
16570
|
+
+ (wallpaperCleared ? weT(",壁纸已关闭") : ""),
|
|
16571
|
+
// hooks 已经跑过了;这里只作为"整页重载"的可选入口(极少数皮肤依赖启动期布局)
|
|
16425
16572
|
reload: true,
|
|
16426
16573
|
});
|
|
16427
16574
|
// 不再整表重拉:本地标记已经对了,省掉那 600 ms 的"没反应"窗口。
|
|
@@ -16636,9 +16783,13 @@ window.__ModuleLoader__.load({
|
|
|
16636
16783
|
return React.createElement(React.Fragment, null, children);
|
|
16637
16784
|
}
|
|
16638
16785
|
|
|
16639
|
-
/**
|
|
16640
|
-
|
|
16641
|
-
|
|
16786
|
+
/**
|
|
16787
|
+
* 页签薄包装:`renderActiveTab` 只认这个签名(与别的 `render*Tab(ctx)` 一致)。
|
|
16788
|
+
* 传进来的 `props` 原样转交给组件 —— 目前只有 `onClearWallpaper`(皮肤与壁纸互斥,
|
|
16789
|
+
* 由挂载点提供"清掉当前壁纸"的入口;本文件不直接碰设置)。
|
|
16790
|
+
*/
|
|
16791
|
+
function renderSkinsTab(props) {
|
|
16792
|
+
return React.createElement(SkinsTabPanel, props || null);
|
|
16642
16793
|
}
|
|
16643
16794
|
|
|
16644
16795
|
// ── 内联模块:src/picker-model.js —— 选择器模型层:过滤判定 + 派生数据 + 分页切片(纯函数 / 显式入参 / 零 DOM)(构建期注入;勿手改本段)──
|
|
@@ -18247,7 +18398,7 @@ window.__ModuleLoader__.load({
|
|
|
18247
18398
|
? qpRenderPlaybackPane(sel, switchQpTab)
|
|
18248
18399
|
// 皮肤包:直接挂设置页那个同一个组件(它自己管列表/下载/激活状态)。
|
|
18249
18400
|
: qpTab === "skins"
|
|
18250
|
-
? React.createElement(SkinsTabPanel,
|
|
18401
|
+
? React.createElement(SkinsTabPanel, { onClearWallpaper: () => setSetting("id", "") })
|
|
18251
18402
|
: null),
|
|
18252
18403
|
!(qpTab === "wallpaper" && userPropsPanelOpen() && propsAvailable) && qpTab === "wallpaper" && React.createElement(React.Fragment, null,
|
|
18253
18404
|
// ── 壁纸列表(搜索 + 视图切换 + 快切)──
|
|
@@ -29865,7 +30016,7 @@ window.__ModuleLoader__.load({
|
|
|
29865
30016
|
if (activeTab === "extensions") return renderExtensionsTab(extensionCtx());
|
|
29866
30017
|
// 「皮肤包」自带异步状态(列表 / 下载中 / 错误),所以它是一个**真组件**而不是
|
|
29867
30018
|
// 纯渲染函数 —— 薄包装只负责挂上去(理由见 src/panel-tabs-skins.js 文件头)。
|
|
29868
|
-
if (activeTab === "skins") return renderSkinsTab();
|
|
30019
|
+
if (activeTab === "skins") return renderSkinsTab({ onClearWallpaper: () => setSetting("id", "") });
|
|
29869
30020
|
if (activeTab === "appearance") return renderAppearanceTab({
|
|
29870
30021
|
setSetting, setTransient,
|
|
29871
30022
|
fontSet: fontSetCtx(),
|
package/lib/routes/skins.js
CHANGED
|
@@ -646,6 +646,40 @@ export function registerSkinRoutes(webServer, c) {
|
|
|
646
646
|
},
|
|
647
647
|
}));
|
|
648
648
|
|
|
649
|
+
/* ── 路径式素材路由 ────────────────────────────────────────────────────────
|
|
650
|
+
* 皮肤的 hooks 契约(`contracts/hooks-api.d.ts`)给的是 `assetBase`:一个**同源 URL
|
|
651
|
+
* 前缀**,hooks 里自己拼 `assetBase + 'assets/x.webp'`。`?id=&p=` 这种查询式前缀拼不出
|
|
652
|
+
* 合法地址,所以这里再提供一条路径式:`/skins/files/<id>/<相对路径>`。
|
|
653
|
+
* 与 asset 完全同一套安全校验(id 白名单 + 路径段 + 越界检查)。
|
|
654
|
+
*/
|
|
655
|
+
disposers.push(webServer.register({
|
|
656
|
+
kind: 'prefix',
|
|
657
|
+
path: `${base}/skins/files`,
|
|
658
|
+
handler: async (req, res) => {
|
|
659
|
+
let pathname = '/';
|
|
660
|
+
try { pathname = new URL(req.url || '/', 'http://x').pathname; } catch { res.statusCode = 400; res.end(); return; }
|
|
661
|
+
const prefix = `${base}/skins/files/`;
|
|
662
|
+
if (!pathname.startsWith(prefix)) { res.statusCode = 404; res.end(); return; }
|
|
663
|
+
const rest = decodeURIComponent(pathname.slice(prefix.length));
|
|
664
|
+
const slash = rest.indexOf('/');
|
|
665
|
+
const id = slash < 0 ? rest : rest.slice(0, slash);
|
|
666
|
+
const rel = slash < 0 ? '' : rest.slice(slash + 1);
|
|
667
|
+
if (!ID_RE.test(id)) { res.statusCode = 400; res.end(); return; }
|
|
668
|
+
const segs = safeRelPath(rel);
|
|
669
|
+
if (!segs || segs.includes('..')) { res.statusCode = 400; res.end(); return; }
|
|
670
|
+
const harness = harnessRoot();
|
|
671
|
+
if (!harness) { res.statusCode = 503; res.end(); return; }
|
|
672
|
+
const root = join(harness, SKINS_DIR_NAME, id);
|
|
673
|
+
const file = join(root, ...segs);
|
|
674
|
+
if (!resolve(file).startsWith(resolve(root) + sep)) { res.statusCode = 400; res.end(); return; }
|
|
675
|
+
if (!existsSync(file) || !statSync(file).isFile()) { res.statusCode = 404; res.end(); return; }
|
|
676
|
+
const ext = (file.slice(file.lastIndexOf('.')).toLowerCase() || '');
|
|
677
|
+
res.setHeader('Content-Type', ASSET_TYPES[ext] || 'application/octet-stream');
|
|
678
|
+
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
679
|
+
res.end(readFileSync(file));
|
|
680
|
+
},
|
|
681
|
+
}));
|
|
682
|
+
|
|
649
683
|
disposers.push(webServer.register({
|
|
650
684
|
kind: 'exact',
|
|
651
685
|
path: `${base}/skins/asset`,
|