dsh-mpkg-wallpaper 3.13.7 → 3.13.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.
Files changed (2) hide show
  1. package/lib/client.js +550 -35
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -2677,7 +2677,12 @@ function mpwThumbFail(el, why) {
2677
2677
  .trim();
2678
2678
  }
2679
2679
 
2680
- /** 解析 project.json 文本,提取标题 + 可调参数信息。 */
2680
+ /** 解析 project.json 文本,提取标题 + **官方 user properties 全字段**。
2681
+ * ①(2026-09-24 用户 bug「WE 自带选项只显示一项且展不开」修复) 旧实现只留
2682
+ * `{key,label,value,type,options,displayOnly,important}` —— `group`(分组头)、
2683
+ * `condition`(条件显示)、`min/max/step/precision`(控件量程)、`order/index`(官方排序)
2684
+ * **全部被丢掉** ⇒ 面板只能渲染成一堆只读文本行、且无法按官方口径分组/联动。
2685
+ * 现在**逐字段保留**(分组/条件/量程/排序都能用),并由 mpwPropsModel 决定渲染形态。 */
2681
2686
  function extractProjectInfo(jsonText) {
2682
2687
  try {
2683
2688
  // ①(P-177 同口径) 包内 project.json 走宽容解析(官方允许尾逗号);健康文件与 JSON.parse 逐位等价
@@ -2685,23 +2690,38 @@ function mpwThumbFail(el, why) {
2685
2690
  const props = json.general && json.general.properties;
2686
2691
  const list = [];
2687
2692
  if (props) {
2693
+ let seq = 0;
2688
2694
  for (const key of Object.keys(props)) {
2689
2695
  if (!safePropKey(key)) continue;
2690
2696
  const p = props[key];
2691
2697
  if (!p || typeof p !== "object") continue;
2698
+ // ①(修正) 原来 `if (!label) continue` 把**没有 text 的条目整条丢掉**
2699
+ // (语料 70 包实测:无 type 的 HTML 营销块 102 条、group 也有空文案的)——
2700
+ // 丢掉的后果不只是"少一行",还会把分组头吞掉 ⇒ 后面所有子项失去归属。
2701
+ // 现在一律保留(渲染与否交给 mpwPropsModel 按官方口径裁定并记账)。
2692
2702
  const label = cleanLabel(p.text);
2693
- if (!label) continue;
2694
2703
  let value = p.value;
2695
2704
  if (Array.isArray(p.options) && p.options.length) {
2696
2705
  const opt = p.options.find((o) => String(o.value) === String(value));
2697
2706
  if (opt) value = opt.label;
2698
2707
  }
2699
2708
  // type "text" 是纯展示性条目(作者信息/说明),不给输入框(⑥)
2700
- const displayOnly = p.type === "text";
2709
+ const displayOnly = p.type === "text" || p.type === "txt" || !p.type;
2701
2710
  // ① 关键开关识别(entry animation 开场动画 / prompt box 提示框)
2702
2711
  const keyText = (label + " " + (p.text || "")).toLowerCase();
2703
2712
  const important = /entry\s*animation|开场动画|入场动画|prompt\s*box|提示框|水印|盗版|防盗|开始时间|随现实时间|timevarying|morningtime|daytime|dusktime|nighttime|时间变化|时间段/.test(keyText);
2704
- list.push({ key, label, value: value === void 0 ? "" : value, type: p.type, options: p.options || null, displayOnly, important });
2713
+ const num = (v) => (typeof v === "number" && isFinite(v) ? v : (typeof v === "string" && v.trim() !== "" && isFinite(Number(v)) ? Number(v) : null));
2714
+ list.push({
2715
+ key, label, value: value === void 0 ? "" : value, type: p.type,
2716
+ options: Array.isArray(p.options) ? p.options : null,
2717
+ displayOnly, important,
2718
+ // ①(新) 官方字段:分组归属、条件显示、控件量程、排序
2719
+ group: typeof p.group === "string" && p.group ? p.group : null,
2720
+ conditions: typeof p.condition === "string" ? p.condition : (typeof p.conditions === "string" ? p.conditions : null),
2721
+ min: num(p.min), max: num(p.max), step: num(p.step),
2722
+ precision: num(p.precision),
2723
+ order: num(p.order), index: num(p.index), seq: seq++,
2724
+ });
2705
2725
  }
2706
2726
  }
2707
2727
  return { title: json.title || "", properties: list };
@@ -2722,6 +2742,253 @@ function mpwThumbFail(el, why) {
2722
2742
  }
2723
2743
  }
2724
2744
 
2745
+ /* ═══ MPW-PROPS-MODEL-BEGIN ═══
2746
+ 官方 user properties(`project.json → general.properties`)的**面板行模型**。
2747
+ 纯函数、无 DOM:同一份代码在 Node 里跑断言(tools/props-panel-wiring-test.mjs)。
2748
+ 口径(行为结论来自渲染器侧同名面板的真包/真机验证;代码为本仓自研,未复制):
2749
+ · 一个 group 类型条目 = **分组头**;子项 = 其后(按官方 order 排序)直到下一个 group 的条目,
2750
+ `group` 字段显式指认时以字段为准。
2751
+ · `condition` 是**表达式**(真包语料:`clock.value`、`tishi.value==0`、`!timevarying.value`、
2752
+ `a.value == true && b.value == true`)⇒ 走白名单递归下降求值,**绝不 eval**;
2753
+ condition 引用的属性自身被门控隐藏时,本项一并隐藏(传递闭包)。
2754
+ · 白名单外/引用缺失 ⇒ 返回 null(**未知**):未知一律**照显示**(宁多显示不误藏)并记账。
2755
+ · 类型 → 控件:bool 开关 / slider 量程滑杆 / combo 下拉 / color 取色 / textinput 文本框;
2756
+ file·directory·scenetexture 需要渲染器侧资源通道,插件侧**只读如实展示**(不猜);
2757
+ `schemecolor`(编辑器配色)与 `usershortcut`(官方语义未实现)**不渲染但逐条记账**。
2758
+ · 排序:`order` → `index` → 文件内顺序。
2759
+ ═══ */
2760
+ const MPW_PROP_CONTROL_TYPES = { bool: "bool", slider: "slider", combo: "combo", color: "color", textinput: "textinput" };
2761
+ const MPW_PROP_READONLY_TYPES = { file: "选择文件(需要渲染器资源通道,插件侧未接线)", directory: "选择文件夹(需要渲染器资源通道,插件侧未接线)", scenetexture: "贴图替换(需要渲染器资源通道,插件侧未接线)" };
2762
+ const MPW_PROP_SKIP_TYPES = { schemecolor: "编辑器配色(官方不在用户属性面板渲染)", usershortcut: "官方语义未实现(不猜)" };
2763
+ /** 官方排序键:order 优先,其次 index,再退文件内出现顺序。 */
2764
+ function mpwPropSorted(allProps) {
2765
+ const list = Array.isArray(allProps) ? allProps.slice() : [];
2766
+ const ord = (p) => (typeof p.order === "number" && isFinite(p.order) ? p.order : (typeof p.index === "number" && isFinite(p.index) ? p.index : 1e9));
2767
+ list.sort((a, b) => (ord(a) - ord(b)) || ((a.seq || 0) - (b.seq || 0)));
2768
+ return list;
2769
+ }
2770
+ /** 条目分类:group / control / readonly / note / skip(不渲染的必须带 why,不静默丢)。 */
2771
+ function mpwPropKind(p) {
2772
+ const type = p && p.type ? String(p.type).toLowerCase() : "";
2773
+ // ① `schemecolor` 是**属性名**(不是类型):它是编辑器配色,官方用户面板不渲染
2774
+ if (p && p.key === "schemecolor") return { kind: "skip", why: MPW_PROP_SKIP_TYPES.schemecolor };
2775
+ if (type === "group") return { kind: "group", why: "" };
2776
+ if (MPW_PROP_CONTROL_TYPES[type]) return { kind: "control", why: "" };
2777
+ if (MPW_PROP_READONLY_TYPES[type]) return { kind: "readonly", why: MPW_PROP_READONLY_TYPES[type] };
2778
+ if (MPW_PROP_SKIP_TYPES[type]) return { kind: "skip", why: MPW_PROP_SKIP_TYPES[type] };
2779
+ if (type === "text" || type === "txt") return { kind: "note", why: "" };
2780
+ // 无 type:官方口径=HTML 营销块,不渲染;有文案的按作者说明行展示(可读性),空文案的记账跳过
2781
+ if (!type) return p.label ? { kind: "note", why: "无 type(官方不在面板渲染;此处按作者说明行展示)" } : { kind: "skip", why: "无 type 且无文案" };
2782
+ return { kind: "skip", why: "未知类型 " + type + "(不猜)" };
2783
+ }
2784
+ /** 条件表达式 → token 流;白名单外返回 null(调用方按"未知"处理)。 */
2785
+ function mpwPropCondTokens(src) {
2786
+ const s = String(src == null ? "" : src);
2787
+ const out = [];
2788
+ let i = 0;
2789
+ while (i < s.length) {
2790
+ const c = s[i];
2791
+ if (/\s/.test(c)) { i++; continue }
2792
+ if (c === "(" || c === ")") { out.push({ t: "punc", v: c }); i++; continue }
2793
+ if (c === "!" && s[i + 1] === "=") { out.push({ t: "op", v: "!=" }); i += 2; continue }
2794
+ if (c === "!") { out.push({ t: "op", v: "!" }); i++; continue }
2795
+ if (c === "&" && s[i + 1] === "&") { out.push({ t: "op", v: "&&" }); i += 2; continue }
2796
+ if (c === "|" && s[i + 1] === "|") { out.push({ t: "op", v: "||" }); i += 2; continue }
2797
+ if (c === "=" && s[i + 1] === "=") { out.push({ t: "op", v: "==" }); i += 2; continue }
2798
+ if (c === '"' || c === "'") {
2799
+ let j = i + 1, buf = "";
2800
+ while (j < s.length && s[j] !== c) { if (s[j] === "\\") j++; buf += s[j] === void 0 ? "" : s[j]; j++ }
2801
+ if (j >= s.length) return null; // 未闭合字符串 = 白名单外
2802
+ out.push({ t: "str", v: buf }); i = j + 1; continue;
2803
+ }
2804
+ if (/[0-9]/.test(c) || (c === "-" && /[0-9.]/.test(s[i + 1] || ""))) {
2805
+ let j = i + 1;
2806
+ while (j < s.length && /[0-9.eE+-]/.test(s[j])) j++;
2807
+ const num = Number(s.slice(i, j));
2808
+ if (!isFinite(num)) return null;
2809
+ out.push({ t: "num", v: num }); i = j; continue;
2810
+ }
2811
+ if (/[A-Za-z_$]/.test(c)) {
2812
+ let j = i;
2813
+ while (j < s.length && /[A-Za-z0-9_$.]/.test(s[j])) j++;
2814
+ const id = s.slice(i, j);
2815
+ if (id === "true" || id === "false") out.push({ t: "bool", v: id === "true" });
2816
+ else if (/^[A-Za-z_$][\w$]*\.value$/.test(id)) out.push({ t: "ref", v: id.slice(0, -6) });
2817
+ else return null; // 只认 `<属性名>.value`
2818
+ i = j; continue;
2819
+ }
2820
+ return null;
2821
+ }
2822
+ return out;
2823
+ }
2824
+ /** 值的真值口径(官方两态 bool / 数值 / 字符串)。 */
2825
+ function mpwPropTruthy(v) {
2826
+ if (typeof v === "boolean") return v;
2827
+ if (typeof v === "number") return v !== 0;
2828
+ if (typeof v === "string") { const s = v.trim().toLowerCase(); return !(s === "" || s === "0" || s === "false" || s === "off" || s === "no"); }
2829
+ return false;
2830
+ }
2831
+ /** `==` 口径:bool 字面量/数值/字符串三态归一到可比对的值。 */
2832
+ function mpwPropEq(a, b) {
2833
+ const numish = (x) => (typeof x === "number" && isFinite(x)) || (typeof x === "string" && x.trim() !== "" && isFinite(Number(x)));
2834
+ if (typeof a === "boolean" || typeof b === "boolean") return mpwPropTruthy(a) === mpwPropTruthy(b);
2835
+ if (numish(a) && numish(b)) return Number(a) === Number(b);
2836
+ return String(a) === String(b);
2837
+ }
2838
+ /** 条件求值(白名单子集):true / false / null(未知 ⇒ 调用方照显示)。 */
2839
+ function mpwPropCondEval(expr, values) {
2840
+ if (expr === null || expr === void 0) return true;
2841
+ const text = String(expr).trim();
2842
+ if (!text) return true;
2843
+ const toks = mpwPropCondTokens(text);
2844
+ if (!toks || !toks.length) return null;
2845
+ let pos = 0;
2846
+ const peek = () => toks[pos];
2847
+ const eat = (t, v) => { const x = toks[pos]; return !!x && x.t === t && (v === void 0 || x.v === v) ? (pos++, x) : null };
2848
+ const val = (n) => {
2849
+ if (!n) return null;
2850
+ if (n.t === "ref") return Object.prototype.hasOwnProperty.call(values, n.v) ? { v: values[n.v] } : null; // 引用缺失 ⇒ 未知
2851
+ return { v: n.t === "bool" ? n.v : (n.t === "num" ? n.v : n.v) };
2852
+ };
2853
+ let bad = false;
2854
+ const prim = () => {
2855
+ if (eat("punc", "(")) { const r = or(); if (!eat("punc", ")")) { bad = true; return null } return r }
2856
+ const n = eat("ref") || eat("bool") || eat("num") || eat("str");
2857
+ if (!n) { bad = true; return null }
2858
+ return val(n);
2859
+ };
2860
+ const cmp = () => {
2861
+ let a = prim();
2862
+ if (bad) return null;
2863
+ const op = eat("op", "==") || eat("op", "!=");
2864
+ if (!op) return a;
2865
+ const b = prim();
2866
+ if (bad) return null;
2867
+ if (a === null || b === null) return null; // 任一侧未知 ⇒ 未知
2868
+ return { v: op.v === "==" ? mpwPropEq(a.v, b.v) : !mpwPropEq(a.v, b.v) };
2869
+ };
2870
+ const unary = () => {
2871
+ if (eat("op", "!")) { const r = unary(); if (bad || r === null) return r; return { v: !mpwPropTruthy(r.v) } }
2872
+ return cmp();
2873
+ };
2874
+ const and = () => {
2875
+ let l = unary();
2876
+ while (eat("op", "&&")) {
2877
+ const r = unary();
2878
+ if (bad) return null;
2879
+ if (l === null || r === null) l = null; else l = { v: mpwPropTruthy(l.v) && mpwPropTruthy(r.v) };
2880
+ }
2881
+ return l;
2882
+ };
2883
+ function or() {
2884
+ let l = and();
2885
+ while (eat("op", "||")) {
2886
+ const r = and();
2887
+ if (bad) return null;
2888
+ if (l === null || r === null) l = null; else l = { v: mpwPropTruthy(l.v) || mpwPropTruthy(r.v) };
2889
+ }
2890
+ return l;
2891
+ }
2892
+ const res = or();
2893
+ if (bad || pos !== toks.length) return null; // 有残余 token ⇒ 白名单外
2894
+ if (res === null) return null;
2895
+ return mpwPropTruthy(res.v);
2896
+ }
2897
+ /** 行模型:把官方属性表变成"(分组头 + 子行)序列 + 记账"。 */
2898
+ function mpwPropsModel(allProps, edits, opts) {
2899
+ const list = mpwPropSorted(allProps);
2900
+ const e = (edits && typeof edits === "object") ? edits : {};
2901
+ const values = {};
2902
+ for (const p of list) values[p.key] = e[p.key] !== void 0 ? e[p.key] : p.value;
2903
+ // ① 分类 + 显式 group 字段指认的集合
2904
+ const kinds = list.map((p) => mpwPropKind(p));
2905
+ const explicitGroups = new Set();
2906
+ for (const p of list) if (p.group) explicitGroups.add(p.group);
2907
+ // ② 归属:显式字段优先;否则"其后(排序后)直到下一个 group 的条目"
2908
+ const rows = [];
2909
+ let curGroup = null;
2910
+ const depthOf = (p, i) => {
2911
+ const k = kinds[i];
2912
+ if (k.kind === "group") { curGroup = p.key; return 0 }
2913
+ if (p.group && explicitGroups.has(p.group)) return 1;
2914
+ return curGroup ? 1 : 0;
2915
+ };
2916
+ list.forEach((p, i) => {
2917
+ const k = kinds[i];
2918
+ const depth = depthOf(p, i);
2919
+ rows.push({
2920
+ key: p.key, label: p.label, type: p.type ? String(p.type).toLowerCase() : "",
2921
+ kind: k.kind, kindWhy: k.why, depth,
2922
+ group: k.kind === "group" ? null : (p.group && explicitGroups.has(p.group) ? p.group : curGroup),
2923
+ value: values[p.key], def: p.value, edited: e[p.key] !== void 0,
2924
+ options: p.options || null, min: p.min, max: p.max, step: p.step, precision: p.precision,
2925
+ // ① 官方的字段名是 `condition`(单数);提取结果里存成 `conditions`(避免与行的
2926
+ // condition 同名字段混淆)⇒ 两个都认,手工构造的属性表也能直接用。
2927
+ condition: (typeof p.conditions === "string" ? p.conditions : (typeof p.condition === "string" ? p.condition : null)), important: !!p.important, displayOnly: !!p.displayOnly,
2928
+ });
2929
+ });
2930
+ // ③ 条件门控(含传递闭包):自身 condition 为假,或其 condition 引用的属性已被隐藏
2931
+ const hidden = {};
2932
+ const refsOf = (r) => {
2933
+ if (!r.condition) return [];
2934
+ const toks = mpwPropCondTokens(r.condition) || [];
2935
+ return toks.filter((t) => t.t === "ref").map((t) => t.v);
2936
+ };
2937
+ for (let round = 0; round < 4; round++) {
2938
+ let changed = false;
2939
+ for (const r of rows) {
2940
+ if (hidden[r.key]) continue;
2941
+ let res;
2942
+ if (r.condition) {
2943
+ const refs = refsOf(r);
2944
+ if (refs.some((k2) => hidden[k2])) res = false; // 父属性被门控 ⇒ 子属性一并隐藏
2945
+ else res = mpwPropCondEval(r.condition, values);
2946
+ } else res = true;
2947
+ r.condResult = res;
2948
+ if (res === false) { hidden[r.key] = true; changed = true; }
2949
+ }
2950
+ if (!changed) break;
2951
+ }
2952
+ // ①(修正) **先算完所有行的 visible,再做"空分组"收口**:
2953
+ // 同一趟里算会让"子项还没轮到 ⇒ visible 仍是 undefined"被当成不可见,分组头全被判空。
2954
+ for (const r of rows) r.visible = !hidden[r.key];
2955
+ for (const r of rows) {
2956
+ // 分组头:没有任何可见子项时也隐藏(不留空壳分组)
2957
+ if (r.kind === "group" && r.visible) {
2958
+ const mine = rows.filter((x) => x.group === r.key && x.visible && x.kind !== "group");
2959
+ r.childCount = mine.length;
2960
+ r.controlCount = mine.filter((x) => x.kind === "control").length;
2961
+ if (!mine.length) r.visible = false;
2962
+ }
2963
+ }
2964
+ const counts = { total: rows.length, groups: 0, controls: 0, readonly: 0, notes: 0, skipped: 0, hidden: 0, condUnknown: 0, visible: 0 };
2965
+ for (const r of rows) {
2966
+ if (r.kind === "group") counts.groups++;
2967
+ else if (r.kind === "control") counts.controls++;
2968
+ else if (r.kind === "readonly") counts.readonly++;
2969
+ else if (r.kind === "note") counts.notes++;
2970
+ else counts.skipped++;
2971
+ if (r.visible) counts.visible++; else if (r.kind !== "skip") counts.hidden++;
2972
+ if (r.condition && r.condResult === null) counts.condUnknown++;
2973
+ }
2974
+ return { rows, counts };
2975
+ }
2976
+ /* ═══ MPW-PROPS-MODEL-END ═══ */
2977
+ /** 官方 color 属性是**线性 0..1 浮点三通道**(`"r g b"`),不是 0..255。取色控件用 #rrggbb 中转。 */
2978
+ function mpwPropColorToHex(v) {
2979
+ const arr = Array.isArray(v) ? v : String(v === void 0 || v === null ? "" : v).trim().split(/[\s,]+/);
2980
+ if (arr.length < 3) return null;
2981
+ const n = arr.slice(0, 3).map(Number);
2982
+ if (!n.every((x) => isFinite(x))) return null;
2983
+ return "#" + n.map((x) => Math.max(0, Math.min(255, Math.round(x * 255))).toString(16).padStart(2, "0")).join("");
2984
+ }
2985
+ function mpwPropHexToColor(hex) {
2986
+ const m = /^#?([0-9a-fA-F]{6})$/.exec(String(hex || "").trim());
2987
+ if (!m) return null;
2988
+ const s = m[1];
2989
+ return [0, 2, 4].map((i) => String(Math.round((parseInt(s.slice(i, i + 2), 16) / 255) * 1e6) / 1e6)).join(" ");
2990
+ }
2991
+
2725
2992
 
2726
2993
  // ═══════════════════════════════════════════════════════════════════
2727
2994
  // 背景 DOM(一个固定 img 层;用 <img> 保证 GIF 动画可靠播放,⑪)
@@ -3160,6 +3427,9 @@ function mpwThumbFail(el, why) {
3160
3427
  try { mpwWebIxDisarm("teardown"); } catch {} // ①(第 11 条) 卸载网页壁纸必须退出交互模式(否则残留输入面)
3161
3428
  try { if (window.__mpwLnReset) window.__mpwLnReset(); } catch {} // ①(批次15 B3) 方向键守卫复位
3162
3429
  if (!frame) return;
3430
+ /* ①(2026-09-25 任务 ㉑ 追加) 撤 src 之前**先明确停源**:撤 src 让文档销毁这条依赖浏览器行为,
3431
+ 而"隐藏但保留"的窗口期(看门狗兜底/总开关关掉)里声音是真会漏出去的 ⇒ 两条都做。 */
3432
+ try { sendRendererAudioPolicy(frame, true, true, "teardown") } catch (e) {}
3163
3433
  disposeWebFrame(frame);
3164
3434
  try { frame.removeAttribute("src"); } catch {}
3165
3435
  frame.style.display = "none";
@@ -3979,6 +4249,7 @@ function mpwThumbFail(el, why) {
3979
4249
  if (wd.fallback) {
3980
4250
  // 兜底后渲染器终于出画(迟到首帧)→ 自动恢复实时渲染
3981
4251
  wd.fallback = false;
4252
+ try { const f0 = bgElements().frame; if (f0) { sendRendererAudioPolicy(f0, true, false, "scene-recovered"); applyWebMute(f0) } } catch (e) {}
3982
4253
  mpwSceneClearFallback();
3983
4254
  mpwSceneSetHint(mpwSceneT("scnRender.wd.recovered", "场景渲染器已出画,自动恢复实时渲染"));
3984
4255
  try { fetch(HOST_BASE + "/diag", { method: "POST", body: JSON.stringify({ kind: "scene-render-health", why: "late-first-frame-recovered", ident: wd.ident, armedAt: wd.armedAt, at: new Date().toISOString() }) }).catch(() => {}) } catch {}
@@ -3990,6 +4261,12 @@ function mpwThumbFail(el, why) {
3990
4261
  if (!wd || wd.fallback) return;
3991
4262
  wd.fallback = true;
3992
4263
  wd.fallbackAt = Date.now();
4264
+ /* ①(2026-09-25 任务 ㉑ 追加) **兜底隐藏 iframe 时,必须把帧内声音停掉**:
4265
+ 这条路径是"**隐藏但保留元素与 src**"(等迟到首帧恢复),而隐藏的 iframe 里音频照样在放 ——
4266
+ 真机现场就是"壁纸没有自动播放(显示静态帧)、声音却在响,而且**不是当前壁纸的**音频"。
4267
+ 跨源帧没有别的通道 ⇒ 走 postMessage `park`(渲染器侧 pause 元素 + ctx.suspend)。 */
4268
+ try { const f0 = bgElements().frame; if (f0) sendRendererAudioPolicy(f0, true, true, "scene-fallback:" + String(why || "")) } catch (e) {}
4269
+ try { window.__mpwSceneFallbackPark = { at: Date.now(), why: String(why || "") } } catch (e) {}
3993
4270
  try {
3994
4271
  const { img, wrap } = bgElements();
3995
4272
  if (wrap) wrap.classList.add("mpw-scene-fallback");
@@ -4289,6 +4566,45 @@ function mpwThumbFail(el, why) {
4289
4566
  } catch {}
4290
4567
  } catch {}
4291
4568
  }
4569
+ /** ①(2026-09-25 任务 ㉑) **场景渲染器帧的图级音频策略**通道(`{type:"mpw-audio-policy"}`)。
4570
+ * 为什么必须单独有它(真机读数,见 docs/reports-issue0924a3-audio-leak.md):
4571
+ * 场景渲染器 iframe 在 `127.0.0.1:8902`、插件页在 `127.0.0.1:3080` ⇒ **不同源**,
4572
+ * 于是 `applyWebMute` 的三条既有通道**一条都到不了帧里**:
4573
+ * · `frame.contentDocument` = null(逐元素写 `muted` 那趟扫不到);
4574
+ * · `window.frameElement` = null(帧内读不到宿主写的 `frame.muted` expando);
4575
+ * · shim policy 只对 `?mpwshim=1` 的网页壁纸帧发。
4576
+ * 实测(静音=开 + 场景档 + 刷新):帧内 4 条 `<audio>` 全是 `muted:false/volume:0.25`、
4577
+ * AudioContext `running`、接 destination 那条边的抽头 rms≈0.013 → **静音开着照样出声**。
4578
+ * `postMessage` 是**跨源唯一可达**的通道;渲染器侧收口是一个 master GainNode(图级归零),
4579
+ * 见 demo.html 的 `mpwAudioSetPolicy` / `mpwAudioConnectMaster`。
4580
+ * 幂等 + **刷新后必发**:`contentWindow` 换了(帧重载/换 src/刷新)⇒ 即使值相同也重发一次 ——
4581
+ * 这正是"刷新之后壁纸又开始出声"的那条缝(旧写法只在挂载/切歌时下发)。 */
4582
+ function sendRendererAudioPolicy(frame, mute, park, why) {
4583
+ try {
4584
+ const cw = frame && frame.contentWindow;
4585
+ if (!cw || typeof cw.postMessage !== "function") return false;
4586
+ const want = { muted: !!mute, volume: npWantVolume() };
4587
+ /* ①(2026-09-25 任务 ㉑ 追加) `park` = “把这一页收起来”(宿主隐藏/看门狗兜底/省电暂停):
4588
+ 渲染器侧收到就**停源**(pause 元素 + ctx.suspend),不是只压增益。 */
4589
+ if (park !== undefined) want.park = !!park;
4590
+ const prev = frame.__mpwRendererAudioPolicy;
4591
+ const stale = !prev || prev.win !== cw;
4592
+ if (!stale && prev.muted === want.muted && prev.volume === want.volume && prev.park === want.park) return false;
4593
+ frame.__mpwRendererAudioPolicy = { win: cw, muted: want.muted, volume: want.volume, park: want.park, at: Date.now() };
4594
+ let posted = false;
4595
+ try { cw.postMessage({ type: "mpw-audio-policy", muted: want.muted, volume: want.volume, park: want.park, why: String(why || "") }, "*"); posted = true } catch (e) { /* 跨源不可达时如实留 false */ }
4596
+ /* 同源时再走一遍**真能力** API(跨源读不到 `__wp` ⇒ 静默跳过;幂等由渲染器侧保证)。 */
4597
+ try { if (cw.__wp && typeof cw.__wp.setAudioPolicy === "function") cw.__wp.setAudioPolicy({ muted: want.muted, volume: want.volume, park: want.park }) } catch (e) {}
4598
+ /* 可查台账(探针/tools/audio-leak-frame-probe.mjs 读它证明"刷新后重新下发过")。 */
4599
+ try {
4600
+ const rec = window.__mpwRendererAudioPush || (window.__mpwRendererAudioPush = { n: 0, last: null, log: [] });
4601
+ rec.n++;
4602
+ rec.last = { at: Date.now(), muted: want.muted, volume: want.volume, park: want.park, why: String(why || ""), stale: stale, posted: posted, sameOrigin: (() => { try { return !!cw.document } catch (e) { return false } })() };
4603
+ rec.log.push(rec.last); if (rec.log.length > 12) rec.log.shift();
4604
+ } catch (e) {}
4605
+ return posted;
4606
+ } catch (e) { return false }
4607
+ }
4292
4608
  /** ①(新) web 壁纸静音:iframe.muted + 内部所有 audio/video 元素 muted。
4293
4609
  * (仅 iframe.muted 对部分用 WebAudio/动态创建的壁纸无效,双保险。)
4294
4610
  * ①(NP-3) 我们自己的曲目播放器在放音时**强制静音帧内**(见 npAudioOwns):壁纸目录里
@@ -4307,6 +4623,14 @@ function mpwThumbFail(el, why) {
4307
4623
  的 `__mpwAudioBusRepatch` 台账),这里是同一类问题的插件侧落点。
4308
4624
  语义不变:`mute` 的算法、释放路径(只撤自己按过的)与失败留痕都不动。 */
4309
4625
  if (frame && frame.muted !== mute) frame.muted = mute;
4626
+ /* ①(2026-09-25 任务 ㉑) **图级静音通道**(跨源唯一可达的那条):场景渲染器帧必须走它 ——
4627
+ 它的音轨挂在 WebAudio 图上,元素 `muted` 只覆盖元素路径,而 8902 与 3080 不同源,
4628
+ 上面那三条老通道一条都到不了。这里**每次 applyWebMute 都尝试下发**(含帧 load、
4629
+ 800ms/2s 周期、以及刷新后的第一次),幂等由 `sendRendererAudioPolicy` 自己保证。 */
4630
+ try { sendRendererAudioPolicy(frame, mute); } catch {}
4631
+ /* ①(2026-09-25 任务 ㉑ 追加) 帧内音频归属/暂停状态也要跟着走:`npFrameSoundBlocked()` 为真
4632
+ (卡片暂停/我们真的在放音)时,把这一页**收起来**(停源),否则只是压增益。 */
4633
+ try { if (npFrameSoundBlocked()) sendRendererAudioPolicy(frame, true, true, "np-owns"); } catch {}
4310
4634
  // ①(2026-09-16 I 项) shim 通道:帧内静音由 shim 执行(不透明源读不到帧内元素)
4311
4635
  if (frame && mpwWebShimUrl(frame.getAttribute("src"))) {
4312
4636
  const want = { muted: mute, speed: npPolicySpeed(), volume: npWantVolume() };
@@ -6434,7 +6758,15 @@ function mpwThumbFail(el, why) {
6434
6758
  });
6435
6759
  } catch (e) {}
6436
6760
  } else {
6437
- try { const f = bgElements().frame; if (f) f.style.display = "none"; } catch {}
6761
+ try {
6762
+ const f = bgElements().frame
6763
+ if (f) {
6764
+ /* ①(2026-09-25 任务 ㉑ 追加) **总开关关掉 = 只隐藏不卸载** ⇒ 帧内音频必须停掉
6765
+ (旧写法只 `display:none`,隐藏 iframe 里的媒体照放)。 */
6766
+ try { sendRendererAudioPolicy(f, true, true, "wallpaper-disabled") } catch (e) {}
6767
+ f.style.display = "none"
6768
+ }
6769
+ } catch {}
6438
6770
  }
6439
6771
  // ①(修正) 刷新 buildCss(hasImage 已含 webUrl)→ .mpw-bgWrap.mpw-web iframe 可见
6440
6772
  try { const se = getStyleEl(); if (se) se.textContent = buildCss(section); } catch (err) { mpwErr("buildCss(web 路径)", err); }
@@ -8272,6 +8604,19 @@ function mpwThumbFail(el, why) {
8272
8604
  /* ①(NP-3) 我们的播放器在放音时,web 壁纸帧内的那份声音要让位(同一首会被放两遍)。 */
8273
8605
  try { return !!(npAudio && !npAudio.paused && (npAudio.currentSrc || npAudio.getAttribute("src") || npAudio.src)) } catch (e) { return false }
8274
8606
  }
8607
+ /** ①(2026-09-25 任务 ㉑) **"我们真的在出声"**(比 `npAudioOwns()` 严一档):元素在放 **且**
8608
+ * 没被静音 **且** 音量 > 0。为什么单独有它:`npFrameSoundBlocked()` 只该在"帧内那份声音会与
8609
+ * **可听见的**我们叠在一起"时压帧内 —— 旧口径用 `npAudioOwns()`(只看 `paused`),
8610
+ * 于是"元素在放但被 muted/volume=0"也算占用 ⇒ 帧内被白白压住(用户报的"壁纸自己没声音")。
8611
+ * 反过来,用户报"静音开着仍有声音"那一条**不靠它**:设置项 `mute` 是独立的一项,
8612
+ * 下面 `npFrameSoundBlocked()` 里仍旧算数。 */
8613
+ function npOwnAudible() {
8614
+ try {
8615
+ if (!npAudioOwns()) return false
8616
+ if (npAudio.muted) return false
8617
+ return Number(npAudio.volume) > 0
8618
+ } catch (e) { return false }
8619
+ }
8275
8620
  /* ══════════════════════════════════════════════════════════════════════════════════
8276
8621
  ④(2026-09-20 真机修复)「卡片的静音/暂停必须**真的管住壁纸自带的音频**」
8277
8622
  ──────────────────────────────────────────────────────────────────────────────────
@@ -8318,8 +8663,11 @@ function mpwThumbFail(el, why) {
8318
8663
  const s = readSection();
8319
8664
  const on = s.npNowPlaying !== void 0 ? !!s.npNowPlaying : DEFAULT_NP_NOW_PLAYING;
8320
8665
  if (!on) return false;
8666
+ /* ①(2026-09-25 任务 ㉑) 口径对齐"**真的在放音**":元素在放但被 muted/音量 0 ⇒ 不占用,
8667
+ 帧内那份声音放行(旧口径只看 `paused` ⇒ 白白压住壁纸自带的 BGM)。
8668
+ ⚠ 这两行必须**紧邻**(`wallpaper-lifecycle-test` 的变异锚点钉的就是它们连在一起的字面量)。 */
8321
8669
  if (npCardPaused) return true;
8322
- return npAudioOwns();
8670
+ return npOwnAudible();
8323
8671
  } catch (e) { return false }
8324
8672
  }
8325
8673
  /** ④ 把"帧内音频现在归谁管"如实发布到 DOM/全局(探针与设置面板都读它,不猜)。 */
@@ -9199,7 +9547,20 @@ function mpwThumbFail(el, why) {
9199
9547
  // (旧写法恢复时会 play() **作者自己**停着的媒体 —— 省电档默认打开后这会变成常态问题)。
9200
9548
  // ①(2026-09-23) 再加一条前置:**这一下真的送达了**(shim 投出成功,或我们确实 pause 到了
9201
9549
  // 至少一个元素)才记账;失败路径不许把"没停成"记成"我们停的"。
9550
+ /* ①(2026-09-25 任务 ㉑ 追加) 跨源帧(场景渲染器 / 不透明源 web 帧):shim 到不了、
9551
+ `contentDocument` 为 null ⇒ 上面两条都等于没停。这时走 postMessage `park`(渲染器侧
9552
+ 真的 pause + suspend),并把"我们停过"如实记账(否则恢复时会去 play 作者自己停着的媒体)。 */
9553
+ /* `shimOk` 只等于"postMessage 投出没抛错" —— 对**不是** shim 的帧(场景渲染器)它同样返回 true,
9554
+ 那条 `{mpw:"mpw:web", op:"pause"}` 发过去根本没人收 ⇒ 必须按"**这个帧是不是 shim 帧**"分流:
9555
+ · shim 帧(`?mpwshim=1`):帧内 shim 自己按下暂停(既有语义,不动);
9556
+ · 非 shim 帧 / 跨源帧:走 `park`(渲染器侧真停源)。 */
9557
+ const isShimFrame = !!(frame && mpwWebShimUrl(frame.getAttribute("src")));
9558
+ let parked = false;
9559
+ if ((!isShimFrame || !shimOk) && n === 0) { try { parked = sendRendererAudioPolicy(frame, true, true, "pause-frame") } catch (e) { parked = false } }
9202
9560
  webFramePausedByUs = !!(shimOk || n > 0);
9561
+ /* ①(2026-09-25 任务 ㉑ 追加) `park` 也是"我们按下的暂停"(跨源帧那条)⇒ 记账,
9562
+ 恢复时才允许撤销(这一行**单独**写:`silent-failure-guards-test` 的变异锚点钉的是上面那行的字面量)。 */
9563
+ if (parked) webFramePausedByUs = true;
9203
9564
  return n;
9204
9565
  } catch { return 0 }
9205
9566
  }
