dsh-folia 2.7.0 → 2.9.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.
Files changed (2) hide show
  1. package/lib/client.js +171 -50
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -16231,38 +16231,69 @@ window.__ModuleLoader__.load({
16231
16231
  }
16232
16232
 
16233
16233
  /**
16234
- * 把一套皮肤的样式**当场**挂进文档。
16234
+ * 取一套皮肤**最终该用的 CSS**。
16235
+ *
16236
+ * 关键:皮肤中心不是把皮肤目录里的 CSS 原样发出去的 ——
16237
+ * `GET /api/skin-center/v2/skins/<id>/stylesheet` → `transformSkinCss()`:**强制作用域化**
16238
+ * `GET /api/skin-center/v2/skins/<id>/patches` → 同上(无则 404)
16239
+ * 作用域化 = 把每条规则包进 `html[data-dsh-skin="<id>"]`。这一步直接决定成败:
16240
+ * **包了作用域的规则特异性更高**,才压得过应用自身的面板/令牌规则。我们早先注入的是
16241
+ * 目录里的裸 CSS,特异性与应用同级 ⇒ 大量规则输掉 ⇒ 配色变了一点、面板依旧不透明。
16235
16242
  *
16236
- * 为什么是 `<style>` 而不是 `<link>`:
16237
- * · `<link rel=stylesheet>` 是**异步**的,加载完成的时机不定,容易输给皮肤中心随后
16238
- * 重新注入的那份;
16239
- * · `<style>` 一赋值就参与样式计算,且追加到 `<body>` **末尾** ⇒ 文档顺序最后,
16240
- * 同等特异性下后写的赢。这是"不刷新换肤"能真正看见的关键。
16243
+ * 所以优先**直接用它加工好的那份**(同源,浏览器自带凭证);只有拿不到才退回我们自己的
16244
+ * `/folia/skins/css`(未作用域化,能变配色但压不住应用规则)。
16245
+ *
16246
+ * @returns {Promise<{ css: string, source: string }>}
16247
+ */
16248
+ async function fetchSkinCss(id) {
16249
+ const enc = encodeURIComponent(id);
16250
+ const origin = (typeof location !== "undefined" && location.origin) ? location.origin : "";
16251
+ const parts = [];
16252
+ let source = "";
16253
+ for (const part of ["stylesheet", "patches"]) {
16254
+ try {
16255
+ const r = await apiFetch(origin + "/api/skin-center/v2/skins/" + enc + "/" + part, {
16256
+ parse: false,
16257
+ cache: "no-store",
16258
+ });
16259
+ if (r && r.ok && r.response) {
16260
+ const text = await r.response.text();
16261
+ if (text && text.trim()) {
16262
+ parts.push(text);
16263
+ source = "skin-center";
16264
+ }
16265
+ }
16266
+ } catch { /* 拿不到就试下一段 / 退回自己的路由 */ }
16267
+ }
16268
+ if (parts.length > 0) return { css: parts.join("\n/* ---- patches ---- */\n"), source };
16269
+ const r2 = await apiFetch("/skins/css?id=" + enc + "&t=" + Date.now(), { parse: false, cache: "no-store" });
16270
+ if (!r2 || !r2.ok || !r2.response) throw new Error("css-http-" + ((r2 && r2.status) || 0));
16271
+ const text = await r2.response.text();
16272
+ if (!text || !text.trim()) throw new Error("css-empty");
16273
+ return { css: text, source: "folia-raw" };
16274
+ }
16275
+
16276
+ /**
16277
+ * 把一套皮肤的样式**当场**挂进文档。
16241
16278
  *
16242
- * 仍然不跑 `hooks.mjs`(粒子等动效),那部分只有整页重载才生效。
16279
+ * 用 `<style>` 而不是 `<link>`:`<style>` 一赋值就参与样式计算,且追加到 `<body>` 末尾
16280
+ * ⇒ 文档顺序最后、同等特异性下后写的赢;`<link>` 是异步的,顺序不由我们定。
16243
16281
  *
16244
16282
  * @param {string} id 皮肤 id
16245
- * @returns {Promise<{ bytes: number, rules: number }>}
16283
+ * @returns {Promise<{ bytes: number, rules: number, source: string }>}
16246
16284
  */
16247
16285
  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");
16286
+ const { css, source } = await fetchSkinCss(id);
16255
16287
  let el = document.getElementById("folia-skin-hot");
16256
16288
  if (!el) {
16257
16289
  el = document.createElement("style");
16258
16290
  el.id = "folia-skin-hot";
16259
- // 挂 body 末尾(不是 head):文档顺序最后 ⇒ 同等特异性下覆盖先前的规则。
16260
16291
  (document.body || document.head).appendChild(el);
16261
16292
  }
16262
16293
  el.textContent = css;
16263
16294
  let rules = 0;
16264
16295
  try { rules = (el.sheet && el.sheet.cssRules && el.sheet.cssRules.length) || 0; } catch { /* 读不到就报 0 */ }
16265
- return { bytes: css.length, rules };
16296
+ return { bytes: css.length, rules, source };
16266
16297
  }
