dsh-folia 3.4.1 → 3.5.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 +90 -620
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -16170,53 +16170,29 @@ window.__ModuleLoader__.load({
|
|
|
16170
16170
|
|
|
16171
16171
|
// ── 内联模块:src/panel-tabs-skins.js —— 「皮肤包」页签:从自建站列皮肤包 → 下载 → 装到 harness/skins → 激活(宿主端 /skins 族)(构建期注入;勿手改本段)──
|
|
16172
16172
|
/**
|
|
16173
|
-
* panel-tabs-skins.js —
|
|
16173
|
+
* panel-tabs-skins.js — 「皮肤包」页签:**管理器**,不是渲染器。
|
|
16174
16174
|
*
|
|
16175
|
-
*
|
|
16176
|
-
* 本页签负责**管理**:列表 / 下载 / 删除 / 选中。选中后应用由皮肤中心的设置页完成 ——
|
|
16177
|
-
* 那是唯一 100% 完整的路径(它的控制器不外露,外部没法调)。
|
|
16178
|
-
* 皮肤中心不在台时才退回内置渲染(等价但不完整),并在界面上**明说**。
|
|
16175
|
+
* ══ 为什么只做管理 ══════════════════════════════════════════════════════════
|
|
16179
16176
|
*
|
|
16180
|
-
*
|
|
16181
|
-
*
|
|
16182
|
-
*
|
|
16183
|
-
*
|
|
16184
|
-
* 组件实例化后 hooks 是稳定的,父级重渲染也不会错位。
|
|
16177
|
+
* 皮肤的**渲染**只能由皮肤中心(@linxin666/dsh-client-ui-skin-center)完成:
|
|
16178
|
+
* 它读 `harness/skins/<id>/`,注入强制作用域化的 CSS、建六个装饰层、跑 hooks、
|
|
16179
|
+
* 铺背景媒体 —— 而且**只在页面加载时**做一次。它的控制器 `activate(id)` 没有导出,
|
|
16180
|
+
* 外部无法调用。
|
|
16185
16181
|
*
|
|
16186
|
-
*
|
|
16187
|
-
*
|
|
16188
|
-
*
|
|
16182
|
+
* 我们曾经试图"复刻"这套渲染(约 600 行:作用域化 CSS / 六层 / hooks / 背景 / 仲裁),
|
|
16183
|
+
* 结果是:怎么补都差一层,而且**能把整个界面画成空白**(2026-10-28 事故)。
|
|
16184
|
+
* 所以现在明确分工:
|
|
16189
16185
|
*
|
|
16190
|
-
*
|
|
16191
|
-
*
|
|
16192
|
-
*
|
|
16193
|
-
*
|
|
16194
|
-
*
|
|
16195
|
-
*
|
|
16186
|
+
* 本页签:列表 · 预览 · 下载安装 · 删除 · 选中(写 active)
|
|
16187
|
+
* 皮肤中心:真正把皮肤画出来(在它自己的 设置 → 皮肤 页面点「应用」)
|
|
16188
|
+
*
|
|
16189
|
+
* 纪律(每一条都对应一次真实事故):
|
|
16190
|
+
* · **绝不**注入 CSS / 造装饰层 / 跑 hooks / 改 html 属性;
|
|
16191
|
+
* · **绝不**尝试刷新页面(桌面壳里 location.reload 被吞过);
|
|
16192
|
+
* · 选中之后只写 active,然后**告诉用户下一步去哪点**;
|
|
16193
|
+
* · 不碰 patch 行、不碰 generation、不跑 pnpm。
|
|
16196
16194
|
*/
|
|
16197
16195
|
|
|
16198
|
-
/** 卡片网格与卡片本体的样式:内联,避免为一个页签去动全局样式表。 */
|
|
16199
|
-
const SKINS_STYLE = {
|
|
16200
|
-
grid: {
|
|
16201
|
-
display: "grid",
|
|
16202
|
-
gridTemplateColumns: "repeat(auto-fill, minmax(168px, 1fr))",
|
|
16203
|
-
gap: "10px",
|
|
16204
|
-
marginTop: "10px",
|
|
16205
|
-
},
|
|
16206
|
-
card: {
|
|
16207
|
-
display: "flex", flexDirection: "column", overflow: "hidden",
|
|
16208
|
-
border: "1px solid rgba(255,255,255,0.12)", borderRadius: "10px",
|
|
16209
|
-
background: "rgba(255,255,255,0.04)",
|
|
16210
|
-
},
|
|
16211
|
-
thumb: { width: "100%", aspectRatio: "4 / 3", objectFit: "cover", display: "block", background: "rgba(0,0,0,0.35)" },
|
|
16212
|
-
body: { padding: "8px 9px 9px", display: "flex", flexDirection: "column", gap: "4px", flex: "1 1 auto" },
|
|
16213
|
-
title: { fontSize: "12.5px", fontWeight: 600, lineHeight: 1.3 },
|
|
16214
|
-
meta: { fontSize: "11px", opacity: 0.62, lineHeight: 1.3 },
|
|
16215
|
-
tags: { display: "flex", flexWrap: "wrap", gap: "3px", marginTop: "2px" },
|
|
16216
|
-
tag: { fontSize: "10px", padding: "1px 5px", borderRadius: "999px", background: "rgba(255,255,255,0.09)", opacity: 0.75 },
|
|
16217
|
-
actions: { display: "flex", gap: "6px", marginTop: "6px" },
|
|
16218
|
-
};
|
|
16219
|
-
|
|
16220
16196
|
function skinsFmtBytes(n) {
|
|
16221
16197
|
const v = Number(n) || 0;
|
|
16222
16198
|
if (v >= 1024 * 1024) return (v / 1024 / 1024).toFixed(1) + " MB";
|
|
@@ -16224,6 +16200,15 @@ window.__ModuleLoader__.load({
|
|
|
16224
16200
|
return v + " B";
|
|
16225
16201
|
}
|
|
16226
16202
|
|
|
16203
|
+
/** 当前是深色还是浅色主题(决定卡片显示哪张预览图)。 */
|
|
16204
|
+
function skinsThemeIsDark() {
|
|
16205
|
+
try {
|
|
16206
|
+
return Boolean(document.body && document.body.hasAttribute("data-ds-dark-theme"));
|
|
16207
|
+
} catch {
|
|
16208
|
+
return false;
|
|
16209
|
+
}
|
|
16210
|
+
}
|
|
16211
|
+
|
|
16227
16212
|
function skinsErrText(code) {
|
|
16228
16213
|
if (!code) return "";
|
|
16229
16214
|
if (code === "disabled") return weT("站点数据源已关闭(DSH_FOLIA_SKINS_SITE=off)");
|
|
@@ -16235,414 +16220,42 @@ window.__ModuleLoader__.load({
|
|
|
16235
16220
|
return code;
|
|
16236
16221
|
}
|
|
16237
16222
|
|
|
16238
|
-
/**
|
|
16239
|
-
|
|
16240
|
-
|
|
16241
|
-
|
|
16242
|
-
|
|
16243
|
-
|
|
16244
|
-
|
|
16245
|
-
|
|
16246
|
-
|
|
16247
|
-
|
|
16248
|
-
|
|
16249
|
-
|
|
16250
|
-
|
|
16251
|
-
|
|
16252
|
-
*/
|
|
16253
|
-
async function fetchSkinCss(id) {
|
|
16254
|
-
const enc = encodeURIComponent(id);
|
|
16255
|
-
const parts = [];
|
|
16256
|
-
let source = "";
|
|
16257
|
-
|
|
16258
|
-
// ① 宿主端代理由它自己去问皮肤中心(带上本次请求的 Cookie 与同源头)。
|
|
16259
|
-
// 前端直连 `/api/skin-center/...` 会被 DSH 网关挡掉,实测一直落到"未作用域化"。
|
|
16260
|
-
try {
|
|
16261
|
-
const viaHost = await apiFetch("/skins/scoped-css?id=" + enc, { parse: false, cache: "no-store" });
|
|
16262
|
-
if (viaHost && viaHost.ok && viaHost.response) {
|
|
16263
|
-
const text = await viaHost.response.text();
|
|
16264
|
-
if (text && text.trim()) {
|
|
16265
|
-
// 响应头说明这份是谁作用域化的:
|
|
16266
|
-
// skin-center = 拿到了皮肤中心加工的那份;local = 宿主自己做的(等价结果)
|
|
16267
|
-
let via = "local";
|
|
16268
|
-
try { via = viaHost.response.headers.get("x-folia-scoped") || "local"; } catch { /* 读不到按 local */ }
|
|
16269
|
-
return { css: text, source: via === "skin-center" ? "skin-center" : "local-scoped" };
|
|
16270
|
-
}
|
|
16271
|
-
}
|
|
16272
|
-
} catch { /* 代理不可用 ⇒ 继续走下面两条 */ }
|
|
16273
|
-
|
|
16274
|
-
// ② 前端直连(皮肤中心同源路由,能通的话最省一次往返)
|
|
16275
|
-
for (const part of ["stylesheet", "patches"]) {
|
|
16276
|
-
try {
|
|
16277
|
-
// 根相对路径 + 原生请求:桌面壳可能跑在 `dsh-app://` 下(`location.origin` 是 "null"),
|
|
16278
|
-
// 拼绝对 URL 会作废;`/api/...` 交给浏览器按文档解析才可靠
|
|
16279
|
-
//(皮肤中心自己的客户端用的也是这种写法)。
|
|
16280
|
-
const r = await apiRawResponse("/api/skin-center/v2/skins/" + enc + "/" + part);
|
|
16281
|
-
if (r && r.ok) {
|
|
16282
|
-
const text = await r.text();
|
|
16283
|
-
if (text && text.trim()) {
|
|
16284
|
-
parts.push(text);
|
|
16285
|
-
source = "skin-center";
|
|
16286
|
-
}
|
|
16287
|
-
}
|
|
16288
|
-
} catch { /* 拿不到就试下一段 / 退回自己的路由 */ }
|
|
16289
|
-
}
|
|
16290
|
-
if (parts.length > 0) return { css: parts.join("\n/* ---- patches ---- */\n"), source };
|
|
16291
|
-
const r2 = await apiFetch("/skins/css?id=" + enc + "&t=" + Date.now(), { parse: false, cache: "no-store" });
|
|
16292
|
-
if (!r2 || !r2.ok || !r2.response) throw new Error("css-http-" + ((r2 && r2.status) || 0));
|
|
16293
|
-
const text = await r2.response.text();
|
|
16294
|
-
if (!text || !text.trim()) throw new Error("css-empty");
|
|
16295
|
-
return { css: text, source: "folia-raw" };
|
|
16296
|
-
}
|
|
16297
|
-
|
|
16298
|
-
/**
|
|
16299
|
-
* 把一套皮肤的样式**当场**挂进文档。
|
|
16300
|
-
*
|
|
16301
|
-
* 用 `<style>` 而不是 `<link>`:`<style>` 一赋值就参与样式计算,且追加到 `<body>` 末尾
|
|
16302
|
-
* ⇒ 文档顺序最后、同等特异性下后写的赢;`<link>` 是异步的,顺序不由我们定。
|
|
16303
|
-
*
|
|
16304
|
-
* @param {string} id 皮肤 id
|
|
16305
|
-
* @returns {Promise<{ bytes: number, rules: number, source: string }>}
|
|
16306
|
-
*/
|
|
16307
|
-
async function applySkinCss(id) {
|
|
16308
|
-
const { css, source } = await fetchSkinCss(id);
|
|
16309
|
-
let el = document.getElementById("folia-skin-hot");
|
|
16310
|
-
if (!el) {
|
|
16311
|
-
el = document.createElement("style");
|
|
16312
|
-
el.id = "folia-skin-hot";
|
|
16313
|
-
(document.body || document.head).appendChild(el);
|
|
16314
|
-
}
|
|
16315
|
-
el.textContent = css;
|
|
16316
|
-
let rules = 0;
|
|
16317
|
-
try { rules = (el.sheet && el.sheet.cssRules && el.sheet.cssRules.length) || 0; } catch { /* 读不到就报 0 */ }
|
|
16318
|
-
return { bytes: css.length, rules, source };
|
|
16319
|
-
}
|
|
16320
|
-
|
|
16321
|
-
/**
|
|
16322
|
-
* 搭出皮肤的**背景层**:`body > [data-dsh-skin-layer="background"]`,z-index -2,
|
|
16323
|
-
* 里面放 `<video>`/`<img>`(object-fit: cover)+ 一层 scrim 渐变。
|
|
16324
|
-
*
|
|
16325
|
-
* 为什么必须自己搭:背景不在 CSS 里,而是 `contributes.backgroundMedia`;皮肤中心只在
|
|
16326
|
-
* 启动时建这个元素。热切换时没人建 ⇒ 只剩下配色变了、背景还是一片空 ——
|
|
16327
|
-
* 用户看到的"水墨丹青没有背景动画"就是这个(它是一段 21 MB 的循环视频)。
|
|
16328
|
-
* 元素契约(z-index -2、媒体作直接子元素)从各皮肤 CSS 反推:
|
|
16329
|
-
* `future-window` 写 `[data-dsh-skin-layer="background"] > img`,
|
|
16330
|
-
* `ice-princess` 的注释直接写明"挂在 z-index -2 这层上"。
|
|
16331
|
-
*
|
|
16332
|
-
* @param {string} id 皮肤 id
|
|
16333
|
-
* @returns {Promise<{ kind: string }>}
|
|
16334
|
-
*/
|
|
16335
|
-
async function applySkinBackground(id) {
|
|
16336
|
-
const resp = await apiJson("/skins/manifest?id=" + encodeURIComponent(id));
|
|
16337
|
-
const doc = (resp && resp.data) || null;
|
|
16338
|
-
const media = doc && doc.backgroundMedia;
|
|
16339
|
-
let dark = false;
|
|
16340
|
-
try { dark = Boolean(document.body && document.body.hasAttribute("data-ds-dark-theme")); } catch { /* 取不到按浅色 */ }
|
|
16341
|
-
const pick = media ? (dark ? (media.dark || media.light) : (media.light || media.dark)) : null;
|
|
16342
|
-
|
|
16343
|
-
const existing = document.querySelector('[data-dsh-skin-layer="background"]');
|
|
16344
|
-
if (!pick || !pick.url) {
|
|
16345
|
-
// 这套皮肤没有背景媒体:把上一次留下的层清掉,别串味。
|
|
16346
|
-
if (existing) existing.remove();
|
|
16347
|
-
return { kind: "none" };
|
|
16348
|
-
}
|
|
16349
|
-
|
|
16350
|
-
let layer = existing;
|
|
16351
|
-
if (!layer) {
|
|
16352
|
-
layer = document.createElement("div");
|
|
16353
|
-
layer.setAttribute("data-dsh-skin-layer", "background");
|
|
16354
|
-
layer.style.cssText = "position:fixed;inset:0;z-index:-2;pointer-events:none;overflow:hidden;";
|
|
16355
|
-
(document.body || document.documentElement).insertBefore(layer, document.body.firstChild);
|
|
16356
|
-
}
|
|
16357
|
-
layer.innerHTML = "";
|
|
16358
|
-
|
|
16359
|
-
let el;
|
|
16360
|
-
if (pick.type === "video") {
|
|
16361
|
-
el = document.createElement("video");
|
|
16362
|
-
el.autoplay = true;
|
|
16363
|
-
el.muted = true;
|
|
16364
|
-
el.loop = true;
|
|
16365
|
-
el.setAttribute("muted", "");
|
|
16366
|
-
el.setAttribute("playsinline", "");
|
|
16367
|
-
el.setAttribute("autoplay", "");
|
|
16368
|
-
el.setAttribute("loop", "");
|
|
16369
|
-
} else {
|
|
16370
|
-
el = document.createElement("img");
|
|
16371
|
-
el.alt = "";
|
|
16372
|
-
el.setAttribute("draggable", "false");
|
|
16373
|
-
}
|
|
16374
|
-
el.src = pick.url;
|
|
16375
|
-
// 皮肤 CSS 通过 `> img` / `> video` 选中它 ⇒ 必须是**直接子元素**。
|
|
16376
|
-
el.setAttribute("aria-hidden", "true");
|
|
16377
|
-
el.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-fit:cover;";
|
|
16378
|
-
layer.appendChild(el);
|
|
16379
|
-
|
|
16380
|
-
if (pick.scrim) {
|
|
16381
|
-
const scrim = document.createElement("div");
|
|
16382
|
-
scrim.style.cssText = "position:absolute;inset:0;pointer-events:none;background:" + pick.scrim + ";";
|
|
16383
|
-
layer.appendChild(scrim);
|
|
16384
|
-
}
|
|
16385
|
-
|
|
16386
|
-
if (pick.type === "video") {
|
|
16387
|
-
const kick = () => { try { const p = el.play(); if (p && typeof p.catch === "function") p.catch(() => {}); } catch { /* 自动播放被拒就静帧 */ } };
|
|
16388
|
-
el.addEventListener("loadeddata", kick, { once: true });
|
|
16389
|
-
kick();
|
|
16390
|
-
}
|
|
16391
|
-
return { kind: pick.type };
|
|
16392
|
-
}
|
|
16393
|
-
|
|
16394
|
-
/* ── 热切换运行时:作用域属性 + 六个装饰层 + hooks ───────────────────────────────
|
|
16395
|
-
*
|
|
16396
|
-
* 皮肤不是"一份 CSS"。它有三层,缺一层就"显示不全":
|
|
16397
|
-
* ① 样式:`stylesheet` + `patches`
|
|
16398
|
-
* ② 背景:`contributes.backgroundMedia`
|
|
16399
|
-
* ③ 作用域与装饰层:`html[data-dsh-skin="<id>"]` + 六个 `[data-dsh-skin-layer=...]`
|
|
16400
|
-
*
|
|
16401
|
-
* 第 ③ 项是"大部分皮肤看起来没生效"的真正原因 —— 65 套里有 **30 套**把规则整段挂在
|
|
16402
|
-
* `html[data-dsh-skin]` 下面,属性不设,那些规则一条都不匹配;另有 **28 套**带
|
|
16403
|
-
* `hooks.mjs`(粒子/交互),契约见皮肤中心 `contracts/hooks-api.d.ts`:
|
|
16404
|
-
* apply({ skinId, scopeAttr, assetBase, layers{...}, theme{get,subscribe}, onCleanup })
|
|
16405
|
-
* 所以这里补齐作用域属性、六个层,并按契约跑 hooks(失败只降级)。
|
|
16406
|
-
*/
|
|
16407
|
-
|
|
16408
|
-
const SKIN_LAYER_NAMES = ["background", "ambient", "bottom", "sidebar", "top", "foreground"];
|
|
16409
|
-
/**
|
|
16410
|
-
* 六层的**精确**样式:逐字对齐皮肤中心 `runtime/decoration-layers.ts` 的 `LAYER_STYLE`。
|
|
16411
|
-
*
|
|
16412
|
-
* 不能自己拍脑袋定层级 —— 它的注释写明了为什么:
|
|
16413
|
-
* · background 用 **-2**:负 z-index 元素画在 html/body 背景**之上**(皮肤自己的
|
|
16414
|
-
* 不透明底色因此在画后面、艺术图在它前面),但**在所有面板之下**;
|
|
16415
|
-
* · ambient 30、top/bottom/sidebar 40、foreground 41:压在面板之上、官方浮层
|
|
16416
|
-
* (>=1000)之下。之前我把非背景层留成 auto ⇒ 装饰条落进文档流最底层,
|
|
16417
|
-
* 表现正是"上面被面板盖住、只有下面露出背景"。
|
|
16418
|
-
*/
|
|
16419
|
-
const SKIN_LAYER_STYLE = {
|
|
16420
|
-
background: "position:fixed;top:0;right:0;bottom:0;left:0;z-index:-2;pointer-events:none;will-change:transform;",
|
|
16421
|
-
ambient: "position:fixed;top:0;right:0;bottom:0;left:0;z-index:30;pointer-events:none;",
|
|
16422
|
-
top: "position:fixed;top:0;left:0;right:0;z-index:40;pointer-events:none;",
|
|
16423
|
-
bottom: "position:fixed;bottom:0;left:0;right:0;z-index:40;pointer-events:none;",
|
|
16424
|
-
sidebar: "position:fixed;top:0;bottom:0;left:0;z-index:40;pointer-events:none;",
|
|
16425
|
-
foreground: "position:fixed;top:0;right:0;bottom:0;left:0;z-index:41;pointer-events:none;",
|
|
16223
|
+
/** 卡片网格与卡片本体样式:内联,不为一个页签去动全局样式表。 */
|
|
16224
|
+
const SKINS_STYLE = {
|
|
16225
|
+
grid: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(168px, 1fr))", gap: "10px", marginTop: "10px" },
|
|
16226
|
+
card: {
|
|
16227
|
+
display: "flex", flexDirection: "column", overflow: "hidden",
|
|
16228
|
+
border: "1px solid rgba(255,255,255,0.12)", borderRadius: "10px", background: "rgba(255,255,255,0.04)",
|
|
16229
|
+
},
|
|
16230
|
+
thumb: { width: "100%", aspectRatio: "4 / 3", objectFit: "cover", display: "block", background: "rgba(0,0,0,0.35)" },
|
|
16231
|
+
body: { padding: "8px 9px 9px", display: "flex", flexDirection: "column", gap: "4px", flex: "1 1 auto" },
|
|
16232
|
+
title: { fontSize: "12.5px", fontWeight: 600, lineHeight: 1.3 },
|
|
16233
|
+
meta: { fontSize: "11px", opacity: 0.62, lineHeight: 1.3 },
|
|
16234
|
+
tags: { display: "flex", flexWrap: "wrap", gap: "3px", marginTop: "2px" },
|
|
16235
|
+
tag: { fontSize: "10px", padding: "1px 5px", borderRadius: "999px", background: "rgba(255,255,255,0.09)", opacity: 0.75 },
|
|
16236
|
+
actions: { display: "flex", gap: "6px", marginTop: "6px" },
|
|
16426
16237
|
};
|
|
16427
|
-
/** 本次激活注册的清理函数(切换皮肤时逐个跑掉,幂等)。 */
|
|
16428
|
-
let skinHotCleanups = [];
|
|
16429
|
-
|
|
16430
|
-
function skinThemeNow() {
|
|
16431
|
-
try {
|
|
16432
|
-
return (document.body && document.body.hasAttribute("data-ds-dark-theme")) ? "dark" : "light";
|
|
16433
|
-
} catch {
|
|
16434
|
-
return "light";
|
|
16435
|
-
}
|
|
16436
|
-
}
|
|
16437
|
-
|
|
16438
|
-
/** 建立/复用六个装饰层,返回契约里那个 `layers` 对象。 */
|
|
16439
|
-
function ensureSkinLayers() {
|
|
16440
|
-
const host = document.body || document.documentElement;
|
|
16441
|
-
const out = {};
|
|
16442
|
-
for (const name of SKIN_LAYER_NAMES) {
|
|
16443
|
-
let el = null;
|
|
16444
|
-
try { el = document.querySelector('[data-dsh-skin-layer="' + name + '"]'); } catch { el = null; }
|
|
16445
|
-
if (!el) {
|
|
16446
|
-
el = document.createElement("div");
|
|
16447
|
-
el.setAttribute("data-dsh-skin-layer", name);
|
|
16448
|
-
el.setAttribute("aria-hidden", "true");
|
|
16449
|
-
host.appendChild(el);
|
|
16450
|
-
}
|
|
16451
|
-
// 与它的 ensureOne 同口径:**每次都对已有层重写样式** —— 皮肤中心的层可能带着
|
|
16452
|
-
// 上一次激活留下的内联覆盖,只建不改会继承脏值。
|
|
16453
|
-
el.style.cssText = SKIN_LAYER_STYLE[name];
|
|
16454
|
-
out[name] = el;
|
|
16455
|
-
}
|
|
16456
|
-
// 皮肤中心自己会给 body 盖 `data-dsh-skin-center`(它的若干基础样式挂在这个前缀下),
|
|
16457
|
-
// 缺了它,面板/列表那些"表面透明"的规则不生效 → 大片面板仍然不透明。
|
|
16458
|
-
try { document.body.setAttribute("data-dsh-skin-center", ""); } catch { /* ignore */ }
|
|
16459
|
-
return out;
|
|
16460
|
-
}
|
|
16461
|
-
|
|
16462
|
-
/**
|
|
16463
|
-
* 按契约跑一套皮肤的 hooks。失败**只降级**(返回原因),绝不打断样式/背景。
|
|
16464
|
-
* @returns {Promise<{ ran: boolean, reason?: string }>}
|
|
16465
|
-
*/
|
|
16466
|
-
async function applySkinHooks(id) {
|
|
16467
|
-
const resp = await apiJson("/skins/manifest?id=" + encodeURIComponent(id));
|
|
16468
|
-
const doc = (resp && resp.data) || null;
|
|
16469
|
-
const rel = doc && doc.hooks;
|
|
16470
|
-
if (!rel) return { ran: false, reason: "no-hooks" };
|
|
16471
|
-
|
|
16472
|
-
const encoded = String(rel).split("/").map(encodeURIComponent).join("/");
|
|
16473
|
-
const r = await apiFetch("/skins/files/" + encodeURIComponent(id) + "/" + encoded, {
|
|
16474
|
-
parse: false,
|
|
16475
|
-
cache: "no-store",
|
|
16476
|
-
});
|
|
16477
|
-
if (!r || !r.ok || !r.response) return { ran: false, reason: "hooks-http-" + ((r && r.status) || 0) };
|
|
16478
|
-
const source = await r.response.text();
|
|
16479
|
-
if (!source || !source.trim()) return { ran: false, reason: "hooks-empty" };
|
|
16480
|
-
|
|
16481
|
-
const blobUrl = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
|
|
16482
|
-
try {
|
|
16483
|
-
const mod = await import(blobUrl);
|
|
16484
|
-
const factory = mod && typeof mod.default === "function" ? mod.default : null;
|
|
16485
|
-
if (!factory) return { ran: false, reason: "no-default-export" };
|
|
16486
|
-
const hooks = factory();
|
|
16487
|
-
if (!hooks || typeof hooks.apply !== "function") return { ran: false, reason: "no-apply" };
|
|
16488
|
-
|
|
16489
|
-
const layers = ensureSkinLayers();
|
|
16490
|
-
const unsubs = [];
|
|
16491
|
-
const ctx = {
|
|
16492
|
-
skinId: id,
|
|
16493
|
-
scopeAttr: id,
|
|
16494
|
-
assetBase: apiUrl("/skins/files/" + encodeURIComponent(id) + "/"),
|
|
16495
|
-
layers,
|
|
16496
|
-
theme: {
|
|
16497
|
-
get: skinThemeNow,
|
|
16498
|
-
subscribe(listener) {
|
|
16499
|
-
try {
|
|
16500
|
-
const obs = new MutationObserver(() => {
|
|
16501
|
-
try { listener(skinThemeNow()); } catch { /* 监听方出错不影响别人 */ }
|
|
16502
|
-
});
|
|
16503
|
-
obs.observe(document.body, { attributes: true, attributeFilter: ["data-ds-dark-theme"] });
|
|
16504
|
-
const off = () => { try { obs.disconnect(); } catch { /* ignore */ } };
|
|
16505
|
-
unsubs.push(off);
|
|
16506
|
-
return off;
|
|
16507
|
-
} catch {
|
|
16508
|
-
return () => {};
|
|
16509
|
-
}
|
|
16510
|
-
},
|
|
16511
|
-
},
|
|
16512
|
-
onCleanup(fn) { if (typeof fn === "function") skinHotCleanups.push(fn); },
|
|
16513
|
-
};
|
|
16514
|
-
hooks.apply(ctx);
|
|
16515
|
-
if (typeof hooks.dispose === "function") {
|
|
16516
|
-
skinHotCleanups.push(() => { try { hooks.dispose(); } catch { /* 幂等 */ } });
|
|
16517
|
-
}
|
|
16518
|
-
skinHotCleanups.push(() => { for (const off of unsubs) { try { off(); } catch { /* ignore */ } } });
|
|
16519
|
-
return { ran: true };
|
|
16520
|
-
} finally {
|
|
16521
|
-
setTimeout(() => { try { URL.revokeObjectURL(blobUrl); } catch { /* ignore */ } }, 10000);
|
|
16522
|
-
}
|
|
16523
|
-
}
|
|
16524
|
-
|
|
16525
|
-
/** 撤掉上一次热切换留下的东西(清理函数 + 注入样式)。作用域属性由下一次直接覆盖。 */
|
|
16526
|
-
function clearSkinHot() {
|
|
16527
|
-
for (const fn of skinHotCleanups.splice(0).reverse()) {
|
|
16528
|
-
try { fn(); } catch { /* 清理幂等,单个失败不影响其余 */ }
|
|
16529
|
-
}
|
|
16530
|
-
try {
|
|
16531
|
-
const el = document.getElementById("folia-skin-hot");
|
|
16532
|
-
if (el) el.remove();
|
|
16533
|
-
} catch { /* ignore */ }
|
|
16534
|
-
}
|
|
16535
|
-
|
|
16536
|
-
/**
|
|
16537
|
-
* 把一套皮肤**完整**应用上去(作用域 + 六层 + 样式 + 背景 + hooks)。
|
|
16538
|
-
* 按钮和"壁纸撤下后自动恢复"走的是**同一个**函数 —— 两条路径不能各写一遍。
|
|
16539
|
-
*/
|
|
16540
|
-
async function applySkinHot(id) {
|
|
16541
|
-
clearSkinHot();
|
|
16542
|
-
try { document.documentElement.setAttribute("data-dsh-skin", id); } catch { /* ignore */ }
|
|
16543
|
-
ensureSkinLayers();
|
|
16544
|
-
const info = await applySkinCss(id);
|
|
16545
|
-
const bg = await applySkinBackground(id);
|
|
16546
|
-
let hooksInfo = { ran: false, reason: "skipped" };
|
|
16547
|
-
try {
|
|
16548
|
-
hooksInfo = await applySkinHooks(id);
|
|
16549
|
-
} catch (e) {
|
|
16550
|
-
hooksInfo = { ran: false, reason: String((e && e.message) || e) };
|
|
16551
|
-
}
|
|
16552
|
-
return { info, bg, hooks: hooksInfo };
|
|
16553
|
-
}
|
|
16554
|
-
|
|
16555
|
-
/** 把皮肤**整个撤掉**,回到应用原生外观(壁纸接管背景时走这条)。 */
|
|
16556
|
-
function teardownSkin() {
|
|
16557
|
-
clearSkinHot();
|
|
16558
|
-
try { document.documentElement.removeAttribute("data-dsh-skin"); } catch { /* ignore */ }
|
|
16559
|
-
try { document.body.removeAttribute("data-dsh-skin-center"); } catch { /* ignore */ }
|
|
16560
|
-
for (const name of SKIN_LAYER_NAMES) {
|
|
16561
|
-
try {
|
|
16562
|
-
const el = document.querySelector('[data-dsh-skin-layer="' + name + '"]');
|
|
16563
|
-
if (el) el.innerHTML = "";
|
|
16564
|
-
} catch { /* ignore */ }
|
|
16565
|
-
}
|
|
16566
|
-
}
|
|
16567
|
-
|
|
16568
|
-
/** 向宿主问"当前选中的皮肤是哪个"(没选中返回空串)。 */
|
|
16569
|
-
async function activeSkinIdFromHost() {
|
|
16570
|
-
try {
|
|
16571
|
-
const resp = await apiJson("/skins");
|
|
16572
|
-
const doc = (resp && resp.data) || null;
|
|
16573
|
-
return (doc && typeof doc.active === "string") ? doc.active : "";
|
|
16574
|
-
} catch {
|
|
16575
|
-
return "";
|
|
16576
|
-
}
|
|
16577
|
-
}
|
|
16578
|
-
|
|
16579
|
-
let skinArbiterStarted = false;
|
|
16580
|
-
/**
|
|
16581
|
-
* **皮肤与壁纸互斥的仲裁器**(两个方向都要管)。
|
|
16582
|
-
*
|
|
16583
|
-
* 现状:点「使用」时我们会主动清掉壁纸(一个方向);但用户**反过来**点壁纸时,
|
|
16584
|
-
* 没人撤掉我们热注入的皮肤 —— 于是皮肤那层不透明样式压在壁纸上面,壁纸看起来被"遮挡"。
|
|
16585
|
-
*
|
|
16586
|
-
* 这里盯 `body[data-we-wallpaper]` 这个标记(**壁纸插件自己在挂/摘**,是双方共同的事实源):
|
|
16587
|
-
* · 标记出现 ⇒ 撤掉皮肤(teardownSkin)
|
|
16588
|
-
* · 标记消失 ⇒ 若选中过皮肤,重新完整应用(applySkinHot)
|
|
16589
|
-
* 这样无论用户先点哪边,最后都是"只有一个在台"。
|
|
16590
|
-
*/
|
|
16591
|
-
function startSkinArbiter() {
|
|
16592
|
-
if (skinArbiterStarted) return;
|
|
16593
|
-
const body = (typeof document !== "undefined") ? document.body : null;
|
|
16594
|
-
if (!body) { setTimeout(startSkinArbiter, 400); return; }
|
|
16595
|
-
skinArbiterStarted = true;
|
|
16596
|
-
let seq = 0;
|
|
16597
|
-
const sync = async () => {
|
|
16598
|
-
const mine = ++seq;
|
|
16599
|
-
const wallpaperOn = body.hasAttribute("data-we-wallpaper");
|
|
16600
|
-
if (wallpaperOn) { teardownSkin(); return; }
|
|
16601
|
-
const id = await activeSkinIdFromHost();
|
|
16602
|
-
if (mine !== seq) return; // 期间又变了,放弃这次
|
|
16603
|
-
if (!id) return;
|
|
16604
|
-
// ⭐ 关键:如果 `html[data-dsh-skin]` 已经等于当前皮肤,说明**皮肤中心自己在启动时
|
|
16605
|
-
// 把它应用好了**(它会在页面加载时走完整流程:作用域化 CSS、六层、hooks、背景、
|
|
16606
|
-
// 校验)。那种情况下我们**什么都不做** —— 任何额外注入都只会干扰它。
|
|
16607
|
-
// 只有它没应用(我们拿不到它的应用路径 / 它没装)时,才用自建那套兜底。
|
|
16608
|
-
try {
|
|
16609
|
-
if (document.documentElement.getAttribute("data-dsh-skin") === id) return;
|
|
16610
|
-
} catch { /* 读不到属性就按"未应用"处理 */ }
|
|
16611
|
-
try { await applySkinHot(id); } catch { /* 失败就保持原生外观 */ }
|
|
16612
|
-
};
|
|
16613
|
-
try {
|
|
16614
|
-
new MutationObserver(() => { sync().catch(() => {}); })
|
|
16615
|
-
.observe(body, { attributes: true, attributeFilter: ["data-we-wallpaper"] });
|
|
16616
|
-
// 启动时就跑一次:处理"上次退出时壁纸在台、皮肤被让位"这类残留状态
|
|
16617
|
-
sync().catch(() => {});
|
|
16618
|
-
} catch { /* 环境不支持观察就只保留按钮路径 */ }
|
|
16619
|
-
}
|
|
16620
16238
|
|
|
16621
16239
|
function SkinsTabPanel(props) {
|
|
16622
|
-
//
|
|
16623
|
-
// 皮肤和壁纸都在铺背景,必须二选一。本组件不直接碰设置,只调用传进来的入口。
|
|
16240
|
+
// 「清掉当前壁纸」的入口由挂载点传入(皮肤与壁纸互斥)。本组件不直接碰设置。
|
|
16624
16241
|
const onClearWallpaper = props && props.onClearWallpaper;
|
|
16242
|
+
|
|
16625
16243
|
const [list, setList] = React.useState([]);
|
|
16626
16244
|
const [active, setActive] = React.useState(null);
|
|
16627
16245
|
const [loading, setLoading] = React.useState(true);
|
|
16628
16246
|
const [error, setError] = React.useState(null);
|
|
16629
16247
|
const [harnessFound, setHarnessFound] = React.useState(true);
|
|
16630
|
-
// 皮肤中心是否在台:决定"应用"这一步交给谁(见文件头那段分工说明)
|
|
16631
16248
|
const [skinCenter, setSkinCenter] = React.useState(false);
|
|
16632
|
-
// 谁提供了皮肤中心行(用于检测重复挂载,见下方警示块)
|
|
16633
16249
|
const [providers, setProviders] = React.useState([]);
|
|
16634
|
-
const [busy, setBusy] = React.useState("");
|
|
16635
|
-
const [note, setNote] = React.useState(null);
|
|
16250
|
+
const [busy, setBusy] = React.useState("");
|
|
16251
|
+
const [note, setNote] = React.useState(null);
|
|
16636
16252
|
const [q, setQ] = React.useState("");
|
|
16637
16253
|
|
|
16638
16254
|
const load = React.useCallback(async (force) => {
|
|
16639
16255
|
setLoading(true);
|
|
16640
16256
|
setError(null);
|
|
16641
16257
|
try {
|
|
16642
|
-
//
|
|
16643
|
-
//(见 src/api-client.js 的 apiFetch)。最初按裸体取值 ⇒ `list=[] 且 error=null`,
|
|
16644
|
-
// 界面显示"没有匹配的皮肤包"却没有任何错误提示 —— 静默空列表比报错难查得多。
|
|
16645
|
-
// `parse: "always"` 让非 2xx 的 `{error}` 也能被读到。
|
|
16258
|
+
// apiJson 返回 { ok, status, data, error } 包装对象,不是响应体本身。
|
|
16646
16259
|
const resp = await apiJson("/skins" + (force ? "?refresh=1" : ""), { parse: "always" });
|
|
16647
16260
|
const r = (resp && resp.data) || null;
|
|
16648
16261
|
setList((r && r.skins) || []);
|
|
@@ -16662,26 +16275,16 @@ window.__ModuleLoader__.load({
|
|
|
16662
16275
|
React.useEffect(() => { load(false); }, [load]);
|
|
16663
16276
|
|
|
16664
16277
|
/**
|
|
16665
|
-
*
|
|
16666
|
-
*
|
|
16667
|
-
*
|
|
16668
|
-
*
|
|
16669
|
-
* "点了一下,半天没反应"。所以「使用」走**乐观更新**:先本地翻转 active 并把提示打出来,
|
|
16670
|
-
* 请求失败再回滚重拉。装 / 删仍然要重拉(已装标记与体积都变了)。
|
|
16278
|
+
* 执行一个动作。
|
|
16279
|
+
* · install / remove:宿主真正干活(下载解压 / 删目录)。
|
|
16280
|
+
* · activate:**只写 active**(`skin-center-active.json`),然后告诉用户下一步。
|
|
16281
|
+
* 不注入任何样式 —— 那一步归皮肤中心。
|
|
16671
16282
|
*/
|
|
16672
16283
|
async function act(kind, id, label) {
|
|
16673
16284
|
if (busy) return;
|
|
16674
16285
|
setBusy(id);
|
|
16675
16286
|
setNote(null);
|
|
16676
|
-
const prevActive = active;
|
|
16677
|
-
if (kind === "activate") {
|
|
16678
|
-
// 乐观:先给反馈,再等网络
|
|
16679
|
-
setActive(id);
|
|
16680
|
-
setList((prev) => prev.map((x) => ({ ...x, active: x.id === id })));
|
|
16681
|
-
setNote({ kind: "ok", text: weT("正在切换到「") + label + weT("」…") });
|
|
16682
|
-
}
|
|
16683
16287
|
try {
|
|
16684
|
-
// 同样是包装对象:`ok` 是 HTTP 2xx,业务结论在 `data.ok`(宿主回 `{ok:false,error}`)。
|
|
16685
16288
|
const resp = await apiPostJson("/skins/" + kind, { id }, { parse: "always" });
|
|
16686
16289
|
const r = (resp && resp.data) || null;
|
|
16687
16290
|
if (resp && resp.ok && r && r.ok) {
|
|
@@ -16689,58 +16292,18 @@ window.__ModuleLoader__.load({
|
|
|
16689
16292
|
setNote({ kind: "ok", text: weT("已安装:") + label });
|
|
16690
16293
|
await load(true);
|
|
16691
16294
|
} else if (kind === "activate") {
|
|
16692
|
-
|
|
16693
|
-
|
|
16694
|
-
|
|
16695
|
-
let applyErr = "";
|
|
16696
|
-
let bgKind = "none";
|
|
16697
|
-
let hooksInfo = { ran: false, reason: "skipped" };
|
|
16698
|
-
let reloadAfter = false;
|
|
16295
|
+
setActive(id);
|
|
16296
|
+
setList((prev) => prev.map((x) => ({ ...x, active: x.id === id })));
|
|
16297
|
+
// 皮肤与壁纸都在铺背景,必须互斥:先让壁纸让位。
|
|
16699
16298
|
try {
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
if (typeof onClearWallpaper === "function") {
|
|
16703
|
-
try { await Promise.resolve(onClearWallpaper()); } catch { /* 清不掉也继续换肤 */ }
|
|
16704
|
-
}
|
|
16705
|
-
// ② 让**皮肤中心自己**应用:写完 active 之后重新加载页面,它启动时会走完整流程
|
|
16706
|
-
// (这是唯一 100% 正确的路径)。自建那套只在"刷新没发生/它没装"时兜底。
|
|
16707
|
-
// 先做一次热应用,保证即使刷新被桌面壳吞掉,用户也立刻看到变化。
|
|
16708
|
-
const applied = await applySkinHot(id);
|
|
16709
|
-
info = applied.info;
|
|
16710
|
-
bgKind = applied.bg.kind;
|
|
16711
|
-
hooksInfo = applied.hooks;
|
|
16712
|
-
reloadAfter = true;
|
|
16713
|
-
} catch (e) {
|
|
16714
|
-
applyErr = String((e && e.message) || e);
|
|
16715
|
-
}
|
|
16299
|
+
if (typeof onClearWallpaper === "function") await Promise.resolve(onClearWallpaper());
|
|
16300
|
+
} catch { /* 清壁纸失败不影响选中本身 */ }
|
|
16716
16301
|
setNote({
|
|
16717
|
-
kind:
|
|
16718
|
-
text:
|
|
16719
|
-
? weT("已选中「") + label + weT("
|
|
16720
|
-
: (
|
|
16721
|
-
? weT("已选中「") + label + weT("」—— 请到 设置 → 皮肤 点「应用」完成(完整效果由皮肤中心渲染)· 预览:")
|
|
16722
|
-
: weT("已切换到「") + label + weT("」—— 未检测到皮肤中心,使用内置渲染,可能显示不全 · 样式:"))
|
|
16723
|
-
+ Math.round(info.bytes / 1024) + " KB / " + info.rules + weT(" 条规则")
|
|
16724
|
-
+ ((info.source === "skin-center" || info.source === "local-scoped") ? weT("(已作用域化)") : weT("(未作用域化,可能显示不全)"))
|
|
16725
|
-
+ (bgKind === "none" ? weT(",无背景媒体") : weT(",背景:") + bgKind)
|
|
16726
|
-
+ (hooksInfo.ran ? weT(",动效已启动") : (hooksInfo.reason === "no-hooks" ? weT(",无动效") : weT(",动效未启动(") + hooksInfo.reason + ")"))
|
|
16727
|
-
+ weT(",主题:") + (skinThemeNow() === "dark" ? weT("深色") : weT("浅色"))
|
|
16728
|
-
+ (typeof onClearWallpaper === "function" ? weT(",壁纸已关闭") : ""),
|
|
16729
|
-
// 主题与背景变体写进提示:皮肤有 light/dark 两套素材,不写清就会"图对不上"。
|
|
16730
|
-
// hooks 已经跑过了;reload 只是"整页重载"的可选入口(极少数皮肤依赖启动期布局)
|
|
16731
|
-
reload: true,
|
|
16302
|
+
kind: "ok",
|
|
16303
|
+
text: skinCenter
|
|
16304
|
+
? weT("已选中「") + label + weT("」—— 请到「设置 → 皮肤」点「应用」完成(这是唯一完整生效的路径)")
|
|
16305
|
+
: weT("已选中「") + label + weT("」—— 未检测到皮肤中心,无法渲染;请安装 @linxin666/dsh-client-ui-skin-center 后重启"),
|
|
16732
16306
|
});
|
|
16733
|
-
// 刷新页面 = 把应用权交还给皮肤中心(它只在页面加载时应用)。
|
|
16734
|
-
// 用 replace 而非 reload:桌面壳里 reload 有时被吞掉。失败也不影响已完成的
|
|
16735
|
-
// 热应用 —— 用户至少能看到变化,只是缺动效那类要靠真刷新补齐。
|
|
16736
|
-
if (reloadAfter) {
|
|
16737
|
-
setTimeout(() => {
|
|
16738
|
-
try { window.location.replace(window.location.href); } catch {
|
|
16739
|
-
try { window.location.reload(); } catch { /* 两种都不可用:保留热应用结果 */ }
|
|
16740
|
-
}
|
|
16741
|
-
}, 900);
|
|
16742
|
-
}
|
|
16743
|
-
// 不再整表重拉:本地标记已经对了,省掉那 600 ms 的"没反应"窗口。
|
|
16744
16307
|
} else if (kind === "remove") {
|
|
16745
16308
|
setNote({ kind: "ok", text: weT("已删除:") + label });
|
|
16746
16309
|
await load(true);
|
|
@@ -16749,18 +16312,9 @@ window.__ModuleLoader__.load({
|
|
|
16749
16312
|
const code = (r && r.error) || "failed";
|
|
16750
16313
|
const detail = (r && r.detail) ? "(" + r.detail + ")" : "";
|
|
16751
16314
|
setNote({ kind: "err", text: weT("操作失败:") + code + detail });
|
|
16752
|
-
if (kind === "activate") {
|
|
16753
|
-
// 回滚乐观更新
|
|
16754
|
-
setActive(prevActive);
|
|
16755
|
-
setList((prev) => prev.map((x) => ({ ...x, active: x.id === prevActive })));
|
|
16756
|
-
}
|
|
16757
16315
|
}
|
|
16758
16316
|
} catch (e) {
|
|
16759
16317
|
setNote({ kind: "err", text: weT("请求失败:") + (e && e.message ? e.message : weT("网络错误")) });
|
|
16760
|
-
if (kind === "activate") {
|
|
16761
|
-
setActive(prevActive);
|
|
16762
|
-
setList((prev) => prev.map((x) => ({ ...x, active: x.id === prevActive })));
|
|
16763
|
-
}
|
|
16764
16318
|
} finally {
|
|
16765
16319
|
setBusy("");
|
|
16766
16320
|
}
|
|
@@ -16771,7 +16325,6 @@ window.__ModuleLoader__.load({
|
|
|
16771
16325
|
? list.filter((s) => (s.name + " " + s.nameEn + " " + s.id + " " + (s.tags || []).join(" "))
|
|
16772
16326
|
.toLowerCase().indexOf(needle) !== -1)
|
|
16773
16327
|
: list;
|
|
16774
|
-
|
|
16775
16328
|
const installedCount = list.filter((s) => s.installed).length;
|
|
16776
16329
|
|
|
16777
16330
|
const header = React.createElement("div", { className: "we-picker__section-head" },
|
|
@@ -16787,10 +16340,19 @@ window.__ModuleLoader__.load({
|
|
|
16787
16340
|
}, loading ? weT("加载中…") : weT("刷新")),
|
|
16788
16341
|
);
|
|
16789
16342
|
|
|
16790
|
-
|
|
16791
|
-
|
|
16792
|
-
|
|
16793
|
-
|
|
16343
|
+
if (!harnessFound) {
|
|
16344
|
+
return React.createElement(React.Fragment, null,
|
|
16345
|
+
React.createElement("div", { className: "we-picker__section" }, header,
|
|
16346
|
+
React.createElement("div", { className: "we-picker__empty" },
|
|
16347
|
+
React.createElement("span", { className: "we-picker__empty-title" }, weT("找不到 DSH 的 skins 目录")),
|
|
16348
|
+
React.createElement("span", { className: "we-picker__hint" },
|
|
16349
|
+
weT("皮肤包要装到 DSH 的 harness/skins 下。可用环境变量 DSH_FOLIA_HARNESS 指定该目录")))));
|
|
16350
|
+
}
|
|
16351
|
+
|
|
16352
|
+
const children = [React.createElement("div", { className: "we-picker__section", key: "sec" }, header)];
|
|
16353
|
+
|
|
16354
|
+
// 重复挂载警示:我们的 patch 只有一行(folia),正常不会出现;
|
|
16355
|
+
// 但用户可能自己加了行,这里如实报出来。
|
|
16794
16356
|
if (providers.length > 1) {
|
|
16795
16357
|
children.push(React.createElement("div", {
|
|
16796
16358
|
key: "dup",
|
|
@@ -16801,33 +16363,21 @@ window.__ModuleLoader__.load({
|
|
|
16801
16363
|
},
|
|
16802
16364
|
}, weT("⚠️ 检测到 ") + providers.length + weT(" 处挂载了皮肤中心(")
|
|
16803
16365
|
+ providers.map((p) => p.bundle).join(" / ")
|
|
16804
|
-
+ weT("
|
|
16366
|
+
+ weT(")。请只保留一处,否则会重复挂载。")));
|
|
16805
16367
|
}
|
|
16806
16368
|
|
|
16807
|
-
//
|
|
16808
|
-
// 不在台 ⇒ 明说我们在兜底,避免用户以为"点了却坏了"。
|
|
16369
|
+
// 分工常驻说明:让用户一开始就知道"应用"在哪一步。
|
|
16809
16370
|
if (list.length > 0) {
|
|
16810
16371
|
children.push(React.createElement("div", {
|
|
16811
16372
|
key: "division",
|
|
16812
16373
|
className: "we-picker__hint",
|
|
16813
16374
|
style: { margin: "2px 0 8px", opacity: 0.8 },
|
|
16814
16375
|
}, skinCenter
|
|
16815
|
-
? weT("
|
|
16816
|
-
: weT("
|
|
16817
|
-
}
|
|
16818
|
-
|
|
16819
|
-
if (!harnessFound) {
|
|
16820
|
-
return React.createElement(React.Fragment, null,
|
|
16821
|
-
React.createElement("div", { className: "we-picker__section" }, header,
|
|
16822
|
-
React.createElement("div", { className: "we-picker__empty" },
|
|
16823
|
-
React.createElement("span", { className: "we-picker__empty-title" }, weT("找不到 DSH 的 skins 目录")),
|
|
16824
|
-
React.createElement("span", { className: "we-picker__hint" },
|
|
16825
|
-
weT("皮肤包要装到 DSH 的 harness/skins 下。可用环境变量 DSH_FOLIA_HARNESS 指定该目录")))));
|
|
16376
|
+
? weT("这里负责下载与管理。选中后请到「设置 → 皮肤」点「应用」完成渲染。")
|
|
16377
|
+
: weT("未检测到皮肤中心:可以下载/管理皮肤包,但无法渲染。请安装 @linxin666/dsh-client-ui-skin-center 并重启。")));
|
|
16826
16378
|
}
|
|
16827
16379
|
|
|
16828
|
-
|
|
16829
|
-
// 常驻一行「当前皮肤」:切换只写文件、皮肤要重启才生效,不给这一行用户就无从判断
|
|
16830
|
-
// 刚才那次点击到底成没成(这是"点了没反应"的一半原因)。
|
|
16380
|
+
// 当前选中(只是"选中",不是"已应用")
|
|
16831
16381
|
if (active) {
|
|
16832
16382
|
const activeName = (list.find((x) => x.id === active) || {}).name || active;
|
|
16833
16383
|
children.push(React.createElement("div", {
|
|
@@ -16835,56 +16385,13 @@ window.__ModuleLoader__.load({
|
|
|
16835
16385
|
className: "we-picker__hint",
|
|
16836
16386
|
style: { margin: "4px 0 8px", display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap" },
|
|
16837
16387
|
},
|
|
16838
|
-
React.createElement("span", null, weT("
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
|
|
16842
|
-
// 把这层区别直接显示出来,省得"看起来不全"时无从判断该怪谁。
|
|
16843
|
-
(() => {
|
|
16844
|
-
let native = false;
|
|
16845
|
-
try { native = document.documentElement.getAttribute("data-dsh-skin") === active; } catch { native = false; }
|
|
16846
|
-
return React.createElement("span", {
|
|
16847
|
-
style: {
|
|
16848
|
-
fontSize: "11px",
|
|
16849
|
-
padding: "1px 6px",
|
|
16850
|
-
borderRadius: "999px",
|
|
16851
|
-
background: native ? "rgba(142,230,168,0.16)" : "rgba(255,196,120,0.16)",
|
|
16852
|
-
color: native ? "#8ee6a8" : "#ffd08a",
|
|
16853
|
-
},
|
|
16854
|
-
title: native
|
|
16855
|
-
? weT("皮肤中心在页面加载时应用的(原生路径,完整)")
|
|
16856
|
-
: weT("皮肤中心没接手,用的是本插件的兜底注入(可能显示不全)"),
|
|
16857
|
-
}, native ? weT("皮肤中心接管") : weT("本地兜底"));
|
|
16858
|
-
})(),
|
|
16388
|
+
React.createElement("span", null, weT("已选中:") + activeName),
|
|
16389
|
+
React.createElement("span", {
|
|
16390
|
+
style: { fontSize: "11px", padding: "1px 6px", borderRadius: "999px", background: "rgba(255,196,120,0.16)", color: "#ffd08a" },
|
|
16391
|
+
}, weT("需在皮肤中心点「应用」")),
|
|
16859
16392
|
));
|
|
16860
16393
|
}
|
|
16861
16394
|
|
|
16862
|
-
// ⚠️ **壁纸在台时皮肤不会生效** —— 这是皮肤中心写死的行为,不是我们的 bug:
|
|
16863
|
-
// · host 侧:peer(= 本插件)持久化的壁纸选择非空时,它从文档里**扣住**皮肤;
|
|
16864
|
-
// · client 侧:它盯 `body[data-we-wallpaper]`,只要那个标记在,就把皮肤让位
|
|
16865
|
-
// (源码注释:`an active WE wallpaper suppresses skin media`)。
|
|
16866
|
-
// 于是"点了使用但界面没变"最可能的原因就是**壁纸还开着**。这里直接读那个标记,
|
|
16867
|
-
// 命中就把话说清楚,并告诉用户该先做什么。
|
|
16868
|
-
let wallpaperOn = false;
|
|
16869
|
-
try {
|
|
16870
|
-
wallpaperOn = Boolean(document.body && document.body.hasAttribute("data-we-wallpaper"));
|
|
16871
|
-
} catch { /* 拿不到 DOM 就当没有 */ }
|
|
16872
|
-
if (wallpaperOn) {
|
|
16873
|
-
children.push(React.createElement("div", {
|
|
16874
|
-
key: "wpwarning",
|
|
16875
|
-
className: "we-picker__hint",
|
|
16876
|
-
style: {
|
|
16877
|
-
margin: "4px 0 10px",
|
|
16878
|
-
padding: "7px 10px",
|
|
16879
|
-
borderRadius: "8px",
|
|
16880
|
-
background: "rgba(255,196,120,0.12)",
|
|
16881
|
-
color: "#ffd08a",
|
|
16882
|
-
lineHeight: 1.5,
|
|
16883
|
-
},
|
|
16884
|
-
}, weT("⚠️ 当前有壁纸在台:皮肤会被壁纸让位,切换不会生效。")
|
|
16885
|
-
+ weT(" 先到「壁纸」页签点「清除」,再回来切换皮肤。")));
|
|
16886
|
-
}
|
|
16887
|
-
|
|
16888
16395
|
if (error) {
|
|
16889
16396
|
children.push(React.createElement("div", {
|
|
16890
16397
|
key: "err",
|
|
@@ -16896,33 +16403,10 @@ window.__ModuleLoader__.load({
|
|
|
16896
16403
|
children.push(React.createElement("div", {
|
|
16897
16404
|
key: "note",
|
|
16898
16405
|
className: "we-picker__hint",
|
|
16899
|
-
style: {
|
|
16900
|
-
|
|
16901
|
-
margin: "6px 0 10px",
|
|
16902
|
-
display: "flex",
|
|
16903
|
-
gap: "10px",
|
|
16904
|
-
alignItems: "center",
|
|
16905
|
-
flexWrap: "wrap",
|
|
16906
|
-
},
|
|
16907
|
-
},
|
|
16908
|
-
React.createElement("span", null, note.text),
|
|
16909
|
-
note.reload
|
|
16910
|
-
? React.createElement("button", {
|
|
16911
|
-
type: "button",
|
|
16912
|
-
className: "we-picker__btn we-picker__btn--primary",
|
|
16913
|
-
style: { fontSize: "11.5px", padding: "3px 10px" },
|
|
16914
|
-
onClick: () => {
|
|
16915
|
-
// 与自动那条路一致:优先硬跳转,退而 reload。只重载前端页面 ——
|
|
16916
|
-
// 对话与设置都在宿主侧,不会丢。
|
|
16917
|
-
try { window.location.replace(window.location.href); } catch {
|
|
16918
|
-
try { window.location.reload(); } catch { /* 极端环境刷不动就让用户手动重启 */ }
|
|
16919
|
-
}
|
|
16920
|
-
},
|
|
16921
|
-
title: weT("整页重载 —— 皮肤动效(粒子等)需要重载才跑;纯配色已即时生效"),
|
|
16922
|
-
}, weT("重载以播放动效"))
|
|
16923
|
-
: null,
|
|
16924
|
-
));
|
|
16406
|
+
style: { color: note.kind === "err" ? "#ff9a9a" : "#8ee6a8", margin: "6px 0 10px", lineHeight: 1.5 },
|
|
16407
|
+
}, note.text));
|
|
16925
16408
|
}
|
|
16409
|
+
|
|
16926
16410
|
children.push(React.createElement("div", {
|
|
16927
16411
|
key: "search",
|
|
16928
16412
|
style: { display: "flex", gap: "8px", alignItems: "center", margin: "2px 0 4px" },
|
|
@@ -16935,8 +16419,7 @@ window.__ModuleLoader__.load({
|
|
|
16935
16419
|
onChange: (e) => setQ(e.target.value),
|
|
16936
16420
|
style: { flex: "1 1 auto" },
|
|
16937
16421
|
}),
|
|
16938
|
-
React.createElement("span", { className: "we-picker__hint" },
|
|
16939
|
-
weT("已装 ") + installedCount + " / " + list.length),
|
|
16422
|
+
React.createElement("span", { className: "we-picker__hint" }, weT("已装 ") + installedCount + " / " + list.length),
|
|
16940
16423
|
));
|
|
16941
16424
|
|
|
16942
16425
|
if (loading && list.length === 0) {
|
|
@@ -16949,12 +16432,9 @@ window.__ModuleLoader__.load({
|
|
|
16949
16432
|
}
|
|
16950
16433
|
|
|
16951
16434
|
if (shown.length > 0) {
|
|
16435
|
+
const darkNow = skinsThemeIsDark();
|
|
16952
16436
|
children.push(React.createElement("div", { key: "grid", style: SKINS_STYLE.grid },
|
|
16953
16437
|
shown.map((s) => {
|
|
16954
|
-
// 预览图**跟随当前主题**:每套皮肤都有 light / dark 两份预览(例:居家模型娘的
|
|
16955
|
-
// 白天房间 vs 夜晚房间)。之前一律取 light,于是"深色模式下看到的效果图"与
|
|
16956
|
-
// "实际渲染的 dark 变体"完全是两张画 —— 用户自然会觉得对不上。
|
|
16957
|
-
const darkNow = skinThemeNow() === "dark";
|
|
16958
16438
|
const previewRel = (darkNow
|
|
16959
16439
|
? (s.previewDark || s.previewLight)
|
|
16960
16440
|
: (s.previewLight || s.previewDark)) || "";
|
|
@@ -16973,15 +16453,11 @@ window.__ModuleLoader__.load({
|
|
|
16973
16453
|
const actions = [];
|
|
16974
16454
|
if (!s.installed) {
|
|
16975
16455
|
actions.push(btn(isBusy ? weT("安装中…") : weT("下载安装"), () => act("install", s.id, s.name), false, true));
|
|
16456
|
+
} else if (!s.active) {
|
|
16457
|
+
actions.push(btn(isBusy ? weT("选中中…") : weT("选择"), () => act("activate", s.id, s.name), false, true));
|
|
16458
|
+
actions.push(btn(weT("删除"), () => act("remove", s.id, s.name), isBusy, false));
|
|
16976
16459
|
} else {
|
|
16977
|
-
|
|
16978
|
-
actions.push(btn(
|
|
16979
|
-
isBusy ? weT("切换中…") : (skinCenter ? weT("选择") : weT("使用")),
|
|
16980
|
-
() => act("activate", s.id, s.name), false, true));
|
|
16981
|
-
actions.push(btn(weT("删除"), () => act("remove", s.id, s.name), isBusy, false));
|
|
16982
|
-
} else {
|
|
16983
|
-
actions.push(btn(weT("使用中"), () => {}, true, false));
|
|
16984
|
-
}
|
|
16460
|
+
actions.push(btn(weT("已选中"), () => {}, true, false));
|
|
16985
16461
|
}
|
|
16986
16462
|
|
|
16987
16463
|
return React.createElement("div", { key: s.id, style: SKINS_STYLE.card },
|
|
@@ -17008,18 +16484,12 @@ window.__ModuleLoader__.load({
|
|
|
17008
16484
|
|
|
17009
16485
|
/**
|
|
17010
16486
|
* 页签薄包装:`renderActiveTab` 只认这个签名(与别的 `render*Tab(ctx)` 一致)。
|
|
17011
|
-
* 传进来的
|
|
17012
|
-
* 由挂载点提供"清掉当前壁纸"的入口;本文件不直接碰设置)。
|
|
16487
|
+
* 传进来的 props 原样转交(目前只有 `onClearWallpaper`)。
|
|
17013
16488
|
*/
|
|
17014
16489
|
function renderSkinsTab(props) {
|
|
17015
16490
|
return React.createElement(SkinsTabPanel, props || null);
|
|
17016
16491
|
}
|
|
17017
16492
|
|
|
17018
|
-
// 模块加载即启动互斥仲裁(不依赖用户是否打开过皮肤包页签):
|
|
17019
|
-
// 壁纸在台 ⇒ 撤掉皮肤;壁纸撤下 ⇒ 恢复选中的皮肤。
|
|
17020
|
-
// 放在模块作用域而不是 React effect 里,是因为它必须在**任何面板渲染之前**就守住这个不变量。
|
|
17021
|
-
try { startSkinArbiter(); } catch { /* 环境异常时退化为纯按钮路径 */ }
|
|
17022
|
-
|
|
17023
16493
|
// ── 内联模块:src/picker-model.js —— 选择器模型层:过滤判定 + 派生数据 + 分页切片(纯函数 / 显式入参 / 零 DOM)(构建期注入;勿手改本段)──
|
|
17024
16494
|
/**
|
|
17025
16495
|
* picker-model.js — 壁纸选择器的**模型层**:过滤判定 + 派生数据 + 分页切片。
|