dsh-folia 4.9.1 → 4.12.2

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 CHANGED
@@ -1,16 +1,23 @@
1
- # dsh-plugin-folia bundle patch — single-row form.
1
+ # dsh-plugin-folia bundle patch.
2
2
  #
3
- # This bundle used to contribute EIGHT host rows (the plugin itself plus seven
4
- # subpackage rows: about-notice / mascot-avatar / media-bridge / fonts / fx /
5
- # parallax / pet-link). Those seven now live in this package under
6
- # `lib/components/` and are mounted from `apply()` in `lib/index.js`, so the row
7
- # set is exactly one row: this bundle.
3
+ # Row set: this bundle + the skin centre it brings along.
8
4
  #
9
- # Why the merge (see lib/index.js "BUNDLE-MERGE"): each subpackage browser half
10
- # used to reach the host half through `require('<main package name>')`, which is
11
- # the most fragile edge in the plugin — it broke every time the main package was
12
- # renamed, and an id mismatch silently blanked the font / effect panels. One
13
- # package means no cross-package module id at all.
5
+ # ── 为什么这里有第二条 row ──────────────────────────────────────────────────
6
+ # 我们不再自己实现皮肤渲染:皮肤包(`skin.json` + `skin.css` + 可选 `hooks.mjs`)由
7
+ # `@linxin666/dsh-client-ui-skin-center` 加载与启用,我们的「助手皮肤」页签只负责把站点 zip
8
+ # 解到它读的那个目录(`$DSH_HOME/skins`)。所以它必须是**装得上的**:本包把它列为
9
+ # `dependencies`,并在下面插一条 row —— 用户装 dsh-folia 就同时得到两件套,不用再单独装。
10
+ #
11
+ # ── 重复 row 为什么不会打起来(实测依据,不是猜测)────────────────────────
12
+ # 1) 宿主半边:它的 `apply` 走 `mountOnce('@linxin666/dsh-client-ui-skin-center', …)`,
13
+ # 以 `globalThis[Symbol.for('dsh-web.mounted-plugins')]` 记账 ⇒ 第二次挂载是空操作,
14
+ # 不会重复注册 webserver 路由。
15
+ # 2) 客户端半边:它的设置分区注册包在 `try { ctx.slots.register(...) } catch { return () => {} }`,
16
+ # 重复注册被吞掉 ⇒ 不会出现两张「皮肤」设置页。
17
+ # 于是「用户已有 dsh-web-all(它也插了一条 row)」与「只用我们」两种情形都成立。
18
+ #
19
+ # 版本:依赖写 `^0.4.8`(本仓实测过的版本线);它若大改 v1alpha1 契约,我们跟着升级即可。
20
+ # 判据:test/verify-skin-center-bundle.mjs 会同时钉住「声明了依赖」与「插了唯一 id 的 row」。
14
21
  #
15
22
  # A patch replaces the targeted row's whole `config`, so only additive `insert`
16
23
  # rows appear here (we never override an existing shipped row).
@@ -18,3 +25,13 @@
18
25
  - insert:
19
26
  - id: folia
20
27
  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