16267
16298
 
16268
16299
  /**
@@ -16320,12 +16351,13 @@ window.__ModuleLoader__.load({
16320
16351
  }
16321
16352
  el.src = pick.url;
16322
16353
  // 皮肤 CSS 通过 `> img` / `> video` 选中它 ⇒ 必须是**直接子元素**。
16354
+ el.setAttribute("aria-hidden", "true");
16323
16355
  el.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-fit:cover;";
16324
16356
  layer.appendChild(el);
16325
16357
 
16326
16358
  if (pick.scrim) {
16327
16359
  const scrim = document.createElement("div");
16328
- scrim.style.cssText = "position:absolute;inset:0;pointer-events:none;background-image:" + pick.scrim + ";";
16360
+ scrim.style.cssText = "position:absolute;inset:0;pointer-events:none;background:" + pick.scrim + ";";
16329
16361
  layer.appendChild(scrim);
16330
16362
  }
16331
16363
 
@@ -16352,6 +16384,24 @@ window.__ModuleLoader__.load({
16352
16384
  */
16353
16385
 
16354
16386
  const SKIN_LAYER_NAMES = ["background", "ambient", "bottom", "sidebar", "top", "foreground"];
16387
+ /**
16388
+ * 六层的**精确**样式:逐字对齐皮肤中心 `runtime/decoration-layers.ts` 的 `LAYER_STYLE`。
16389
+ *
16390
+ * 不能自己拍脑袋定层级 —— 它的注释写明了为什么:
16391
+ * · background 用 **-2**:负 z-index 元素画在 html/body 背景**之上**(皮肤自己的
16392
+ * 不透明底色因此在画后面、艺术图在它前面),但**在所有面板之下**;
16393
+ * · ambient 30、top/bottom/sidebar 40、foreground 41:压在面板之上、官方浮层
16394
+ * (>=1000)之下。之前我把非背景层留成 auto ⇒ 装饰条落进文档流最底层,
16395
+ * 表现正是"上面被面板盖住、只有下面露出背景"。
16396
+ */
16397
+ const SKIN_LAYER_STYLE = {
16398
+ background: "position:fixed;top:0;right:0;bottom:0;left:0;z-index:-2;pointer-events:none;will-change:transform;",
16399
+ ambient: "position:fixed;top:0;right:0;bottom:0;left:0;z-index:30;pointer-events:none;",
16400
+ top: "position:fixed;top:0;left:0;right:0;z-index:40;pointer-events:none;",
16401
+ bottom: "position:fixed;bottom:0;left:0;right:0;z-index:40;pointer-events:none;",
16402
+ sidebar: "position:fixed;top:0;bottom:0;left:0;z-index:40;pointer-events:none;",
16403
+ foreground: "position:fixed;top:0;right:0;bottom:0;left:0;z-index:41;pointer-events:none;",
16404
+ };
16355
16405
  /** 本次激活注册的清理函数(切换皮肤时逐个跑掉,幂等)。 */
16356
16406
  let skinHotCleanups = [];
16357
16407
 
@@ -16373,14 +16423,17 @@ window.__ModuleLoader__.load({
16373
16423
  if (!el) {
16374
16424
  el = document.createElement("div");
16375
16425
  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;" : "");
16426
+ el.setAttribute("aria-hidden", "true");
16380
16427
  host.appendChild(el);
16381
16428
  }
16429
+ // 与它的 ensureOne 同口径:**每次都对已有层重写样式** —— 皮肤中心的层可能带着
16430
+ // 上一次激活留下的内联覆盖,只建不改会继承脏值。
16431
+ el.style.cssText = SKIN_LAYER_STYLE[name];
16382
16432
  out[name] = el;
16383
16433
  }
16434
+ // 皮肤中心自己会给 body 盖 `data-dsh-skin-center`(它的若干基础样式挂在这个前缀下),
16435
+ // 缺了它,面板/列表那些"表面透明"的规则不生效 → 大片面板仍然不透明。
16436
+ try { document.body.setAttribute("data-dsh-skin-center", ""); } catch { /* ignore */ }
16384
16437
  return out;
16385
16438
  }
16386
16439
 
@@ -16458,6 +16511,84 @@ window.__ModuleLoader__.load({
16458
16511
  } catch { /* ignore */ }
16459
16512
  }
