dsh-folia 2.5.0 → 2.6.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 +120 -28
- package/lib/routes/skins.js +50 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -16231,30 +16231,110 @@ window.__ModuleLoader__.load({
|
|
|
16231
16231
|
}
|
|
16232
16232
|
|
|
16233
16233
|
/**
|
|
16234
|
-
*
|
|
16234
|
+
* 把一套皮肤的样式**当场**挂进文档。
|
|
16235
16235
|
*
|
|
16236
|
-
*
|
|
16237
|
-
*
|
|
16238
|
-
*
|
|
16239
|
-
*
|
|
16236
|
+
* 为什么是 `<style>` 而不是 `<link>`:
|
|
16237
|
+
* · `<link rel=stylesheet>` 是**异步**的,加载完成的时机不定,容易输给皮肤中心随后
|
|
16238
|
+
* 重新注入的那份;
|
|
16239
|
+
* · `<style>` 一赋值就参与样式计算,且追加到 `<body>` **末尾** ⇒ 文档顺序最后,
|
|
16240
|
+
* 同等特异性下后写的赢。这是"不刷新换肤"能真正看见的关键。
|
|
16241
|
+
*
|
|
16242
|
+
* 仍然不跑 `hooks.mjs`(粒子等动效),那部分只有整页重载才生效。
|
|
16240
16243
|
*
|
|
16241
16244
|
* @param {string} id 皮肤 id
|
|
16245
|
+
* @returns {Promise<{ bytes: number, rules: number }>}
|
|
16242
16246
|
*/
|
|
16243
|
-
function applySkinCss(id) {
|
|
16244
|
-
|
|
16245
|
-
|
|
16246
|
-
|
|
16247
|
-
|
|
16248
|
-
|
|
16249
|
-
|
|
16250
|
-
|
|
16251
|
-
|
|
16252
|
-
|
|
16253
|
-
el
|
|
16254
|
-
|
|
16255
|
-
|
|
16256
|
-
|
|
16247
|
+
async function applySkinCss(id) {
|
|
16248
|
+
const resp = await apiFetch("/skins/css?id=" + encodeURIComponent(id) + "&t=" + Date.now(), {
|
|
16249
|
+
parse: false,
|
|
16250
|
+
cache: "no-store",
|
|
16251
|
+
});
|
|
16252
|
+
if (!resp || !resp.ok || !resp.response) throw new Error("css-http-" + ((resp && resp.status) || 0));
|
|
16253
|
+
const css = await resp.response.text();
|
|
16254
|
+
if (!css || !css.trim()) throw new Error("css-empty");
|
|
16255
|
+
let el = document.getElementById("folia-skin-hot");
|
|
16256
|
+
if (!el) {
|
|
16257
|
+
el = document.createElement("style");
|
|
16258
|
+
el.id = "folia-skin-hot";
|
|
16259
|
+
// 挂 body 末尾(不是 head):文档顺序最后 ⇒ 同等特异性下覆盖先前的规则。
|
|
16260
|
+
(document.body || document.head).appendChild(el);
|
|
16257
16261
|
}
|
|
16262
|
+
el.textContent = css;
|
|
16263
|
+
let rules = 0;
|
|
16264
|
+
try { rules = (el.sheet && el.sheet.cssRules && el.sheet.cssRules.length) || 0; } catch { /* 读不到就报 0 */ }
|
|
16265
|
+
return { bytes: css.length, rules };
|
|
16266
|
+
}
|
|
16267
|
+
|
|
16268
|
+
/**
|
|
16269
|
+
* 搭出皮肤的**背景层**:`body > [data-dsh-skin-layer="background"]`,z-index -2,
|
|
16270
|
+
* 里面放 `<video>`/`<img>`(object-fit: cover)+ 一层 scrim 渐变。
|
|
16271
|
+
*
|
|
16272
|
+
* 为什么必须自己搭:背景不在 CSS 里,而是 `contributes.backgroundMedia`;皮肤中心只在
|
|
16273
|
+
* 启动时建这个元素。热切换时没人建 ⇒ 只剩下配色变了、背景还是一片空 ——
|
|
16274
|
+
* 用户看到的"水墨丹青没有背景动画"就是这个(它是一段 21 MB 的循环视频)。
|
|
16275
|
+
* 元素契约(z-index -2、媒体作直接子元素)从各皮肤 CSS 反推:
|
|
16276
|
+
* `future-window` 写 `[data-dsh-skin-layer="background"] > img`,
|
|
16277
|
+
* `ice-princess` 的注释直接写明"挂在 z-index -2 这层上"。
|
|
16278
|
+
*
|
|
16279
|
+
* @param {string} id 皮肤 id
|
|
16280
|
+
* @returns {Promise<{ kind: string }>}
|
|
16281
|
+
*/
|
|
16282
|
+
async function applySkinBackground(id) {
|
|
16283
|
+
const resp = await apiJson("/skins/manifest?id=" + encodeURIComponent(id));
|
|
16284
|
+
const doc = (resp && resp.data) || null;
|
|
16285
|
+
const media = doc && doc.backgroundMedia;
|
|
16286
|
+
let dark = false;
|
|
16287
|
+
try { dark = Boolean(document.body && document.body.hasAttribute("data-ds-dark-theme")); } catch { /* 取不到按浅色 */ }
|
|
16288
|
+
const pick = media ? (dark ? (media.dark || media.light) : (media.light || media.dark)) : null;
|
|
16289
|
+
|
|
16290
|
+
const existing = document.querySelector('[data-dsh-skin-layer="background"]');
|
|
16291
|
+
if (!pick || !pick.url) {
|
|
16292
|
+
// 这套皮肤没有背景媒体:把上一次留下的层清掉,别串味。
|
|
16293
|
+
if (existing) existing.remove();
|
|
16294
|
+
return { kind: "none" };
|
|
16295
|
+
}
|
|
16296
|
+
|
|
16297
|
+
let layer = existing;
|
|
16298
|
+
if (!layer) {
|
|
16299
|
+
layer = document.createElement("div");
|
|
16300
|
+
layer.setAttribute("data-dsh-skin-layer", "background");
|
|
16301
|
+
layer.style.cssText = "position:fixed;inset:0;z-index:-2;pointer-events:none;overflow:hidden;";
|
|
16302
|
+
(document.body || document.documentElement).insertBefore(layer, document.body.firstChild);
|
|
16303
|
+
}
|
|
16304
|
+
layer.innerHTML = "";
|
|
16305
|
+
|
|
16306
|
+
let el;
|
|
16307
|
+
if (pick.type === "video") {
|
|
16308
|
+
el = document.createElement("video");
|
|
16309
|
+
el.autoplay = true;
|
|
16310
|
+
el.muted = true;
|
|
16311
|
+
el.loop = true;
|
|
16312
|
+
el.setAttribute("muted", "");
|
|
16313
|
+
el.setAttribute("playsinline", "");
|
|
16314
|
+
el.setAttribute("autoplay", "");
|
|
16315
|
+
el.setAttribute("loop", "");
|
|
16316
|
+
} else {
|
|
16317
|
+
el = document.createElement("img");
|
|
16318
|
+
el.alt = "";
|
|
16319
|
+
el.setAttribute("draggable", "false");
|
|
16320
|
+
}
|
|
16321
|
+
el.src = pick.url;
|
|
16322
|
+
// 皮肤 CSS 通过 `> img` / `> video` 选中它 ⇒ 必须是**直接子元素**。
|
|
16323
|
+
el.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-fit:cover;";
|
|
16324
|
+
layer.appendChild(el);
|
|
16325
|
+
|
|
16326
|
+
if (pick.scrim) {
|
|
16327
|
+
const scrim = document.createElement("div");
|
|
16328
|
+
scrim.style.cssText = "position:absolute;inset:0;pointer-events:none;background-image:" + pick.scrim + ";";
|
|
16329
|
+
layer.appendChild(scrim);
|
|
16330
|
+
}
|
|
16331
|
+
|
|
16332
|
+
if (pick.type === "video") {
|
|
16333
|
+
const kick = () => { try { const p = el.play(); if (p && typeof p.catch === "function") p.catch(() => {}); } catch { /* 自动播放被拒就静帧 */ } };
|
|
16334
|
+
el.addEventListener("loadeddata", kick, { once: true });
|
|
16335
|
+
kick();
|
|
16336
|
+
}
|
|
16337
|
+
return { kind: pick.type };
|
|
16258
16338
|
}
|
|
16259
16339
|
|
|
16260
16340
|
function SkinsTabPanel() {
|
|
@@ -16319,14 +16399,28 @@ window.__ModuleLoader__.load({
|
|
|
16319
16399
|
setNote({ kind: "ok", text: weT("已安装:") + label });
|
|
16320
16400
|
await load(true);
|
|
16321
16401
|
} else if (kind === "activate") {
|
|
16322
|
-
//
|
|
16323
|
-
//
|
|
16324
|
-
|
|
16402
|
+
// **当场换肤**:把皮肤样式同步注入文档,不刷新、不重启。
|
|
16403
|
+
// 注入结果直接写进提示里(字节数 / 规则条数):万一样式还是没盖住皮肤中心,
|
|
16404
|
+
// 这一行就是判断"是没注入"还是"注入了但被压住"的依据,不用再猜。
|
|
16405
|
+
let info = null;
|
|
16406
|
+
let applyErr = "";
|
|
16407
|
+
let bgKind = "none";
|
|
16408
|
+
try {
|
|
16409
|
+
info = await applySkinCss(id);
|
|
16410
|
+
// 样式之后立刻搭背景层:没有它,凡是靠 backgroundMedia 的皮肤(水墨丹青、
|
|
16411
|
+
// 鲸鱼娘、深海女仆工坊…)都只会"变配色不变画面"。
|
|
16412
|
+
const bg = await applySkinBackground(id);
|
|
16413
|
+
bgKind = bg.kind;
|
|
16414
|
+
} catch (e) {
|
|
16415
|
+
applyErr = String((e && e.message) || e);
|
|
16416
|
+
}
|
|
16325
16417
|
setNote({
|
|
16326
|
-
kind: "ok",
|
|
16327
|
-
text:
|
|
16328
|
-
? weT("
|
|
16329
|
-
: weT("
|
|
16418
|
+
kind: applyErr ? "err" : "ok",
|
|
16419
|
+
text: applyErr
|
|
16420
|
+
? weT("已切换为「") + label + weT("」,但样式注入失败:") + applyErr
|
|
16421
|
+
: weT("已切换到「") + label + weT("」—— 样式 ")
|
|
16422
|
+
+ Math.round(info.bytes / 1024) + " KB / " + info.rules + weT(" 条规则")
|
|
16423
|
+
+ (bgKind === "none" ? weT(",该皮肤无背景媒体") : weT(",背景:") + bgKind),
|
|
16330
16424
|
// hooks(粒子等动效)只有整页重载才会跑:给一个可选入口,不强制。
|
|
16331
16425
|
reload: true,
|
|
16332
16426
|
});
|
|
@@ -16528,8 +16622,6 @@ window.__ModuleLoader__.load({
|
|
|
16528
16622
|
: React.createElement("div", { style: SKINS_STYLE.thumb }),
|
|
16529
16623
|
React.createElement("div", { style: SKINS_STYLE.body },
|
|
16530
16624
|
React.createElement("div", { style: SKINS_STYLE.title }, s.name || s.id),
|
|
16531
|
-
React.createElement("div", { style: SKINS_STYLE.meta },
|
|
16532
|
-
(s.author ? s.author : weT("未知作者")) + " · " + skinsFmtBytes(s.zipBytes)),
|
|
16533
16625
|
s.tagline ? React.createElement("div", { style: SKINS_STYLE.meta }, s.tagline) : null,
|
|
16534
16626
|
(s.tags || []).length
|
|
16535
16627
|
? React.createElement("div", { style: SKINS_STYLE.tags },
|
package/lib/routes/skins.js
CHANGED
|
@@ -544,12 +544,62 @@ export function registerSkinRoutes(webServer, c) {
|
|
|
544
544
|
'.webp': 'image/webp',
|
|
545
545
|
'.gif': 'image/gif',
|
|
546
546
|
'.avif': 'image/avif',
|
|
547
|
+
'.mp4': 'video/mp4',
|
|
548
|
+
'.webm': 'video/webm',
|
|
547
549
|
'.woff': 'font/woff',
|
|
548
550
|
'.woff2': 'font/woff2',
|
|
549
551
|
'.ttf': 'font/ttf',
|
|
550
552
|
'.otf': 'font/otf',
|
|
551
553
|
};
|
|
552
554
|
|
|
555
|
+
/* ── 背景层数据 ────────────────────────────────────────────────────────────
|
|
556
|
+
* 一套皮肤的"背景"不在 CSS 里,而是 `contributes.backgroundMedia`:
|
|
557
|
+
* { light: { type: 'video'|'image', src: 'assets/x.mp4', scrim: '<css 渐变>' }, dark: {...} }
|
|
558
|
+
* 皮肤中心负责把这层画出来;它的 DOM 契约(从各皮肤 CSS 反推)是
|
|
559
|
+
* `body [data-dsh-skin-layer='background']`,z-index -2,视频/图片作为子元素
|
|
560
|
+
*(`future-window` 直接写 `> img`,`frutiger-aero` 用 `::after` 叠气泡)。
|
|
561
|
+
* 热切换时没人建这层 ⇒ 背景全空("水墨丹青没有背景动画"就是这个原因)。
|
|
562
|
+
*/
|
|
563
|
+
disposers.push(webServer.register({
|
|
564
|
+
kind: 'exact',
|
|
565
|
+
path: `${base}/skins/manifest`,
|
|
566
|
+
handler: async (req, res) => {
|
|
567
|
+
const url = new URL(req.url || '/', 'http://x');
|
|
568
|
+
const id = url.searchParams.get('id') || '';
|
|
569
|
+
if (!ID_RE.test(id)) { sendJson(res, 400, { ok: false, error: 'bad-id' }); return; }
|
|
570
|
+
const harness = harnessRoot();
|
|
571
|
+
if (!harness) { sendJson(res, 503, { ok: false, error: 'harness-not-found' }); return; }
|
|
572
|
+
const dir = join(harness, SKINS_DIR_NAME, id);
|
|
573
|
+
let doc;
|
|
574
|
+
try {
|
|
575
|
+
doc = JSON.parse(readFileSync(join(dir, 'skin.json'), 'utf8'));
|
|
576
|
+
} catch {
|
|
577
|
+
sendJson(res, 404, { ok: false, error: 'no-manifest' });
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const c = (doc && doc.contributes) || {};
|
|
581
|
+
const media = c.backgroundMedia && typeof c.backgroundMedia === 'object' ? c.backgroundMedia : null;
|
|
582
|
+
/** `src` 相对路径 → 我们的素材路由(浏览器只认这条)。 */
|
|
583
|
+
const abs = (rel) => {
|
|
584
|
+
if (typeof rel !== 'string' || !rel) return '';
|
|
585
|
+
if (/^(?:[a-z]+:|\/)/i.test(rel)) return rel;
|
|
586
|
+
return `${base}/skins/asset?id=${encodeURIComponent(id)}&p=${encodeURIComponent(rel)}`;
|
|
587
|
+
};
|
|
588
|
+
const shape = (m) => (m && typeof m === 'object' ? {
|
|
589
|
+
type: m.type === 'video' ? 'video' : 'image',
|
|
590
|
+
url: abs(m.src),
|
|
591
|
+
scrim: typeof m.scrim === 'string' ? m.scrim : '',
|
|
592
|
+
} : null);
|
|
593
|
+
sendJson(res, 200, {
|
|
594
|
+
ok: true,
|
|
595
|
+
id,
|
|
596
|
+
name: typeof doc.name === 'string' ? doc.name : id,
|
|
597
|
+
backgroundMedia: media ? { light: shape(media.light), dark: shape(media.dark) } : null,
|
|
598
|
+
hooks: typeof c.hooks === 'string' ? c.hooks : '',
|
|
599
|
+
});
|
|
600
|
+
},
|
|
601
|
+
}));
|
|
602
|
+
|
|
553
603
|
disposers.push(webServer.register({
|
|
554
604
|
kind: 'exact',
|
|
555
605
|
path: `${base}/skins/css`,
|