+ - id: skin-center
37
+ name: '@linxin666/dsh-client-ui-skin-center'
package/lib/client.js CHANGED
@@ -780,6 +780,52 @@ window.__ModuleLoader__.load({
780
780
  "…": "…",
781
781
  // 开机动画:已开启但无选片时的提示(4.8.2)
782
782
  "已开启,但还没有选片 —— 下载一条后点「使用」": "Enabled, but no clip is selected yet — download one, then press Use",
783
+
784
+ // ── 助手皮肤页签(4.10.0)────────────────────────────────────────────────
785
+ "助手皮肤": "Assistant skins",
786
+ "🎨 助手皮肤": "🎨 Assistant skins",
787
+ "皮肤包安装到皮肤中心目录;装完到「设置 → 皮肤中心」里点选即可启用。皮肤中心与本插件读同一个目录,无需重启。": "Skin packs are installed into the skin centre's directory; pick one in Settings → Skin centre to enable it. It reads the same folder, so no restart is needed.",
788
+ "本机已装({n})": "Installed ({n})",
789
+ "站点皮肤({n})": "Skins on the site ({n})",
790
+ "还没有安装任何皮肤 —— 从下面的站点清单里挑一个。": "No skins installed yet — pick one from the site list below.",
791
+ "站点上还没有皮肤。": "No skins on the site yet.",
792
+ "含动效钩子": "animated hooks",
793
+ "已安装": "Installed",
794
+ "已安装过:{name}": "Already installed: {name}",
795
+ "已安装:{name} —— 到「设置 → 皮肤中心」点它即可启用": "Installed: {name} — pick it in Settings → Skin centre to enable",
796
+ "卸载中…": "Uninstalling…",
797
+ "动态": "Animated",
798
+ "这个包里没有 skin.json,不是皮肤包": "No skin.json in this archive — not a skin pack",
799
+ "包里有非法路径(已整包拒绝)": "The archive contains an unsafe path (rejected entirely)",
800
+ "包太大了(上限 200 MB)": "Archive is too large (200 MB limit)",
801
+ "下载地址不在白名单里": "The download host is not allow-listed",
802
+ "站点被关闭了(DSH_FOLIA_SITE=off)": "The site is disabled (DSH_FOLIA_SITE=off)",
803
+ "这个文件不是 zip": "This file is not a zip",
804
+
805
+ // ── 助手皮肤:皮肤中心缺失时的引导(4.10.1)──────────────────────────
806
+ "皮肤中心没装:它是**独立插件** @linxin666/dsh-client-ui-skin-center(不在 dsh-web-all 里,那个只是聚合包)。装它才能启用皮肤:": "The skin centre is not installed: it is its own plugin, @linxin666/dsh-client-ui-skin-center (dsh-web-all is only an aggregate). Install it to enable skins:",
807
+ "已复制安装命令": "Install command copied",
808
+ "复制失败,请手动选中上面的命令": "Copy failed — select the command above manually",
809
+ "复制命令": "Copy command",
810
+ "先装皮肤中心,否则皮肤装好也没法启用": "Install the skin centre first, or the skin cannot be enabled",
811
+
812
+ // ── 助手皮肤:页签内直接切换(4.12.0)────────────────────────────────
813
+ " · 使用中:{name}": " · active: {name}",
814
+ "✓ ": "✓ ",
815
+ "正在恢复…": "Restoring…",
816
+ "恢复默认外观": "Restore default look",
817
+ "应用": "Apply",
818
+ "切换中…": "Switching…",
819
+ "使用中": "Active",
820
+ "正在使用,先切到别的再卸": "Currently active — switch to another skin before removing",
821
+ "已切换到:{name}": "Switched to: {name}",
822
+ "已恢复官方默认外观": "Restored the default look",
823
+ "这个皮肤不在本机(先安装)": "This skin is not installed locally — install it first",
824
+ "装皮肤中心后才能切换皮肤(见下面的安装命令)": "The skin centre is required to switch skins (install command below)",
825
+ "皮肤 id 不合法": "Invalid skin id",
826
+ "已安装:{name} —— 点「应用」就能切换": "Installed: {name} — press Apply to switch",
827
+ "本页签可以直接切换皮肤:装完点「应用」即可,无需打开设置页。皮肤包落在皮肤中心目录,两边读同一份。":
828
+ "This tab switches skins directly: install, then press Apply — no need to open the settings page. Packs land in the skin centre's folder and both sides read the same files.",
783
829
  };
784
830
 
785
831
  // ── 内联模块:src/i18n-copy-picker.js —— 英文词表域:壁纸选择器与快捷面板(picker-* · quick-panel · sidebar-right · theme-follow)(构建期注入;勿手改本段)──
@@ -18213,6 +18259,314 @@ window.__ModuleLoader__.load({
18213
18259
 
18214
18260
  // 仅供单测驱动(构建期内联时会剥掉 export,不影响产物)。
18215
18261
 
18262
+ // ── 内联模块:src/panel-tabs-skins.js —— 「助手皮肤」页签:站点皮肤清单/安装/卸载 — 渲染由皮肤中心接管,我们只把 zip 解到 $DSH_HOME/skins(构建期注入;勿手改本段)──
18263
+ /**
18264
+ * panel-tabs-skins.js — 「助手皮肤」页签:站点皮肤清单 / 安装 / 卸载 / 去皮肤中心启用。
18265
+ *
18266
+ * 与「桌宠」页签同构(fetch → busy → note → 卡片列表)。差别在**安装之后谁负责渲染**:
18267
+ * 桌宠由本插件自己画(pet-layer.js),皮肤则由皮肤中心接管 —— 我们只把 zip 解到
18268
+ * `$DSH_HOME/skins/<id>/`,皮肤中心下一次 catalog 扫描就会认领(它的缓存键里带父目录 mtime)。
18269
+ * 所以这个页签的职责边界是:**装/卸 + 告诉你下一步去哪启用**,不重复实现渲染。
18270
+ */
18271
+
18272
+ const SKIN_STYLE = {
18273
+ row: { display: "flex", alignItems: "center", gap: "10px", flexWrap: "wrap" },
18274
+ card: {
18275
+ display: "flex", flexDirection: "column", gap: "6px", padding: "10px",
18276
+ borderRadius: "10px", border: "1px solid rgba(255,255,255,0.14)",
18277
+ background: "rgba(255,255,255,0.04)", width: "188px",
18278
+ },
18279
+ thumb: {
18280
+ width: "100%", aspectRatio: "16 / 9", borderRadius: "6px",
18281
+ backgroundSize: "cover", backgroundPosition: "center",
18282
+ backgroundColor: "rgba(0,0,0,0.25)",
18283
+ },
18284
+ title: { fontSize: "13px", fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" },
18285
+ meta: { fontSize: "11px", opacity: 0.72 },
18286
+ grid: { display: "flex", gap: "10px", flexWrap: "wrap", marginTop: "10px" },
18287
+ badge: {
18288
+ fontSize: "10px", padding: "1px 6px", borderRadius: "4px",
18289
+ background: "rgba(30,90,160,0.55)", alignSelf: "flex-start",
18290
+ },
18291
+ };
18292
+
18293
+ function skinFmtBytes(n) {
18294
+ const v = Number(n) || 0;
18295
+ if (!v) return "";
18296
+ if (v < 1024 * 1024) return Math.round(v / 1024) + " KB";
18297
+ return (v / 1048576).toFixed(1) + " MB";
18298
+ }
18299
+
18300
+ /**
18301
+ * 皮肤 id 的归一键:站点上的 slug 是**全站唯一**的,撞名时上传工具会加 `skin-` 前缀
18302
+ * (实测:blue-throated-bee-eater 同时是桌宠 id ⇒ 站上叫 skin-blue-throated-bee-eater),
18303
+ * 而本机目录名用的是 skin.json 里的原始 id。两边比对必须先归一,否则"已安装"会显示成未安装。
18304
+ */
18305
+ function normSkinId(raw) {
18306
+ const s = String(raw || "").trim().toLowerCase();
18307
+ return s.startsWith("skin-") ? s.slice(5) : s;
18308
+ }
18309
+
18310
+ function SkinsTabPanel() {
18311
+ const [state, setState] = React.useState(null);
18312
+ const [busy, setBusy] = React.useState("");
18313
+ const [note, setNote] = React.useState(null);
18314
+
18315
+ const load = React.useCallback(async (force) => {
18316
+ try {
18317
+ const resp = await apiJson("/skins/list" + (force ? "?refresh=1" : ""), { parse: "always" });
18318
+ const doc = (resp && resp.data) || null;
18319
+ if (resp && resp.ok && doc && doc.ok) {
18320
+ // 读"当前在用哪套皮肤"必须**由浏览器发**,不能走宿主代理:
18321
+ // `/api/skin-center/v2/*` 的读写被 DSH 自己的会话鉴权挡着,宿主 fetch 没有 cookie
18322
+ // ⇒ 代理拿到 401(实测报错「操作失败:skin-center-401」)。
18323
+ // 但**不许裸 fetch**(src/api-client.js 的不变量:业务代码零裸 fetch)——
18324
+ // apiUrl() 对带 scheme 的地址原样放行,而 location.origin 是同源 ⇒ 照旧带会话。
18325
+ if (doc.skinCenter === "present") {
18326
+ try {
18327
+ const a = await apiJson(location.origin + "/api/skin-center/v2/active", { parse: "always" });
18328
+ if (a && a.ok) doc.active = (a.data && a.data.active) || "";
18329
+ } catch { /* 读不到就显示"无使用中" */ }
18330
+ }
18331
+ setState(doc);
18332
+ if (doc.siteError && doc.siteError !== "site-off") {
18333
+ setNote({ ok: false, text: weT("站点清单读不到:{msg}", { msg: String(doc.siteError) }) });
18334
+ }
18335
+ } else {
18336
+ setNote({ ok: false, text: weT("读取失败:{msg}", { msg: (doc && doc.error) || "unknown" }) });
18337
+ }
18338
+ } catch (e) {
18339
+ setNote({ ok: false, text: weT("请求失败:{msg}", { msg: (e && e.message) || weT("网络错误") }) });
18340
+ }
18341
+ }, []);
18342
+
18343
+ React.useEffect(() => { load(false); }, [load]);
18344
+
18345
+ const act = async (kind, payload, label) => {
18346
+ // 结构性操作才锁整块 UI;`activate` 是幂等的、而且要让人能连点换皮肤,所以不锁。
18347
+ const structural = kind === "install" || kind === "remove";
18348
+ if (structural) setBusy(kind + ":" + ((payload && payload.id) || ""));
18349
+ if (kind === "activate") setBusy("activate:" + ((payload && payload.id) || ""));
18350
+ setNote(null);
18351
+ try {
18352
+ // 切换皮肤**由浏览器直接写**皮肤中心自己的端点(同源请求自带会话);
18353
+ // 走宿主代理会 401 —— DSH 对 `/api/skin-center/v2/*` 的读写都做会话鉴权,宿主 fetch 没有 cookie。
18354
+ if (kind === "activate") {
18355
+ const wantId = (payload && payload.id) || null;
18356
+ // 只允许切到本机已装的那几套(state.installed 是权威清单)
18357
+ if (wantId) {
18358
+ const known = ((state && state.installed) || []).some((s) => normSkinId(s.id) === normSkinId(wantId));
18359
+ if (!known) {
18360
+ setNote({ ok: false, text: weT("这个皮肤不在本机(先安装)") });
18361
+ return;
18362
+ }
18363
+ }
18364
+ // 切换皮肤**由浏览器直接写**皮肤中心自己的端点(同源、自带会话);
18365
+ // 走宿主代理会 401 —— DSH 对 `/api/skin-center/v2/*` 的读写都做会话鉴权,宿主 fetch 没有 cookie。
18366
+ // 统一走 api-client(不裸 fetch):绝对 URL 原样放行,`location.origin` 是同源 ⇒ 会话照旧带上。
18367
+ const resp = await apiPostJson(location.origin + "/api/skin-center/v2/active", { active: wantId }, { parse: "always" });
18368
+ if (!(resp && resp.ok)) {
18369
+ setNote({ ok: false, text: weT("操作失败:{code}", { code: "skin-center-" + ((resp && resp.status) || 0) }) });
18370
+ return;
18371
+ }
18372
+ // 两条收敛路径:① 广播皮肤中心自己监听的 `dsh-skin-applied`(立即就地切换);
18373
+ // ② 它另外每 2 秒轮询 /api/skin-center/v2/active(事件丢了也会在 2 秒内跟上)。
18374
+ try {
18375
+ window.dispatchEvent(new CustomEvent("dsh-skin-applied", { detail: { id: wantId } }));
18376
+ } catch { /* 老环境没有 CustomEvent 也无所谓,②会兜住 */ }
18377
+ setNote({
18378
+ ok: true,
18379
+ text: wantId ? weT("已切换到:{name}", { name: label || "" }) : weT("已恢复官方默认外观"),
18380
+ });
18381
+ await load(true);
18382
+ return;
18383
+ }
18384
+
18385
+ const resp = await apiPostJson("/skins/" + kind, payload || {}, { parse: "always" });
18386
+ const r = (resp && resp.data) || null;
18387
+ if (resp && resp.ok && r && r.ok) {
18388
+ if (kind === "install") {
18389
+ setNote({
18390
+ ok: true,
18391
+ text: r.exists
18392
+ ? weT("已安装过:{name}", { name: label || "" })
18393
+ : weT("已安装:{name} —— 点「应用」就能切换", { name: label || "" }),
18394
+ });
18395
+ }
18396
+ if (kind === "remove") setNote({ ok: true, text: weT("已卸载:{name}", { name: label || "" }) });
18397
+ await load(true);
18398
+ } else {
18399
+ const code = (r && r.error) || "failed";
18400
+ const map = {
18401
+ 'no-skin-json': weT("这个包里没有 skin.json,不是皮肤包"),
18402
+ 'unsafe-entry': weT("包里有非法路径(已整包拒绝)"),
18403
+ 'too-large': weT("包太大了(上限 200 MB)"),
18404
+ 'host-not-allowed': weT("下载地址不在白名单里"),
18405
+ 'site-off': weT("站点被关闭了(DSH_FOLIA_SITE=off)"),
18406
+ 'not-a-zip': weT("这个文件不是 zip"),
18407
+ 'not-installed': weT("这个皮肤不在本机(先安装)"),
18408
+ 'skin-center-missing': weT("装皮肤中心后才能切换皮肤(见下面的安装命令)"),
18409
+ 'bad-id': weT("皮肤 id 不合法"),
18410
+ };
18411
+ setNote({ ok: false, text: map[code] || weT("操作失败:{code}", { code }) });
18412
+ }
18413
+ } catch (e) {
18414
+ setNote({ ok: false, text: weT("请求失败:{msg}", { msg: (e && e.message) || weT("网络错误") }) });
18415
+ } finally {
18416
+ setBusy("");
18417
+ }
18418
+ };
18419
+
18420
+ const children = [];
18421
+ children.push(React.createElement("div", { key: "head", style: SKIN_STYLE.row },
18422
+ React.createElement("div", { style: { fontSize: "13px", fontWeight: 600 } }, weT("🎨 助手皮肤")),
18423
+ React.createElement("button", {
18424
+ className: "we-picker__btn", type: "button",
18425
+ disabled: Boolean(busy),
18426
+ onClick: () => load(true),
18427
+ }, weT("刷新")),
18428
+ ));
18429
+
18430
+ // 皮肤中心在不在,决定这段提示说什么:
18431
+ // 在 → "装完去皮肤中心点它"(本页签只负责落盘)
18432
+ // 不在 → 明说它是个**独立插件**(不在 dsh-web-all 里),给一条可复制的安装命令
18433
+ // unknown(探测失败)→ 保守地按"在"讲,不吓唬用户
18434
+ const skinCenter = (state && state.skinCenter) || "unknown";
18435
+ if (skinCenter === "absent") {
18436
+ children.push(React.createElement("div", {
18437
+ key: "hint-absent",
18438
+ className: "we-picker__hint",
18439
+ style: { margin: "6px 0 4px", color: "#ffd08a", lineHeight: "1.6" },
18440
+ }, weT("皮肤中心没装:它是**独立插件** @linxin666/dsh-client-ui-skin-center(不在 dsh-web-all 里,那个只是聚合包)。装它才能启用皮肤:")));
18441
+ children.push(React.createElement("div", { key: "cmd", style: { display: "flex", gap: "8px", alignItems: "center", margin: "0 0 6px" } },
18442
+ React.createElement("code", {
18443
+ style: { fontSize: "11px", padding: "3px 8px", borderRadius: "6px", background: "rgba(255,255,255,0.08)" },
18444
+ }, "dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center"),
18445
+ React.createElement("button", {
18446
+ className: "we-picker__btn", type: "button",
18447
+ onClick: () => { try { navigator.clipboard.writeText("dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center"); setNote({ ok: true, text: weT("已复制安装命令") }); } catch { setNote({ ok: false, text: weT("复制失败,请手动选中上面的命令") }); } },
18448
+ }, weT("复制命令")),
18449
+ ));
18450
+ } else {
18451
+ children.push(React.createElement("div", {
18452
+ key: "hint",
18453
+ className: "we-picker__hint",
18454
+ style: { margin: "6px 0 4px", opacity: 0.8 },
18455
+ }, weT("本页签可以直接切换皮肤:装完点「应用」即可,无需打开设置页。皮肤包落在皮肤中心目录,两边读同一份。")));
18456
+ }
18457
+
18458
+ // 提示行常驻占位(与开机动画/桌宠页签同一条纪律:不因 note 出现/消失让面板跳高度)
18459
+ children.push(React.createElement("div", {
18460
+ key: "note",
18461
+ className: "we-picker__hint",
18462
+ style: {
18463
+ color: note && !note.ok ? "#ff9a9a" : "#8ee6a8",
18464
+ margin: "2px 0 8px", minHeight: "18px", lineHeight: "18px",
18465
+ opacity: note ? 1 : 0, transition: "opacity 120ms linear",
18466
+ },
18467
+ }, note ? note.text : "\u00a0"));
18468
+
18469
+ const installed = (state && state.installed) || [];
18470
+ const activeId = (state && state.active) || "";
18471
+ /** 这个 id 是不是"正在用"的那套(两边都归一后比,站点 slug 可能带 skin- 前缀)。 */
18472
+ const isActiveSkin = (id) => Boolean(activeId) && normSkinId(activeId) === normSkinId(id);
18473
+ if (state) {
18474
+ children.push(React.createElement("div", { key: "installed-head", className: "we-picker__hint" },
18475
+ weT("本机已装({n})", { n: installed.length }),
18476
+ activeId ? weT(" · 使用中:{name}", { name: activeId }) : ""));
18477
+ // 「恢复默认」:清空选择、回到官方外观。只有确实有皮肤在用的时候才有意义。
18478
+ if (activeId) {
18479
+ children.push(React.createElement("button", {
18480
+ key: "clear",
18481
+ className: "we-picker__btn", type: "button",
18482
+ style: { alignSelf: "flex-start", margin: "0 0 6px" },
18483
+ disabled: Boolean(busy),
18484
+ onClick: () => act("activate", { id: "" }, ""),
18485
+ }, busy === "activate:" ? weT("正在恢复…") : weT("恢复默认外观")));
18486
+ }
18487
+ children.push(React.createElement("div", { key: "installed", style: SKIN_STYLE.grid },
18488
+ installed.length === 0
18489
+ ? [React.createElement("span", { key: "none", className: "we-picker__hint" }, weT("还没有安装任何皮肤 —— 从下面的站点清单里挑一个。"))]
18490
+ : installed.map((s) => {
18491
+ const isActive = isActiveSkin(s.id);
18492
+ const busyThis = busy === "activate:" + s.id;
18493
+ return React.createElement("div", {
18494
+ key: s.id,
18495
+ style: isActive ? { ...SKIN_STYLE.card, borderColor: "rgba(120,220,150,0.75)" } : SKIN_STYLE.card,
18496
+ },
18497
+ React.createElement("div", { style: { ...SKIN_STYLE.title, color: s.accent || undefined } },
18498
+ (isActive ? weT("✓ ") : "") + (s.name || s.id)),
18499
+ React.createElement("div", { style: SKIN_STYLE.meta },
18500
+ [s.version ? "v" + s.version : "", skinFmtBytes(s.bytes), s.hasHooks ? weT("含动效钩子") : ""]
18501
+ .filter(Boolean).join(" · ")),
18502
+ React.createElement("div", { style: { display: "flex", gap: "6px", alignItems: "center" } },
18503
+ // 「应用」= 写皮肤中心的 active + 广播 `dsh-skin-applied`,就地切换(不刷新页面)。
18504
+ isActive
18505
+ ? React.createElement("span", { style: { ...SKIN_STYLE.badge, background: "rgba(60,160,90,0.55)" } }, weT("使用中"))
18506
+ : React.createElement("button", {
18507
+ className: "we-picker__btn", type: "button",
18508
+ disabled: Boolean(busy),
18509
+ onClick: () => act("activate", { id: s.id }, s.name),
18510
+ }, busyThis ? weT("切换中…") : weT("应用")),
18511
+ React.createElement("button", {
18512
+ className: "we-picker__btn", type: "button",
18513
+ disabled: Boolean(busy) || isActive,
18514
+ title: isActive ? weT("正在使用,先切到别的再卸") : undefined,
18515
+ onClick: () => act("remove", { id: s.id }, s.name),
18516
+ }, busy === "remove:" + s.id ? weT("卸载中…") : weT("卸载")),
18517
+ ),
18518
+ );
18519
+ }),
18520
+ ));
18521
+ }
18522
+
18523
+ const site = (state && state.site) || [];
18524
+ children.push(React.createElement("div", { key: "site-head", className: "we-picker__hint", style: { marginTop: "10px" } },
18525
+ weT("站点皮肤({n})", { n: site.length })));
18526
+ children.push(React.createElement("div", { key: "site", style: SKIN_STYLE.grid },
18527
+ site.length === 0
18528
+ ? [React.createElement("span", { key: "none", className: "we-picker__hint" }, weT("站点上还没有皮肤。"))]
18529
+ : site.map((s) => {
18530
+ const busyThis = busy === "install:" + s.id;
18531
+ return React.createElement("div", { key: s.id, style: SKIN_STYLE.card },
18532
+ s.thumbUrl
18533
+ ? React.createElement("div", {
18534
+ style: { ...SKIN_STYLE.thumb, backgroundImage: "url(" + JSON.stringify(s.thumbUrl) + ")" },
18535
+ })
18536
+ : React.createElement("div", { style: SKIN_STYLE.thumb }),
18537
+ React.createElement("div", { style: SKIN_STYLE.title, title: s.title }, s.title),
18538
+ React.createElement("div", { style: SKIN_STYLE.meta },
18539
+ [skinFmtBytes(s.bytes), (s.tags || []).includes("动态") ? weT("动态") : ""].filter(Boolean).join(" · ")),
18540
+ React.createElement("div", { style: { display: "flex", gap: "6px", alignItems: "center", flexWrap: "wrap" } },
18541
+ s.installed
18542
+ // 已装的站点卡片也要能**直接应用**:用户是照着站点缩略图挑的,
18543
+ // 让他再滚回上面的「本机已装」区去点一次是多余的一步(实测反馈)。
18544
+ // 站点 slug 与本机目录名可能差一个 `skin-` 前缀 ⇒ 用归一后的比较。
18545
+ ? [
18546
+ isActiveSkin(s.id)
18547
+ ? React.createElement("span", { key: "on", style: { ...SKIN_STYLE.badge, background: "rgba(60,160,90,0.55)" } }, weT("使用中"))
18548
+ : React.createElement("button", {
18549
+ key: "apply",
18550
+ className: "we-picker__btn", type: "button",
18551
+ disabled: Boolean(busy),
18552
+ onClick: () => act("activate", { id: normSkinId(s.id) }, s.title),
18553
+ }, busy === "activate:" + normSkinId(s.id) ? weT("切换中…") : weT("应用")),
18554
+ React.createElement("span", { key: "done", style: SKIN_STYLE.badge }, weT("已安装")),
18555
+ ]
18556
+ : React.createElement("button", {
18557
+ className: "we-picker__btn", type: "button",
18558
+ disabled: Boolean(busy),
18559
+ title: skinCenter === "absent" ? weT("先装皮肤中心,否则皮肤装好也没法启用") : undefined,
18560
+ onClick: () => act("install", { id: s.id, url: s.url }, s.title),
18561
+ }, busyThis ? weT("安装中…") : weT("安装")),
18562
+ ),
18563
+ );
18564
+ }),
18565
+ ));
18566
+
18567
+ return React.createElement("div", { className: "we-picker", style: { display: "flex", flexDirection: "column" } }, children);
18568
+ }
18569
+
18216
18570
  // ── 内联模块:src/features.js —— 客户端功能注册表:页签条 / 整区面板 / 常驻挂载层。装配点(quick-panel、client.js)只查表,不写功能名(构建期注入;勿手改本段)──
18217
18571
  /**
18218
18572
  * features.js(客户端)— **功能注册表**:页签声明、面板工厂、挂载钩子。
@@ -18241,6 +18595,8 @@ window.__ModuleLoader__.load({
18241
18595
  { id: "playback", get label() { return weT("播放"); } },
18242
18596
  // 桌宠是"桌面上多出来的一只东西",与壁纸/开机动画并列。
18243
18597
  { id: "pets", get label() { return weT("桌宠"); } },
18598
+ // 助手皮肤:装的是皮肤中心的皮肤包。我们只管装/卸,渲染与启用归皮肤中心。
18599
+ { id: "skins", get label() { return weT("助手皮肤"); } },
18244
18600
  ];
18245
18601
 
18246
18602
  /**
@@ -18250,6 +18606,7 @@ window.__ModuleLoader__.load({
18250
18606
  const WE_FEATURE_PANELS = {
18251
18607
  boot: BootTabPanel,
18252
18608
  pets: PetsTabPanel,
18609
+ skins: SkinsTabPanel,
18253
18610
  };
18254
18611
 
18255
18612
  /**
package/lib/features.js CHANGED
@@ -126,12 +126,15 @@ export const FEATURES = [
126
126
  host: [
127
127
  'lib/routes/settings.js',
128
128
  'lib/routes/presets.js',
129
+ // 助手皮肤:站点皮肤包 → $DSH_HOME/skins(皮肤中心读同一目录,写盘即生效)。
130
+ 'lib/routes/skins.js',
129
131
  'lib/settings-schema.js',
130
132
  'lib/components/fonts/routes/fontsets.js',
131
133
  'lib/components/fonts/routes/system-fonts.js',
132
134
  ],
133
135
  client: [
134
136
  'src/panel-tabs-appearance.js',
137
+ 'src/panel-tabs-skins.js',
135
138
  'src/glass.js',
136
139
  'src/glass-panel.js',
137
140
  'src/glass-surfaces.js',
@@ -151,7 +154,7 @@ export const FEATURES = [
151
154
  'src/about-assets.js',
152
155
  'src/nav-icon.js',
153
156
  ],
154
- tab: ['appearance'],
157
+ tab: ['appearance', 'skins'],
155
158
  },
156
159
  {
157
160
  id: 'playback',
@@ -19,6 +19,7 @@ import { registerInventoryRoutes, warmInventorySiteCache } from './routes/invent
19
19
  import { registerModulesRoutes, warmModulesCache } from './routes/modules.js';
20
20
  import { registerBootRoutes } from './routes/boot.js';
21
21
  import { registerSiteThumbRoutes } from './routes/site-thumb.js';
22
+ import { registerSkinsRoutes } from './routes/skins.js';
22
23
  import { registerPetsRoutes } from './routes/pets.js';
23
24
 
24
25
  /**
@@ -71,6 +72,14 @@ export const FEATURE_ROUTE_FAMILIES = [
71
72
  register: registerPetsRoutes,
72
73
  deps: (c) => ({ disposers: c.disposers, base: c.base, log: c.log }),
73
74
  },
75
+ {
76
+ id: 'skins',
77
+ file: 'lib/routes/skins.js',
78
+ register: registerSkinsRoutes,
79
+ // 皮肤安装目标是 $DSH_HOME/skins(皮肤中心读同一个目录):由路由自己按 DSH_HOME 解析,
80
+ // 不额外引入注入面 —— 少一处注入,就少一种"改了目录却没生效"的可能。
81
+ deps: (c) => ({ disposers: c.disposers, base: c.base, log: c.log }),
82
+ },
74
83
  ];
75
84
 
76
85
  /**