dsh-folia 4.9.0 → 4.10.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 CHANGED
@@ -781,16 +781,26 @@ window.__ModuleLoader__.load({
781
781
  // 开机动画:已开启但无选片时的提示(4.8.2)
782
782
  "已开启,但还没有选片 —— 下载一条后点「使用」": "Enabled, but no clip is selected yet — download one, then press Use",
783
783
 
784
- // ── 站点工程包(zip):点击即解码的文案(4.9.0)──────────────────────────
785
- "正在解码工程包…(首次会下载,之后立即生效)": "Decoding the project package… (downloads on first use, instant afterwards)",
786
- "这个工程包是可执行程序(application),不能当壁纸": "This package is an application, not a wallpaper",
787
- "解码失败:{code}": "Decode failed: {code}",
788
- "已就绪:{name}": "Ready: {name}",
789
- "已解码,但清单里还没出现 —— 点「刷新」重试": "Decoded, but it has not shown up yet — press Refresh",
790
- "{n} 个站点工程包 · 点一下就能当背景(首次会下载)": "{n} site project packages · one click and it becomes your background (downloads on first use)",
791
- "点击解码并应用(首次需下载)": "Click to decode and apply (downloads the first time)",
792
- "解码中…": "Decoding…",
793
- "工程包 · 点击解码": "Project package · click to decode",
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",
794
804
  };
795
805
 
796
806
  // ── 内联模块:src/i18n-copy-picker.js —— 英文词表域:壁纸选择器与快捷面板(picker-* · quick-panel · sidebar-right · theme-follow)(构建期注入;勿手改本段)──
@@ -4632,41 +4642,6 @@ window.__ModuleLoader__.load({
4632
4642
  /* Scene-wallpaper "实时帧" badge — top-right under the hide button.
4633
4643
  作用域钉在**缩略图卡片**里:卡头的可播放计数徽标(.we-picker__card-head 下)
4634
4644
  同名,不能被这条 absolute 角标规则盖掉(那一枚走 .we-picker__card-badge 的基样式,行内计数)。 */
4635
- /* ── 站点工程包条带(.we-picker__pkg-*)────────────────────────────────────
4636
- 为什么单列一条:工程包(zip)不能进 <video>,点它要先解码一次。
4637
- 形态上刻意与网格卡片区分(横向小卡 + 明确的"点击解码"徽标),
4638
- 避免用户把它当成"点了立刻切的壁纸"而误以为失败。 */
4639
- .we-picker__pkg-strip { margin: 6px 0 10px; }
4640
- .we-picker__pkg-row {
4641
- display: flex; gap: 8px; overflow-x: auto; padding: 2px 0 6px;
4642
- scrollbar-width: thin;
4643
- }
4644
- .we-picker__pkg {
4645
- position: relative; flex: 0 0 auto;
4646
- width: 168px; padding: 6px;
4647
- display: flex; flex-direction: column; gap: 4px;
4648
- border-radius: 8px; cursor: pointer;
4649
- background: rgba(255, 255, 255, 0.06);
4650
- border: 1px solid rgba(255, 255, 255, 0.12);
4651
- transition: background 120ms linear, border-color 120ms linear;
4652
- }
4653
- .we-picker__pkg:hover { background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.24); }
4654
- .we-picker__pkg.is-busy { opacity: 0.7; cursor: progress; }
4655
- .we-picker__pkg-thumb {
4656
- display: block; width: 100%; aspect-ratio: 16 / 9;
4657
- border-radius: 6px; background-size: cover; background-position: center;
4658
- background-color: rgba(0, 0, 0, 0.25);
4659
- }
4660
- .we-picker__pkg-title {
4661
- font-size: 0.72em; line-height: 1.35; max-height: 2.7em;
4662
- overflow: hidden; text-overflow: ellipsis;
4663
- }
4664
- .we-picker__pkg-badge {
4665
- font-size: 0.66em; opacity: 0.85;
4666
- padding: 1px 6px; border-radius: 4px;
4667
- background: rgba(30, 90, 160, 0.55);
4668
- align-self: flex-start;
4669
- }
4670
4645
  .we-picker__card .we-picker__card-badge {
4671
4646
  position: absolute; top: 4px; right: 4px; z-index: 1;
4672
4647
  padding: 1px 6px; font-size: 0.62em; line-height: 1.6;
@@ -16483,21 +16458,6 @@ window.__ModuleLoader__.load({
16483
16458
  );
16484
16459
  }
16485
16460
 
16486
- /**
16487
- * **可安装的工程包**(站点上的 WE 工程包 zip):`type === "package"`。
16488
- *
16489
- * 为什么不并进 `playableList`:网格的语义是"点一下就切过去",而工程包要先解码一次
16490
- * (下载 + 解包 + 登记成 WE 工程目录)才能播。混进网格会让轮播候选与"可播放"计数都失真。
16491
- * 所以它自己一档,由选择器在网格上方单独渲染一条条带,点击走 `onPickCard` 的解码分支。
16492
- */
16493
- function packageWallpapers(wallpapers, ratingFilter, typeFilter, hiddenIds, sourceFilter) {
16494
- return wallpapers.filter((w) => w && w.type === "package"
16495
- && !isHiddenWallpaper(w.id, hiddenIds)
16496
- && matchesRatingFilter(w, ratingFilter)
16497
- && matchesSourceFilter(w, sourceFilter)
16498
- && (typeFilter === "all" || typeFilter === "package" || typeFilter === "video"));
16499
- }
16500
-
16501
16461
  /** 已隐藏列表(保持库存顺序)。 */
16502
16462
  function hiddenWallpapers(wallpapers, hiddenIds) {
16503
16463
  return wallpapers.filter((w) => isHiddenWallpaper(w.id, hiddenIds));
@@ -16530,9 +16490,6 @@ window.__ModuleLoader__.load({
16530
16490
  typeCounts[w.type] = (typeCounts[w.type] || 0) + 1;
16531
16491
  }
16532
16492
  const editorList = playableWallpapers(list, ratingFilter, typeFilter, hiddenIds, sourceFilter);
16533
- // 工程包条带(见 packageWallpapers 的注释):只受分级/来源过滤,不受"可播放"闸门影响。
16534
- const packageList = packageWallpapers(list, ratingFilter, typeFilter, hiddenIds, sourceFilter)
16535
- .filter((w) => !query || String(w.title || "").toLowerCase().indexOf(query) !== -1);
16536
16493
  const hiddenList = hiddenWallpapers(list, hiddenIds);
16537
16494
  return {
16538
16495
  query,
@@ -16541,8 +16498,6 @@ window.__ModuleLoader__.load({
16541
16498
  ratingCounts,
16542
16499
  typeCounts,
16543
16500
  editorList,
16544
- packageList,
16545
- packageCount: packageList.length,
16546
16501
  hiddenList,
16547
16502
  sourceCounts,
16548
16503
  normalPage: pageSlice(playableList, input.page),
@@ -16799,42 +16754,6 @@ window.__ModuleLoader__.load({
16799
16754
  );
16800
16755
  }
16801
16756
 
16802
- /**
16803
- * 站点工程包条带(`type === "package"`):它们不能被 <video> 播,点一下要先解码。
16804
- *
16805
- * 为什么不放进网格:网格语义是"点一下就切过去",包里没有可直接播的媒体;混进去会让
16806
- * "可播放 N 张"的计数与轮播候选都失真。所以独立一条,文案也直说"会下载一次"。
16807
- */
16808
- function renderPackageStrip(ctx) {
16809
- const { packageList, packageInstalling, packageNote, sel } = ctx;
16810
- if (!packageList || packageList.length === 0) return null;
16811
- const cards = packageList.map((w) => React.createElement("div", {
16812
- key: "pkg-" + w.id,
16813
- role: "button",
16814
- tabIndex: 0,
16815
- className: "we-picker__pkg" + (packageInstalling === w.id ? " is-busy" : ""),
16816
- title: weT("点击解码并应用(首次需下载)"),
16817
- onClick: () => ctx.onPickCard(w),
16818
- onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); ctx.onPickCard(w); } },
16819
- },
16820
- w.preview ? React.createElement("span", {
16821
- className: "we-picker__pkg-thumb",
16822
- style: { backgroundImage: "url(" + JSON.stringify(w.preview) + ")" },
16823
- }) : null,
16824
- React.createElement("span", { className: "we-picker__pkg-title" }, String(w.title || "")),
16825
- React.createElement("span", { className: "we-picker__pkg-badge" },
16826
- packageInstalling === w.id ? weT("解码中…") : weT("工程包 · 点击解码")),
16827
- ));
16828
- return React.createElement("div", { className: "we-picker__pkg-strip" },
16829
- React.createElement("div", { className: "we-picker__row" },
16830
- React.createElement("span", { className: "we-picker__hint" },
16831
- weT("{n} 个站点工程包 · 点一下就能当背景(首次会下载)", { n: packageList.length })),
16832
- ),
16833
- React.createElement("div", { className: "we-picker__pkg-row" }, cards),
16834
- packageNote ? React.createElement("span", { className: "we-picker__hint" }, packageNote) : null,
16835
- );
16836
- }
16837
-
16838
16757
  function renderPickerNormalBody(ctx, draft, draftIdSet, normalWin) {
16839
16758
  const { sel, playableList, onToggleBatchMode, armedConfirm, onBatchHide, onDisarmConfirm, cdMode, gridRef, query } = ctx;
16840
16759
  return React.createElement("div", { className: "we-picker__modal-body" },
@@ -16860,8 +16779,6 @@ window.__ModuleLoader__.load({
16860
16779
  weT("隐藏选中的 {n} 张壁纸?可在「已隐藏」中随时恢复。", { n: sel.batchSelected.length }),
16861
16780
  onBatchHide, onDisarmConfirm),
16862
16781
  renderPickerFilterRow(ctx),
16863
- // 工程包条带:在网格之上、过滤行之下 —— 它属于"从这里挑",但不参与网格的虚拟化计数。
16864
- renderPackageStrip(ctx),
16865
16782
  React.createElement("div", { ref: cdMode ? undefined : gridRef, className: "we-picker__grid" },
16866
16783
  // "Close wallpaper" card — equivalent of the first <option> in the
16867
16784
  // select-based UI. Rendered as a <div role="button"> like every other card:
@@ -18317,6 +18234,186 @@ window.__ModuleLoader__.load({
18317
18234
 
18318
18235
  // 仅供单测驱动(构建期内联时会剥掉 export,不影响产物)。
18319
18236
 
18237
+ // ── 内联模块:src/panel-tabs-skins.js —— 「助手皮肤」页签:站点皮肤清单/安装/卸载 — 渲染由皮肤中心接管,我们只把 zip 解到 $DSH_HOME/skins(构建期注入;勿手改本段)──
18238
+ /**
18239
+ * panel-tabs-skins.js — 「助手皮肤」页签:站点皮肤清单 / 安装 / 卸载 / 去皮肤中心启用。
18240
+ *
18241
+ * 与「桌宠」页签同构(fetch → busy → note → 卡片列表)。差别在**安装之后谁负责渲染**:
18242
+ * 桌宠由本插件自己画(pet-layer.js),皮肤则由皮肤中心接管 —— 我们只把 zip 解到
18243
+ * `$DSH_HOME/skins/<id>/`,皮肤中心下一次 catalog 扫描就会认领(它的缓存键里带父目录 mtime)。
18244
+ * 所以这个页签的职责边界是:**装/卸 + 告诉你下一步去哪启用**,不重复实现渲染。
18245
+ */
18246
+
18247
+ const SKIN_STYLE = {
18248
+ row: { display: "flex", alignItems: "center", gap: "10px", flexWrap: "wrap" },
18249
+ card: {
18250
+ display: "flex", flexDirection: "column", gap: "6px", padding: "10px",
18251
+ borderRadius: "10px", border: "1px solid rgba(255,255,255,0.14)",
18252
+ background: "rgba(255,255,255,0.04)", width: "188px",
18253
+ },
18254
+ thumb: {
18255
+ width: "100%", aspectRatio: "16 / 9", borderRadius: "6px",
18256
+ backgroundSize: "cover", backgroundPosition: "center",
18257
+ backgroundColor: "rgba(0,0,0,0.25)",
18258
+ },
18259
+ title: { fontSize: "13px", fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" },
18260
+ meta: { fontSize: "11px", opacity: 0.72 },
18261
+ grid: { display: "flex", gap: "10px", flexWrap: "wrap", marginTop: "10px" },
18262
+ badge: {
18263
+ fontSize: "10px", padding: "1px 6px", borderRadius: "4px",
18264
+ background: "rgba(30,90,160,0.55)", alignSelf: "flex-start",
18265
+ },
18266
+ };
18267
+
18268
+ function skinFmtBytes(n) {
18269
+ const v = Number(n) || 0;
18270
+ if (!v) return "";
18271
+ if (v < 1024 * 1024) return Math.round(v / 1024) + " KB";
18272
+ return (v / 1048576).toFixed(1) + " MB";
18273
+ }
18274
+
18275
+ function SkinsTabPanel() {
18276
+ const [state, setState] = React.useState(null);
18277
+ const [busy, setBusy] = React.useState("");
18278
+ const [note, setNote] = React.useState(null);
18279
+
18280
+ const load = React.useCallback(async (force) => {
18281
+ try {
18282
+ const resp = await apiJson("/skins/list" + (force ? "?refresh=1" : ""), { parse: "always" });
18283
+ const doc = (resp && resp.data) || null;
18284
+ if (resp && resp.ok && doc && doc.ok) {
18285
+ setState(doc);
18286
+ if (doc.siteError && doc.siteError !== "site-off") {
18287
+ setNote({ ok: false, text: weT("站点清单读不到:{msg}", { msg: String(doc.siteError) }) });
18288
+ }
18289
+ } else {
18290
+ setNote({ ok: false, text: weT("读取失败:{msg}", { msg: (doc && doc.error) || "unknown" }) });
18291
+ }
18292
+ } catch (e) {
18293
+ setNote({ ok: false, text: weT("请求失败:{msg}", { msg: (e && e.message) || weT("网络错误") }) });
18294
+ }
18295
+ }, []);
18296
+
18297
+ React.useEffect(() => { load(false); }, [load]);
18298
+
18299
+ const act = async (kind, payload, label) => {
18300
+ const structural = kind === "install" || kind === "remove";
18301
+ if (structural) setBusy(kind + ":" + ((payload && payload.id) || ""));
18302
+ setNote(null);
18303
+ try {
18304
+ const resp = await apiPostJson("/skins/" + kind, payload || {}, { parse: "always" });
18305
+ const r = (resp && resp.data) || null;
18306
+ if (resp && resp.ok && r && r.ok) {
18307
+ if (kind === "install") {
18308
+ setNote({
18309
+ ok: true,
18310
+ text: r.exists
18311
+ ? weT("已安装过:{name}", { name: label || "" })
18312
+ : weT("已安装:{name} —— 到「设置 → 皮肤中心」点它即可启用", { name: label || "" }),
18313
+ });
18314
+ }
18315
+ if (kind === "remove") setNote({ ok: true, text: weT("已卸载:{name}", { name: label || "" }) });
18316
+ await load(true);
18317
+ } else {
18318
+ const code = (r && r.error) || "failed";
18319
+ const map = {
18320
+ 'no-skin-json': weT("这个包里没有 skin.json,不是皮肤包"),
18321
+ 'unsafe-entry': weT("包里有非法路径(已整包拒绝)"),
18322
+ 'too-large': weT("包太大了(上限 200 MB)"),
18323
+ 'host-not-allowed': weT("下载地址不在白名单里"),
18324
+ 'site-off': weT("站点被关闭了(DSH_FOLIA_SITE=off)"),
18325
+ 'not-a-zip': weT("这个文件不是 zip"),
18326
+ };
18327
+ setNote({ ok: false, text: map[code] || weT("操作失败:{code}", { code }) });
18328
+ }
18329
+ } catch (e) {
18330
+ setNote({ ok: false, text: weT("请求失败:{msg}", { msg: (e && e.message) || weT("网络错误") }) });
18331
+ } finally {
18332
+ setBusy("");
18333
+ }
18334
+ };
18335
+
18336
+ const children = [];
18337
+ children.push(React.createElement("div", { key: "head", style: SKIN_STYLE.row },
18338
+ React.createElement("div", { style: { fontSize: "13px", fontWeight: 600 } }, weT("🎨 助手皮肤")),
18339
+ React.createElement("button", {
18340
+ className: "we-picker__btn", type: "button",
18341
+ disabled: Boolean(busy),
18342
+ onClick: () => load(true),
18343
+ }, weT("刷新")),
18344
+ ));
18345
+
18346
+ children.push(React.createElement("div", {
18347
+ key: "hint",
18348
+ className: "we-picker__hint",
18349
+ style: { margin: "6px 0 4px", opacity: 0.8 },
18350
+ }, weT("皮肤包安装到皮肤中心目录;装完到「设置 → 皮肤中心」里点选即可启用。皮肤中心与本插件读同一个目录,无需重启。")));
18351
+
18352
+ // 提示行常驻占位(与开机动画/桌宠页签同一条纪律:不因 note 出现/消失让面板跳高度)
18353
+ children.push(React.createElement("div", {
18354
+ key: "note",
18355
+ className: "we-picker__hint",
18356
+ style: {
18357
+ color: note && !note.ok ? "#ff9a9a" : "#8ee6a8",
18358
+ margin: "2px 0 8px", minHeight: "18px", lineHeight: "18px",
18359
+ opacity: note ? 1 : 0, transition: "opacity 120ms linear",
18360
+ },
18361
+ }, note ? note.text : "\u00a0"));
18362
+
18363
+ const installed = (state && state.installed) || [];
18364
+ if (state) {
18365
+ children.push(React.createElement("div", { key: "installed-head", className: "we-picker__hint" },
18366
+ weT("本机已装({n})", { n: installed.length })));
18367
+ children.push(React.createElement("div", { key: "installed", style: SKIN_STYLE.grid },
18368
+ installed.length === 0
18369
+ ? [React.createElement("span", { key: "none", className: "we-picker__hint" }, weT("还没有安装任何皮肤 —— 从下面的站点清单里挑一个。"))]
18370
+ : installed.map((s) => React.createElement("div", { key: s.id, style: SKIN_STYLE.card },
18371
+ React.createElement("div", { style: { ...SKIN_STYLE.title, color: s.accent || undefined } }, s.name || s.id),
18372
+ React.createElement("div", { style: SKIN_STYLE.meta },
18373
+ [s.version ? "v" + s.version : "", skinFmtBytes(s.bytes), s.hasHooks ? weT("含动效钩子") : ""]
18374
+ .filter(Boolean).join(" · ")),
18375
+ React.createElement("div", { style: { display: "flex", gap: "6px", alignItems: "center" } },
18376
+ React.createElement("button", {
18377
+ className: "we-picker__btn", type: "button",
18378
+ disabled: Boolean(busy),
18379
+ onClick: () => act("remove", { id: s.id }, s.name),
18380
+ }, busy === "remove:" + s.id ? weT("卸载中…") : weT("卸载")),
18381
+ ),
18382
+ )),
18383
+ ));
18384
+ }
18385
+
18386
+ const site = (state && state.site) || [];
18387
+ children.push(React.createElement("div", { key: "site-head", className: "we-picker__hint", style: { marginTop: "10px" } },
18388
+ weT("站点皮肤({n})", { n: site.length })));
18389
+ children.push(React.createElement("div", { key: "site", style: SKIN_STYLE.grid },
18390
+ site.length === 0
18391
+ ? [React.createElement("span", { key: "none", className: "we-picker__hint" }, weT("站点上还没有皮肤。"))]
18392
+ : site.map((s) => {
18393
+ const busyThis = busy === "install:" + s.id;
18394
+ return React.createElement("div", { key: s.id, style: SKIN_STYLE.card },
18395
+ s.thumbUrl
18396
+ ? React.createElement("div", {
18397
+ style: { ...SKIN_STYLE.thumb, backgroundImage: "url(" + JSON.stringify(s.thumbUrl) + ")" },
18398
+ })
18399
+ : React.createElement("div", { style: SKIN_STYLE.thumb }),
18400
+ React.createElement("div", { style: SKIN_STYLE.title, title: s.title }, s.title),
18401
+ React.createElement("div", { style: SKIN_STYLE.meta },
18402
+ [skinFmtBytes(s.bytes), (s.tags || []).includes("动态") ? weT("动态") : ""].filter(Boolean).join(" · ")),
18403
+ s.installed
18404
+ ? React.createElement("div", { style: SKIN_STYLE.badge }, weT("已安装"))
18405
+ : React.createElement("button", {
18406
+ className: "we-picker__btn", type: "button",
18407
+ disabled: Boolean(busy),
18408
+ onClick: () => act("install", { id: s.id, url: s.url }, s.title),
18409
+ }, busyThis ? weT("安装中…") : weT("安装")),
18410
+ );
18411
+ }),
18412
+ ));
18413
+
18414
+ return React.createElement("div", { className: "we-picker", style: { display: "flex", flexDirection: "column" } }, children);
18415
+ }
18416
+
18320
18417
  // ── 内联模块:src/features.js —— 客户端功能注册表:页签条 / 整区面板 / 常驻挂载层。装配点(quick-panel、client.js)只查表,不写功能名(构建期注入;勿手改本段)──
18321
18418
  /**
18322
18419
  * features.js(客户端)— **功能注册表**:页签声明、面板工厂、挂载钩子。
@@ -18345,6 +18442,8 @@ window.__ModuleLoader__.load({
18345
18442
  { id: "playback", get label() { return weT("播放"); } },
18346
18443
  // 桌宠是"桌面上多出来的一只东西",与壁纸/开机动画并列。
18347
18444
  { id: "pets", get label() { return weT("桌宠"); } },
18445
+ // 助手皮肤:装的是皮肤中心的皮肤包。我们只管装/卸,渲染与启用归皮肤中心。
18446
+ { id: "skins", get label() { return weT("助手皮肤"); } },
18348
18447
  ];
18349
18448
 
18350
18449
  /**
@@ -18354,6 +18453,7 @@ window.__ModuleLoader__.load({
18354
18453
  const WE_FEATURE_PANELS = {
18355
18454
  boot: BootTabPanel,
18356
18455
  pets: PetsTabPanel,
18456
+ skins: SkinsTabPanel,
18357
18457
  };
18358
18458
 
18359
18459
  /**
@@ -30485,50 +30585,7 @@ window.__ModuleLoader__.load({
30485
30585
  };
30486
30586
  const onBatchCancel = () => { disarmConfirm(); setTransient("batchMode", false); setTransient("batchSelected", []); emit(); };
30487
30587
  const onSearchInput = (e) => { setTransient("search", e.target.value); emit(); };
30488
- /**
30489
- * 把一个站点工程包**解码成本机 WE 工程目录**并立刻应用("点一下就用")。
30490
- *
30491
- * 为什么放在点击里而不是"先下载再点":用户要的是"点这张壁纸就是背景"。
30492
- * 第一次点会多等几秒(下载 + 解包),之后同一条目已在盘上 ⇒ 宿主直接 reused,几乎瞬时。
30493
- */
30494
- async function installSitePackage(w) {
30495
- selection.packageInstalling = w.id;
30496
- selection.packageNote = weT("正在解码工程包…(首次会下载,之后立即生效)");
30497
- emit();
30498
- try {
30499
- const res = await apiPostJson("/site-package/install", { url: w.media, name: w.title }, { parse: "always" });
30500
- const d = (res && res.data) || null;
30501
- if (!(res && res.ok && d && d.ok)) {
30502
- const code = (d && d.error) || "failed";
30503
- selection.packageNote = code === "application-package"
30504
- ? weT("这个工程包是可执行程序(application),不能当壁纸")
30505
- : weT("解码失败:{code}", { code: String(code) });
30506
- return;
30507
- }
30508
- await loadInventory();
30509
- const list = (selection.inventory && selection.inventory.wallpapers) || [];
30510
- const target = list.find((x) => x && x.id === d.id);
30511
- if (target) {
30512
- selection.packageNote = weT("已就绪:{name}", { name: target.title || w.title || "" });
30513
- applySelection(target.id, { fromManual: true });
30514
- } else {
30515
- selection.packageNote = weT("已解码,但清单里还没出现 —— 点「刷新」重试");
30516
- }
30517
- } catch (e) {
30518
- selection.packageNote = weT("解码失败:{code}", { code: (e && e.message) ? e.message : weT("网络错误") });
30519
- } finally {
30520
- selection.packageInstalling = "";
30521
- emit();
30522
- }
30523
- }
30524
-
30525
30588
  const onPickCard = (w) => {
30526
- // 工程包(站点上的 WE 工程包 zip):点它不该"切过去"—— 它还不能播。
30527
- // 这里的语义是「解码一次,然后切过去」:用户看到的是"点一下就能用",没有单独的下载步骤。
30528
- if (w && w.type === "package" && !selection.batchMode && !(selection.pickerDraft && selection.editing)) {
30529
- void installSitePackage(w);
30530
- return;
30531
- }
30532
30589
  // 轮播编辑器的下钻(pickerDraft):点卡片 = 加入/移出**草稿**,不切当前壁纸;
30533
30590
  // 草稿对象被就地增删(与 importPlaylistIntoDraft / 面板编辑器同一口径),
30534
30591
  // 「保存」时才由 saveEditingGroup 落盘。
@@ -30877,7 +30934,7 @@ window.__ModuleLoader__.load({
30877
30934
  // 进去、把结果取出来 —— 组件体不再就地算这些(分级/类型/隐藏的判定也在那边)。
30878
30935
  const {
30879
30936
  query, playableList, basePlayable, ratingCounts, typeCounts, sourceCounts, hiddenList,
30880
- packageList, packageCount, editorPageView,
30937
+ editorPageView,
30881
30938
  } = pickerModel({
30882
30939
  wallpapers: list,
30883
30940
  hiddenIds: selection.hiddenIds,
@@ -31050,12 +31107,6 @@ window.__ModuleLoader__.load({
31050
31107
  onClear, onRatingFilterChange, onTypeFilterChange, onSourceFilterChange,
31051
31108
  onShowNormalView, onShowHiddenView,
31052
31109
  onToggleBatchMode, onArmBatchHide, onBatchHide, onBatchCancel, onSearchInput, onPickCard,
31053
- // 站点工程包(type=package):来自上面 pickerModel 的解构值(本作用域已有),
31054
- // 点击走 onPickCard 的解码分支(installSitePackage)。
31055
- packageList: packageList || [],
31056
- packageCount: packageCount || 0,
31057
- packageInstalling: sel.packageInstalling || "",
31058
- packageNote: sel.packageNote || "",
31059
31110
  }) : React.createElement(React.Fragment, null,
31060
31111
  // 页签内容 → **每个页签末尾一个重置**(issue #192 用户口径:重置指定选项卡)→
31061
31112
  // 全局重置(只在「系统」页签末尾,即设置页里最接近"最底部"的位置)。
package/lib/features.js CHANGED
@@ -37,10 +37,6 @@ export const FEATURES = [
37
37
  role: '本机/上传/站点三源合并成壁纸库;着色、图层、播放与轮换由客户端接管',
38
38
  host: [
39
39
  'lib/routes/inventory.js',
40
- // 站点工程包(WE 工程包 zip)→ 本机 WE 工程目录:点卡片时"解码一次就能用"。
41
- // 产物仍是壁纸,所以归壁纸功能;zip 读取器是它专用的安全入口。
42
- 'lib/routes/site-package.js',
43
- 'lib/zip-read.js',
44
40
  'lib/routes/media-bytes.js',
45
41
  'lib/routes/media-derived.js',
46
42
  'lib/routes/scene-frame.js',
@@ -130,12 +126,15 @@ export const FEATURES = [
130
126
  host: [
131
127
  'lib/routes/settings.js',
132
128
  'lib/routes/presets.js',
129
+ // 助手皮肤:站点皮肤包 → $DSH_HOME/skins(皮肤中心读同一目录,写盘即生效)。
130
+ 'lib/routes/skins.js',
133
131
  'lib/settings-schema.js',
134
132
  'lib/components/fonts/routes/fontsets.js',
135
133
  'lib/components/fonts/routes/system-fonts.js',
136
134
  ],
137
135
  client: [
138
136
  'src/panel-tabs-appearance.js',
137
+ 'src/panel-tabs-skins.js',
139
138
  'src/glass.js',
140
139
  'src/glass-panel.js',
141
140
  'src/glass-surfaces.js',
@@ -155,7 +154,7 @@ export const FEATURES = [
155
154
  'src/about-assets.js',
156
155
  'src/nav-icon.js',
157
156
  ],
158
- tab: ['appearance'],
157
+ tab: ['appearance', 'skins'],
159
158
  },
160
159
  {
161
160
  id: 'playback',
package/lib/index.js CHANGED
@@ -158,7 +158,7 @@ const {
158
158
  sceneVideoProbeKey, sceneVideoProbeSet, getMediaInfo, ensureDirOnce, customFrameDir, avatarDir,
159
159
  mascotDir, ensureUploadDir, transcodeToFps, registerTranscodeWaiter, transcodeCachePathFor,
160
160
  transcodeJobs, readUploadMeta, metaEntry, setUploadMeta, removeUploadMeta, resolveUploadFile,
161
- setUploadDir, getUploadDir, UPLOAD_MAX_BYTES, WEBWALLGL_DIR, WEBWALLGL_PKG_DIR, GLASS_PRESET_BUILTIN_DIR,
161
+ setUploadDir, UPLOAD_MAX_BYTES, WEBWALLGL_DIR, WEBWALLGL_PKG_DIR, GLASS_PRESET_BUILTIN_DIR,
162
162
  WE_ASSETS_SOURCE_ID, weAssetsAvailable, getWeAssetsDir, setWeAssetsDir, listWeAssetNames,
163
163
  userPropsFor, isBetterSidebarLoaded, GPU_FRAME_MAX_BYTES, GPU_FRAME_AREA_TOL, GPU_WRITE_INFLIGHT, gpuFrameFileFor,
164
164
  looksLikePng, pngSizeOf, pngDimsOfBuf, sceneFrameSlot, generateVideoPreview, ensureImagePreview,
@@ -294,8 +294,6 @@ export function apply(ctx) {
294
294
  registerFeatureRoutes(webServer, {
295
295
  disposers, base: BASE, log, mediaOriginApi, buildInventory,
296
296
  componentProbe, ensureImagePreview,
297
- // 站点工程包安装要落到"当前"存储位置:传访问器,不传值。
298
- getUploadDir,
299
297
  });
300
298
 
301
299
  // 启动预热(有意不阻塞):站点清单与模块索引各拉一次,让第一次打开面板不冷。
@@ -19,8 +19,8 @@ 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
- import { registerSitePackageRoutes } from './routes/site-package.js';
24
24
 
25
25
  /**
26
26
  * 已迁移的功能族。
@@ -73,15 +73,12 @@ export const FEATURE_ROUTE_FAMILIES = [
73
73
  deps: (c) => ({ disposers: c.disposers, base: c.base, log: c.log }),
74
74
  },
75
75
  {
76
- id: 'site-package',
77
- file: 'lib/routes/site-package.js',
78
- register: registerSitePackageRoutes,
79
- // 站点工程包要"解到本机存储位置",所以必须拿到**上传目录访问器**(不是值快照:
80
- // 用户中途改目录后,安装应该落到新目录)。siteBase 缺省时路由自己回落到站点默认基址。
81
- deps: (c) => ({
82
- disposers: c.disposers, base: c.base, log: c.log,
83
- getUploadDir: c.getUploadDir, siteBase: c.siteBase,
84
- }),
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 }),
85
82
  },
86
83
  ];
87
84