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.
- package/lib/client.js +550 -35
- 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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 },
|
|
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
|
-
|
|
18879
|
-
const
|
|
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
|
-
//
|
|
19804
|
-
//
|
|
19805
|
-
//
|
|
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 ? "" : `(${
|
|
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
|
-
// ──
|
|
19812
|
-
mpkgMeta && mpkgMeta.info && section.fromMpkg && allProps.length ?
|
|
19813
|
-
//
|
|
19814
|
-
if (
|
|
19815
|
-
|
|
19816
|
-
|
|
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
|
-
|
|
19819
|
-
|
|
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(
|
|
19823
|
-
const o =
|
|
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
|
-
|
|
19829
|
-
|
|
19830
|
-
h("
|
|
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 :
|
|
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": "
|
|
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": "
|
|
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