dsh-folia 2.8.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 +144 -45
  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
  /**
@@ -16480,6 +16511,84 @@ window.__ModuleLoader__.load({
16480
16511
  } catch { /* ignore */ }
16481
16512
  }
16482
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
+
16483
16592
  function SkinsTabPanel(props) {
16484
16593
  // 壁纸让位:挂载点(设置页 / 首页抽屉)各自把"清掉当前壁纸"的回调传进来 ——
16485
16594
  // 皮肤和壁纸都在铺背景,必须二选一。本组件不直接碰设置,只调用传进来的入口。
@@ -16545,51 +16654,36 @@ window.__ModuleLoader__.load({
16545
16654
  setNote({ kind: "ok", text: weT("已安装:") + label });
16546
16655
  await load(true);
16547
16656
  } else if (kind === "activate") {
16548
- // **当场换肤**:把皮肤样式同步注入文档,不刷新、不重启。
16549
- // 注入结果直接写进提示里(字节数 / 规则条数):万一样式还是没盖住皮肤中心,
16550
- // 这一行就是判断"是没注入"还是"注入了但被压住"的依据,不用再猜。
16657
+ // **当场换肤**:作用域 + 六层 + 样式 + 背景 + hooks,全在 applySkinHot 里
16658
+ //(与"壁纸撤下后自动恢复"共用同一实现,两条路径不会漂移)。
16551
16659
  let info = null;
16552
16660
  let applyErr = "";
16553
16661
  let bgKind = "none";
16554
16662
  let hooksInfo = { ran: false, reason: "skipped" };
16555
16663
  try {
16556
- // ① 先清掉上一次热切换留下的清理函数与注入样式(避免两套皮肤叠加)
16557
- clearSkinHot();
16558
- // ② 作用域属性:30/65 套皮肤的规则整段挂在 html[data-dsh-skin="<id>"] 下,
16559
- // 不设这个属性,那些规则一条都不匹配 —— 这是"显示不全"的最大来源。
16560
- try { document.documentElement.setAttribute("data-dsh-skin", id); } catch { /* ignore */ }
16561
- // ③ 六个装饰层(hooks 与部分皮肤 CSS 都要用)
16562
- ensureSkinLayers();
16563
- // ④ 样式、⑤ 背景、⑥ hooks
16564
- info = await applySkinCss(id);
16565
- const bg = await applySkinBackground(id);
16566
- bgKind = bg.kind;
16567
- try {
16568
- hooksInfo = await applySkinHooks(id);
16569
- } catch (e) {
16570
- 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 { /* 清不掉也继续换肤 */ }
16571
16668
  }
16669
+ // ② 应用皮肤
16670
+ const applied = await applySkinHot(id);
16671
+ info = applied.info;
16672
+ bgKind = applied.bg.kind;
16673
+ hooksInfo = applied.hooks;
16572
16674
  } catch (e) {
16573
16675
  applyErr = String((e && e.message) || e);
16574
16676
  }
16575
- // ⑦ 壁纸让位:皮肤和壁纸都在铺背景,必须互斥。清掉 `id` 之后壁纸层会自己撤下
16576
- // (同时 `data-we-wallpaper` 标记消失,皮肤中心也不会再扣住皮肤)。
16577
- let wallpaperCleared = false;
16578
- try {
16579
- if (typeof onClearWallpaper === "function") {
16580
- await Promise.resolve(onClearWallpaper());
16581
- wallpaperCleared = true;
16582
- }
16583
- } catch { /* 清壁纸失败不影响换肤本身 */ }
16584
16677
  setNote({
16585
16678
  kind: applyErr ? "err" : "ok",
16586
16679
  text: applyErr
16587
16680
  ? weT("已切换为「") + label + weT("」,但样式注入失败:") + applyErr
16588
16681
  : weT("已切换到「") + label + weT("」—— 样式 ")
16589
16682
  + Math.round(info.bytes / 1024) + " KB / " + info.rules + weT(" 条规则")
16683
+ + (info.source === "skin-center" ? weT("(已作用域化)") : weT("(未作用域化,可能显示不全)"))
16590
16684
  + (bgKind === "none" ? weT(",无背景媒体") : weT(",背景:") + bgKind)
16591
16685
  + (hooksInfo.ran ? weT(",动效已启动") : (hooksInfo.reason === "no-hooks" ? weT(",无动效") : weT(",动效未启动(") + hooksInfo.reason + ")"))
16592
- + (wallpaperCleared ? weT(",壁纸已关闭") : ""),
16686
+ + (typeof onClearWallpaper === "function" ? weT(",壁纸已关闭") : ""),
16593
16687
  // hooks 已经跑过了;这里只作为"整页重载"的可选入口(极少数皮肤依赖启动期布局)
16594
16688
  reload: true,
16595
16689
  });
@@ -16814,6 +16908,11 @@ window.__ModuleLoader__.load({
16814
16908
  return React.createElement(SkinsTabPanel, props || null);
16815
16909
  }
16816
16910
 
16911
+ // 模块加载即启动互斥仲裁(不依赖用户是否打开过皮肤包页签):
16912
+ // 壁纸在台 ⇒ 撤掉皮肤;壁纸撤下 ⇒ 恢复选中的皮肤。
16913
+ // 放在模块作用域而不是 React effect 里,是因为它必须在**任何面板渲染之前**就守住这个不变量。
16914
+ try { startSkinArbiter(); } catch { /* 环境异常时退化为纯按钮路径 */ }
16915
+
16817
16916
  // ── 内联模块:src/picker-model.js —— 选择器模型层:过滤判定 + 派生数据 + 分页切片(纯函数 / 显式入参 / 零 DOM)(构建期注入;勿手改本段)──
16818
16917
  /**
16819
16918
  * picker-model.js — 壁纸选择器的**模型层**:过滤判定 + 派生数据 + 分页切片。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-folia",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
5
5
  "type": "module",
6
6
  "engines": {