dsh-folia 4.12.6 → 4.13.1
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/cordis.patch.yml +17 -30
- package/lib/client.js +241 -12
- package/lib/routes/skins.js +41 -6
- package/package.json +2 -2
package/cordis.patch.yml
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
# dsh-plugin-folia bundle patch.
|
|
2
2
|
#
|
|
3
|
-
#
|
|
3
|
+
# 两条 row:我们自己的 `folia` + 随身携带的皮肤中心 `skin-center`。
|
|
4
4
|
#
|
|
5
|
-
# ──
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
5
|
+
# ── 皮肤中心为什么"带过一次、去掉、又带回来"(两次都是实测结论)──────────────
|
|
6
|
+
# 1) 4.12.x:带着它(dependencies + row)→ 0.4.12 的 engines 要求 `dsh >= 0.2.1-rc.3`,
|
|
7
|
+
# 而当时桌面端是 0.2.1-rc.2 ⇒ 市场把这条行**保持为 off**:
|
|
8
|
+
# ".dsh-market/log.ndjson": "plugin kept off: @linxin666/dsh-client-ui-skin-center"
|
|
9
|
+
# `/api/skin-center/v2/*` 整族变成路由 miss(带会话实测 404),页签点「应用」必失败。
|
|
10
|
+
# 当时把依赖钉到 0.4.8 只在一两次启动里有效,桌面端会把 0.4.12 投影回来 ⇒ 又 off。
|
|
11
|
+
# 2) 现在:桌面端基线升到 0.2.1-rc.3+(本机实测 dsh=0.2.1-rc.3),engines 满足 ⇒ 可以正常携带。
|
|
12
|
+
# 携带的意义:用户装 dsh-folia 就同时得到"皮肤渲染方",不必再自己去市场找。
|
|
10
13
|
#
|
|
11
|
-
# ──
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# 版本:依赖写 `^0.4.8`(本仓实测过的版本线);它若大改 v1alpha1 契约,我们跟着升级即可。
|
|
20
|
-
# 判据:test/verify-skin-center-bundle.mjs 会同时钉住「声明了依赖」与「插了唯一 id 的 row」。
|
|
14
|
+
# ── 仍然保留的纪律 ─────────────────────────────────────────────────────────
|
|
15
|
+
# · 重复挂载是安全的(实测):宿主侧 apply 走 `mountOnce`,客户端 slots.register 包 try/catch
|
|
16
|
+
# ⇒ 用户市场里再装一份也不会打起来(第二次是空操作)。
|
|
17
|
+
# · 若某台机器 dsh 低于 0.4.12 的要求:市场会 keep off,我们的页签会如实显示
|
|
18
|
+
# "皮肤中心未运行",壁纸/桌宠/开机动画/玻璃**完全不受影响**(探测四态 + 应用禁用)。
|
|
19
|
+
# · row id 不得以 `folia-` 开头:`lib/subpackage-boot.js#subpackageSpecifiers` 会把
|
|
20
|
+
# 「id 以 folia- 开头的行」当成我们自己的子包去仓库解析(解析不到就抛)。
|
|
21
|
+
# · 依赖版本**精确**(不带 ^):0.4.9+ 的 engines 会随上游再抬,我们不能让用户被动中招。
|
|
21
22
|
#
|
|
22
23
|
# A patch replaces the targeted row's whole `config`, so only additive `insert`
|
|
23
24
|
# rows appear here (we never override an existing shipped row).
|
|
@@ -25,19 +26,5 @@
|
|
|
25
26
|
- insert:
|
|
26
27
|
- id: folia
|
|
27
28
|
name: 'dsh-folia'
|
|
28
|
-
# 皮肤中心:独立 npm 包(BSD-3-Clause),本包作为依赖携带。
|
|
29
|
-
#
|
|
30
|
-
# ⚠️ row id **不能**以 `folia-` 开头:`lib/subpackage-boot.js#subpackageSpecifiers` 把
|
|
31
|
-
# 「id 以 `folia-` 开头的行」当作**我们自己的子包行**做启动自检(解析不到就抛,因为宿主
|
|
32
|
-
# 对行级失败是静默的)。给第三方行起 `folia-skin-center` 这种 id 会被误判成"我们的子包"
|
|
33
|
-
# ⇒ 自检去仓库 node_modules 里解析它,开发树里没有这个依赖就直接抛,整个 apply 失败。
|
|
34
|
-
# 所以这里用不撞前缀的 `skin-center`(也与 web-all 的 `web-ui-skin-center`、
|
|
35
|
-
# 官方包的 `ui-skin-center` 都不同名)。
|
|
36
|
-
#
|
|
37
|
-
# ⚠️ 版本**钉死 0.4.8**(package.json 里也是 exact):0.4.9+ 起的 peer 要求
|
|
38
|
-
# `@deepseek-ai/dsh >= 0.2.1-rc.3`,而当前桌面端跑的是 0.2.1-rc.2 —— 于是市场把这条行
|
|
39
|
-
# **保持为 off**(`.dsh-market/log.ndjson` 里写着 "plugin kept off: …skin-center"),
|
|
40
|
-
# 皮肤中心的 /api/skin-center/v2/* 全部变成路由 miss(401/404),页签里点「应用」必失败。
|
|
41
|
-
# 0.4.8 的 peer 是 `>=0.2.1-rc.2`,与现在的宿主匹配。等桌面端升到 rc.3 再放开版本范围。
|
|
42
29
|
- id: skin-center
|
|
43
30
|
name: '@linxin666/dsh-client-ui-skin-center'
|
package/lib/client.js
CHANGED
|
@@ -18317,6 +18317,17 @@ window.__ModuleLoader__.load({
|
|
|
18317
18317
|
const resp = await apiJson("/skins/list" + (force ? "?refresh=1" : ""), { parse: "always" });
|
|
18318
18318
|
const doc = (resp && resp.data) || null;
|
|
18319
18319
|
if (resp && resp.ok && doc && doc.ok) {
|
|
18320
|
+
// 皮肤中心在不在跑,**由浏览器判定**,不信宿主探测的结论。
|
|
18321
|
+
// 宿主 fetch 不带会话:`/api/**` 一旦被鉴权挡在前面就只会拿到 401,而 401 既可能是
|
|
18322
|
+
// "装了但需要会话"也可能是"路由没挂载",从宿主那一侧**无法区分**(实测踩过:
|
|
18323
|
+
// 报文纯 401 就把整个页签的「应用」按钮锁死)。浏览器同源请求天然带会话,
|
|
18324
|
+
// 拿得到就是真在跑,拿不到再退回宿主给的三态。
|
|
18325
|
+
if (doc.skinCenter !== "present") {
|
|
18326
|
+
try {
|
|
18327
|
+
const cat = await apiJson("/api/skin-center/v2/catalog", { parse: "always" });
|
|
18328
|
+
if (cat && cat.ok && cat.data && cat.data.ok) doc.skinCenter = "present";
|
|
18329
|
+
} catch { /* 仍旧用宿主给的状态 */ }
|
|
18330
|
+
}
|
|
18320
18331
|
// 读"当前在用哪套皮肤":**先直连**(同源相对路径 `/api/**`,凭据天然带全),
|
|
18321
18332
|
// 直连不通(页面的 origin 可能不等于宿主 HTTP 服务的 origin ⇒ 路由没到)再退回宿主代理
|
|
18322
18333
|
// `/folia/skins/active` —— 代理会把浏览器这一侧的 cookie 与同源头转发过去。
|
|
@@ -18329,7 +18340,12 @@ window.__ModuleLoader__.load({
|
|
|
18329
18340
|
}
|
|
18330
18341
|
setState(doc);
|
|
18331
18342
|
if (doc.siteError && doc.siteError !== "site-off") {
|
|
18332
|
-
|
|
18343
|
+
// `site-locked` = 站点整站上锁(登录页 + {"code":"site_locked"}),不是网络/插件问题。
|
|
18344
|
+
// 给它一句能照做的说明,而不是甩一个 http-401(实测:这个错误码让人误判成插件坏了)。
|
|
18345
|
+
const msg = doc.siteError === "site-locked"
|
|
18346
|
+
? weT("站点已锁定:只对已登录的管理员开放(需站点侧放行 GET /api/public/wallpapers)")
|
|
18347
|
+
: String(doc.siteError);
|
|
18348
|
+
setNote({ ok: false, text: weT("站点清单读不到:{msg}", { msg }) });
|
|
18333
18349
|
}
|
|
18334
18350
|
} else {
|
|
18335
18351
|
setNote({ ok: false, text: weT("读取失败:{msg}", { msg: (doc && doc.error) || "unknown" }) });
|
|
@@ -18383,6 +18399,15 @@ window.__ModuleLoader__.load({
|
|
|
18383
18399
|
});
|
|
18384
18400
|
return;
|
|
18385
18401
|
}
|
|
18402
|
+
// 关键:让**皮肤赢**。皮肤中心检测到 `body[data-we-wallpaper]` 时会**主动停画**
|
|
18403
|
+
// (它的 withhold 合约:壁纸在台时它不上台),所以"写成功但画面不变"完全合理 ——
|
|
18404
|
+
// 用户在这个页签点「应用」,意图就是让皮肤接管外观,因此这里主动进入「皮肤让路」态:
|
|
18405
|
+
// 清掉我们的壁纸层 + 摘下玻璃门控(client.js 里既有的 enterSkinYield,记忆会落盘,
|
|
18406
|
+
// 点「恢复默认外观」时按记忆放回壁纸)。
|
|
18407
|
+
try {
|
|
18408
|
+
if (wantId && typeof enterSkinYield === "function") enterSkinYield("folia-tab-apply");
|
|
18409
|
+
if (!wantId && typeof exitSkinYield === "function") exitSkinYield("folia-tab-clear");
|
|
18410
|
+
} catch { /* 让路是增强:失败也不影响皮肤中心的写入 */ }
|
|
18386
18411
|
// 两条收敛路径:① 广播皮肤中心自己监听的 `dsh-skin-applied`(立即就地切换);
|
|
18387
18412
|
// ② 它另外每 2 秒轮询 /api/skin-center/v2/active(事件丢了也会在 2 秒内跟上)。
|
|
18388
18413
|
try {
|
|
@@ -18390,7 +18415,9 @@ window.__ModuleLoader__.load({
|
|
|
18390
18415
|
} catch { /* 老环境没有 CustomEvent 也无所谓,②会兜住 */ }
|
|
18391
18416
|
setNote({
|
|
18392
18417
|
ok: true,
|
|
18393
|
-
text: wantId
|
|
18418
|
+
text: wantId
|
|
18419
|
+
? weT("已切换到:{name}(本插件的壁纸与玻璃已让位)", { name: label || "" })
|
|
18420
|
+
: weT("已恢复官方默认外观(壁纸已按记忆放回)"),
|
|
18394
18421
|
});
|
|
18395
18422
|
await load(true);
|
|
18396
18423
|
return;
|
|
@@ -18446,11 +18473,19 @@ window.__ModuleLoader__.load({
|
|
|
18446
18473
|
// 不在 → 明说它是个**独立插件**(不在 dsh-web-all 里),给一条可复制的安装命令
|
|
18447
18474
|
// unknown(探测失败)→ 保守地按"在"讲,不吓唬用户
|
|
18448
18475
|
const skinCenter = (state && state.skinCenter) || "unknown";
|
|
18476
|
+
// 用户在插件市场里手动禁用了它 —— 这是最准确的原因,优先于所有 http 状态码推测。
|
|
18477
|
+
const marketDisabled = Boolean(state && state.skinCenterDisabled);
|
|
18449
18478
|
// `present` 是**唯一**允许点「应用」的状态:只有真的拿到 catalog JSON 才算它在跑。
|
|
18450
18479
|
// `absent`=路由不存在(插件没装/未启用);`auth-gated`=鉴权在路由前面挡住(无法判定);
|
|
18451
18480
|
// `unknown`=探测失败。后三种都如实告诉用户"现在点应用不会成功",而不是让他点了看 404。
|
|
18452
18481
|
const canApply = skinCenter === "present";
|
|
18453
|
-
if (
|
|
18482
|
+
if (marketDisabled) {
|
|
18483
|
+
children.push(React.createElement("div", {
|
|
18484
|
+
key: "hint-disabled",
|
|
18485
|
+
className: "we-picker__hint",
|
|
18486
|
+
style: { margin: "6px 0 4px", color: "#ffd08a", lineHeight: "1.6" },
|
|
18487
|
+
}, weT("皮肤中心被**你在插件市场里禁用了**(市场记录里它在 disabled 列表里)。皮肤的实际渲染方就是它,禁用它 ⇒ 这里只能装/卸皮肤包,不能应用。到插件市场把它重新启用即可(dsh-folia 已把它作为依赖随身携带,不需要另装)。")));
|
|
18488
|
+
} else if (skinCenter === "absent") {
|
|
18454
18489
|
children.push(React.createElement("div", {
|
|
18455
18490
|
key: "hint-absent",
|
|
18456
18491
|
className: "we-picker__hint",
|
|
@@ -18471,7 +18506,7 @@ window.__ModuleLoader__.load({
|
|
|
18471
18506
|
className: "we-picker__hint",
|
|
18472
18507
|
style: { margin: "6px 0 4px", color: "#ffd08a", lineHeight: "1.6" },
|
|
18473
18508
|
}, skinCenter === "auth-gated"
|
|
18474
|
-
? weT("
|
|
18509
|
+
? weT("读不到皮肤中心的清单(浏览器侧也被鉴权挡住)。先点一次「刷新」;仍不行就说明它没在运行 —— 去设置里确认皮肤中心已启用。")
|
|
18475
18510
|
: weT("皮肤中心状态未知(探测超时/网络错误)。点「刷新」重试。")));
|
|
18476
18511
|
} else {
|
|
18477
18512
|
children.push(React.createElement("div", {
|
|
@@ -18481,7 +18516,7 @@ window.__ModuleLoader__.load({
|
|
|
18481
18516
|
}, weT("本页签可以直接切换皮肤:装完点「应用」即可,无需打开设置页。皮肤包落在皮肤中心目录,两边读同一份。")));
|
|
18482
18517
|
}
|
|
18483
18518
|
// 皮肤中心不在跑时,「应用」按钮直接禁用并给出原因 —— 比"点了报错"诚实。
|
|
18484
|
-
const applyBlockedReason = canApply ? "" : weT("
|
|
18519
|
+
const applyBlockedReason = canApply ? "" : weT("读不到皮肤中心的清单,先点「刷新」;仍不行说明它没在运行");
|
|
18485
18520
|
|
|
18486
18521
|
// 提示行常驻占位(与开机动画/桌宠页签同一条纪律:不因 note 出现/消失让面板跳高度)
|
|
18487
18522
|
children.push(React.createElement("div", {
|
|
@@ -18617,6 +18652,170 @@ window.__ModuleLoader__.load({
|
|
|
18617
18652
|
return React.createElement("div", { className: "we-picker", style: { display: "flex", flexDirection: "column" } }, children);
|
|
18618
18653
|
}
|
|
18619
18654
|
|
|
18655
|
+
// ── 内联模块:src/skin-compat.js —— 皮肤兼容层:把宿主槽位翻成分栏词表(data-pane / data-dsh-frame)—— 皮肤包写的是 web-all 的 compat 标记,没装 web-all 时否则整套选择器不命中(构建期注入;勿手改本段)──
|
|
18656
|
+
/**
|
|
18657
|
+
* 皮肤兼容层:把宿主(stock DSH)的**槽位词表**翻译成皮肤包认的**分栏词表**。
|
|
18658
|
+
*
|
|
18659
|
+
* 为什么需要它(实测,不是推测):
|
|
18660
|
+
* · 皮肤包的 CSS 大量使用 `[data-pane="sidebar"]` / `[data-pane="conversation"]` /
|
|
18661
|
+
* `[data-pane="details"]` / `[data-dsh-frame]`。样例计数(按行):
|
|
18662
|
+
* verdandi 60+、xp 36+、deep-current 12、cafe-roastery 10、ember-fall 6、blueprint 4 …
|
|
18663
|
+
* 全catalog 里 `data-pane=` 出现 250+ 次。
|
|
18664
|
+
* · 但**这些属性不是宿主写的**,是 `@linxin666/dsh-web-all` 的 compat shim 写的:
|
|
18665
|
+
* web-all/lib/client.js 的 `COLUMN_SHIMS = [["[class*=\"sidebarCol\"]","data-pane=\"sidebar\""], …]`
|
|
18666
|
+
* 而皮肤中心自己也写明:"the `data-pane=\"conversation\"` attribute is stamped by the
|
|
18667
|
+
* dsh-web-all compat shim"(skin-center/lib/client.js:4171 附近的注释)。
|
|
18668
|
+
* ⇒ 没装 web-all 时,这些选择器**一条都不命中**:皮肤不是"坏了",是它认的分栏标记不在。
|
|
18669
|
+
* 症状就是"皮肤显示不全"(布局/侧栏/输入区没被皮肤接管),而装了 web-all 就正常。
|
|
18670
|
+
*
|
|
18671
|
+
* 我们的做法:**只读宿主槽位,补写分栏属性**。
|
|
18672
|
+
* · `[data-slot="sidebar"]` → 它的父元素(`div.[class*=sidebarCol]`)标 `data-pane="sidebar"`
|
|
18673
|
+
* · `[data-slot="main"]` → 它的父元素(`div.[class*=centerCol]`)标 `data-pane="conversation"`
|
|
18674
|
+
* · `[data-slot="rightbar"]` → 它的父元素(`div.[class*=rightbarCol]`)标 `data-pane="details"`
|
|
18675
|
+
* · `#root` 下的画框元素 → `data-dsh-frame`
|
|
18676
|
+
*
|
|
18677
|
+
* ⚠️ **锚点必须取"列那一级"的槽名**(sidebar / main / rightbar),取深层槽名是无效的:
|
|
18678
|
+
* 宿主 `renderSlot` 的锚点是 `ANCHOR_STYLE = { display: "contents" }` 的**无布局包裹元素**
|
|
18679
|
+
* 与列容器之间的嵌套(app.asar 实证,JSX 顺序即 DOM 顺序):
|
|
18680
|
+
*
|
|
18681
|
+
* div.frame
|
|
18682
|
+
* ├ div.sidebarCol ← children: renderSlot("sidebar") ⇒ 锚点的父元素就是列
|
|
18683
|
+
* ├ div.centerCol ← children: renderSlot("main") ⇒ 同上
|
|
18684
|
+
* └ div.rightbarCol ← children: renderSlot("rightbar") ⇒ 同上
|
|
18685
|
+
*
|
|
18686
|
+
* 而 `main.conversation` 是 `main` 的子槽(`ConversationPanel` 里 renderSlot),
|
|
18687
|
+
* `rightbar.session` 更在 `div.session` 之内 ⇒ 它们的**父元素是另一个 display:contents 锚点
|
|
18688
|
+
* 或内层包裹 div**,把 data-pane 写在那里既没有布局效果、也不是方案认的那一列。
|
|
18689
|
+
* ⇒ 所以这里既用列级槽名,又让 `paneHost` **穿过 `[data-slot]` 锚点**再落笔;
|
|
18690
|
+
* 深层槽名只作退路(宿主哪天不再给列级槽名时仍能命中)。
|
|
18691
|
+
*
|
|
18692
|
+
* 三条纪律(与仓库既有的跨插件协议一致):
|
|
18693
|
+
* ① **只写自己的标记**:不碰皮肤中心的 `data-dsh-skin` / `data-dsh-surface` / 任何 `data-we-*`;
|
|
18694
|
+
* ② **幂等**:已存在同名属性就不改(别人(web-all)先写过就以它为准);
|
|
18695
|
+
* ③ **可失败**:整段包在 try 里,异常绝不影响壁纸/玻璃主路径;观察器只在 document 就绪后装。
|
|
18696
|
+
*
|
|
18697
|
+
* 属性本身是惰性的(宿主 CSS 不用 data-pane),所以可以在启动时就补,不必等皮肤上台。
|
|
18698
|
+
*/
|
|
18699
|
+
|
|
18700
|
+
const PANE_ANCHORS = [
|
|
18701
|
+
// 列级槽名优先(实证:这三种锚点的父元素就是列容器)。
|
|
18702
|
+
{ pane: 'sidebar', selectors: ['[data-slot="sidebar"]'] },
|
|
18703
|
+
{ pane: 'conversation', selectors: ['[data-slot="main"]', '[data-slot="main.conversation"]'] },
|
|
18704
|
+
{ pane: 'details', selectors: ['[data-slot="rightbar"]', '[data-slot="rightbar.session"]', '[data-sidebar-right-panel]'] },
|
|
18705
|
+
];
|
|
18706
|
+
|
|
18707
|
+
/** 从锚点元素上溯(穿过 display:contents 的 `[data-slot]` 锚点),得到"那一列"的容器。 */
|
|
18708
|
+
function paneHost(anchor) {
|
|
18709
|
+
let el = anchor && anchor.parentElement;
|
|
18710
|
+
// 宿主的槽位锚点都是 display:contents 包裹元素(renderSlot 的 ANCHOR_STYLE),
|
|
18711
|
+
// 属性写在它身上没有布局效果 ⇒ 必须一直走到第一个"真盒子"。
|
|
18712
|
+
let guard = 0;
|
|
18713
|
+
while (el && el.hasAttribute && el.hasAttribute('data-slot') && guard < 12) {
|
|
18714
|
+
el = el.parentElement;
|
|
18715
|
+
guard += 1;
|
|
18716
|
+
}
|
|
18717
|
+
if (!el || el === document.body) return null;
|
|
18718
|
+
if (el.id === 'root') return null;
|
|
18719
|
+
if (el.hasAttribute('data-dsh-frame')) return null;
|
|
18720
|
+
return el;
|
|
18721
|
+
}
|
|
18722
|
+
|
|
18723
|
+
function stampPanes() {
|
|
18724
|
+
let stamped = 0;
|
|
18725
|
+
for (const rule of PANE_ANCHORS) {
|
|
18726
|
+
// 每条规则**各自**判断有没有命中:早先用累计计数会让后一条规则在第一条选择器空手时
|
|
18727
|
+
// 直接跳过退路选择器(conversation/details 的退路因此永远不被尝试)。
|
|
18728
|
+
let ruleStamped = 0;
|
|
18729
|
+
for (const selector of rule.selectors) {
|
|
18730
|
+
let nodes;
|
|
18731
|
+
try { nodes = document.querySelectorAll(selector); } catch { continue; }
|
|
18732
|
+
for (const node of nodes) {
|
|
18733
|
+
const host = paneHost(node);
|
|
18734
|
+
if (!host || host.hasAttribute('data-pane')) continue;
|
|
18735
|
+
host.setAttribute('data-pane', rule.pane);
|
|
18736
|
+
stamped += 1;
|
|
18737
|
+
ruleStamped += 1;
|
|
18738
|
+
}
|
|
18739
|
+
if (ruleStamped) break; // 同一 pane 命中一种锚点即可
|
|
18740
|
+
}
|
|
18741
|
+
}
|
|
18742
|
+
return stamped;
|
|
18743
|
+
}
|
|
18744
|
+
|
|
18745
|
+
function stampFrame() {
|
|
18746
|
+
try {
|
|
18747
|
+
if (document.querySelector('[data-dsh-frame]')) return 0;
|
|
18748
|
+
const root = document.getElementById('root') || document.body;
|
|
18749
|
+
if (!root) return 0;
|
|
18750
|
+
// 与 web-all 同口径:**列容器的父元素**就是画框(`sidebarCol.parentElement`);类名只作退路。
|
|
18751
|
+
const sidebarHost = document.querySelector('[data-pane="sidebar"]');
|
|
18752
|
+
const fromColumn = (sidebarHost && sidebarHost.parentElement) || null;
|
|
18753
|
+
const frame = (fromColumn && fromColumn !== document.body && fromColumn.id !== 'root' ? fromColumn : null)
|
|
18754
|
+
|| root.querySelector('div[class*="_frame"]')
|
|
18755
|
+
|| root.firstElementChild;
|
|
18756
|
+
if (!frame || frame.hasAttribute('data-dsh-frame')) return 0;
|
|
18757
|
+
if (frame === document.body || frame.id === 'root') return 0;
|
|
18758
|
+
frame.setAttribute('data-dsh-frame', '');
|
|
18759
|
+
return 1;
|
|
18760
|
+
} catch { return 0; }
|
|
18761
|
+
}
|
|
18762
|
+
|
|
18763
|
+
function stampAll() {
|
|
18764
|
+
try { return stampPanes() + stampFrame(); } catch { return 0; }
|
|
18765
|
+
}
|
|
18766
|
+
|
|
18767
|
+
/**
|
|
18768
|
+
* 定时器可缺省:**挂载点跑在不保证有定时的宿主里**。
|
|
18769
|
+
* 实测(2026-10-30,`test/verify-playback-controls.mjs` / `verify-transcode-state.mjs`):
|
|
18770
|
+
* 那几个守卫用只实现 `document` 的 fake DOM 直接 apply 客户端,**没有 `setTimeout`** ——
|
|
18771
|
+
* 这里裸调 `setTimeout` 会 `ReferenceError`,而 `ctx.effect(() => feat.mount())` 不做保护,
|
|
18772
|
+
* 异常会顺着 apply 链把**整个宿主插件**带崩(一次 17 条判据全红)。
|
|
18773
|
+
* ⇒ 有就用(优先 `window.setTimeout`,与仓库其它层同口径),没有就**当场做**,绝不抛。
|
|
18774
|
+
*/
|
|
18775
|
+
function scheduleAfter(fn, ms) {
|
|
18776
|
+
try {
|
|
18777
|
+
const st = (typeof window !== 'undefined' && typeof window.setTimeout === 'function')
|
|
18778
|
+
? window.setTimeout.bind(window)
|
|
18779
|
+
: (typeof setTimeout === 'function' ? setTimeout : null);
|
|
18780
|
+
if (!st) { fn(); return null; }
|
|
18781
|
+
return st(fn, ms);
|
|
18782
|
+
} catch { try { fn(); } catch { /* ignore */ } return null; }
|
|
18783
|
+
}
|
|
18784
|
+
|
|
18785
|
+
function cancelAfter(id) {
|
|
18786
|
+
try {
|
|
18787
|
+
if (id === null || id === undefined) return;
|
|
18788
|
+
if (typeof clearTimeout === 'function') clearTimeout(id);
|
|
18789
|
+
else if (typeof window !== 'undefined' && typeof window.clearTimeout === 'function') window.clearTimeout(id);
|
|
18790
|
+
} catch { /* ignore */ }
|
|
18791
|
+
}
|
|
18792
|
+
|
|
18793
|
+
/** 装上兼容层;返回幂等 teardown(与仓库其它 install* 同形)。整段可失败(异常不外溢)。 */
|
|
18794
|
+
function installSkinCompat() {
|
|
18795
|
+
if (typeof document === 'undefined') return () => {};
|
|
18796
|
+
try {
|
|
18797
|
+
stampAll();
|
|
18798
|
+
let observer = null;
|
|
18799
|
+
let timer = null;
|
|
18800
|
+
const schedule = () => {
|
|
18801
|
+
if (timer !== null) return;
|
|
18802
|
+
timer = scheduleAfter(() => { timer = null; stampAll(); }, 120);
|
|
18803
|
+
};
|
|
18804
|
+
try {
|
|
18805
|
+
observer = new MutationObserver(schedule);
|
|
18806
|
+
// 只观察结构性变化(子树增删);不观察属性,避免别人改属性时我们无限自激。
|
|
18807
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
18808
|
+
} catch { observer = null; }
|
|
18809
|
+
// 宿主是 SPA:路由切换会整块换掉列容器,所以补一次延迟重扫。
|
|
18810
|
+
const late = scheduleAfter(stampAll, 1500);
|
|
18811
|
+
return () => {
|
|
18812
|
+
try { if (observer) observer.disconnect(); } catch { /* ignore */ }
|
|
18813
|
+
if (timer !== null) { cancelAfter(timer); timer = null; }
|
|
18814
|
+
cancelAfter(late);
|
|
18815
|
+
};
|
|
18816
|
+
} catch { return () => {}; }
|
|
18817
|
+
}
|
|
18818
|
+
|
|
18620
18819
|
// ── 内联模块:src/features.js —— 客户端功能注册表:页签条 / 整区面板 / 常驻挂载层。装配点(quick-panel、client.js)只查表,不写功能名(构建期注入;勿手改本段)──
|
|
18621
18820
|
/**
|
|
18622
18821
|
* features.js(客户端)— **功能注册表**:页签声明、面板工厂、挂载钩子。
|
|
@@ -18665,6 +18864,9 @@ window.__ModuleLoader__.load({
|
|
|
18665
18864
|
*/
|
|
18666
18865
|
const WE_FEATURE_MOUNTS = [
|
|
18667
18866
|
{ id: "pets", mount: mountPetLayer },
|
|
18867
|
+
// 皮肤兼容层:补写 `data-pane` / `data-dsh-frame`(皮肤包写的是 web-all 的 compat 词表)。
|
|
18868
|
+
// 它只写自己的惰性属性、幂等、可失败,所以随墙纸引擎常驻,不依赖皮肤是否在台上。
|
|
18869
|
+
{ id: "skin-compat", mount: installSkinCompat },
|
|
18668
18870
|
];
|
|
18669
18871
|
|
|
18670
18872
|
/** 页签 → 功能归属(宿主清单 lib/features.js 的客户端镜像,供守卫对账)。 */
|
|
@@ -27189,9 +27391,13 @@ window.__ModuleLoader__.load({
|
|
|
27189
27391
|
* 才回收)。所以只认"过一拍还在"的出现:宽限结束时 DOM 仍带标记、且我方确实有要退的东西
|
|
27190
27392
|
* (壁纸在台 ⇒ 清层;或**无壁纸**且设置已就位 ⇒ 只剩玻璃要让)⇒ 才是动作。
|
|
27191
27393
|
* ⚠️ 450ms 必须显著短于对方的 1500ms 宽限,否则真实动作会在我们确认前被它回收。
|
|
27394
|
+
*
|
|
27395
|
+
* `opts.forced`:**标记值发生了变化**(换了一套皮肤)—— 那是用户刚做的一次显式动作,
|
|
27396
|
+
* 不存在"启动上妆"的竞态可言,所以连"我方是否有层"这条腿都不用再问(见 syncSkinYield)。
|
|
27192
27397
|
*/
|
|
27193
|
-
function scheduleSkinYieldEnter(reason) {
|
|
27398
|
+
function scheduleSkinYieldEnter(reason, opts) {
|
|
27194
27399
|
if (skinYieldEnterTimer !== null || skinYielded) return;
|
|
27400
|
+
const forced = !!(opts && opts.forced);
|
|
27195
27401
|
const fire = () => {
|
|
27196
27402
|
skinYieldEnterTimer = null;
|
|
27197
27403
|
try {
|
|
@@ -27203,8 +27409,11 @@ window.__ModuleLoader__.load({
|
|
|
27203
27409
|
// · 无壁纸腿:没有层可清、也没有竞态可防,但玻璃整族仍盖在皮肤上(玻璃与壁纸
|
|
27204
27410
|
// 正交,data-we-glass-page 恒挂)⇒ 这条腿也要进,否则无壁纸用户中途试穿 /
|
|
27205
27411
|
// 应用皮肤永远摘不掉玻璃(安装期的 adopt 只管启动那一拍)。
|
|
27206
|
-
|
|
27207
|
-
if (!
|
|
27412
|
+
// · forced(换皮肤 = 用户刚动作):跳过启动竞态那套判据,直接让路。
|
|
27413
|
+
if (!forced) {
|
|
27414
|
+
const wallMarkerOn = !!(document.body && document.body.hasAttribute("data-we-wallpaper"));
|
|
27415
|
+
if (!wallMarkerOn && (selection.id || !selection.loaded)) return;
|
|
27416
|
+
}
|
|
27208
27417
|
enterSkinYield(reason + "/dom");
|
|
27209
27418
|
} catch { /* ignore */ }
|
|
27210
27419
|
};
|
|
@@ -27248,10 +27457,24 @@ window.__ModuleLoader__.load({
|
|
|
27248
27457
|
} catch { fire(); }
|
|
27249
27458
|
}
|
|
27250
27459
|
|
|
27251
|
-
/**
|
|
27460
|
+
/** 上次看到的 `data-dsh-skin` 值(null = 还没看过;用于区分"启动上妆"与"用户换了一套")。 */
|
|
27461
|
+
let skinAttrSeen = null;
|
|
27462
|
+
|
|
27463
|
+
/**
|
|
27464
|
+
* 合议当前状态:皮肤"要求上台"且我方确实在台上 ⇒ 退场;持续不在台上则排定复位。
|
|
27465
|
+
*
|
|
27466
|
+
* **没有优先级,只有"谁最后动的"**(用户明确要求):
|
|
27467
|
+
* · 皮肤标记**值发生变化** = 用户刚换/刚应用了一套皮肤 ⇒ 我方立刻让路(哪怕壁纸正在画);
|
|
27468
|
+
* · 壁纸被选中 = `media-prep` 的钩子会 `exitSkinYield` ⇒ 皮肤侧 withhold、壁纸接管;
|
|
27469
|
+
* · 启动那一拍(值没变)= 不改判:我方有壁纸就让壁纸画(皮肤自己 withhold),
|
|
27470
|
+
* 我方无壁纸且在皮肤上 ⇒ 认领让路态(见 installSkinInterop 的 adoptOnLoaded)。
|
|
27471
|
+
*/
|
|
27252
27472
|
function syncSkinYield(reason) {
|
|
27253
27473
|
try {
|
|
27254
|
-
|
|
27474
|
+
const attr = document.documentElement.getAttribute("data-dsh-skin");
|
|
27475
|
+
const changed = attr !== skinAttrSeen;
|
|
27476
|
+
skinAttrSeen = attr;
|
|
27477
|
+
if (attr) {
|
|
27255
27478
|
cancelSkinYieldExit();
|
|
27256
27479
|
// ⚠️ 「我方在台上」有两条腿:壁纸在台(标记在场 ⇒ 有层要让)与无壁纸(设置已就位
|
|
27257
27480
|
// 且 id 为空 ⇒ 只剩玻璃要让)。启动竞态只长在壁纸腿上——对方运行时会先于我们的
|
|
@@ -27260,7 +27483,8 @@ window.__ModuleLoader__.load({
|
|
|
27260
27483
|
// 动作会清掉壁纸(实测报障:有壁纸时刷新掉壁纸)。正确的收场是:让我们的层建起来
|
|
27261
27484
|
// → 标记挂上 → 对方 watcher 自行 withhold 撤掉皮肤。无壁纸腿没有竞态(没有层可清),
|
|
27262
27485
|
// 随宽限进即可。不是立即退场:先排定一次"过一拍再确认"(见 scheduleSkinYieldEnter)。
|
|
27263
|
-
|
|
27486
|
+
// 但 `changed`(换了一套皮肤)是用户的显式动作,不走那套竞态判据(forced)。
|
|
27487
|
+
scheduleSkinYieldEnter(reason, { forced: changed });
|
|
27264
27488
|
return;
|
|
27265
27489
|
}
|
|
27266
27490
|
cancelSkinYieldEnter();
|
|
@@ -27271,6 +27495,9 @@ window.__ModuleLoader__.load({
|
|
|
27271
27495
|
/** 装上互操作(单腿:DOM 观察);返回幂等 teardown。 */
|
|
27272
27496
|
function installSkinInterop() {
|
|
27273
27497
|
if (typeof document === "undefined" || !document.documentElement) return () => {};
|
|
27498
|
+
// 启动快照:把当前标记记为"已看过",这样**启动那一拍不会被当成一次换皮肤动作**
|
|
27499
|
+
// (否则每次刷新都会误判成用户刚应用皮肤,进而清掉壁纸)。
|
|
27500
|
+
try { skinAttrSeen = document.documentElement.getAttribute("data-dsh-skin"); } catch { skinAttrSeen = null; }
|
|
27274
27501
|
let observer = null;
|
|
27275
27502
|
try {
|
|
27276
27503
|
observer = new MutationObserver(() => syncSkinYield("dom"));
|
|
@@ -27280,7 +27507,9 @@ window.__ModuleLoader__.load({
|
|
|
27280
27507
|
// · 皮肤在台上(文档交付时 tap 已画)+ 我方**没有**选中的壁纸 ⇒ 本来就该在让路态
|
|
27281
27508
|
// (玻璃退场)——认领,不清层;记忆从**盘上落的那份**拿(enter 写的
|
|
27282
27509
|
// skinYieldRestoreId / skinYieldRestoreRotation),没落过盘才退回空记忆;
|
|
27283
|
-
// · 皮肤在台上 + 我方有选中的壁纸 ⇒
|
|
27510
|
+
// · 皮肤在台上 + 我方有选中的壁纸 ⇒ **启动不改判**:保持现状(壁纸画、皮肤自己 withhold)。
|
|
27511
|
+
// 这不是"优先级",而是启动那一拍没有"谁最后动"的信息;运行期任一方动作都会立刻改判
|
|
27512
|
+
// (换皮肤 ⇒ syncSkinYield 的 forced 分支;选壁纸 ⇒ media-prep 的 exitSkinYield)。
|
|
27284
27513
|
// · 皮肤**已不在**台上 + 我方没有壁纸 + 盘上留着让路记忆 ⇒ 启动复位:按记忆放回
|
|
27285
27514
|
// (应用关闭期间皮肤被移除 / 卸载的那条路 —— 没有它,记忆落了盘也无人消费)。
|
|
27286
27515
|
// ⚠️ 认领必须等**宿主设置到位**(`selection.loaded`):本地缓存可能过期(本窗口缓存空、
|
package/lib/routes/skins.js
CHANGED
|
@@ -83,6 +83,23 @@ function siteBase() {
|
|
|
83
83
|
* 判据用**它自己的 catalog 路由**:该路由不需要鉴权(实测 200),404 就是没装。
|
|
84
84
|
*/
|
|
85
85
|
const skinCenterProbe = { at: 0, state: 'unknown', detail: '' };
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 皮肤中心是不是被**用户自己在插件市场里禁用了**。
|
|
89
|
+
*
|
|
90
|
+
* 为什么要单独读:它被禁用时 DSH 的鉴权中间件先于路由回答 401,和"需要会话"长得一模一样,
|
|
91
|
+
* 于是页签只能写出"浏览器侧也被鉴权挡住"这种既对又没用的话(实测被用户当成插件故障)。
|
|
92
|
+
* 市场的 `.dsh-market/state.json` 里有 `disabled` 数组 —— 读它是**只读事实**,比猜准得多。
|
|
93
|
+
*/
|
|
94
|
+
function readMarketDisabled() {
|
|
95
|
+
try {
|
|
96
|
+
const p = join(dshHome(), 'profiles', 'web', '.dsh-market', 'state.json');
|
|
97
|
+
const j = JSON.parse(readFileSync(p, 'utf8'));
|
|
98
|
+
const list = Array.isArray(j && j.disabled) ? j.disabled : [];
|
|
99
|
+
return { known: true, skinCenterDisabled: list.includes('@linxin666/dsh-client-ui-skin-center') };
|
|
100
|
+
} catch { return { known: false, skinCenterDisabled: false }; }
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
async function probeSkinCenter(hostHeader, force) {
|
|
87
104
|
const now = Date.now();
|
|
88
105
|
if (!force && skinCenterProbe.at && now - skinCenterProbe.at < 30000) return skinCenterProbe.state;
|
|
@@ -144,11 +161,26 @@ async function fetchSiteSkins(force) {
|
|
|
144
161
|
mime: String(it.mime || ''),
|
|
145
162
|
url: String(it.url || ''),
|
|
146
163
|
thumbUrl: it.thumbUrl ? String(it.thumbUrl) : '',
|
|
147
|
-
|
|
164
|
+
// 站点现在把 tags 直接给**数组**(旧版给的是字符串化 JSON)。两种都收:
|
|
165
|
+
// 只认字符串会让「动态」这类标记静默丢失(实测:线上返回 ["皮肤","catppuccin",…])。
|
|
166
|
+
tags: (() => {
|
|
167
|
+
const raw = it.tags;
|
|
168
|
+
if (Array.isArray(raw)) return raw.map((t) => String(t));
|
|
169
|
+
try { const parsed = JSON.parse(raw || '[]'); return Array.isArray(parsed) ? parsed.map((t) => String(t)) : []; } catch { return []; }
|
|
170
|
+
})(),
|
|
148
171
|
}));
|
|
149
172
|
siteCache.error = null;
|
|
150
173
|
} catch (e) {
|
|
151
|
-
|
|
174
|
+
// 把站点的**真实原因**带出来:站点整站上锁时只回 401 + `{"error":{"code":"site_locked"}}`,
|
|
175
|
+
// 只显示 "http-401" 会让人以为是插件/网络问题(实测就栽在这条上)。
|
|
176
|
+
const msg = (e && e.message) || 'fetch-failed';
|
|
177
|
+
const status = /^http-(\d+)$/.exec(msg);
|
|
178
|
+
if (status) {
|
|
179
|
+
const code = Number(status[1]);
|
|
180
|
+
siteCache.error = (code === 401 || code === 403) ? 'site-locked' : msg;
|
|
181
|
+
} else {
|
|
182
|
+
siteCache.error = msg;
|
|
183
|
+
}
|
|
152
184
|
siteCache.items = siteCache.items || [];
|
|
153
185
|
}
|
|
154
186
|
siteCache.at = now;
|
|
@@ -320,16 +352,19 @@ export function registerSkinsRoutes(webServer, c) {
|
|
|
320
352
|
installedKeys.add(normSkinId(s.id));
|
|
321
353
|
installedKeys.add(normSkinId(s.dirId));
|
|
322
354
|
}
|
|
355
|
+
const market = readMarketDisabled();
|
|
323
356
|
sendJson(res, 200, {
|
|
324
357
|
ok: true,
|
|
325
358
|
skinCenter,
|
|
359
|
+
// 用户在插件市场里**手动禁用**它 ⇒ 这是我们能给出的最准确的原因(比 401 含糊其辞好)
|
|
360
|
+
skinCenterDisabled: market.skinCenterDisabled,
|
|
361
|
+
skinCenterMarketKnown: market.known,
|
|
326
362
|
skinCenterDetail: skinCenterProbe.detail || '',
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
// 0.4.9+ 要求 rc.3,装了更高版本就会被市场关掉。
|
|
363
|
+
// 版本线索:我们随身携带的是 0.4.12(engines 要求 dsh >= 0.2.1-rc.3)。
|
|
364
|
+
// 低于该内核的机器会被市场 keep off —— 页签据此告诉用户"要么升级桌面端、要么别指望它"。
|
|
330
365
|
skinCenterHint: skinCenter === 'present'
|
|
331
366
|
? ''
|
|
332
|
-
: 'skin-center-0.4.
|
|
367
|
+
: 'skin-center-0.4.12',
|
|
333
368
|
siteBase: siteBase(),
|
|
334
369
|
siteError: site.error,
|
|
335
370
|
installed,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-folia",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.13.1",
|
|
4
4
|
"description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
}
|
|
194
194
|
},
|
|
195
195
|
"dependencies": {
|
|
196
|
-
"@linxin666/dsh-client-ui-skin-center": "0.4.
|
|
196
|
+
"@linxin666/dsh-client-ui-skin-center": "0.4.12",
|
|
197
197
|
"webwallgl": "^3.0.0",
|
|
198
198
|
"x11": "^4.3.1"
|
|
199
199
|
},
|