dsh-folia 2.5.1 → 2.5.8

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,401 +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
- * 为什么是一个**真正的 React 组件**(而不是像别的页签那样是纯渲染函数):
16176
- * 这个页签有自己的异步状态(列表 / 加载中 / 下载中 / 错误),而页签渲染器是在
16177
- * `renderActiveTab` 里被**条件调用**的普通函数 —— 在里面放 hooks 会违反 hooks 顺序。
16178
- * 所以这里导出一个组件 `SkinsTabPanel`,由薄包装 `renderSkinsTab(ctx)` 挂上去:
16179
- * 组件实例化后 hooks 是稳定的,父级重渲染也不会错位。
16180
- *
16181
- * 契约与别的 `panel-tabs-*.js` 相同:构建期被内联进 bundle 的工厂作用域,
16182
- * 因此 `React` / `apiUrl` / `apiJson` / `apiPostJson` / `weT` 都是同作用域的既有名字,
16183
- * 本文件**不 import 任何东西**。
16184
- *
16185
- * 数据流(全部走宿主端,浏览器不直连站点):
16186
- * apiJson("/skins") → 索引 + 已装/在用标记
16187
- * apiPostJson("/skins/install", {id}) → 下载 / 校验 / 解压
16188
- * apiPostJson("/skins/activate", {id}) → 写 skin-center-active.json
16189
- * apiPostJson("/skins/remove", {id}) → 删目录
16190
- * apiUrl("/skins/preview?p=…") → 预览图代理
16191
- */
16192
-
16193
- /** 卡片网格与卡片本体的样式:内联,避免为一个页签去动全局样式表。 */
16194
- const SKINS_STYLE = {
16195
- grid: {
16196
- display: "grid",
16197
- gridTemplateColumns: "repeat(auto-fill, minmax(168px, 1fr))",
16198
- gap: "10px",
16199
- marginTop: "10px",
16200
- },
16201
- card: {
16202
- display: "flex", flexDirection: "column", overflow: "hidden",
16203
- border: "1px solid rgba(255,255,255,0.12)", borderRadius: "10px",
16204
- background: "rgba(255,255,255,0.04)",
16205
- },
16206
- thumb: { width: "100%", aspectRatio: "4 / 3", objectFit: "cover", display: "block", background: "rgba(0,0,0,0.35)" },
16207
- body: { padding: "8px 9px 9px", display: "flex", flexDirection: "column", gap: "4px", flex: "1 1 auto" },
16208
- title: { fontSize: "12.5px", fontWeight: 600, lineHeight: 1.3 },
16209
- meta: { fontSize: "11px", opacity: 0.62, lineHeight: 1.3 },
16210
- tags: { display: "flex", flexWrap: "wrap", gap: "3px", marginTop: "2px" },
16211
- tag: { fontSize: "10px", padding: "1px 5px", borderRadius: "999px", background: "rgba(255,255,255,0.09)", opacity: 0.75 },
16212
- actions: { display: "flex", gap: "6px", marginTop: "6px" },
16213
- };
16214
-
16215
- function skinsFmtBytes(n) {
16216
- const v = Number(n) || 0;
16217
- if (v >= 1024 * 1024) return (v / 1024 / 1024).toFixed(1) + " MB";
16218
- if (v >= 1024) return Math.round(v / 1024) + " KB";
16219
- return v + " B";
16220
- }
16221
-
16222
- function skinsErrText(code) {
16223
- if (!code) return "";
16224
- if (code === "disabled") return weT("站点数据源已关闭(DSH_FOLIA_SKINS_SITE=off)");
16225
- if (code === "timeout") return weT("站点超时,点「刷新」重试");
16226
- if (code === "tls") return weT("站点证书校验失败");
16227
- if (code === "network") return weT("连不上站点(网络或域名问题)");
16228
- if (/^http-/.test(code)) return weT("站点返回错误:") + code.slice(5);
16229
- if (code === "json") return weT("站点索引格式不对");
16230
- return code;
16231
- }
16232
-
16233
- /**
16234
- * 把一套皮肤的样式**当场**挂进文档。
16235
- *
16236
- * 为什么是 `<style>` 而不是 `<link>`:
16237
- * · `<link rel=stylesheet>` 是**异步**的,加载完成的时机不定,容易输给皮肤中心随后
16238
- * 重新注入的那份;
16239
- * · `<style>` 一赋值就参与样式计算,且追加到 `<body>` **末尾** ⇒ 文档顺序最后,
16240
- * 同等特异性下后写的赢。这是"不刷新换肤"能真正看见的关键。
16241
- *
16242
- * 仍然不跑 `hooks.mjs`(粒子等动效),那部分只有整页重载才生效。
16243
- *
16244
- * @param {string} id 皮肤 id
16245
- * @returns {Promise<{ bytes: number, rules: number }>}
16246
- */
16247
- async function applySkinCss(id) {
16248
- const resp = await apiFetch("/skins/css?id=" + encodeURIComponent(id) + "&t=" + Date.now(), {
16249
- parse: false,
16250
- cache: "no-store",
16251
- });
16252
- if (!resp || !resp.ok || !resp.response) throw new Error("css-http-" + ((resp && resp.status) || 0));
16253
- const css = await resp.response.text();
16254
- if (!css || !css.trim()) throw new Error("css-empty");
16255
- let el = document.getElementById("folia-skin-hot");
16256
- if (!el) {
16257
- el = document.createElement("style");
16258
- el.id = "folia-skin-hot";
16259
- // 挂 body 末尾(不是 head):文档顺序最后 ⇒ 同等特异性下覆盖先前的规则。
16260
- (document.body || document.head).appendChild(el);
16261
- }
16262
- el.textContent = css;
16263
- let rules = 0;
16264
- try { rules = (el.sheet && el.sheet.cssRules && el.sheet.cssRules.length) || 0; } catch { /* 读不到就报 0 */ }
16265
- return { bytes: css.length, rules };
16266
- }
16267
-
16268
- function SkinsTabPanel() {
16269
- const [list, setList] = React.useState([]);
16270
- const [active, setActive] = React.useState(null);
16271
- const [loading, setLoading] = React.useState(true);
16272
- const [error, setError] = React.useState(null);
16273
- const [harnessFound, setHarnessFound] = React.useState(true);
16274
- const [busy, setBusy] = React.useState(""); // 正在安装 / 激活的那个 id
16275
- const [note, setNote] = React.useState(null); // 一次性提示(成功 / 失败原因)
16276
- const [q, setQ] = React.useState("");
16277
-
16278
- const load = React.useCallback(async (force) => {
16279
- setLoading(true);
16280
- setError(null);
16281
- try {
16282
- // ⚠️ `apiJson` 返回的是 **`{ ok, status, data, error }` 包装对象**,不是响应体本身
16283
- //(见 src/api-client.js 的 apiFetch)。最初按裸体取值 ⇒ `list=[] 且 error=null`,
16284
- // 界面显示"没有匹配的皮肤包"却没有任何错误提示 —— 静默空列表比报错难查得多。
16285
- // `parse: "always"` 让非 2xx 的 `{error}` 也能被读到。
16286
- const resp = await apiJson("/skins" + (force ? "?refresh=1" : ""), { parse: "always" });
16287
- const r = (resp && resp.data) || null;
16288
- setList((r && r.skins) || []);
16289
- setActive((r && r.active) || null);
16290
- setError((r && r.error) || (resp && resp.error) || null);
16291
- setHarnessFound(!r || r.harnessFound !== false);
16292
- } catch (e) {
16293
- setError("network");
16294
- setList([]);
16295
- } finally {
16296
- setLoading(false);
16297
- }
16298
- }, []);
16299
-
16300
- React.useEffect(() => { load(false); }, [load]);
16301
-
16302
- /**
16303
- * 执行一个动作(install / activate / remove)。
16304
- *
16305
- * ⚠️ **「使用」必须立刻有反馈**:切换皮肤只写 `skin-center-active.json`,host 端 6 ms 就应,
16306
- * 而皮肤要重启才生效 —— 如果等一次 `?refresh=1`(约 600 ms)再改标记,用户看到的就是
16307
- * "点了一下,半天没反应"。所以「使用」走**乐观更新**:先本地翻转 active 并把提示打出来,
16308
- * 请求失败再回滚重拉。装 / 删仍然要重拉(已装标记与体积都变了)。
16309
- */
16310
- async function act(kind, id, label) {
16311
- if (busy) return;
16312
- setBusy(id);
16313
- setNote(null);
16314
- const prevActive = active;
16315
- if (kind === "activate") {
16316
- // 乐观:先给反馈,再等网络
16317
- setActive(id);
16318
- setList((prev) => prev.map((x) => ({ ...x, active: x.id === id })));
16319
- setNote({ kind: "ok", text: weT("正在切换到「") + label + weT("」…") });
16320
- }
16321
- try {
16322
- // 同样是包装对象:`ok` 是 HTTP 2xx,业务结论在 `data.ok`(宿主回 `{ok:false,error}`)。
16323
- const resp = await apiPostJson("/skins/" + kind, { id }, { parse: "always" });
16324
- const r = (resp && resp.data) || null;
16325
- if (resp && resp.ok && r && r.ok) {
16326
- if (kind === "install") {
16327
- setNote({ kind: "ok", text: weT("已安装:") + label });
16328
- await load(true);
16329
- } else if (kind === "activate") {
16330
- // **当场换肤**:把皮肤样式同步注入文档,不刷新、不重启。
16331
- // 注入结果直接写进提示里(字节数 / 规则条数):万一样式还是没盖住皮肤中心,
16332
- // 这一行就是判断"是没注入"还是"注入了但被压住"的依据,不用再猜。
16333
- let info = null;
16334
- let applyErr = "";
16335
- try {
16336
- info = await applySkinCss(id);
16337
- } catch (e) {
16338
- applyErr = String((e && e.message) || e);
16339
- }
16340
- setNote({
16341
- kind: applyErr ? "err" : "ok",
16342
- text: applyErr
16343
- ? weT("已切换为「") + label + weT("」,但样式注入失败:") + applyErr
16344
- : weT("已切换到「") + label + weT("」—— 已注入 ")
16345
- + Math.round(info.bytes / 1024) + " KB / " + info.rules + weT(" 条样式规则"),
16346
- // hooks(粒子等动效)只有整页重载才会跑:给一个可选入口,不强制。
16347
- reload: true,
16348
- });
16349
- // 不再整表重拉:本地标记已经对了,省掉那 600 ms 的"没反应"窗口。
16350
- } else if (kind === "remove") {
16351
- setNote({ kind: "ok", text: weT("已删除:") + label });
16352
- await load(true);
16353
- }
16354
- } else {
16355
- const code = (r && r.error) || "failed";
16356
- const detail = (r && r.detail) ? "(" + r.detail + ")" : "";
16357
- setNote({ kind: "err", text: weT("操作失败:") + code + detail });
16358
- if (kind === "activate") {
16359
- // 回滚乐观更新
16360
- setActive(prevActive);
16361
- setList((prev) => prev.map((x) => ({ ...x, active: x.id === prevActive })));
16362
- }
16363
- }
16364
- } catch (e) {
16365
- setNote({ kind: "err", text: weT("请求失败:") + (e && e.message ? e.message : weT("网络错误")) });
16366
- if (kind === "activate") {
16367
- setActive(prevActive);
16368
- setList((prev) => prev.map((x) => ({ ...x, active: x.id === prevActive })));
16369
- }
16370
- } finally {
16371
- setBusy("");
16372
- }
16373
- }
16374
-
16375
- const needle = q.trim().toLowerCase();
16376
- const shown = needle
16377
- ? list.filter((s) => (s.name + " " + s.nameEn + " " + s.id + " " + (s.tags || []).join(" "))
16378
- .toLowerCase().indexOf(needle) !== -1)
16379
- : list;
16380
-
16381
- const installedCount = list.filter((s) => s.installed).length;
16382
-
16383
- const header = React.createElement("div", { className: "we-picker__section-head" },
16384
- React.createElement("span", { className: "we-picker__section-label" },
16385
- weT("🎨 皮肤包") + (list.length ? " · " + list.length : "")),
16386
- React.createElement("button", {
16387
- type: "button",
16388
- className: "we-picker__btn",
16389
- style: { marginLeft: "auto" },
16390
- disabled: loading,
16391
- onClick: () => load(true),
16392
- title: weT("重新从站点拉取列表"),
16393
- }, loading ? weT("加载中…") : weT("刷新")),
16394
- );
16395
-
16396
- if (!harnessFound) {
16397
- return React.createElement(React.Fragment, null,
16398
- React.createElement("div", { className: "we-picker__section" }, header,
16399
- React.createElement("div", { className: "we-picker__empty" },
16400
- React.createElement("span", { className: "we-picker__empty-title" }, weT("找不到 DSH 的 skins 目录")),
16401
- React.createElement("span", { className: "we-picker__hint" },
16402
- weT("皮肤包要装到 DSH 的 harness/skins 下。可用环境变量 DSH_FOLIA_HARNESS 指定该目录")))));
16403
- }
16404
-
16405
- const children = [React.createElement("div", { className: "we-picker__section", key: "sec" }, header)];
16406
- // 常驻一行「当前皮肤」:切换只写文件、皮肤要重启才生效,不给这一行用户就无从判断
16407
- // 刚才那次点击到底成没成(这是"点了没反应"的一半原因)。
16408
- if (active) {
16409
- const activeName = (list.find((x) => x.id === active) || {}).name || active;
16410
- children.push(React.createElement("div", {
16411
- key: "active",
16412
- className: "we-picker__hint",
16413
- style: { margin: "4px 0 8px", display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap" },
16414
- },
16415
- React.createElement("span", null, weT("当前皮肤:") + activeName),
16416
- React.createElement("span", {
16417
- style: { fontSize: "11px", padding: "1px 6px", borderRadius: "999px", background: "rgba(142,230,168,0.16)", color: "#8ee6a8" },
16418
- }, weT("已应用")),
16419
- ));
16420
- }
16421
-
16422
- // ⚠️ **壁纸在台时皮肤不会生效** —— 这是皮肤中心写死的行为,不是我们的 bug:
16423
- // · host 侧:peer(= 本插件)持久化的壁纸选择非空时,它从文档里**扣住**皮肤;
16424
- // · client 侧:它盯 `body[data-we-wallpaper]`,只要那个标记在,就把皮肤让位
16425
- // (源码注释:`an active WE wallpaper suppresses skin media`)。
16426
- // 于是"点了使用但界面没变"最可能的原因就是**壁纸还开着**。这里直接读那个标记,
16427
- // 命中就把话说清楚,并告诉用户该先做什么。
16428
- let wallpaperOn = false;
16429
- try {
16430
- wallpaperOn = Boolean(document.body && document.body.hasAttribute("data-we-wallpaper"));
16431
- } catch { /* 拿不到 DOM 就当没有 */ }
16432
- if (wallpaperOn) {
16433
- children.push(React.createElement("div", {
16434
- key: "wpwarning",
16435
- className: "we-picker__hint",
16436
- style: {
16437
- margin: "4px 0 10px",
16438
- padding: "7px 10px",
16439
- borderRadius: "8px",
16440
- background: "rgba(255,196,120,0.12)",
16441
- color: "#ffd08a",
16442
- lineHeight: 1.5,
16443
- },
16444
- }, weT("⚠️ 当前有壁纸在台:皮肤会被壁纸让位,切换不会生效。")
16445
- + weT(" 先到「壁纸」页签点「清除」,再回来切换皮肤。")));
16446
- }
16447
-
16448
- if (error) {
16449
- children.push(React.createElement("div", {
16450
- key: "err",
16451
- className: "we-picker__hint",
16452
- style: { color: "#ff9a9a", margin: "6px 0 10px" },
16453
- }, skinsErrText(error)));
16454
- }
16455
- if (note) {
16456
- children.push(React.createElement("div", {
16457
- key: "note",
16458
- className: "we-picker__hint",
16459
- style: {
16460
- color: note.kind === "err" ? "#ff9a9a" : "#8ee6a8",
16461
- margin: "6px 0 10px",
16462
- display: "flex",
16463
- gap: "10px",
16464
- alignItems: "center",
16465
- flexWrap: "wrap",
16466
- },
16467
- },
16468
- React.createElement("span", null, note.text),
16469
- note.reload
16470
- ? React.createElement("button", {
16471
- type: "button",
16472
- className: "we-picker__btn we-picker__btn--primary",
16473
- style: { fontSize: "11.5px", padding: "3px 10px" },
16474
- onClick: () => {
16475
- // 与自动那条路一致:优先硬跳转,退而 reload。只重载前端页面 ——
16476
- // 对话与设置都在宿主侧,不会丢。
16477
- try { window.location.replace(window.location.href); } catch {
16478
- try { window.location.reload(); } catch { /* 极端环境刷不动就让用户手动重启 */ }
16479
- }
16480
- },
16481
- title: weT("整页重载 —— 皮肤动效(粒子等)需要重载才跑;纯配色已即时生效"),
16482
- }, weT("重载以播放动效"))
16483
- : null,
16484
- ));
16485
- }
16486
- children.push(React.createElement("div", {
16487
- key: "search",
16488
- style: { display: "flex", gap: "8px", alignItems: "center", margin: "2px 0 4px" },
16489
- },
16490
- React.createElement("input", {
16491
- type: "text",
16492
- value: q,
16493
- placeholder: weT("搜索名称 / 作者 / 标签"),
16494
- className: "we-picker__search",
16495
- onChange: (e) => setQ(e.target.value),
16496
- style: { flex: "1 1 auto" },
16497
- }),
16498
- React.createElement("span", { className: "we-picker__hint" },
16499
- weT("已装 ") + installedCount + " / " + list.length),
16500
- ));
16501
-
16502
- if (loading && list.length === 0) {
16503
- children.push(React.createElement("div", { key: "loading", className: "we-picker__empty" },
16504
- React.createElement("span", { className: "we-picker__empty-title" }, weT("正在从站点拉取皮肤包…"))));
16505
- } else if (!error && shown.length === 0) {
16506
- children.push(React.createElement("div", { key: "empty", className: "we-picker__empty" },
16507
- React.createElement("span", { className: "we-picker__empty-title" }, weT("没有匹配的皮肤包")),
16508
- React.createElement("span", { className: "we-picker__hint" }, weT("换个关键词,或点「刷新」重新拉取"))));
16509
- }
16510
-
16511
- if (shown.length > 0) {
16512
- children.push(React.createElement("div", { key: "grid", style: SKINS_STYLE.grid },
16513
- shown.map((s) => {
16514
- const previewRel = s.previewLight || s.previewDark || "";
16515
- const previewSrc = previewRel
16516
- ? apiUrl("/skins/preview?p=" + encodeURIComponent(previewRel))
16517
- : "";
16518
- const isBusy = busy === s.id;
16519
- const btn = (label, onClick, disabled, primary) => React.createElement("button", {
16520
- type: "button",
16521
- className: "we-picker__btn" + (primary ? " we-picker__btn--primary" : ""),
16522
- style: { flex: "1 1 0", fontSize: "11.5px", padding: "4px 6px" },
16523
- disabled: Boolean(disabled) || Boolean(busy),
16524
- onClick,
16525
- }, label);
16526
-
16527
- const actions = [];
16528
- if (!s.installed) {
16529
- actions.push(btn(isBusy ? weT("安装中…") : weT("下载安装"), () => act("install", s.id, s.name), false, true));
16530
- } else {
16531
- if (!s.active) {
16532
- actions.push(btn(isBusy ? weT("切换中…") : weT("使用"), () => act("activate", s.id, s.name), false, true));
16533
- actions.push(btn(weT("删除"), () => act("remove", s.id, s.name), isBusy, false));
16534
- } else {
16535
- actions.push(btn(weT("使用中"), () => {}, true, false));
16536
- }
16537
- }
16538
-
16539
- return React.createElement("div", { key: s.id, style: SKINS_STYLE.card },
16540
- previewSrc
16541
- ? React.createElement("img", {
16542
- src: previewSrc, alt: s.name, loading: "lazy", draggable: false, style: SKINS_STYLE.thumb,
16543
- })
16544
- : React.createElement("div", { style: SKINS_STYLE.thumb }),
16545
- React.createElement("div", { style: SKINS_STYLE.body },
16546
- React.createElement("div", { style: SKINS_STYLE.title }, s.name || s.id),
16547
- s.tagline ? React.createElement("div", { style: SKINS_STYLE.meta }, s.tagline) : null,
16548
- (s.tags || []).length
16549
- ? React.createElement("div", { style: SKINS_STYLE.tags },
16550
- s.tags.slice(0, 4).map((t) => React.createElement("span", { key: t, style: SKINS_STYLE.tag }, t)))
16551
- : null,
16552
- React.createElement("div", { style: SKINS_STYLE.actions }, actions),
16553
- ),
16554
- );
16555
- })));
16556
- }
16557
-
16558
- return React.createElement(React.Fragment, null, children);
16559
- }
16560
-
16561
- /** 页签薄包装:`renderActiveTab` 只认这个签名(与别的 `render*Tab(ctx)` 一致)。 */
16562
- function renderSkinsTab(ctx) {
16563
- return React.createElement(SkinsTabPanel, null);
16564
- }
16565
-
16566
16174
  // ── 内联模块:src/picker-model.js —— 选择器模型层:过滤判定 + 派生数据 + 分页切片(纯函数 / 显式入参 / 零 DOM)(构建期注入;勿手改本段)──
16567
16175
  /**
16568
16176
  * picker-model.js — 壁纸选择器的**模型层**:过滤判定 + 派生数据 + 分页切片。
@@ -16667,8 +16275,25 @@ window.__ModuleLoader__.load({
16667
16275
  return w.type === "scene" && Boolean(w.frameUrl);
16668
16276
  }
16669
16277
 
16670
- function isRotatableWallpaper(w, ratingFilter, typeFilter) {
16671
- 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);
16672
16297
  }
16673
16298
 
16674
16299
  // 「正在应用的壁纸可以继续播」判据:可播放 ∧ 分级闸门,**不过滤类型**。过滤档分两种
@@ -16692,9 +16317,9 @@ window.__ModuleLoader__.load({
16692
16317
  }
16693
16318
 
16694
16319
  /** 可轮换列表:可播放类型 ∧ 命中两级过滤 ∧ 未隐藏(网格与轮换编辑器共用的那份)。 */
16695
- function playableWallpapers(wallpapers, ratingFilter, typeFilter, hiddenIds) {
16320
+ function playableWallpapers(wallpapers, ratingFilter, typeFilter, hiddenIds, sourceFilter) {
16696
16321
  return wallpapers.filter(
16697
- (w) => isRotatableWallpaper(w, ratingFilter, typeFilter) && !isHiddenWallpaper(w.id, hiddenIds),
16322
+ (w) => isRotatableWallpaper(w, ratingFilter, typeFilter, sourceFilter) && !isHiddenWallpaper(w.id, hiddenIds),
16698
16323
  );
16699
16324
  }
16700
16325
 
@@ -16708,15 +16333,19 @@ window.__ModuleLoader__.load({
16708
16333
  const hiddenIds = input.hiddenIds || [];
16709
16334
  const ratingFilter = input.ratingFilter;
16710
16335
  const typeFilter = input.typeFilter;
16336
+ const sourceFilter = input.sourceFilter;
16711
16337
  // 标题搜索(选择器模态框):在两级过滤之上再收窄可播放网格。大小写不敏感的**子串**匹配。
16712
16338
  const query = (input.search || "").trim().toLowerCase();
16713
16339
  // 只有可播放的 Video/Web/Image 才进网格 —— Scene/Application 不能嵌进 web UI,
16714
16340
  // 隐掉它们网格才有用。已隐藏(软删除)的壁纸离开这里去「已隐藏」区;两级过滤再收窄。
16715
16341
  const playableList = list.filter((w) =>
16716
- isRotatableWallpaper(w, ratingFilter, typeFilter) && !isHiddenWallpaper(w.id, hiddenIds)
16342
+ isRotatableWallpaper(w, ratingFilter, typeFilter, sourceFilter) && !isHiddenWallpaper(w.id, hiddenIds)
16717
16343
  && (!query || String(w.title || "").toLowerCase().indexOf(query) !== -1));
16718
16344
  // 两个过滤下拉的分档计数(可播放、未隐藏):反映实际可用量,与当前生效的过滤无关。
16719
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;
16720
16349
  // 单遍聚合 —— 分档计数各刷一遍 basePlayable 是 10 次 O(n)(5 个分级档 + 5 个类型档)。
16721
16350
  const ratingCounts = { everyone: 0, pg13: 0, mature: 0, unrated: 0 };
16722
16351
  const typeCounts = { video: 0, web: 0, image: 0, scene: 0 };
@@ -16725,7 +16354,7 @@ window.__ModuleLoader__.load({
16725
16354
  ratingCounts[r] = (ratingCounts[r] || 0) + 1;
16726
16355
  typeCounts[w.type] = (typeCounts[w.type] || 0) + 1;
16727
16356
  }
16728
- const editorList = playableWallpapers(list, ratingFilter, typeFilter, hiddenIds);
16357
+ const editorList = playableWallpapers(list, ratingFilter, typeFilter, hiddenIds, sourceFilter);
16729
16358
  const hiddenList = hiddenWallpapers(list, hiddenIds);
16730
16359
  return {
16731
16360
  query,
@@ -16735,6 +16364,7 @@ window.__ModuleLoader__.load({
16735
16364
  typeCounts,
16736
16365
  editorList,
16737
16366
  hiddenList,
16367
+ sourceCounts,
16738
16368
  normalPage: pageSlice(playableList, input.page),
16739
16369
  hiddenPageView: pageSlice(hiddenList, input.hiddenPage),
16740
16370
  editorPageView: pageSlice(editorList, input.editorPage),
@@ -17107,7 +16737,7 @@ window.__ModuleLoader__.load({
17107
16737
  }
17108
16738
 
17109
16739
  function renderPickerFilterRow(ctx) {
17110
- const { sel, basePlayable, ratingCounts, typeCounts, onSearchInput, onRatingFilterChange, onTypeFilterChange } = ctx;
16740
+ const { sel, basePlayable, ratingCounts, typeCounts, sourceCounts, onSearchInput, onRatingFilterChange, onTypeFilterChange, onSourceFilterChange } = ctx;
17111
16741
  return React.createElement("div", { className: "we-picker__row we-picker__filter-row" },
17112
16742
  // 标题搜索:几百上千张壁纸时最快的定位方式。输入即过滤,与分级/类型过滤叠加。
17113
16743
  React.createElement("input", {
@@ -17117,6 +16747,20 @@ window.__ModuleLoader__.load({
17117
16747
  "aria-label": weT("搜索壁纸标题"),
17118
16748
  onInput: onSearchInput,
17119
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
+ ),
17120
16764
  React.createElement("span", { className: "we-picker__hint we-picker__label" }, weT("内容分级")),
17121
16765
  React.createElement("select", {
17122
16766
  className: "we-picker__playlist-select",
@@ -17308,6 +16952,215 @@ window.__ModuleLoader__.load({
17308
16952
  return React.createElement("div", { key: p.name, className: "we-picker__props-row" }, label, control);
17309
16953
  }
17310
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
+ opt(weT("最短显示"), "minShowMs", [[0, "不限制"], [600, "0.6 秒"], [1200, "1.2 秒"], [2000, "2 秒"]]),
17112
+ opt(weT("最长停留"), "maxHoldMs", [[10000, "10 秒"], [20000, "20 秒(默认)"], [30000, "30 秒"], [60000, "60 秒"]]),
17113
+ ));
17114
+ }
17115
+
17116
+ children.push(React.createElement("div", { key: "count", className: "we-picker__hint", style: { margin: "2px 0 4px" } },
17117
+ weT("已下载 ") + installedCount + " / " + clips.length));
17118
+
17119
+ if (clips.length) {
17120
+ children.push(React.createElement("div", { key: "grid", style: BOOT_STYLE.grid },
17121
+ clips.map((c) => {
17122
+ const isBusy = busy === c.id;
17123
+ const isActive = c.id === activeId;
17124
+ const btn = (label, onClick, disabled, primary) => React.createElement("button", {
17125
+ type: "button",
17126
+ className: "we-picker__btn" + (primary ? " we-picker__btn--primary" : ""),
17127
+ style: { flex: "1 1 0", fontSize: "11.5px", padding: "4px 6px" },
17128
+ disabled: Boolean(disabled) || Boolean(busy),
17129
+ onClick,
17130
+ }, label);
17131
+ const actions = [];
17132
+ if (!c.installed) {
17133
+ actions.push(btn(isBusy ? (weT("下载中… ") + tick + "s") : weT("下载"), () => act("download", { id: c.id }, c.title), false, true));
17134
+ } else if (!isActive) {
17135
+ actions.push(btn(weT("使用"), () => act("select", { id: c.id }, c.title), false, true));
17136
+ actions.push(btn(weT("删除"), () => act("remove", { id: c.id }, c.title), isBusy, false));
17137
+ } else {
17138
+ actions.push(btn(weT("使用中"), () => {}, true, false));
17139
+ }
17140
+ return React.createElement("div", { key: c.id, style: BOOT_STYLE.card },
17141
+ React.createElement("div", { style: BOOT_STYLE.title }, c.title || c.id),
17142
+ React.createElement("div", { style: BOOT_STYLE.meta },
17143
+ [c.durationSec ? Number(c.durationSec).toFixed(1) + "s" : null,
17144
+ bootFmtBytes(c.localBytes || c.bytes)].filter(Boolean).join(" · ")),
17145
+ c.faststart === false
17146
+ ? React.createElement("div", { style: { ...BOOT_STYLE.meta, color: "#ffd08a" } }, weT("未做 faststart,播放可能更慢"))
17147
+ : null,
17148
+ React.createElement("div", { style: { ...BOOT_STYLE.row, marginTop: "4px" } }, actions),
17149
+ );
17150
+ })));
17151
+ } else if (state) {
17152
+ children.push(React.createElement("div", { key: "empty", className: "we-picker__empty" },
17153
+ React.createElement("span", { className: "we-picker__empty-title" }, weT("没有可用的开机动画")),
17154
+ React.createElement("span", { className: "we-picker__hint" }, weT("素材来自站点;站点不可达时这里为空,但不影响启动"))));
17155
+ }
17156
+
17157
+ return React.createElement(React.Fragment, null, children);
17158
+ }
17159
+
17160
+ function renderBootTab() {
17161
+ return React.createElement(BootTabPanel, null);
17162
+ }
17163
+
17311
17164
  // ── 内联模块:src/quick-panel.js —— 快捷播放面板(官方侧栏 tab 与低版本抽屉共用同一份内容):当前壁纸 + 轮播 + 列表快切 + 声音 + 设置入口。列表虚拟滚动(spacer + 可视窗口)(构建期注入;勿手改本段)──
17312
17165
  /**
17313
17166
  * quick-panel.js — 快捷播放面板(侧边栏的**唯一**内容):当前壁纸 + 轮播 + 三档页签
@@ -17478,10 +17331,9 @@ window.__ModuleLoader__.load({
17478
17331
  // 对照很常见)。与设置页的页签表(PICKER_TABS)是两件事 —— 这里只镜像用户点名要的
17479
17332
  // 两页,外加壁纸页。
17480
17333
  const QP_TABS = [
17334
+ // 开机动画放在「壁纸」左边:它和壁纸是并列的两种"开机时看到什么",不是壁纸的子功能。
17335
+ { id: "boot", get label() { return weT("开机动画"); } },
17481
17336
  { id: "wallpaper", get label() { return weT("壁纸"); } },
17482
- // 「皮肤包」与设置页那个页签是**同一个组件**(SkinsTabPanel):首页这里只是把它挂进抽屉,
17483
- // 不另写一份列表逻辑 —— 两份实现迟早会漂移。
17484
- { id: "skins", get label() { return weT("皮肤包"); } },
17485
17337
  { id: "appearance", get label() { return weT("外观"); } },
17486
17338
  { id: "playback", get label() { return weT("播放"); } },
17487
17339
  ];
@@ -17609,7 +17461,7 @@ window.__ModuleLoader__.load({
17609
17461
  React.createElement("span", { className: "we-qp__item-thumb" },
17610
17462
  w.preview
17611
17463
  ? React.createElement("img", {
17612
- src: w.preview, alt: "", loading: "lazy",
17464
+ src: w.preview, alt: "", loading: "lazy", decoding: "async",
17613
17465
  onError: (e) => { e.target.style.display = "none"; },
17614
17466
  onLoad: (e) => { e.target.style.opacity = "1"; },
17615
17467
  })
@@ -17637,7 +17489,7 @@ window.__ModuleLoader__.load({
17637
17489
  },
17638
17490
  w.preview
17639
17491
  ? React.createElement("img", {
17640
- src: w.preview, alt: "", loading: "lazy",
17492
+ src: w.preview, alt: "", loading: "lazy", decoding: "async",
17641
17493
  onError: (e) => { e.target.style.display = "none"; },
17642
17494
  onLoad: (e) => { e.target.style.opacity = "1"; },
17643
17495
  })
@@ -17846,7 +17698,17 @@ window.__ModuleLoader__.load({
17846
17698
  );
17847
17699
  }
17848
17700
 
17849
- function qpRenderViewBar(sel, view, typeFilter, ratingFilter, switchView) {
17701
+ // 来源档(本机 Wallpaper Engine 库 / 我们站点):与设置页同一个键 `sourceFilter`,
17702
+ // 同样"两处同步"——这里切,设置页跟着变,反之亦然。只筛列表,不拦正在播的壁纸。
17703
+ function qpSources() {
17704
+ return [
17705
+ { id: "all", label: weT("全部来源") },
17706
+ { id: "local", label: weT("本机") },
17707
+ { id: "site", label: weT("站点") },
17708
+ ];
17709
+ }
17710
+
17711
+ function qpRenderViewBar(sel, view, typeFilter, ratingFilter, switchView, sourceFilter) {
17850
17712
  return React.createElement("div", { className: "we-qp__viewbar" },
17851
17713
  React.createElement("input", {
17852
17714
  className: "we-picker__text we-qp__search", type: "text",
@@ -17880,6 +17742,16 @@ window.__ModuleLoader__.load({
17880
17742
  },
17881
17743
  ...qpTypes().map((t) => React.createElement("option", { key: t.id, value: t.id }, t.label)),
17882
17744
  ),
17745
+ // 来源筛选:本机 Wallpaper Engine 库 vs 我们站点(与设置页同一个键)。
17746
+ React.createElement("select", {
17747
+ className: "we-picker__select we-qp__source",
17748
+ value: sourceFilter,
17749
+ onChange: (e) => { setSetting("sourceFilter", e.target.value); emit(); },
17750
+ "aria-label": weT("来源筛选"),
17751
+ title: weT("按来源筛选:本机 Wallpaper Engine 库,还是从我们站点拉下来的壁纸"),
17752
+ },
17753
+ ...qpSources().map((s) => React.createElement("option", { key: s.id, value: s.id }, s.label)),
17754
+ ),
17883
17755
  // 列表 / 卡片:标签式切换(滑动胶囊做激活指示、无分段底 —— 样式见
17884
17756
  // .we-qp__viewtabs),偏好记 localStorage(QP_VIEW_KEY),两个壳共用一份。
17885
17757
  // 语义仍是开关(role=group + aria-pressed),只是外观走标签形态。
@@ -17945,7 +17817,7 @@ window.__ModuleLoader__.load({
17945
17817
  );
17946
17818
  }
17947
17819
 
17948
- function qpRenderLibrarySection(sel, view, typeFilter, ratingFilter, listRef, rows, cardsView, vTopH, vBtmH, q, switchView) {
17820
+ function qpRenderLibrarySection(sel, view, typeFilter, ratingFilter, listRef, rows, cardsView, vTopH, vBtmH, q, switchView, sourceFilter) {
17949
17821
  return React.createElement("div", { className: "we-qp__section we-qp__library" },
17950
17822
  !sel.loaded
17951
17823
  ? React.createElement("span", { className: "we-picker__hint" }, weT("扫描 Wallpaper Engine…"))
@@ -17959,7 +17831,7 @@ window.__ModuleLoader__.load({
17959
17831
  }, weT(sel.loading ? "刷新中…" : "重试")),
17960
17832
  )
17961
17833
  : React.createElement(React.Fragment, null,
17962
- qpRenderViewBar(sel, view, typeFilter, ratingFilter, switchView),
17834
+ qpRenderViewBar(sel, view, typeFilter, ratingFilter, switchView, sourceFilter),
17963
17835
  qpRenderList(listRef, rows, view, cardsView, vTopH, vBtmH, q, ratingFilter, typeFilter, sel),
17964
17836
  ),
17965
17837
  );
@@ -18079,8 +17951,11 @@ window.__ModuleLoader__.load({
18079
17951
  const ratingFilter = qpRatings().some((r) => r.id === sel.contentRatingFilter)
18080
17952
  ? sel.contentRatingFilter : "all";
18081
17953
  const playable = playableInventory();
17954
+ const sourceFilter = qpSources().some((s) => s.id === sel.sourceFilter) ? sel.sourceFilter : "all";
18082
17955
  const filtered = playable.filter((w) => {
18083
17956
  if (typeFilter !== "all" && w.type !== typeFilter) return false;
17957
+ // 站点条目带 `source: "folia"`;其余(本机工坊 + 上传)一律算本机。
17958
+ if (sourceFilter !== "all" && ((w.source === "folia") ? "site" : "local") !== sourceFilter) return false;
18084
17959
  if (q && String(w.title || "").toLowerCase().indexOf(q) === -1) return false;
18085
17960
  return true;
18086
17961
  });
@@ -18115,7 +17990,13 @@ window.__ModuleLoader__.load({
18115
17990
  // 设置页同名页签 —— 玻璃详细调节仍只在侧栏(本页,折叠块),设置页那边留玻璃预设、
18116
17991
  // 全量配置与**字体节**(三次修订后字体节两档都画);播放页的出图来源 /
18117
17992
  // 实时帧仍住设置页。
18118
- const foot = qpTab === "appearance"
17993
+ const foot = qpTab === "boot"
17994
+ ? {
17995
+ label: weT("打开设置 ›"),
17996
+ target: "",
17997
+ title: weT("开机动画的全部参数都在侧栏「壁纸引擎」设置里"),
17998
+ }
17999
+ : qpTab === "appearance"
18119
18000
  ? {
18120
18001
  label: weT("更多外观设置 ›"),
18121
18002
  target: "appearance",
@@ -18127,17 +18008,11 @@ window.__ModuleLoader__.load({
18127
18008
  target: "playback",
18128
18009
  title: weT("在设置页打开「播放」页签 —— 出图来源 / 实时帧 / 帧率上限在那里"),
18129
18010
  }
18130
- : qpTab === "skins"
18131
- ? {
18132
- label: weT("皮肤包设置 ›"),
18133
- target: "skins",
18134
- title: weT("在设置页打开「皮肤包」页签 —— 下载 / 切换 / 删除皮肤包"),
18135
- }
18136
- : {
18137
- label: weT("壁纸引擎设置 ›"),
18138
- target: "",
18139
- title: weT("打开设置对话框的「壁纸引擎」分区(外观 / 播放 / 系统与全部配置)"),
18140
- };
18011
+ : {
18012
+ label: weT("壁纸引擎设置 ›"),
18013
+ target: "",
18014
+ title: weT("打开设置对话框的「壁纸引擎」分区(外观 / 播放 / 系统与全部配置)"),
18015
+ };
18141
18016
 
18142
18017
  return React.createElement("div", { className: "we-qp we-qp--" + dock },
18143
18018
  qpRenderCurrent(sel, current, playbackLive),
@@ -18153,7 +18028,9 @@ window.__ModuleLoader__.load({
18153
18028
  React.createElement("div", { className: tabBodyClass, ref: qpPaneRef },
18154
18029
  qpTab === "wallpaper" && userPropsPanelOpen() && propsAvailable
18155
18030
  && React.createElement("div", { className: "we-qp__propsview we-qp__propsview--drill" }, renderUserPropsPanel()),
18156
- !(qpTab === "wallpaper" && userPropsPanelOpen() && propsAvailable) && (qpTab === "appearance"
18031
+ // 开机动画档:整区就是那个面板(它自己管加载/下载/开关)。
18032
+ qpTab === "boot" && React.createElement(BootTabPanel, null),
18033
+ !(qpTab === "wallpaper" && userPropsPanelOpen() && propsAvailable) && qpTab !== "boot" && (qpTab === "appearance"
18157
18034
  ? qpRenderAppearancePane(sel, {
18158
18035
  fontOpen, onToggleFontSection: toggleFontSection,
18159
18036
  uiColorOpen, onToggleUiColorSection: toggleUiColorSection,
@@ -18167,15 +18044,12 @@ window.__ModuleLoader__.load({
18167
18044
  })
18168
18045
  : qpTab === "playback"
18169
18046
  ? qpRenderPlaybackPane(sel, switchQpTab)
18170
- // 皮肤包:直接挂设置页那个同一个组件(它自己管列表/下载/激活状态)。
18171
- : qpTab === "skins"
18172
- ? React.createElement(SkinsTabPanel, null)
18173
- : null),
18047
+ : null),
18174
18048
  !(qpTab === "wallpaper" && userPropsPanelOpen() && propsAvailable) && qpTab === "wallpaper" && React.createElement(React.Fragment, null,
18175
18049
  // ── 壁纸列表(搜索 + 视图切换 + 快切)──
18176
18050
  // 库还没回来 / 扫描失败:**只有这一页**给一句话 —— 外观 / 播放两页不依赖库,
18177
18051
  // 照常可用(不能整块替换掉,那样连外观都进不去)。
18178
- qpRenderLibrarySection(sel, view, typeFilter, ratingFilter, listRef, rows, cardsView, vTopH, vBtmH, q, switchView),
18052
+ qpRenderLibrarySection(sel, view, typeFilter, ratingFilter, listRef, rows, cardsView, vTopH, vBtmH, q, switchView, sourceFilter),
18179
18053
  // 声音(与播放页那份同源:同一份渲染器、同一状态)。包一层 .we-qp__section
18180
18054
  // 只为保留这一节的分隔线与间距(渲染器自己那份 section 是给设置页排版的)。
18181
18055
  React.createElement("div", { className: "we-qp__section" },
@@ -27166,9 +27040,6 @@ window.__ModuleLoader__.load({
27166
27040
  const PICKER_TAB_KEY = "dsh-folia:picker-tab";
27167
27041
  const PICKER_TABS = [
27168
27042
  { id: "library", get label() { return weT("壁纸库"); } },
27169
- // 「皮肤包」挨着「壁纸库」:两者都是"给背景换东西",用户心智在同一处。
27170
- // 它列的是**自建站**上的皮肤包(宿主端 /skins 族),不是皮肤中心的内置列表。
27171
- { id: "skins", get label() { return weT("皮肤包"); } },
27172
27043
  { id: "appearance", get label() { return weT("外观"); } },
27173
27044
  { id: "playback", get label() { return weT("播放"); } },
27174
27045
  { id: "system", get label() { return weT("系统"); } },
@@ -29170,6 +29041,12 @@ window.__ModuleLoader__.load({
29170
29041
  setSetting("typeFilter", e.target.value);
29171
29042
  revalidateSelection();
29172
29043
  };
29044
+ // 来源筛选(本机 Wallpaper Engine 库 / 我们站点):只筛列表与轮播候选,
29045
+ // 与类型档同一个效力 —— 不写进播放闸门,切换不会把正在播的壁纸掐掉。
29046
+ const onSourceFilterChange = (e) => {
29047
+ setSetting("sourceFilter", e.target.value);
29048
+ revalidateSelection();
29049
+ };
29173
29050
  // Card style: classic (CD-rack) vs fixed (overlap-proof).
29174
29051
  const onLayoutChange = (value) => {
29175
29052
  setSetting("pickerLayout", value);
@@ -29714,7 +29591,7 @@ window.__ModuleLoader__.load({
29714
29591
  // 标题子串匹配(空查询 = 不过滤),每页 24 张,页号越界自动 clamp。这里只把状态喂
29715
29592
  // 进去、把结果取出来 —— 组件体不再就地算这些(分级/类型/隐藏的判定也在那边)。
29716
29593
  const {
29717
- query, playableList, basePlayable, ratingCounts, typeCounts, hiddenList,
29594
+ query, playableList, basePlayable, ratingCounts, typeCounts, sourceCounts, hiddenList,
29718
29595
  editorPageView,
29719
29596
  } = pickerModel({
29720
29597
  wallpapers: list,
@@ -29722,6 +29599,7 @@ window.__ModuleLoader__.load({
29722
29599
  search: sel.search,
29723
29600
  ratingFilter: sel.contentRatingFilter,
29724
29601
  typeFilter: sel.typeFilter,
29602
+ sourceFilter: sel.sourceFilter,
29725
29603
  editorPage: sel.editorPage,
29726
29604
  });
29727
29605
  // CD-rack mode: compact one-page grid (no pagination, no virtualization —
@@ -29785,9 +29663,6 @@ window.__ModuleLoader__.load({
29785
29663
  onReopenNotice: () => { noticeReopenRequest = true; emit(); },
29786
29664
  });
29787
29665
  if (activeTab === "extensions") return renderExtensionsTab(extensionCtx());
29788
- // 「皮肤包」自带异步状态(列表 / 下载中 / 错误),所以它是一个**真组件**而不是
29789
- // 纯渲染函数 —— 薄包装只负责挂上去(理由见 src/panel-tabs-skins.js 文件头)。
29790
- if (activeTab === "skins") return renderSkinsTab();
29791
29666
  if (activeTab === "appearance") return renderAppearanceTab({
29792
29667
  setSetting, setTransient,
29793
29668
  fontSet: fontSetCtx(),
@@ -29884,10 +29759,10 @@ window.__ModuleLoader__.load({
29884
29759
  // 壁纸网格,ESC / 顶部「返回」退出,切页签也会退出(见 switchTab)。
29885
29760
  sel.pickerOpen ? renderPickerModal({
29886
29761
  sel, closePicker, current, playbackLive, playableList, hiddenList,
29887
- cdMode, query, basePlayable, ratingCounts, typeCounts,
29762
+ cdMode, query, basePlayable, ratingCounts, typeCounts, sourceCounts,
29888
29763
  gridRef: pickerGridRef, vwin: pickerVwin,
29889
29764
  armedConfirm: sel.armedConfirm, onArmConfirm: armConfirm, onDisarmConfirm: disarmConfirm,
29890
- onClear, onRatingFilterChange, onTypeFilterChange,
29765
+ onClear, onRatingFilterChange, onTypeFilterChange, onSourceFilterChange,
29891
29766
  onShowNormalView, onShowHiddenView,
29892
29767
  onToggleBatchMode, onArmBatchHide, onBatchHide, onBatchCancel, onSearchInput, onPickCard,
29893
29768
  }) : React.createElement(React.Fragment, null,