16460
16513
 
16514
+ /**
16515
+ * 把一套皮肤**完整**应用上去(作用域 + 六层 + 样式 + 背景 + hooks)。
16516
+ * 按钮和"壁纸撤下后自动恢复"走的是**同一个**函数 —— 两条路径不能各写一遍。
16517
+ */
16518
+ async function applySkinHot(id) {
16519
+ clearSkinHot();
16520
+ try { document.documentElement.setAttribute("data-dsh-skin", id); } catch { /* ignore */ }
16521
+ ensureSkinLayers();
16522
+ const info = await applySkinCss(id);
16523
+ const bg = await applySkinBackground(id);
16524
+ let hooksInfo = { ran: false, reason: "skipped" };
16525
+ try {
16526
+ hooksInfo = await applySkinHooks(id);
16527
+ } catch (e) {
16528
+ hooksInfo = { ran: false, reason: String((e && e.message) || e) };
16529
+ }
16530
+ return { info, bg, hooks: hooksInfo };
16531
+ }
16532
+
16533
+ /** 把皮肤**整个撤掉**,回到应用原生外观(壁纸接管背景时走这条)。 */
16534
+ function teardownSkin() {
16535
+ clearSkinHot();
16536
+ try { document.documentElement.removeAttribute("data-dsh-skin"); } catch { /* ignore */ }
16537
+ try { document.body.removeAttribute("data-dsh-skin-center"); } catch { /* ignore */ }
16538
+ for (const name of SKIN_LAYER_NAMES) {
16539
+ try {
16540
+ const el = document.querySelector('[data-dsh-skin-layer="' + name + '"]');
16541
+ if (el) el.innerHTML = "";
16542
+ } catch { /* ignore */ }
16543
+ }
16544
+ }
16545
+
16546
+ /** 向宿主问"当前选中的皮肤是哪个"(没选中返回空串)。 */
16547
+ async function activeSkinIdFromHost() {
16548
+ try {
16549
+ const resp = await apiJson("/skins");
16550
+ const doc = (resp && resp.data) || null;
16551
+ return (doc && typeof doc.active === "string") ? doc.active : "";
16552
+ } catch {
16553
+ return "";
16554
+ }
16555
+ }
16556
+
16557
+ let skinArbiterStarted = false;
16558
+ /**
16559
+ * **皮肤与壁纸互斥的仲裁器**(两个方向都要管)。
16560
+ *
16561
+ * 现状:点「使用」时我们会主动清掉壁纸(一个方向);但用户**反过来**点壁纸时,
16562
+ * 没人撤掉我们热注入的皮肤 —— 于是皮肤那层不透明样式压在壁纸上面,壁纸看起来被"遮挡"。
16563
+ *
16564
+ * 这里盯 `body[data-we-wallpaper]` 这个标记(**壁纸插件自己在挂/摘**,是双方共同的事实源):
16565
+ * · 标记出现 ⇒ 撤掉皮肤(teardownSkin)
16566
+ * · 标记消失 ⇒ 若选中过皮肤,重新完整应用(applySkinHot)
16567
+ * 这样无论用户先点哪边,最后都是"只有一个在台"。
16568
+ */
16569
+ function startSkinArbiter() {
16570
+ if (skinArbiterStarted) return;
16571
+ const body = (typeof document !== "undefined") ? document.body : null;
16572
+ if (!body) { setTimeout(startSkinArbiter, 400); return; }
16573
+ skinArbiterStarted = true;
16574
+ let seq = 0;
16575
+ const sync = async () => {
16576
+ const mine = ++seq;
16577
+ const wallpaperOn = body.hasAttribute("data-we-wallpaper");
16578
+ if (wallpaperOn) { teardownSkin(); return; }
16579
+ const id = await activeSkinIdFromHost();
16580
+ if (mine !== seq) return; // 期间又变了,放弃这次
16581
+ if (!id) return;
16582
+ try { await applySkinHot(id); } catch { /* 失败就保持原生外观 */ }
16583
+ };
16584
+ try {
16585
+ new MutationObserver(() => { sync().catch(() => {}); })
16586
+ .observe(body, { attributes: true, attributeFilter: ["data-we-wallpaper"] });
16587
+ // 启动时就跑一次:处理"上次退出时壁纸在台、皮肤被让位"这类残留状态
16588
+ sync().catch(() => {});
16589
+ } catch { /* 环境不支持观察就只保留按钮路径 */ }
16590
+ }
16591
+
16461
16592
  function SkinsTabPanel(props) {
16462
16593
  // 壁纸让位:挂载点(设置页 / 首页抽屉)各自把"清掉当前壁纸"的回调传进来 ——
16463
16594
  // 皮肤和壁纸都在铺背景,必须二选一。本组件不直接碰设置,只调用传进来的入口。
@@ -16523,51 +16654,36 @@ window.__ModuleLoader__.load({
16523
16654
  setNote({ kind: "ok", text: weT("已安装:") + label });
16524
16655
  await load(true);
16525
16656
  } else if (kind === "activate") {
16526
- // **当场换肤**:把皮肤样式同步注入文档,不刷新、不重启。
16527
- // 注入结果直接写进提示里(字节数 / 规则条数):万一样式还是没盖住皮肤中心,
16528
- // 这一行就是判断"是没注入"还是"注入了但被压住"的依据,不用再猜。
16657
+ // **当场换肤**:作用域 + 六层 + 样式 + 背景 + hooks,全在 applySkinHot 里
16658
+ //(与"壁纸撤下后自动恢复"共用同一实现,两条路径不会漂移)。
16529
16659
  let info = null;
16530
16660
  let applyErr = "";
16531
16661
  let bgKind = "none";
16532
16662
  let hooksInfo = { ran: false, reason: "skipped" };
16533
16663
  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
16542
- info = await applySkinCss(id);
16543
- const bg = await applySkinBackground(id);
16544
- bgKind = bg.kind;
16545
- try {
16546
- hooksInfo = await applySkinHooks(id);
16547
- } catch (e) {
16548
- hooksInfo = { ran: false, reason: String((e && e.message) || e) };
16664
+ // ① 壁纸先让位:清掉 `id` 之后壁纸层撤下、`data-we-wallpaper` 消失,
16665
+ // 皮肤中心也不会再扣住皮肤。(反方向由 startSkinArbiter 负责。)
16666
+ if (typeof onClearWallpaper === "function") {
16667
+ try { await Promise.resolve(onClearWallpaper()); } catch { /* 清不掉也继续换肤 */ }
16549
16668
  }
16669
+ // ② 应用皮肤
16670
+ const applied = await applySkinHot(id);
16671
+ info = applied.info;
16672
+ bgKind = applied.bg.kind;
16673
+ hooksInfo = applied.hooks;
16550
16674
  } catch (e) {
16551
16675
  applyErr = String((e && e.message) || e);
16552
16676
  }
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 { /* 清壁纸失败不影响换肤本身 */ }
16562
16677
  setNote({
16563
16678
  kind: applyErr ? "err" : "ok",
16564
16679
  text: applyErr
16565
16680
  ? weT("已切换为「") + label + weT("」,但样式注入失败:") + applyErr
16566
16681
  : weT("已切换到「") + label + weT("」—— 样式 ")
16567
16682
  + Math.round(info.bytes / 1024) + " KB / " + info.rules + weT(" 条规则")
16683
+ + (info.source === "skin-center" ? weT("(已作用域化)") : weT("(未作用域化,可能显示不全)"))
16568
16684
  + (bgKind === "none" ? weT(",无背景媒体") : weT(",背景:") + bgKind)
16569
16685
  + (hooksInfo.ran ? weT(",动效已启动") : (hooksInfo.reason === "no-hooks" ? weT(",无动效") : weT(",动效未启动(") + hooksInfo.reason + ")"))
16570
- + (wallpaperCleared ? weT(",壁纸已关闭") : ""),
16686
+ + (typeof onClearWallpaper === "function" ? weT(",壁纸已关闭") : ""),
16571
16687
  // hooks 已经跑过了;这里只作为"整页重载"的可选入口(极少数皮肤依赖启动期布局)
16572
16688
  reload: true,
16573
16689
  });
@@ -16792,6 +16908,11 @@ window.__ModuleLoader__.load({
16792
16908
  return React.createElement(SkinsTabPanel, props || null);
16793
16909
  }
16794
16910
 
16911
+ // 模块加载即启动互斥仲裁(不依赖用户是否打开过皮肤包页签):
16912
+ // 壁纸在台 ⇒ 撤掉皮肤;壁纸撤下 ⇒ 恢复选中的皮肤。
16913
+ // 放在模块作用域而不是 React effect 里,是因为它必须在**任何面板渲染之前**就守住这个不变量。
16914
+ try { startSkinArbiter(); } catch { /* 环境异常时退化为纯按钮路径 */ }
16915
+
16795
16916
  // ── 内联模块:src/picker-model.js —— 选择器模型层:过滤判定 + 派生数据 + 分页切片(纯函数 / 显式入参 / 零 DOM)(构建期注入;勿手改本段)──
16796
16917
  /**
16797
16918
  * picker-model.js — 壁纸选择器的**模型层**:过滤判定 + 派生数据 + 分页切片。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-folia",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
5
5
  "type": "module",
6
6
  "engines": {