dsh-folia 3.9.1 → 4.0.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
@@ -7217,6 +7217,8 @@ window.__ModuleLoader__.load({
7217
7217
  contentRatingFilter: "everyone",
7218
7218
  // Wallpaper-type filter (all / video / web / image / scene). "all" disables it.
7219
7219
  typeFilter: "all",
7220
+ // 壁纸来源:all / local(本机 Wallpaper Engine 库 + 上传)/ site(我们站点)
7221
+ sourceFilter: "all",
7220
7222
  // Thumbnail-card style: "classic" (WE's original aspect-ratio 16/9 cards —
7221
7223
  // the CD-like look the author liked; can overlap in older browsers) or
7222
7224
  // "fixed" (fixed-height cards that never overlap). The vinyl
@@ -7661,6 +7663,7 @@ window.__ModuleLoader__.load({
7661
7663
  objectFit: { kind: 'enum', values: OBJECT_FIT_VALUES },
7662
7664
  contentRatingFilter: { kind: 'enum', values: RATING_VALUES },
7663
7665
  typeFilter: { kind: 'enum', values: TYPE_VALUES },
7666
+ sourceFilter: { kind: 'enum', values: ['all', 'local', 'site'] },
7664
7667
  pickerLayout: { kind: 'enum', values: PICKER_LAYOUT_VALUES },
7665
7668
  edgeCompat: { kind: 'boolTrue' },
7666
7669
  accent: { kind: 'hex' },
@@ -7912,7 +7915,7 @@ window.__ModuleLoader__.load({
7912
7915
  library: [
7913
7916
  'switchTransition', 'switchTransitionDir', 'switchTransitionSpeed',
7914
7917
  'rotationEnabled', 'rotationGroupId', 'rotationGroups', 'rotationSeeded',
7915
- 'contentRatingFilter', 'typeFilter', 'pickerLayout',
7918
+ 'contentRatingFilter', 'typeFilter', 'sourceFilter', 'pickerLayout',
7916
7919
  ],
7917
7920
  // 外观:玻璃 / 侧栏与四角 / 标题栏 / 思考块 / 浮层 / 主题预设 / 字体 / UI 配色。
7918
7921
  appearance: [
@@ -8547,7 +8550,7 @@ window.__ModuleLoader__.load({
8547
8550
  'id', 'hiddenIds', 'rotationEnabled', 'rotationGroupId', 'rotationGroups', 'rotationSeeded',
8548
8551
  'playbackRate', 'videoVolume', 'videoAudioEnabled', 'fpsCap', 'sceneLive', 'sceneLiveFps',
8549
8552
  'liveBootDelay', 'sceneLiveFailures', 'pauseOnHidden', 'pauseOnBlur', 'pauseOnBattery',
8550
- 'flip', 'objectFit', 'contentRatingFilter', 'typeFilter', 'pickerLayout', 'edgeCompat',
8553
+ 'flip', 'objectFit', 'contentRatingFilter', 'typeFilter', 'sourceFilter', 'pickerLayout', 'edgeCompat',
8551
8554
  'userProps', 'noticeSeen', 'layerPositionX', 'layerPositionY', 'layerScale',
8552
8555
  ],
8553
8556
  // 玻璃 / 材质 / 侧栏 / 过渡(`accent`、`glassMode` 与皮肤让路之外的整套玻璃登记表都在这里)
@@ -16168,382 +16171,6 @@ window.__ModuleLoader__.load({
16168
16171
  );
16169
16172
  }
16170
16173
 
16171
- // ── 内联模块:src/panel-tabs-skins.js —— 「皮肤包」页签:从自建站列皮肤包 → 下载 → 装到 harness/skins → 激活(宿主端 /skins 族)(构建期注入;勿手改本段)──
16172
- /**
16173
- * panel-tabs-skins.js — 「皮肤包」页签:**管理器**,不是渲染器。
16174
- *
16175
- * ══ 为什么只做管理 ══════════════════════════════════════════════════════════
16176
- *
16177
- * 皮肤的**渲染**只能由皮肤中心(@linxin666/dsh-client-ui-skin-center)完成:
16178
- * 它读 `harness/skins/<id>/`,注入强制作用域化的 CSS、建六个装饰层、跑 hooks、
16179
- * 铺背景媒体 —— 而且**只在页面加载时**做一次。它的控制器 `activate(id)` 没有导出,
16180
- * 外部无法调用。
16181
- *
16182
- * 我们曾经试图"复刻"这套渲染(约 600 行:作用域化 CSS / 六层 / hooks / 背景 / 仲裁),
16183
- * 结果是:怎么补都差一层,而且**能把整个界面画成空白**(2026-10-28 事故)。
16184
- * 所以现在明确分工:
16185
- *
16186
- * 本页签:列表 · 预览 · 下载安装 · 删除 · 选中(写 active)
16187
- * 皮肤中心:真正把皮肤画出来(在它自己的 设置 → 皮肤 页面点「应用」)
16188
- *
16189
- * 纪律(每一条都对应一次真实事故):
16190
- * · **绝不**注入 CSS / 造装饰层 / 跑 hooks / 改 html 属性;
16191
- * · **绝不**尝试刷新页面(桌面壳里 location.reload 被吞过);
16192
- * · 选中之后只写 active,然后**告诉用户下一步去哪点**;
16193
- * · 不碰 patch 行、不碰 generation、不跑 pnpm。
16194
- */
16195
-
16196
- function skinsFmtBytes(n) {
16197
- const v = Number(n) || 0;
16198
- if (v >= 1024 * 1024) return (v / 1024 / 1024).toFixed(1) + " MB";
16199
- if (v >= 1024) return Math.round(v / 1024) + " KB";
16200
- return v + " B";
16201
- }
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
-
16212
- function skinsErrText(code) {
16213
- if (!code) return "";
16214
- if (code === "disabled") return weT("站点数据源已关闭(DSH_FOLIA_SKINS_SITE=off)");
16215
- if (code === "timeout") return weT("站点超时,点「刷新」重试");
16216
- if (code === "tls") return weT("站点证书校验失败");
16217
- if (code === "network") return weT("连不上站点(网络或域名问题)");
16218
- if (/^http-/.test(code)) return weT("站点返回错误:") + code.slice(5);
16219
- if (code === "json") return weT("站点索引格式不对");
16220
- return code;
16221
- }
16222
-
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" },
16237
- };
16238
-
16239
- function SkinsTabPanel(props) {
16240
- // 「清掉当前壁纸」的入口由挂载点传入(皮肤与壁纸互斥)。本组件不直接碰设置。
16241
- const onClearWallpaper = props && props.onClearWallpaper;
16242
-
16243
- const [list, setList] = React.useState([]);
16244
- const [active, setActive] = React.useState(null);
16245
- const [loading, setLoading] = React.useState(true);
16246
- const [error, setError] = React.useState(null);
16247
- const [harnessFound, setHarnessFound] = React.useState(true);
16248
- const [skinCenter, setSkinCenter] = React.useState(false);
16249
- const [providers, setProviders] = React.useState([]);
16250
- const [busy, setBusy] = React.useState("");
16251
- const [note, setNote] = React.useState(null);
16252
- const [q, setQ] = React.useState("");
16253
-
16254
- const load = React.useCallback(async (force) => {
16255
- setLoading(true);
16256
- setError(null);
16257
- try {
16258
- // apiJson 返回 { ok, status, data, error } 包装对象,不是响应体本身。
16259
- const resp = await apiJson("/skins" + (force ? "?refresh=1" : ""), { parse: "always" });
16260
- const r = (resp && resp.data) || null;
16261
- setList((r && r.skins) || []);
16262
- setActive((r && r.active) || null);
16263
- setError((r && r.error) || (resp && resp.error) || null);
16264
- setHarnessFound(!r || r.harnessFound !== false);
16265
- setSkinCenter(Boolean(r && r.skinCenter));
16266
- setProviders(Array.isArray(r && r.skinCenterProviders) ? r.skinCenterProviders : []);
16267
- } catch (e) {
16268
- setError("network");
16269
- setList([]);
16270
- } finally {
16271
- setLoading(false);
16272
- }
16273
- }, []);
16274
-
16275
- React.useEffect(() => { load(false); }, [load]);
16276
-
16277
- // 轮询验证的清理(组件卸载/重新选中时别留下定时器)。
16278
- const pollRef = React.useRef(null);
16279
- React.useEffect(() => () => {
16280
- if (pollRef.current !== null) { clearInterval(pollRef.current); pollRef.current = null; }
16281
- }, []);
16282
-
16283
- /**
16284
- * 写 active 之后**验证皮肤中心有没有真的应用**。
16285
- *
16286
- * 为什么不需要刷新页面:皮肤中心的客户端运行时自带轮询 ——
16287
- *
16288
- * const SELECTION_POLL_MS = 2e3;
16289
- * function watchPersistedSelection(store, intervalMs = SELECTION_POLL_MS) {
16290
- * const tick = async () => {
16291
- * const next = await readPersistedSelection(store); // GET /api/skin-center/v2/active
16292
- * if (next === undefined || next === applied) return;
16293
- * applied = next;
16294
- * await convergeOnSelection(store, next); // ← 自动应用
16295
- * };
16296
- * store.window.setInterval(tick, intervalMs); // 注意是 store.window.setInterval
16297
- * }
16298
- *
16299
- * 那个 `/active` 读的就是我们刚写的 `skin-center-active.json`。所以写完之后它会在
16300
- * 2 秒内自己收敛过去 —— 之前"必须刷新"的假象,是我们自己的热注入在覆盖它的结果。
16301
- *
16302
- * 它的收敛成功标志很明确:`<html data-dsh-skin="<id>">`。我们就盯这个属性:
16303
- * 出现了 = 真的应用了;5 次(约 6 秒)还没出现 = 没应用,如实说。
16304
- */
16305
- function verifyApplied(id, label) {
16306
- if (pollRef.current !== null) { clearInterval(pollRef.current); pollRef.current = null; }
16307
- let tries = 0;
16308
- pollRef.current = setInterval(() => {
16309
- tries += 1;
16310
- let stamped = null;
16311
- try { stamped = document.documentElement.getAttribute("data-dsh-skin"); } catch { stamped = null; }
16312
- if (stamped === id) {
16313
- clearInterval(pollRef.current); pollRef.current = null;
16314
- setNote({ kind: "ok", text: weT("已应用:「") + label + weT("」—— 皮肤中心已收敛(无需刷新)") });
16315
- return;
16316
- }
16317
- if (tries >= 9) { // 约 11 秒:host 会先写 null 再写目标 id(900ms),引擎每 2s 轮询一次
16318
- clearInterval(pollRef.current); pollRef.current = null;
16319
- setNote({
16320
- kind: "err",
16321
- text: weT("已写入「") + label + weT("」,但皮肤中心没有应用。可能原因:壁纸仍在台(皮肤会被抑制)或这套皮肤校验未通过。可到「设置 → 皮肤」点「应用」确认。"),
16322
- });
16323
- }
16324
- }, 1200);
16325
- }
16326
-
16327
- /**
16328
- * 执行一个动作。
16329
- * · install / remove:宿主真正干活(下载解压 / 删目录)。
16330
- * · activate:**只写 active**(`skin-center-active.json`)—— 不注入任何样式,
16331
- * 应用由皮肤中心自己轮询完成,我们只负责验证结果。
16332
- */
16333
- async function act(kind, id, label) {
16334
- if (busy) return;
16335
- setBusy(id);
16336
- setNote(null);
16337
- try {
16338
- const resp = await apiPostJson("/skins/" + kind, { id }, { parse: "always" });
16339
- const r = (resp && resp.data) || null;
16340
- if (resp && resp.ok && r && r.ok) {
16341
- if (kind === "install") {
16342
- setNote({ kind: "ok", text: weT("已安装:") + label });
16343
- await load(true);
16344
- } else if (kind === "activate") {
16345
- setActive(id);
16346
- setList((prev) => prev.map((x) => ({ ...x, active: x.id === id })));
16347
- // 皮肤与壁纸都在铺背景,必须互斥:皮肤中心在壁纸在台时会**主动抑制皮肤**,
16348
- // 所以这一步不是"顺手",而是"应用能否成功"的前提。
16349
- try {
16350
- if (typeof onClearWallpaper === "function") await Promise.resolve(onClearWallpaper());
16351
- } catch { /* 清壁纸失败不影响选中本身 */ }
16352
- if (skinCenter) {
16353
- setNote({ kind: "ok", text: weT("已选中「") + label + weT("」,等待皮肤中心应用(约 2 秒)…") });
16354
- verifyApplied(id, label);
16355
- } else {
16356
- setNote({
16357
- kind: "err",
16358
- text: weT("已写入「") + label + weT("」,但本机没有皮肤中心,无法渲染。请安装 @linxin666/dsh-client-ui-skin-center 后重启。"),
16359
- });
16360
- }
16361
- } else if (kind === "remove") {
16362
- setNote({ kind: "ok", text: weT("已删除:") + label });
16363
- await load(true);
16364
- }
16365
- } else {
16366
- const code = (r && r.error) || "failed";
16367
- const detail = (r && r.detail) ? "(" + r.detail + ")" : "";
16368
- setNote({ kind: "err", text: weT("操作失败:") + code + detail });
16369
- }
16370
- } catch (e) {
16371
- setNote({ kind: "err", text: weT("请求失败:") + (e && e.message ? e.message : weT("网络错误")) });
16372
- } finally {
16373
- setBusy("");
16374
- }
16375
- }
16376
-
16377
- const needle = q.trim().toLowerCase();
16378
- const shown = needle
16379
- ? list.filter((s) => (s.name + " " + s.nameEn + " " + s.id + " " + (s.tags || []).join(" "))
16380
- .toLowerCase().indexOf(needle) !== -1)
16381
- : list;
16382
- const installedCount = list.filter((s) => s.installed).length;
16383
-
16384
- const header = React.createElement("div", { className: "we-picker__section-head" },
16385
- React.createElement("span", { className: "we-picker__section-label" },
16386
- weT("🎨 皮肤包") + (list.length ? " · " + list.length : "")),
16387
- React.createElement("button", {
16388
- type: "button",
16389
- className: "we-picker__btn",
16390
- style: { marginLeft: "auto" },
16391
- disabled: loading,
16392
- onClick: () => load(true),
16393
- title: weT("重新从站点拉取列表"),
16394
- }, loading ? weT("加载中…") : weT("刷新")),
16395
- );
16396
-
16397
- if (!harnessFound) {
16398
- return React.createElement(React.Fragment, null,
16399
- React.createElement("div", { className: "we-picker__section" }, header,
16400
- React.createElement("div", { className: "we-picker__empty" },
16401
- React.createElement("span", { className: "we-picker__empty-title" }, weT("找不到 DSH 的 skins 目录")),
16402
- React.createElement("span", { className: "we-picker__hint" },
16403
- weT("皮肤包要装到 DSH 的 harness/skins 下。可用环境变量 DSH_FOLIA_HARNESS 指定该目录")))));
16404
- }
16405
-
16406
- const children = [React.createElement("div", { className: "we-picker__section", key: "sec" }, header)];
16407
-
16408
- // 重复挂载警示:我们的 patch 只有一行(folia),正常不会出现;
16409
- // 但用户可能自己加了行,这里如实报出来。
16410
- if (providers.length > 1) {
16411
- children.push(React.createElement("div", {
16412
- key: "dup",
16413
- className: "we-picker__hint",
16414
- style: {
16415
- margin: "4px 0 10px", padding: "7px 10px", borderRadius: "8px",
16416
- background: "rgba(255,120,120,0.14)", color: "#ff9a9a", lineHeight: 1.5,
16417
- },
16418
- }, weT("⚠️ 检测到 ") + providers.length + weT(" 处挂载了皮肤中心(")
16419
- + providers.map((p) => p.bundle).join(" / ")
16420
- + weT(")。请只保留一处,否则会重复挂载。")));
16421
- }
16422
-
16423
- // 分工常驻说明:让用户一开始就知道"应用"在哪一步。
16424
- if (list.length > 0) {
16425
- children.push(React.createElement("div", {
16426
- key: "division",
16427
- className: "we-picker__hint",
16428
- style: { margin: "2px 0 8px", opacity: 0.8 },
16429
- }, skinCenter
16430
- ? weT("这里负责下载与管理。选中后请到「设置 → 皮肤」点「应用」完成渲染。")
16431
- : weT("未检测到皮肤中心:可以下载/管理皮肤包,但无法渲染。请安装 @linxin666/dsh-client-ui-skin-center 并重启。")));
16432
- }
16433
-
16434
- // 当前选中(只是"选中",不是"已应用")
16435
- if (active) {
16436
- const activeName = (list.find((x) => x.id === active) || {}).name || active;
16437
- children.push(React.createElement("div", {
16438
- key: "active",
16439
- className: "we-picker__hint",
16440
- style: { margin: "4px 0 8px", display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap" },
16441
- },
16442
- React.createElement("span", null, weT("已选中:") + activeName),
16443
- React.createElement("span", {
16444
- style: { fontSize: "11px", padding: "1px 6px", borderRadius: "999px", background: "rgba(255,196,120,0.16)", color: "#ffd08a" },
16445
- }, weT("需在皮肤中心点「应用」")),
16446
- ));
16447
- }
16448
-
16449
- if (error) {
16450
- children.push(React.createElement("div", {
16451
- key: "err",
16452
- className: "we-picker__hint",
16453
- style: { color: "#ff9a9a", margin: "6px 0 10px" },
16454
- }, skinsErrText(error)));
16455
- }
16456
- if (note) {
16457
- children.push(React.createElement("div", {
16458
- key: "note",
16459
- className: "we-picker__hint",
16460
- style: { color: note.kind === "err" ? "#ff9a9a" : "#8ee6a8", margin: "6px 0 10px", lineHeight: 1.5 },
16461
- }, note.text));
16462
- }
16463
-
16464
- children.push(React.createElement("div", {
16465
- key: "search",
16466
- style: { display: "flex", gap: "8px", alignItems: "center", margin: "2px 0 4px" },
16467
- },
16468
- React.createElement("input", {
16469
- type: "text",
16470
- value: q,
16471
- placeholder: weT("搜索名称 / 作者 / 标签"),
16472
- className: "we-picker__search",
16473
- onChange: (e) => setQ(e.target.value),
16474
- style: { flex: "1 1 auto" },
16475
- }),
16476
- React.createElement("span", { className: "we-picker__hint" }, weT("已装 ") + installedCount + " / " + list.length),
16477
- ));
16478
-
16479
- if (loading && list.length === 0) {
16480
- children.push(React.createElement("div", { key: "loading", className: "we-picker__empty" },
16481
- React.createElement("span", { className: "we-picker__empty-title" }, weT("正在从站点拉取皮肤包…"))));
16482
- } else if (!error && shown.length === 0) {
16483
- children.push(React.createElement("div", { key: "empty", className: "we-picker__empty" },
16484
- React.createElement("span", { className: "we-picker__empty-title" }, weT("没有匹配的皮肤包")),
16485
- React.createElement("span", { className: "we-picker__hint" }, weT("换个关键词,或点「刷新」重新拉取"))));
16486
- }
16487
-
16488
- if (shown.length > 0) {
16489
- const darkNow = skinsThemeIsDark();
16490
- children.push(React.createElement("div", { key: "grid", style: SKINS_STYLE.grid },
16491
- shown.map((s) => {
16492
- const previewRel = (darkNow
16493
- ? (s.previewDark || s.previewLight)
16494
- : (s.previewLight || s.previewDark)) || "";
16495
- const previewSrc = previewRel
16496
- ? apiUrl("/skins/preview?p=" + encodeURIComponent(previewRel))
16497
- : "";
16498
- const isBusy = busy === s.id;
16499
- const btn = (label, onClick, disabled, primary) => React.createElement("button", {
16500
- type: "button",
16501
- className: "we-picker__btn" + (primary ? " we-picker__btn--primary" : ""),
16502
- style: { flex: "1 1 0", fontSize: "11.5px", padding: "4px 6px" },
16503
- disabled: Boolean(disabled) || Boolean(busy),
16504
- onClick,
16505
- }, label);
16506
-
16507
- const actions = [];
16508
- if (!s.installed) {
16509
- actions.push(btn(isBusy ? weT("安装中…") : weT("下载安装"), () => act("install", s.id, s.name), false, true));
16510
- } else if (!s.active) {
16511
- actions.push(btn(isBusy ? weT("选中中…") : weT("选择"), () => act("activate", s.id, s.name), false, true));
16512
- actions.push(btn(weT("删除"), () => act("remove", s.id, s.name), isBusy, false));
16513
- } else {
16514
- actions.push(btn(weT("已选中"), () => {}, true, false));
16515
- }
16516
-
16517
- return React.createElement("div", { key: s.id, style: SKINS_STYLE.card },
16518
- previewSrc
16519
- ? React.createElement("img", {
16520
- src: previewSrc, alt: s.name, loading: "lazy", draggable: false, style: SKINS_STYLE.thumb,
16521
- })
16522
- : React.createElement("div", { style: SKINS_STYLE.thumb }),
16523
- React.createElement("div", { style: SKINS_STYLE.body },
16524
- React.createElement("div", { style: SKINS_STYLE.title }, s.name || s.id),
16525
- s.tagline ? React.createElement("div", { style: SKINS_STYLE.meta }, s.tagline) : null,
16526
- (s.tags || []).length
16527
- ? React.createElement("div", { style: SKINS_STYLE.tags },
16528
- s.tags.slice(0, 4).map((t) => React.createElement("span", { key: t, style: SKINS_STYLE.tag }, t)))
16529
- : null,
16530
- React.createElement("div", { style: SKINS_STYLE.actions }, actions),
16531
- ),
16532
- );
16533
- })));
16534
- }
16535
-
16536
- return React.createElement(React.Fragment, null, children);
16537
- }
16538
-
16539
- /**
16540
- * 页签薄包装:`renderActiveTab` 只认这个签名(与别的 `render*Tab(ctx)` 一致)。
16541
- * 传进来的 props 原样转交(目前只有 `onClearWallpaper`)。
16542
- */
16543
- function renderSkinsTab(props) {
16544
- return React.createElement(SkinsTabPanel, props || null);
16545
- }
16546
-
16547
16174
  // ── 内联模块:src/picker-model.js —— 选择器模型层:过滤判定 + 派生数据 + 分页切片(纯函数 / 显式入参 / 零 DOM)(构建期注入;勿手改本段)──
16548
16175
  /**
16549
16176
  * picker-model.js — 壁纸选择器的**模型层**:过滤判定 + 派生数据 + 分页切片。
@@ -16648,8 +16275,25 @@ window.__ModuleLoader__.load({
16648
16275
  return w.type === "scene" && Boolean(w.frameUrl);
16649
16276
  }
16650
16277
 
16651
- function isRotatableWallpaper(w, ratingFilter, typeFilter) {
16652
- return isPlayableType(w) && matchesRatingFilter(w, ratingFilter) && matchesTypeFilter(w, typeFilter);
16278
+ /**
16279
+ * 壁纸来源:`site` = 从我们站点拉下来的(inventory 里带 `source: "folia"`),
16280
+ * 其余一律算 `local`(本机 Wallpaper Engine 库 / 用户上传 —— 它们都没有那个标记)。
16281
+ *
16282
+ * 为什么不给本机条目补一个 `source`:那要动宿主侧的 inventory 产出,而客户端只靠
16283
+ * “有没有 folia 标记”就能判对,改面更小。
16284
+ */
16285
+ function sourceOf(w) {
16286
+ return w && w.source === "folia" ? "site" : "local";
16287
+ }
16288
+
16289
+ function matchesSourceFilter(w, sourceFilter) {
16290
+ if (!sourceFilter || sourceFilter === "all") return true;
16291
+ return sourceOf(w) === sourceFilter;
16292
+ }
16293
+
16294
+ function isRotatableWallpaper(w, ratingFilter, typeFilter, sourceFilter) {
16295
+ return isPlayableType(w) && matchesRatingFilter(w, ratingFilter)
16296
+ && matchesTypeFilter(w, typeFilter) && matchesSourceFilter(w, sourceFilter);
16653
16297
  }
16654
16298
 
16655
16299
  // 「正在应用的壁纸可以继续播」判据:可播放 ∧ 分级闸门,**不过滤类型**。过滤档分两种
@@ -16673,9 +16317,9 @@ window.__ModuleLoader__.load({
16673
16317
  }
16674
16318
 
16675
16319
  /** 可轮换列表:可播放类型 ∧ 命中两级过滤 ∧ 未隐藏(网格与轮换编辑器共用的那份)。 */
16676
- function playableWallpapers(wallpapers, ratingFilter, typeFilter, hiddenIds) {
16320
+ function playableWallpapers(wallpapers, ratingFilter, typeFilter, hiddenIds, sourceFilter) {
16677
16321
  return wallpapers.filter(
16678
- (w) => isRotatableWallpaper(w, ratingFilter, typeFilter) && !isHiddenWallpaper(w.id, hiddenIds),
16322
+ (w) => isRotatableWallpaper(w, ratingFilter, typeFilter, sourceFilter) && !isHiddenWallpaper(w.id, hiddenIds),
16679
16323
  );
16680
16324
  }
16681
16325
 
@@ -16689,15 +16333,19 @@ window.__ModuleLoader__.load({
16689
16333
  const hiddenIds = input.hiddenIds || [];
16690
16334
  const ratingFilter = input.ratingFilter;
16691
16335
  const typeFilter = input.typeFilter;
16336
+ const sourceFilter = input.sourceFilter;
16692
16337
  // 标题搜索(选择器模态框):在两级过滤之上再收窄可播放网格。大小写不敏感的**子串**匹配。
16693
16338
  const query = (input.search || "").trim().toLowerCase();
16694
16339
  // 只有可播放的 Video/Web/Image 才进网格 —— Scene/Application 不能嵌进 web UI,
16695
16340
  // 隐掉它们网格才有用。已隐藏(软删除)的壁纸离开这里去「已隐藏」区;两级过滤再收窄。
16696
16341
  const playableList = list.filter((w) =>
16697
- isRotatableWallpaper(w, ratingFilter, typeFilter) && !isHiddenWallpaper(w.id, hiddenIds)
16342
+ isRotatableWallpaper(w, ratingFilter, typeFilter, sourceFilter) && !isHiddenWallpaper(w.id, hiddenIds)
16698
16343
  && (!query || String(w.title || "").toLowerCase().indexOf(query) !== -1));
16699
16344
  // 两个过滤下拉的分档计数(可播放、未隐藏):反映实际可用量,与当前生效的过滤无关。
16700
16345
  const basePlayable = list.filter((w) => isPlayableType(w) && !isHiddenWallpaper(w.id, hiddenIds));
16346
+ // 来源分档计数(可播放、未隐藏、**不受来源过滤影响** —— 它要能反映“切过去有多少张”)。
16347
+ const sourceCounts = { local: 0, site: 0 };
16348
+ for (const w of basePlayable) sourceCounts[sourceOf(w)] += 1;
16701
16349
  // 单遍聚合 —— 分档计数各刷一遍 basePlayable 是 10 次 O(n)(5 个分级档 + 5 个类型档)。
16702
16350
  const ratingCounts = { everyone: 0, pg13: 0, mature: 0, unrated: 0 };
16703
16351
  const typeCounts = { video: 0, web: 0, image: 0, scene: 0 };
@@ -16706,7 +16354,7 @@ window.__ModuleLoader__.load({
16706
16354
  ratingCounts[r] = (ratingCounts[r] || 0) + 1;
16707
16355
  typeCounts[w.type] = (typeCounts[w.type] || 0) + 1;
16708
16356
  }
16709
- const editorList = playableWallpapers(list, ratingFilter, typeFilter, hiddenIds);
16357
+ const editorList = playableWallpapers(list, ratingFilter, typeFilter, hiddenIds, sourceFilter);
16710
16358
  const hiddenList = hiddenWallpapers(list, hiddenIds);
16711
16359
  return {
16712
16360
  query,
@@ -16716,6 +16364,7 @@ window.__ModuleLoader__.load({
16716
16364
  typeCounts,
16717
16365
  editorList,
16718
16366
  hiddenList,
16367
+ sourceCounts,
16719
16368
  normalPage: pageSlice(playableList, input.page),
16720
16369
  hiddenPageView: pageSlice(hiddenList, input.hiddenPage),
16721
16370
  editorPageView: pageSlice(editorList, input.editorPage),
@@ -17088,7 +16737,7 @@ window.__ModuleLoader__.load({
17088
16737
  }
17089
16738
 
17090
16739
  function renderPickerFilterRow(ctx) {
17091
- const { sel, basePlayable, ratingCounts, typeCounts, onSearchInput, onRatingFilterChange, onTypeFilterChange } = ctx;
16740
+ const { sel, basePlayable, ratingCounts, typeCounts, sourceCounts, onSearchInput, onRatingFilterChange, onTypeFilterChange, onSourceFilterChange } = ctx;
17092
16741
  return React.createElement("div", { className: "we-picker__row we-picker__filter-row" },
17093
16742
  // 标题搜索:几百上千张壁纸时最快的定位方式。输入即过滤,与分级/类型过滤叠加。
17094
16743
  React.createElement("input", {
@@ -17098,6 +16747,20 @@ window.__ModuleLoader__.load({
17098
16747
  "aria-label": weT("搜索壁纸标题"),
17099
16748
  onInput: onSearchInput,
17100
16749
  }),
16750
+ // 来源:本机 Wallpaper Engine 库 vs 我们站点。放在最前面 —— 它是最粗的一刀,
16751
+ // 用户常常只想看其中一边(本机几十张 vs 站点几条,混在一起没法挑)。
16752
+ React.createElement("span", { className: "we-picker__hint we-picker__label" }, weT("来源")),
16753
+ React.createElement("select", {
16754
+ className: "we-picker__playlist-select",
16755
+ value: sel.sourceFilter || "all",
16756
+ onChange: onSourceFilterChange,
16757
+ "aria-label": weT("来源"),
16758
+ title: weT("按来源过滤:本机 Wallpaper Engine 库,还是从我们站点拉下来的壁纸"),
16759
+ },
16760
+ React.createElement("option", { value: "all" }, weT("全部({n})", { n: basePlayable.length })),
16761
+ React.createElement("option", { value: "local" }, weT("本机 Wallpaper Engine({n})", { n: (sourceCounts && sourceCounts.local) || 0 })),
16762
+ React.createElement("option", { value: "site" }, weT("站点壁纸({n})", { n: (sourceCounts && sourceCounts.site) || 0 })),
16763
+ ),
17101
16764
  React.createElement("span", { className: "we-picker__hint we-picker__label" }, weT("内容分级")),
17102
16765
  React.createElement("select", {
17103
16766
  className: "we-picker__playlist-select",
@@ -17289,6 +16952,226 @@ window.__ModuleLoader__.load({
17289
16952
  return React.createElement("div", { key: p.name, className: "we-picker__props-row" }, label, control);
17290
16953
  }
17291
16954
 
16955
+ // ── 内联模块:src/panel-tabs-boot.js —— 「开机动画」页签:候选/下载/选择/开关。播放不在前端 —— 启动那一刻应用还没挂载,由宿主注入的脚本负责(构建期注入;勿手改本段)──
16956
+ /**
16957
+ * panel-tabs-boot.js — 「开机动画」页签(放在「壁纸」左侧)。
16958
+ *
16959
+ * 它只做四件事:看候选、下载、选一个、开关。**播放不在这里** —— 播放发生在启动那一刻,
16960
+ * 由宿主注入的脚本完成(`lib/boot/index.js` 的 BOOT_SCREEN_SOURCE),因为那时候应用
16961
+ * 还没挂载,任何 React 组件都还不存在。
16962
+ *
16963
+ * 数据全部来自宿主(`/folia/boot/*`),浏览器不直连站点:
16964
+ * GET /folia/boot/list 候选 + 本地状态
16965
+ * POST /folia/boot/download 下一条到本地
16966
+ * POST /folia/boot/select 选中并启用
16967
+ * POST /folia/boot/toggle 开/关
16968
+ * POST /folia/boot/settings 淡出/最短显示/最长停留
16969
+ * POST /folia/boot/remove 删本地
16970
+ */
16971
+
16972
+ function bootFmtBytes(n) {
16973
+ const v = Number(n) || 0;
16974
+ if (v >= 1024 * 1024) return (v / 1024 / 1024).toFixed(1) + " MB";
16975
+ if (v >= 1024) return Math.round(v / 1024) + " KB";
16976
+ return v + " B";
16977
+ }
16978
+
16979
+ const BOOT_STYLE = {
16980
+ grid: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))", gap: "10px", marginTop: "8px" },
16981
+ card: {
16982
+ display: "flex", flexDirection: "column", gap: "6px", padding: "9px 10px",
16983
+ border: "1px solid rgba(255,255,255,0.12)", borderRadius: "10px", background: "rgba(255,255,255,0.04)",
16984
+ },
16985
+ title: { fontSize: "12.5px", fontWeight: 600 },
16986
+ meta: { fontSize: "11px", opacity: 0.62 },
16987
+ row: { display: "flex", gap: "6px", alignItems: "center" },
16988
+ };
16989
+
16990
+ function BootTabPanel() {
16991
+ const [state, setState] = React.useState(null);
16992
+ const [busy, setBusy] = React.useState("");
16993
+ const [note, setNote] = React.useState(null);
16994
+ // 下载没有进度通道(宿主一次性 fetch 完才返回),但等待必须"看得见":
16995
+ // 每秒 +1 的秒表足以让用户知道它没卡死。
16996
+ const [tick, setTick] = React.useState(0);
16997
+
16998
+ const load = React.useCallback(async (force) => {
16999
+ try {
17000
+ const resp = await apiJson("/boot/list" + (force ? "?refresh=1" : ""), { parse: "always" });
17001
+ const r = (resp && resp.data) || null;
17002
+ setState(r || { ok: false, clips: [], error: "network" });
17003
+ } catch (e) {
17004
+ setState({ ok: false, clips: [], error: "network" });
17005
+ }
17006
+ }, []);
17007
+
17008
+ React.useEffect(() => { load(false); }, [load]);
17009
+
17010
+ React.useEffect(() => {
17011
+ if (!busy) return undefined;
17012
+ const t = setInterval(() => setTick((n) => n + 1), 1000);
17013
+ return () => clearInterval(t);
17014
+ }, [busy]);
17015
+
17016
+ async function act(kind, payload, label) {
17017
+ if (busy) return;
17018
+ setBusy(payload && payload.id ? payload.id : kind);
17019
+ setTick(0);
17020
+ setNote(null);
17021
+ try {
17022
+ const resp = await apiPostJson("/boot/" + kind, payload || {}, { parse: "always" });
17023
+ const r = (resp && resp.data) || null;
17024
+ if (resp && resp.ok && r && r.ok) {
17025
+ if (kind === "download") setNote({ ok: true, text: weT("已下载:") + label });
17026
+ if (kind === "select") setNote({ ok: true, text: weT("已选中「") + label + weT("」,下次启动生效") });
17027
+ if (kind === "remove") setNote({ ok: true, text: weT("已删除:") + label });
17028
+ if (kind === "toggle") setNote({ ok: true, text: payload.enabled ? weT("开机动画已开启") : weT("开机动画已关闭") });
17029
+ if (kind === "settings") setNote({ ok: true, text: weT("已保存") });
17030
+ await load(true);
17031
+ } else {
17032
+ const code = (r && r.error) || "failed";
17033
+ setNote({ ok: false, text: weT("操作失败:") + code + (r && r.detail ? "(" + r.detail + ")" : "") });
17034
+ }
17035
+ } catch (e) {
17036
+ setNote({ ok: false, text: weT("请求失败:") + (e && e.message ? e.message : weT("网络错误")) });
17037
+ } finally {
17038
+ setBusy("");
17039
+ }
17040
+ }
17041
+
17042
+ const clips = (state && state.clips) || [];
17043
+ const enabled = Boolean(state && state.enabled);
17044
+ const activeId = (state && state.clipId) || "";
17045
+ const installedCount = clips.filter((c) => c.installed).length;
17046
+
17047
+ const children = [];
17048
+
17049
+ children.push(React.createElement("div", { className: "we-picker__section-head", key: "head" },
17050
+ React.createElement("span", { className: "we-picker__section-label" }, weT("🎬 开机动画") + (clips.length ? " · " + clips.length : "")),
17051
+ React.createElement("button", {
17052
+ type: "button", className: "we-picker__btn", style: { marginLeft: "auto" },
17053
+ disabled: Boolean(busy), onClick: () => load(true),
17054
+ }, weT("刷新")),
17055
+ ));
17056
+
17057
+ // 开关 + 当前选择
17058
+ children.push(React.createElement("div", {
17059
+ key: "switch", style: { ...BOOT_STYLE.row, margin: "6px 0 2px", flexWrap: "wrap" },
17060
+ },
17061
+ React.createElement("label", { style: { display: "flex", gap: "6px", alignItems: "center", fontSize: "12px" } },
17062
+ React.createElement("input", {
17063
+ type: "checkbox", checked: enabled, disabled: Boolean(busy) || !activeId,
17064
+ onChange: (e) => act("toggle", { enabled: e.target.checked }, ""),
17065
+ }),
17066
+ weT("启用(下次启动播放)"),
17067
+ ),
17068
+ React.createElement("span", { className: "we-picker__hint" },
17069
+ activeId ? weT("当前:") + ((clips.find((c) => c.id === activeId) || {}).title || activeId) : weT("未选择")),
17070
+ ));
17071
+
17072
+ children.push(React.createElement("div", { key: "hint", className: "we-picker__hint", style: { margin: "2px 0 6px", opacity: 0.75 } },
17073
+ weT("下载一条并「使用」后,每次启动都会先播它;默认播完才淡出进入界面。点击或按 Esc 可立即跳过。「带声音」若被浏览器的自动播放策略拦下,会自动退回静音播放。")));
17074
+
17075
+ if (state && state.error && !clips.length) {
17076
+ children.push(React.createElement("div", { key: "err", className: "we-picker__hint", style: { color: "#ff9a9a" } },
17077
+ weT("候选清单读不到:") + state.error));
17078
+ }
17079
+
17080
+ if (note) {
17081
+ children.push(React.createElement("div", { key: "note", className: "we-picker__hint", style: { color: note.ok ? "#8ee6a8" : "#ff9a9a", margin: "4px 0 8px" } }, note.text));
17082
+ }
17083
+
17084
+ // 淡出 / 停留
17085
+ if (state) {
17086
+ const opt = (label, key, list) => React.createElement("label", { style: { display: "flex", gap: "4px", alignItems: "center", fontSize: "12px" } },
17087
+ label,
17088
+ React.createElement("select", {
17089
+ className: "we-picker__playlist-select",
17090
+ value: String(state[key]),
17091
+ disabled: Boolean(busy),
17092
+ onChange: (e) => act("settings", { [key]: Number(e.target.value) }, ""),
17093
+ }, list.map(([v, t]) => React.createElement("option", { key: v, value: String(v) }, t))),
17094
+ );
17095
+ children.push(React.createElement("div", { key: "opts", style: { ...BOOT_STYLE.row, gap: "14px", flexWrap: "wrap", margin: "2px 0 8px" } },
17096
+ // 进入方式:默认「播完再进」——之前只有「就绪即进」,15 秒的片子 1.2 秒就被掐掉。
17097
+ React.createElement("label", { style: { display: "flex", gap: "4px", alignItems: "center", fontSize: "12px" } },
17098
+ weT("进入方式"),
17099
+ React.createElement("select", {
17100
+ className: "we-picker__playlist-select",
17101
+ value: String(state.enterMode || "tail"),
17102
+ disabled: Boolean(busy),
17103
+ onChange: (e) => act("settings", { enterMode: e.target.value }, ""),
17104
+ },
17105
+ React.createElement("option", { value: "tail" }, weT("播完再进(默认)")),
17106
+ React.createElement("option", { value: "ready" }, weT("界面就绪就进")),
17107
+ React.createElement("option", { value: "click" }, weT("点一下才进")),
17108
+ ),
17109
+ ),
17110
+ opt(weT("淡出时长"), "fadeMs", [[400, "0.4 秒"], [600, "0.6 秒"], [900, "0.9 秒"], [1200, "1.2 秒"], [1600, "1.6 秒"]]),
17111
+ // 声音:默认关(带声音的自动播放会被浏览器策略拦)。打开后若仍被拦,
17112
+ // 脚本会自动退回静音播放 —— 画面优先,不会黑屏。
17113
+ React.createElement("label", { style: { display: "flex", gap: "6px", alignItems: "center", fontSize: "12px" } },
17114
+ React.createElement("input", {
17115
+ type: "checkbox",
17116
+ checked: Boolean(state.sound),
17117
+ disabled: Boolean(busy),
17118
+ onChange: (e) => act("settings", { sound: e.target.checked }, ""),
17119
+ }),
17120
+ weT("带声音"),
17121
+ ),
17122
+ opt(weT("最短显示"), "minShowMs", [[0, "不限制"], [600, "0.6 秒"], [1200, "1.2 秒"], [2000, "2 秒"]]),
17123
+ opt(weT("最长停留"), "maxHoldMs", [[10000, "10 秒"], [20000, "20 秒(默认)"], [30000, "30 秒"], [60000, "60 秒"]]),
17124
+ ));
17125
+ }
17126
+
17127
+ children.push(React.createElement("div", { key: "count", className: "we-picker__hint", style: { margin: "2px 0 4px" } },
17128
+ weT("已下载 ") + installedCount + " / " + clips.length));
17129
+
17130
+ if (clips.length) {
17131
+ children.push(React.createElement("div", { key: "grid", style: BOOT_STYLE.grid },
17132
+ clips.map((c) => {
17133
+ const isBusy = busy === c.id;
17134
+ const isActive = c.id === activeId;
17135
+ const btn = (label, onClick, disabled, primary) => React.createElement("button", {
17136
+ type: "button",
17137
+ className: "we-picker__btn" + (primary ? " we-picker__btn--primary" : ""),
17138
+ style: { flex: "1 1 0", fontSize: "11.5px", padding: "4px 6px" },
17139
+ disabled: Boolean(disabled) || Boolean(busy),
17140
+ onClick,
17141
+ }, label);
17142
+ const actions = [];
17143
+ if (!c.installed) {
17144
+ actions.push(btn(isBusy ? (weT("下载中… ") + tick + "s") : weT("下载"), () => act("download", { id: c.id }, c.title), false, true));
17145
+ } else if (!isActive) {
17146
+ actions.push(btn(weT("使用"), () => act("select", { id: c.id }, c.title), false, true));
17147
+ actions.push(btn(weT("删除"), () => act("remove", { id: c.id }, c.title), isBusy, false));
17148
+ } else {
17149
+ actions.push(btn(weT("使用中"), () => {}, true, false));
17150
+ }
17151
+ return React.createElement("div", { key: c.id, style: BOOT_STYLE.card },
17152
+ React.createElement("div", { style: BOOT_STYLE.title }, c.title || c.id),
17153
+ React.createElement("div", { style: BOOT_STYLE.meta },
17154
+ [c.durationSec ? Number(c.durationSec).toFixed(1) + "s" : null,
17155
+ bootFmtBytes(c.localBytes || c.bytes)].filter(Boolean).join(" · ")),
17156
+ c.faststart === false
17157
+ ? React.createElement("div", { style: { ...BOOT_STYLE.meta, color: "#ffd08a" } }, weT("未做 faststart,播放可能更慢"))
17158
+ : null,
17159
+ React.createElement("div", { style: { ...BOOT_STYLE.row, marginTop: "4px" } }, actions),
17160
+ );
17161
+ })));
17162
+ } else if (state) {
17163
+ children.push(React.createElement("div", { key: "empty", className: "we-picker__empty" },
17164
+ React.createElement("span", { className: "we-picker__empty-title" }, weT("没有可用的开机动画")),
17165
+ React.createElement("span", { className: "we-picker__hint" }, weT("素材来自站点;站点不可达时这里为空,但不影响启动"))));
17166
+ }
17167
+
17168
+ return React.createElement(React.Fragment, null, children);
17169
+ }
17170
+
17171
+ function renderBootTab() {
17172
+ return React.createElement(BootTabPanel, null);
17173
+ }
17174
+
17292
17175
  // ── 内联模块:src/quick-panel.js —— 快捷播放面板(官方侧栏 tab 与低版本抽屉共用同一份内容):当前壁纸 + 轮播 + 列表快切 + 声音 + 设置入口。列表虚拟滚动(spacer + 可视窗口)(构建期注入;勿手改本段)──
17293
17176
  /**
17294
17177
  * quick-panel.js — 快捷播放面板(侧边栏的**唯一**内容):当前壁纸 + 轮播 + 三档页签
@@ -17459,10 +17342,9 @@ window.__ModuleLoader__.load({
17459
17342
  // 对照很常见)。与设置页的页签表(PICKER_TABS)是两件事 —— 这里只镜像用户点名要的
17460
17343
  // 两页,外加壁纸页。
17461
17344
  const QP_TABS = [
17345
+ // 开机动画放在「壁纸」左边:它和壁纸是并列的两种"开机时看到什么",不是壁纸的子功能。
17346
+ { id: "boot", get label() { return weT("开机动画"); } },
17462
17347
  { id: "wallpaper", get label() { return weT("壁纸"); } },
17463
- // 「皮肤包」与设置页那个页签是**同一个组件**(SkinsTabPanel):首页这里只是把它挂进抽屉,
17464
- // 不另写一份列表逻辑 —— 两份实现迟早会漂移。
17465
- { id: "skins", get label() { return weT("皮肤包"); } },
17466
17348
  { id: "appearance", get label() { return weT("外观"); } },
17467
17349
  { id: "playback", get label() { return weT("播放"); } },
17468
17350
  ];
@@ -17590,7 +17472,7 @@ window.__ModuleLoader__.load({
17590
17472
  React.createElement("span", { className: "we-qp__item-thumb" },
17591
17473
  w.preview
17592
17474
  ? React.createElement("img", {
17593
- src: w.preview, alt: "", loading: "lazy",
17475
+ src: w.preview, alt: "", loading: "lazy", decoding: "async",
17594
17476
  onError: (e) => { e.target.style.display = "none"; },
17595
17477
  onLoad: (e) => { e.target.style.opacity = "1"; },
17596
17478
  })
@@ -17618,7 +17500,7 @@ window.__ModuleLoader__.load({
17618
17500
  },
17619
17501
  w.preview
17620
17502
  ? React.createElement("img", {
17621
- src: w.preview, alt: "", loading: "lazy",
17503
+ src: w.preview, alt: "", loading: "lazy", decoding: "async",
17622
17504
  onError: (e) => { e.target.style.display = "none"; },
17623
17505
  onLoad: (e) => { e.target.style.opacity = "1"; },
17624
17506
  })
@@ -17827,7 +17709,17 @@ window.__ModuleLoader__.load({
17827
17709
  );
17828
17710
  }
17829
17711
 
17830
- function qpRenderViewBar(sel, view, typeFilter, ratingFilter, switchView) {
17712
+ // 来源档(本机 Wallpaper Engine 库 / 我们站点):与设置页同一个键 `sourceFilter`,
17713
+ // 同样"两处同步"——这里切,设置页跟着变,反之亦然。只筛列表,不拦正在播的壁纸。
17714
+ function qpSources() {
17715
+ return [
17716
+ { id: "all", label: weT("全部来源") },
17717
+ { id: "local", label: weT("本机") },
17718
+ { id: "site", label: weT("站点") },
17719
+ ];
17720
+ }
17721
+
17722
+ function qpRenderViewBar(sel, view, typeFilter, ratingFilter, switchView, sourceFilter) {
17831
17723
  return React.createElement("div", { className: "we-qp__viewbar" },
17832
17724
  React.createElement("input", {
17833
17725
  className: "we-picker__text we-qp__search", type: "text",
@@ -17861,6 +17753,16 @@ window.__ModuleLoader__.load({
17861
17753
  },
17862
17754
  ...qpTypes().map((t) => React.createElement("option", { key: t.id, value: t.id }, t.label)),
17863
17755
  ),
17756
+ // 来源筛选:本机 Wallpaper Engine 库 vs 我们站点(与设置页同一个键)。
17757
+ React.createElement("select", {
17758
+ className: "we-picker__select we-qp__source",
17759
+ value: sourceFilter,
17760
+ onChange: (e) => { setSetting("sourceFilter", e.target.value); emit(); },
17761
+ "aria-label": weT("来源筛选"),
17762
+ title: weT("按来源筛选:本机 Wallpaper Engine 库,还是从我们站点拉下来的壁纸"),
17763
+ },
17764
+ ...qpSources().map((s) => React.createElement("option", { key: s.id, value: s.id }, s.label)),
17765
+ ),
17864
17766
  // 列表 / 卡片:标签式切换(滑动胶囊做激活指示、无分段底 —— 样式见
17865
17767
  // .we-qp__viewtabs),偏好记 localStorage(QP_VIEW_KEY),两个壳共用一份。
17866
17768
  // 语义仍是开关(role=group + aria-pressed),只是外观走标签形态。
@@ -17926,7 +17828,7 @@ window.__ModuleLoader__.load({
17926
17828
  );
17927
17829
  }
17928
17830
 
17929
- function qpRenderLibrarySection(sel, view, typeFilter, ratingFilter, listRef, rows, cardsView, vTopH, vBtmH, q, switchView) {
17831
+ function qpRenderLibrarySection(sel, view, typeFilter, ratingFilter, listRef, rows, cardsView, vTopH, vBtmH, q, switchView, sourceFilter) {
17930
17832
  return React.createElement("div", { className: "we-qp__section we-qp__library" },
17931
17833
  !sel.loaded
17932
17834
  ? React.createElement("span", { className: "we-picker__hint" }, weT("扫描 Wallpaper Engine…"))
@@ -17940,7 +17842,7 @@ window.__ModuleLoader__.load({
17940
17842
  }, weT(sel.loading ? "刷新中…" : "重试")),
17941
17843
  )
17942
17844
  : React.createElement(React.Fragment, null,
17943
- qpRenderViewBar(sel, view, typeFilter, ratingFilter, switchView),
17845
+ qpRenderViewBar(sel, view, typeFilter, ratingFilter, switchView, sourceFilter),
17944
17846
  qpRenderList(listRef, rows, view, cardsView, vTopH, vBtmH, q, ratingFilter, typeFilter, sel),
17945
17847
  ),
17946
17848
  );
@@ -18060,8 +17962,11 @@ window.__ModuleLoader__.load({
18060
17962
  const ratingFilter = qpRatings().some((r) => r.id === sel.contentRatingFilter)
18061
17963
  ? sel.contentRatingFilter : "all";
18062
17964
  const playable = playableInventory();
17965
+ const sourceFilter = qpSources().some((s) => s.id === sel.sourceFilter) ? sel.sourceFilter : "all";
18063
17966
  const filtered = playable.filter((w) => {
18064
17967
  if (typeFilter !== "all" && w.type !== typeFilter) return false;
17968
+ // 站点条目带 `source: "folia"`;其余(本机工坊 + 上传)一律算本机。
17969
+ if (sourceFilter !== "all" && ((w.source === "folia") ? "site" : "local") !== sourceFilter) return false;
18065
17970
  if (q && String(w.title || "").toLowerCase().indexOf(q) === -1) return false;
18066
17971
  return true;
18067
17972
  });
@@ -18096,7 +18001,13 @@ window.__ModuleLoader__.load({
18096
18001
  // 设置页同名页签 —— 玻璃详细调节仍只在侧栏(本页,折叠块),设置页那边留玻璃预设、
18097
18002
  // 全量配置与**字体节**(三次修订后字体节两档都画);播放页的出图来源 /
18098
18003
  // 实时帧仍住设置页。
18099
- const foot = qpTab === "appearance"
18004
+ const foot = qpTab === "boot"
18005
+ ? {
18006
+ label: weT("打开设置 ›"),
18007
+ target: "",
18008
+ title: weT("开机动画的全部参数都在侧栏「壁纸引擎」设置里"),
18009
+ }
18010
+ : qpTab === "appearance"
18100
18011
  ? {
18101
18012
  label: weT("更多外观设置 ›"),
18102
18013
  target: "appearance",
@@ -18108,17 +18019,11 @@ window.__ModuleLoader__.load({
18108
18019
  target: "playback",
18109
18020
  title: weT("在设置页打开「播放」页签 —— 出图来源 / 实时帧 / 帧率上限在那里"),
18110
18021
  }
18111
- : qpTab === "skins"
18112
- ? {
18113
- label: weT("皮肤包设置 ›"),
18114
- target: "skins",
18115
- title: weT("在设置页打开「皮肤包」页签 —— 下载 / 切换 / 删除皮肤包"),
18116
- }
18117
- : {
18118
- label: weT("壁纸引擎设置 ›"),
18119
- target: "",
18120
- title: weT("打开设置对话框的「壁纸引擎」分区(外观 / 播放 / 系统与全部配置)"),
18121
- };
18022
+ : {
18023
+ label: weT("壁纸引擎设置 ›"),
18024
+ target: "",
18025
+ title: weT("打开设置对话框的「壁纸引擎」分区(外观 / 播放 / 系统与全部配置)"),
18026
+ };
18122
18027
 
18123
18028
  return React.createElement("div", { className: "we-qp we-qp--" + dock },
18124
18029
  qpRenderCurrent(sel, current, playbackLive),
@@ -18134,7 +18039,9 @@ window.__ModuleLoader__.load({
18134
18039
  React.createElement("div", { className: tabBodyClass, ref: qpPaneRef },
18135
18040
  qpTab === "wallpaper" && userPropsPanelOpen() && propsAvailable
18136
18041
  && React.createElement("div", { className: "we-qp__propsview we-qp__propsview--drill" }, renderUserPropsPanel()),
18137
- !(qpTab === "wallpaper" && userPropsPanelOpen() && propsAvailable) && (qpTab === "appearance"
18042
+ // 开机动画档:整区就是那个面板(它自己管加载/下载/开关)。
18043
+ qpTab === "boot" && React.createElement(BootTabPanel, null),
18044
+ !(qpTab === "wallpaper" && userPropsPanelOpen() && propsAvailable) && qpTab !== "boot" && (qpTab === "appearance"
18138
18045
  ? qpRenderAppearancePane(sel, {
18139
18046
  fontOpen, onToggleFontSection: toggleFontSection,
18140
18047
  uiColorOpen, onToggleUiColorSection: toggleUiColorSection,
@@ -18148,15 +18055,12 @@ window.__ModuleLoader__.load({
18148
18055
  })
18149
18056
  : qpTab === "playback"
18150
18057
  ? qpRenderPlaybackPane(sel, switchQpTab)
18151
- // 皮肤包:直接挂设置页那个同一个组件(它自己管列表/下载/激活状态)。
18152
- : qpTab === "skins"
18153
- ? React.createElement(SkinsTabPanel, { onClearWallpaper: () => setSetting("id", "") })
18154
- : null),
18058
+ : null),
18155
18059
  !(qpTab === "wallpaper" && userPropsPanelOpen() && propsAvailable) && qpTab === "wallpaper" && React.createElement(React.Fragment, null,
18156
18060
  // ── 壁纸列表(搜索 + 视图切换 + 快切)──
18157
18061
  // 库还没回来 / 扫描失败:**只有这一页**给一句话 —— 外观 / 播放两页不依赖库,
18158
18062
  // 照常可用(不能整块替换掉,那样连外观都进不去)。
18159
- qpRenderLibrarySection(sel, view, typeFilter, ratingFilter, listRef, rows, cardsView, vTopH, vBtmH, q, switchView),
18063
+ qpRenderLibrarySection(sel, view, typeFilter, ratingFilter, listRef, rows, cardsView, vTopH, vBtmH, q, switchView, sourceFilter),
18160
18064
  // 声音(与播放页那份同源:同一份渲染器、同一状态)。包一层 .we-qp__section
18161
18065
  // 只为保留这一节的分隔线与间距(渲染器自己那份 section 是给设置页排版的)。
18162
18066
  React.createElement("div", { className: "we-qp__section" },
@@ -22517,34 +22421,6 @@ window.__ModuleLoader__.load({
22517
22421
  /** DELETE(多数接口不返回体;需要时传 parse: true)。 */
22518
22422
  const apiDelete = (path, options) => apiFetch(path, Object.assign({ method: 'DELETE', parse: false }, options));
22519
22423
 
22520
- /**
22521
- * 根相对路径的**原生**请求(**不补 BASE**),返回原始 `Response`(失败给 null)。
22522
- *
22523
- * 用途:访问**同源其它插件**的路由 —— 典型是皮肤中心的 `/api/skin-center/v2/...`
22524
- *(它把皮肤 CSS 做了强制作用域化,只有那条路由给的版本才能压过应用自身规则)。
22525
- *
22526
- * 为什么必须是**根相对路径**而不是 `location.origin + path`:桌面壳里文档可能跑在
22527
- * 自定义协议下(`dsh-app://…`),此时 `location.origin` 是 `"null"`,拼出来的绝对 URL
22528
- * 直接作废。`/api/...` 交给浏览器按文档解析,两种协议下都成立 —— 皮肤中心自己的客户端
22529
- * 用的也正是这种写法。
22530
- *
22531
- * @param {string} path 以 `/` 开头的同源路径
22532
- * @param {{ method?: string, cache?: string, headers?: object, fetch?: Function }} [options]
22533
- * @returns {Promise<Response|null>}
22534
- */
22535
- async function apiRawResponse(path, options) {
22536
- const o = options || {};
22537
- const doFetch = pickFetch(o.fetch);
22538
- if (!doFetch) return null;
22539
- const init = { method: o.method || 'GET', cache: o.cache || 'no-store' };
22540
- if (o.headers) init.headers = o.headers;
22541
- try {
22542
- return await doFetch(String(path), init);
22543
- } catch {
22544
- return null;
22545
- }
22546
- }
22547
-
22548
22424
  // ── 内联模块:src/occlusion.js —— 遮挡投递:/occlusion 轮询 + 缩放 + postMessage(1Hz,含退避与可见性暂停)(构建期注入;勿手改本段)──
22549
22425
  /**
22550
22426
  * occlusion.js — Linux 壳窗遮挡的**客户端轮询器**:宿主 `/occlusion` 快照 → 画布局部
@@ -25729,38 +25605,6 @@ window.__ModuleLoader__.load({
25729
25605
  try { return skinYielded === true; } catch { return false; }
25730
25606
  }
25731
25607
 
25732
- /**
25733
- * 让路期间把**整张插件样式表**关掉。
25734
- *
25735
- * 为什么必须关整张:皮肤是一套完整的外观(自己的底色、圆角、字体色、边框)。我们的
25736
- * `styles-base.js` 给应用外壳铺透明/玻璃/自定义配色,选择器落在同一批元素上,于是皮肤
25737
- * 的规则被我们盖掉 —— 表现就是"皮肤显示不全,关掉插件就完整"。让路态此前只关了玻璃
25738
- * 效果族(`data-we-*` 属性门控),**样式表本体一直在**,所以这个洞一直存在。
25739
- *
25740
- * 用 `sheet.disabled` 而不是摘 `<style>`:摘了再插会触发整页重排;disabled 是零成本开关,
25741
- * 退出让路时立刻原样恢复。
25742
- */
25743
- function syncPluginCssForSkin() {
25744
- // ⚠️ 默认**不**整表禁用。3.9.0 试过一次"皮肤在台就关掉整张样式表",代价是:
25745
- // 我们的面板失去可读底色,皮肤的底图直接透上来,字全糊 —— 插件等于不能用了。
25746
- // 皮肤和壁纸是两个都在重绘整个界面的产品,CSS 层面无法各让一半:整表关掉
25747
- // 皮肤完整但我们不可用,整表开着我们可用但皮肤被压。折中只保留"玻璃效果族
25748
- // 退场"(它已经由 data-we-* 属性门控),样式表本体始终在 —— 保证可读性优先。
25749
- // 需要皮肤完全接管时,用环境变量 DSH_FOLIA_YIELD_CSS=1 显式开启整表让位。
25750
- // 前端没有 process.env(那是宿主侧的),开关放在 localStorage:
25751
- // localStorage.setItem("folia.yieldCss", "1") → 刷新后启用整表让位
25752
- let want = false;
25753
- try { want = window.localStorage.getItem("folia.yieldCss") === "1"; } catch { want = false; }
25754
- if (!want) return;
25755
- try {
25756
- const tag = document.querySelector('style[data-plugin-css="dsh-folia/styles-v3"]');
25757
- if (tag === null) return;
25758
- const off = skinYieldActive() === true;
25759
- if (tag.sheet) tag.sheet.disabled = off;
25760
- else tag.disabled = off;
25761
- } catch { /* 拿不到 sheet(还在解析 / 被 CSP 拦)就跳过,不影响其它门控 */ }
25762
- }
25763
-
25764
25608
  /** 皮肤上台 ⇒ 退场(幂等)。 */
25765
25609
  function enterSkinYield(reason) {
25766
25610
  if (skinYielded) return;
@@ -25775,7 +25619,6 @@ window.__ModuleLoader__.load({
25775
25619
  applySelection(""); // 清层:url=null ⇒ syncLayers 摘层 / 遮罩 / data-we-wallpaper
25776
25620
  persistSelection();
25777
25621
  applyEffects(); // 让路态里玻璃门控整族摘除
25778
- syncPluginCssForSkin(); // 连同整张样式表一起让位 —— 否则皮肤被我们的外框盖住(显示不全)
25779
25622
  emit();
25780
25623
  reportClientDiag("skin-yield", "enter · " + reason + " · restore=" + (skinYieldMemory.id || "-"));
25781
25624
  }
@@ -25798,7 +25641,6 @@ window.__ModuleLoader__.load({
25798
25641
  setSetting("skinYieldRestoreRotation", false);
25799
25642
  persistSelection();
25800
25643
  applyEffects();
25801
- syncPluginCssForSkin(); // 皮肤退场:把我们的样式表原样开回来
25802
25644
  emit();
25803
25645
  reportClientDiag("skin-yield", "exit · " + reason);
25804
25646
  }
@@ -25918,7 +25760,6 @@ window.__ModuleLoader__.load({
25918
25760
  rotationEnabled: rid ? selection.skinYieldRestoreRotation === true : selection.rotationEnabled === true,
25919
25761
  };
25920
25762
  applyEffects(); // 认领即刻摘玻璃门控(首次 applyEffects 可能已经跑过)
25921
- syncPluginCssForSkin(); // 启动即认领时也要让出样式表:否则一开机皮肤就被外框盖着
25922
25763
  reportClientDiag("skin-yield", "adopt · install/dom");
25923
25764
  } else if (!document.documentElement.hasAttribute("data-dsh-skin") && !selection.id
25924
25765
  && String(selection.skinYieldRestoreId || "")) {
@@ -25940,7 +25781,6 @@ window.__ModuleLoader__.load({
25940
25781
  try { unsubAdopt(); } catch { /* ignore */ }
25941
25782
  skinYielded = false;
25942
25783
  skinYieldMemory = null;
25943
- syncPluginCssForSkin(); // 清理后让路态清零 ⇒ 样式表恢复
25944
25784
  };
25945
25785
  }
25946
25786
 
@@ -27211,9 +27051,6 @@ window.__ModuleLoader__.load({
27211
27051
  const PICKER_TAB_KEY = "dsh-folia:picker-tab";
27212
27052
  const PICKER_TABS = [
27213
27053
  { id: "library", get label() { return weT("壁纸库"); } },
27214
- // 「皮肤包」挨着「壁纸库」:两者都是"给背景换东西",用户心智在同一处。
27215
- // 它列的是**自建站**上的皮肤包(宿主端 /skins 族),不是皮肤中心的内置列表。
27216
- { id: "skins", get label() { return weT("皮肤包"); } },
27217
27054
  { id: "appearance", get label() { return weT("外观"); } },
27218
27055
  { id: "playback", get label() { return weT("播放"); } },
27219
27056
  { id: "system", get label() { return weT("系统"); } },
@@ -29215,6 +29052,12 @@ window.__ModuleLoader__.load({
29215
29052
  setSetting("typeFilter", e.target.value);
29216
29053
  revalidateSelection();
29217
29054
  };
29055
+ // 来源筛选(本机 Wallpaper Engine 库 / 我们站点):只筛列表与轮播候选,
29056
+ // 与类型档同一个效力 —— 不写进播放闸门,切换不会把正在播的壁纸掐掉。
29057
+ const onSourceFilterChange = (e) => {
29058
+ setSetting("sourceFilter", e.target.value);
29059
+ revalidateSelection();
29060
+ };
29218
29061
  // Card style: classic (CD-rack) vs fixed (overlap-proof).
29219
29062
  const onLayoutChange = (value) => {
29220
29063
  setSetting("pickerLayout", value);
@@ -29759,7 +29602,7 @@ window.__ModuleLoader__.load({
29759
29602
  // 标题子串匹配(空查询 = 不过滤),每页 24 张,页号越界自动 clamp。这里只把状态喂
29760
29603
  // 进去、把结果取出来 —— 组件体不再就地算这些(分级/类型/隐藏的判定也在那边)。
29761
29604
  const {
29762
- query, playableList, basePlayable, ratingCounts, typeCounts, hiddenList,
29605
+ query, playableList, basePlayable, ratingCounts, typeCounts, sourceCounts, hiddenList,
29763
29606
  editorPageView,
29764
29607
  } = pickerModel({
29765
29608
  wallpapers: list,
@@ -29767,6 +29610,7 @@ window.__ModuleLoader__.load({
29767
29610
  search: sel.search,
29768
29611
  ratingFilter: sel.contentRatingFilter,
29769
29612
  typeFilter: sel.typeFilter,
29613
+ sourceFilter: sel.sourceFilter,
29770
29614
  editorPage: sel.editorPage,
29771
29615
  });
29772
29616
  // CD-rack mode: compact one-page grid (no pagination, no virtualization —
@@ -29830,9 +29674,6 @@ window.__ModuleLoader__.load({
29830
29674
  onReopenNotice: () => { noticeReopenRequest = true; emit(); },
29831
29675
  });
29832
29676
  if (activeTab === "extensions") return renderExtensionsTab(extensionCtx());
29833
- // 「皮肤包」自带异步状态(列表 / 下载中 / 错误),所以它是一个**真组件**而不是
29834
- // 纯渲染函数 —— 薄包装只负责挂上去(理由见 src/panel-tabs-skins.js 文件头)。
29835
- if (activeTab === "skins") return renderSkinsTab({ onClearWallpaper: () => setSetting("id", "") });
29836
29677
  if (activeTab === "appearance") return renderAppearanceTab({
29837
29678
  setSetting, setTransient,
29838
29679
  fontSet: fontSetCtx(),
@@ -29929,10 +29770,10 @@ window.__ModuleLoader__.load({
29929
29770
  // 壁纸网格,ESC / 顶部「返回」退出,切页签也会退出(见 switchTab)。
29930
29771
  sel.pickerOpen ? renderPickerModal({
29931
29772
  sel, closePicker, current, playbackLive, playableList, hiddenList,
29932
- cdMode, query, basePlayable, ratingCounts, typeCounts,
29773
+ cdMode, query, basePlayable, ratingCounts, typeCounts, sourceCounts,
29933
29774
  gridRef: pickerGridRef, vwin: pickerVwin,
29934
29775
  armedConfirm: sel.armedConfirm, onArmConfirm: armConfirm, onDisarmConfirm: disarmConfirm,
29935
- onClear, onRatingFilterChange, onTypeFilterChange,
29776
+ onClear, onRatingFilterChange, onTypeFilterChange, onSourceFilterChange,
29936
29777
  onShowNormalView, onShowHiddenView,
29937
29778
  onToggleBatchMode, onArmBatchHide, onBatchHide, onBatchCancel, onSearchInput, onPickCard,
29938
29779
  }) : React.createElement(React.Fragment, null,