@@ -9210,6 +9571,8 @@ function mpwThumbFail(el, why) {
9210
9571
  if (!frame) return;
9211
9572
  if (!webFramePausedByUs) return; // ⑤ 我们没停过 ⇒ 一个字节都不碰
9212
9573
  webFramePausedByUs = false;
9574
+ /* ①(2026-09-25 任务 ㉑ 追加) 撤销我们自己按下的"收起来"(跨源帧那条)。 */
9575
+ try { sendRendererAudioPolicy(frame, !!readSection().mute || npFrameSoundBlocked(), false, "resume-frame") } catch (e) {}
9213
9576
  webShimCallChecked(frame, { op: "pause", value: false }, "resumeWebFrame");
9214
9577
  const doc = frame.contentDocument;
9215
9578
  if (doc) {
@@ -11458,6 +11821,17 @@ function installAudioBus(win, opts = {}) {
11458
11821
  pauseWebFrame: () => { try { return pauseWebFrame() } catch (e) { return String(e && e.message || e) } },
11459
11822
  resumeWebFrame: () => { try { resumeWebFrame(); return true } catch (e) { return String(e && e.message || e) } },
11460
11823
  pauseWallpaperVideo: () => { try { pauseWallpaperVideo(); return true } catch (e) { return String(e && e.message || e) } },
11824
+ /* ①(2026-09-25 任务 ㉑ 追加) 场景看门狗兜底:真机"壁纸显示静态帧、声音却在响(且不是当前
11825
+ 壁纸的音频)"那条路径。跑的是**真实** `sceneFallbackActivate`;钩子只负责播种最小
11826
+ watchdog 状态(`__mpwSceneWd`),否则它自己会因 `!wd` 提前返回(判据就变成空转)。 */
11827
+ sceneFallback: (why) => {
11828
+ try {
11829
+ if (!__mpwSceneWd) __mpwSceneWd = { ident: "test-hook", info: null, url: "", armedAt: Date.now(), secs: 0, confirmed: false, fallback: false, lastPostAt: 0, checkTimer: 0, fallbackAt: 0 };
11830
+ sceneFallbackActivate(String(why || "test"));
11831
+ return { fallback: !!(__mpwSceneWd && __mpwSceneWd.fallback), park: window.__mpwSceneFallbackPark || null };
11832
+ } catch (e) { return String(e && e.message || e) }
11833
+ },
11834
+ sceneWd: () => { try { return __mpwSceneWd ? { ident: __mpwSceneWd.ident, fallback: !!__mpwSceneWd.fallback, confirmed: !!__mpwSceneWd.confirmed } : null } catch (e) { return null } },
11461
11835
  applyFrostInline: (sec) => { try { applyFrostInline(sec || readSection()); return true } catch (e) { return String(e && e.message || e) } },
11462
11836
  startFrostObserver: () => { try { startFrostObserver(); return true } catch (e) { return String(e && e.message || e) } },
11463
11837
  startCompatObserver: () => { try { startCompatObserver(); return true } catch (e) { return String(e && e.message || e) } },
@@ -17171,7 +17545,13 @@ body[data-mpw-modal] [role="dialog"] {
17171
17545
  }
17172
17546
  };
17173
17547
  const [url, setUrl] = react.useState("");
17174
- const [propsExpanded, setPropsExpanded] = react.useState(false);
17548
+ // ①(2026-09-24 用户 bug 修复) **默认展开**:旧默认 false ⇒ 面板只剩一个「展开全部(N)」
17549
+ // 按钮,用户视角就是"只显示了一项、展不开"。现在默认把官方属性**逐项摊开**。
17550
+ const [propsExpanded, setPropsExpanded] = react.useState(true);
17551
+ // ①(新) 分组折叠(按 group key 记;默认全展开)+ 文本框草稿(失焦/回车才落盘,避免逐字符重挂壁纸)
17552
+ const [propGroupsClosed, setPropGroupsClosed] = react.useState({});
17553
+ const [propDraft, setPropDraft] = react.useState({});
17554
+ const propTimer = react.useRef(null);
17175
17555
  const [conflictModal, setConflictModal] = react.useState(false);
17176
17556
  const [previewModal, setPreviewModal] = react.useState(false);
17177
17557
  const [errorModal, setErrorModal] = react.useState(false);
@@ -18855,12 +19235,14 @@ body[data-mpw-modal] [role="dialog"] {
18855
19235
 
18856
19236
 
18857
19237
  const propEdits = (section.propEdits && section.propEdits[section.mpkgKey]) || {};
18858
- const setProp = (key, value) => {
19238
+ /** ①(2026-09-24 用户 bug 修复) 可调参数**写入通道**(原 `setProp` 是死代码,全仓 0 处调用)。
19239
+ * `instant=false` 走 writeSection 的防抖路径(滑杆拖动/文本输入不每步重挂壁纸)。 */
19240
+ const setProp = (key, value, instant) => {
18859
19241
  const key2 = section.mpkgKey;
18860
19242
  if (!key2 || !safePropKey(key)) return;
18861
19243
  const edits = Object.assign({}, propEdits, { [key]: value });
18862
19244
  const propEditsAll = Object.assign({}, section.propEdits || {}, { [key2]: edits });
18863
- commit({ propEdits: propEditsAll }, true);
19245
+ commit({ propEdits: propEditsAll }, instant !== false);
18864
19246
  // ①(2026-09-16 I 项) 网页壁纸 shim 通道:编辑立刻下发到已挂载的帧(沙箱下父页读不到帧内 DOM,
18865
19247
  // 只能靠 postMessage;作者侧 applyUserProperties 会收到增量)。
18866
19248
  try {
@@ -18873,10 +19255,12 @@ body[data-mpw-modal] [role="dialog"] {
18873
19255
  const tvProp = allProps.find((p) => /随现实时间|timevarying|real time/.test(p.label + " " + p.key));
18874
19256
  const tvEnabled = tvProp ? (propEdits[tvProp.key] !== void 0 ? !!propEdits[tvProp.key] : !!tvProp.value) : true;
18875
19257
  const TIME_KEYS = new Set(["morningtime", "daytime", "dusktime", "nighttime", "display"]);
18876
- // ④(重做) 参数渲染暂不可用(浏览器显示的是预渲染素材,改了不生效):
18877
- // 默认全部折叠,只显示「展开全部」按钮;点击展开后只读展示全部参数。
18878
- const propsShown = tvEnabled ? allProps : allProps.filter((p) => !TIME_KEYS.has(p.key));
18879
- const propsToShow = propsExpanded ? propsShown : [];
19258
+ /* ①(2026-09-24 用户 bug「WE 自带选项只显示一项且展不开」修复) —— 面板行模型:
19259
+ 由 `mpwPropsModel` 按官方口径产出(分组头 + 子项 + condition 显隐 + 控件类型 + 记账),
19260
+ 面板**默认展开**(原来默认折叠 ⇒ 用户只看到一个「展开全部」按钮)。 */
19261
+ const propModel = mpwPropsModel(allProps, propEdits);
19262
+ const propRows = propModel.rows.filter((r) => r.visible && (tvEnabled || !TIME_KEYS.has(r.key)));
19263
+ const propsShownCount = propRows.filter((r) => r.kind !== "group").length;
18880
19264
  // ②(修正) web 壁纸选项项数(折叠按钮显示用):分辨率+语言+音量+开关
18881
19265
  const webCfgCount = (() => {
18882
19266
  if (!webCfg || !webCfg.model) return 0;
@@ -19799,37 +20183,160 @@ body[data-mpw-modal] [role="dialog"] {
19799
20183
  ]),
19800
20184
 
19801
20185
 
19802
- // 可调参数展示(④(重做):默认折叠成「展开全部」按钮。
19803
- // ②(修正) mpkg → 只读 + "暂不可用"(浏览器显示的是预渲染素材,改不了);
19804
- // web 壁纸(L2D 类 loadJson SettingModel)→ 同一折叠区,**可改**(分辨率/
19805
- // 语言/音量写入 iframe 同源 localStorage)。两者共用 propsExpanded 折叠。
20186
+ // 可调参数展示(①(2026-09-24 用户 bug「WE 自带选项只显示一项且展不开」修复):
20187
+ // · **默认展开**(旧默认折叠 ⇒ 面板只剩「展开全部」一个按钮 = 用户说的"只显示一项");
20188
+ // · 每个官方属性渲染**真控件**(bool 开关 / slider 量程滑杆 / combo 下拉 / color 取色 /
20189
+ // textinput 文本框),onChange 走 `setProp` **真的写进 propEdits 并下发**(此前是死代码);
20190
+ // · `type:"group"` = 分组头(可折叠,默认展开),子项按官方 order 跟随;
20191
+ // · `condition` 为假的项**不显示**(含传递闭包),白名单外的表达式按"未知"照显示并记账;
20192
+ // · file/directory/scenetexture 需要渲染器资源通道 ⇒ **只读如实展示 + 写明原因**(不猜);
20193
+ // · schemecolor/usershortcut 等不渲染的条目**逐条记账**(下方一行提示,不静默)。
20194
+ // ②(修正) mpkg → 控件可调但画面是否即时变化取决于壁纸类型(预渲染素材=下次生效);
20195
+ // web 壁纸(L2D 类 loadJson SettingModel)→ 同一折叠区,分辨率/语言/音量写入 iframe 同源 localStorage。
19806
20196
  ((mpkgMeta && mpkgMeta.info && section.fromMpkg && allProps.length) || webCfg) ? h("div", { className: "mpw_field" }, [
19807
20197
  h("label", { className: "mpw_label" },
19808
- `${t("props.title")}${webCfg ? "" : `(${t("props.unavailable")})`}`),
20198
+ `${t("props.title")}${webCfg ? "" : `(${propsShownCount})`}`),
19809
20199
  h("p", { className: "mpw_hint" }, webCfg ? t("webcfg.desc") : t("props.desc")),
19810
20200
  propsExpanded ? h("div", { className: "mpw_props" }, [
19811
- // ── mpkg 只读参数列表 ──
19812
- mpkgMeta && mpkgMeta.info && section.fromMpkg && allProps.length ? propsToShow.map((p) => {
19813
- // 纯展示性条目(作者信息/说明):不渲染输入框
19814
- if (p.displayOnly) {
19815
- return h("div", { className: "mpw_prop mpw_static", key: p.key },
19816
- h("span", { className: "mpw_propLabel" }, p.label));
20201
+ // ── 官方 user properties(分组 + 控件;写入 propEdits)──
20202
+ mpkgMeta && mpkgMeta.info && section.fromMpkg && allProps.length ? propRows.map((row) => {
20203
+ // 分组头:可折叠(默认展开),带子项数
20204
+ if (row.kind === "group") {
20205
+ const closed = !!propGroupsClosed[row.key];
20206
+ return h("div", { className: "mpw_prop mpw_propGroup", key: "g:" + row.key }, [
20207
+ h("button", {
20208
+ className: "mpw_reset mpw_moreBtn", type: "button", "data-mpw-propgroup": row.key,
20209
+ "aria-expanded": closed ? "false" : "true",
20210
+ onClick: () => setPropGroupsClosed(Object.assign({}, propGroupsClosed, { [row.key]: !closed }))
20211
+ }, (closed ? "▸ " : "▾ ") + (row.label || row.key) + `(${row.childCount})`)
20212
+ ]);
19817
20213
  }
19818
- const edited = propEdits[p.key] !== void 0 ? propEdits[p.key] : p.value;
19819
- const isBool = typeof p.value === "boolean";
20214
+ // 被折叠的分组 ⇒ 其子项不渲染
20215
+ if (row.group && propGroupsClosed[row.group]) return null;
20216
+ // 官方口径不渲染的条目(schemecolor / usershortcut / 未知类型):
20217
+ // **不渲染控件**(只在上面的"未渲染条目"一行里如实登记)
20218
+ if (row.kind === "skip") return null;
20219
+ const indent = row.group ? { marginLeft: 12 } : null;
20220
+ const shown = row.value;
19820
20221
  const fmt = (v) => {
19821
20222
  if (typeof v === "boolean") return v ? t("props.on") : t("props.off");
19822
- if (Array.isArray(p.options) && p.options.length) {
19823
- const o = p.options.find((x) => String(x.value) === String(v));
20223
+ if (Array.isArray(row.options) && row.options.length) {
20224
+ const o = row.options.find((x) => String(x.value) === String(v));
19824
20225
  return o ? cleanLabel(o.label) : String(v);
19825
20226
  }
19826
- return String(v);
20227
+ return String(v === void 0 || v === null ? "" : v);
19827
20228
  };
19828
- return h("div", { className: "mpw_prop", key: p.key }, [
19829
- h("div", { className: "mpw_propLabel" }, h("b", null, p.label)),
19830
- h("span", { className: "mpw_propValue" }, fmt(edited))
20229
+ // 说明行(type text/txt 或无 type 的作者文案):不渲染输入控件
20230
+ if (row.kind === "note") {
20231
+ return h("div", { className: "mpw_prop mpw_static", key: row.key, style: indent },
20232
+ h("span", { className: "mpw_propLabel" }, row.label || row.key));
20233
+ }
20234
+ const labelEl = h("div", { className: "mpw_propLabel" }, [
20235
+ h("b", null, row.label || row.key),
20236
+ row.edited ? h("span", { className: "mpw_hint" }, " · " + t("props.edited")) : null
20237
+ ]);
20238
+ // file/directory/scenetexture:官方语义需要渲染器资源通道 ⇒ 只读 + 写明原因
20239
+ if (row.kind === "readonly") {
20240
+ return h("div", { className: "mpw_prop", key: row.key, style: indent }, [
20241
+ labelEl,
20242
+ h("span", { className: "mpw_propValue" }, fmt(shown)),
20243
+ h("span", { className: "mpw_hint" }, row.kindWhy)
20244
+ ]);
20245
+ }
20246
+ // ── 真控件 ──
20247
+ if (row.kind === "control" && row.type === "bool") {
20248
+ return h("div", { className: "mpw_prop", key: row.key, style: indent }, [
20249
+ h("div", { className: "mpw_inline" }, [
20250
+ h(Toggle, { checked: !!shown, onChange: (v) => setProp(row.key, !!v) }),
20251
+ h("span", { className: "mpw_propLabel" }, row.label || row.key),
20252
+ row.edited ? h("span", { className: "mpw_hint" }, " · " + t("props.edited")) : null
20253
+ ])
20254
+ ]);
20255
+ }
20256
+ if (row.kind === "control" && row.type === "slider") {
20257
+ const min = typeof row.min === "number" ? row.min : 0;
20258
+ const max = typeof row.max === "number" ? row.max : 1;
20259
+ const step = typeof row.step === "number" && row.step > 0 ? row.step : ((max - min) / 100 || 0.01);
20260
+ const base = Number(shown);
20261
+ // ① 与插件既有滑杆同一口径:拖动时**即时显示**,落盘/重挂防抖 350ms
20262
+ // (逐个 input 事件都 commit → applyFromStorage 会把壁纸层反复重挂)
20263
+ const live = propDraft[row.key] !== void 0 ? Number(propDraft[row.key]) : (isFinite(base) ? base : min);
20264
+ const flush = (v) => {
20265
+ if (propTimer.current) { clearTimeout(propTimer.current); propTimer.current = null }
20266
+ setProp(row.key, v, false);
20267
+ };
20268
+ return h("div", { className: "mpw_prop", key: row.key, style: indent }, [
20269
+ labelEl,
20270
+ h("div", { className: "mpw_inline" }, [
20271
+ h("input", {
20272
+ className: "mpw_slider", type: "range", min, max, step,
20273
+ "data-mpw-prop": row.key,
20274
+ value: live,
20275
+ onChange: (ev) => {
20276
+ const v = Number(ev.target.value);
20277
+ setPropDraft(Object.assign({}, propDraft, { [row.key]: v }));
20278
+ if (propTimer.current) clearTimeout(propTimer.current);
20279
+ propTimer.current = setTimeout(() => { propTimer.current = null; flush(v) }, 350);
20280
+ },
20281
+ onMouseUp: (ev) => flush(Number(ev.target.value)),
20282
+ onTouchEnd: (ev) => flush(live),
20283
+ onBlur: (ev) => flush(Number(ev.target.value))
20284
+ }),
20285
+ h("span", { className: "mpw_value" }, fmt(live))
20286
+ ])
20287
+ ]);
20288
+ }
20289
+ if (row.kind === "control" && row.type === "combo") {
20290
+ const opts = (row.options || []).map((o) => ({ value: String(o.value), label: cleanLabel(o.label) }));
20291
+ return h("div", { className: "mpw_prop", key: row.key, style: indent }, [
20292
+ labelEl,
20293
+ h(SelectBox, {
20294
+ name: "prop:" + row.key, value: String(shown === void 0 ? "" : shown),
20295
+ placeholder: fmt(shown), options: opts,
20296
+ onChange: (v) => setProp(row.key, v)
20297
+ })
20298
+ ]);
20299
+ }
20300
+ if (row.kind === "control" && row.type === "color") {
20301
+ const hex = mpwPropColorToHex(shown);
20302
+ return h("div", { className: "mpw_prop", key: row.key, style: indent }, [
20303
+ labelEl,
20304
+ h("div", { className: "mpw_inline" }, [
20305
+ h("input", {
20306
+ className: "mpw_colorSwatch", type: "color", "data-mpw-prop": row.key,
20307
+ value: hex || "#000000",
20308
+ onChange: (ev) => setProp(row.key, mpwPropHexToColor(ev.target.value))
20309
+ }),
20310
+ h("span", { className: "mpw_hint" }, (hex || "") + " (" + fmt(shown) + ")")
20311
+ ])
20312
+ ]);
20313
+ }
20314
+ // textinput / 其余可编辑文本
20315
+ const draftKey = row.key;
20316
+ return h("div", { className: "mpw_prop", key: row.key, style: indent }, [
20317
+ labelEl,
20318
+ h("input", {
20319
+ className: "mpw_input", type: "text", "data-mpw-prop": row.key,
20320
+ value: propDraft[draftKey] !== void 0 ? propDraft[draftKey] : fmt(shown),
20321
+ onChange: (ev) => setPropDraft(Object.assign({}, propDraft, { [draftKey]: ev.target.value })),
20322
+ onBlur: () => {
20323
+ const v = propDraft[draftKey];
20324
+ if (v === void 0) return;
20325
+ setProp(draftKey, v);
20326
+ const next = Object.assign({}, propDraft); delete next[draftKey]; setPropDraft(next);
20327
+ },
20328
+ onKeyDown: (ev) => { if (ev.key === "Enter") { try { ev.currentTarget.blur() } catch (e) {} } }
20329
+ })
19831
20330
  ]);
19832
20331
  }) : null,
20332
+ // 记账:未渲染的官方条目(schemecolor / usershortcut / 未知类型)如实列出,不静默
20333
+ mpkgMeta && mpkgMeta.info && section.fromMpkg && propModel.counts.skipped
20334
+ ? h("p", { className: "mpw_hint" },
20335
+ t("props.skipped") + propModel.rows.filter((r) => r.kind === "skip").map((r) => r.key + ":" + r.kindWhy).join(";"))
20336
+ : null,
20337
+ mpkgMeta && mpkgMeta.info && section.fromMpkg && propModel.counts.hidden
20338
+ ? h("p", { className: "mpw_hint" }, t("props.hiddenByCond") + propModel.counts.hidden)
20339
+ : null,
19833
20340
  !tvEnabled ? h("p", { className: "mpw_hint" }, t("props.tvOff")) : null,
19834
20341
  // ── web 壁纸可改选项(分辨率/语言/音量,写入 iframe localStorage)──
19835
20342
  webCfg ? h("div", { className: "mpw_props" }, [
@@ -19876,13 +20383,15 @@ body[data-mpw-modal] [role="dialog"] {
19876
20383
  const next = Object.assign({}, s, { propEdits: pe });
19877
20384
  writeSection(next, true);
19878
20385
  setSection(next);
20386
+ // ① 恢复默认时把滑杆/文本框的本地草稿一并清掉(否则控件仍显示已改的值)
20387
+ setPropDraft({});
19879
20388
  setHint(t("props.resetDone"));
19880
20389
  } }, t("props.resetWallpaper"))
19881
20390
  ])
19882
20391
  ]) : null,
19883
20392
  h("div", { className: "mpw_inline" }, [
19884
20393
  h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => setPropsExpanded(!propsExpanded) },
19885
- propsExpanded ? t("props.collapse") : t("props.expand") + `(${webCfg ? webCfgCount : propsShown.length})`)
20394
+ propsExpanded ? t("props.collapse") : t("props.expand") + `(${webCfg ? webCfgCount : propsShownCount})`)
19886
20395
  ])
19887
20396
  ]) : null,
19888
20397
  // ①(新) 省电(从「其他」tab 挪到壁纸设置最下面):遮挡暂停三档(tab 内结尾)
@@ -20924,11 +21433,14 @@ body[data-mpw-modal] [role="dialog"] {
20924
21433
  "webcfg.saveFail": "保存失败:",
20925
21434
  "webcfg.readFail": "读取壁纸自身设置失败(为避免覆盖你已改过的选项,本次**不写回**):",
20926
21435
  "webcfg.fail": "应用失败:",
20927
- "props.desc": "浏览器显示的是壁纸引擎预渲染的素材,修改参数不会改变画面。以下为壁纸自带的参数及当前值,供对照:如需修改,请在壁纸引擎 App 中调整",
21436
+ "props.desc": "以下为壁纸**自带**的官方参数(project.json → general.properties),逐项可调:改动会保存到本插件设置,并即时下发给已加载的网页壁纸;静态帧/视频类素材的画面要等下次应用(或由场景渲染器应用)才反映改动",
20928
21437
  "props.unavailable": "暂不可用",
20929
21438
  "props.on": "开",
20930
21439
  "props.off": "关",
20931
21440
  "props.expand": "展开全部",
21441
+ "props.edited": "已改",
21442
+ "props.skipped": "未渲染的条目(如实登记,不做猜测):",
21443
+ "props.hiddenByCond": "按壁纸自带 condition 隐藏的条目数:",
20932
21444
  "props.important": "★ 关键开关",
20933
21445
  "props.resetWallpaper": "重置壁纸参数",
20934
21446
  "props.resetDone": "壁纸参数已恢复默认",
@@ -21397,11 +21909,14 @@ body[data-mpw-modal] [role="dialog"] {
21397
21909
  "webcfg.saveFail": "Save failed: ",
21398
21910
  "webcfg.readFail": "Could not read the wallpaper's own settings (nothing was written back, so your existing options are not overwritten): ",
21399
21911
  "webcfg.fail": "Apply failed: ",
21400
- "props.desc": "The browser shows pre-rendered wallpaper assets, so editing these options cannot change the picture. Listed below are the wallpaper's own parameters and their current values for reference; change them in the Wallpaper Engine app instead",
21912
+ "props.desc": "These are the wallpaper's own official options (project.json → general.properties), adjustable per item: changes are saved to this plugin's settings and pushed live to a loaded web wallpaper; for still-frame/video assets the picture reflects them after the next apply (or when a scene renderer applies them)",
21401
21913
  "props.unavailable": "unavailable",
21402
21914
  "props.on": "On",
21403
21915
  "props.off": "Off",
21404
21916
  "props.expand": "Show all",
21917
+ "props.edited": "edited",
21918
+ "props.skipped": "Not rendered (recorded as-is, never guessed): ",
21919
+ "props.hiddenByCond": "Items hidden by the wallpaper's own condition: ",
21405
21920
  "props.important": "★ Key switch",
21406
21921
  "props.resetWallpaper": "Reset wallpaper options",
21407
21922
  "props.resetDone": "Wallpaper options restored to defaults",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mpkg-wallpaper",
3
- "version": "3.13.7",
3
+ "version": "3.13.8",
4
4
  "description": "DSH Web 壁纸引擎 Wallpaper Engine mpkg 背景插件:浏览器内直接解析 .mpkg(preview.gif 动态背景/内嵌 mp4 视频/多时段切换),整屏统一虚化/对话框/弹层/遮罩独立虚化、镜头缩放平移、时钟、冲突检测。",
5
5
  "private": false,
6
6
  "type": "module",