dsh-mpkg-wallpaper 3.8.1 → 3.8.2
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 +930 -65
- package/lib/now-playing.js +371 -4
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -206,6 +206,22 @@ function mpwTrace(ev, data) {
|
|
|
206
206
|
宿主同一位置已经有**别的插件**注入的元素时,我们**不挂/撤下**并留一行可读 warn,
|
|
207
207
|
而不是叠着画。 */
|
|
208
208
|
const DEFAULT_NP_NOW_PLAYING = true;
|
|
209
|
+
/* ①(NP-4 2026-09-19 真机修复) 音量电平(0..100)。
|
|
210
|
+
为什么要有它:NP-3 的"第四个键"只是**静音开关**(布尔 muted),全仓**没有任何**
|
|
211
|
+
能改电平的落点(真机读数:section 里与音量有关的键 0 个)⇒ 用户现场
|
|
212
|
+
"打开之后也不能调整它开启的音量"。默认 100:与 `HTMLMediaElement.volume` 的默认值
|
|
213
|
+
相同,也与 web 帧主音量的缺省(web-wallpaper.js 的 masterVolume=1)相同
|
|
214
|
+
⇒ **默认档逐字节零行为变化**(判据见 tools/np-control-test.mjs 的 E 组)。 */
|
|
215
|
+
const DEFAULT_NP_VOLUME = 100;
|
|
216
|
+
/* ①(NP-4)② 「播放/暂停是否同时控制壁纸的播放/暂停」联动开关,默认**开**。
|
|
217
|
+
默认必须是开:改动之前 NP 的播放/暂停/进度就是直接落在壁纸媒体元素上的
|
|
218
|
+
(视频档:`vid.play()/pause()`;见 npTransport)——默认关就等于改掉了既有行为,
|
|
219
|
+
而本条明确要求"不许在做这件事时改变既有默认行为"。
|
|
220
|
+
语义(口径见 docs/NOW-PLAYING-DSH.md §7.8):
|
|
221
|
+
开 ⇒ 传输动作同时驱动壁纸自己的媒体 + 我们自己的播放器;
|
|
222
|
+
关 ⇒ 只驱动我们自己的播放器(壁纸的画面播放不受控件影响);
|
|
223
|
+
壁纸媒体是**唯一**声源时(视频档没有旁挂音频)控件如实禁用,不假装。 */
|
|
224
|
+
const DEFAULT_NP_LINK_WALLPAPER = true;
|
|
209
225
|
const HOST_BASE = "/api/mpkg-wallpaper";
|
|
210
226
|
// ①(MERGED-3 1.3) 诊断开关速查区离线副本(渲染器离线时用)。
|
|
211
227
|
// 在线数据源 = <渲染器>/diag-flags.json(由 we-scene-demo/diag-flag-check.mjs 脚本生成,勿手改);
|
|
@@ -1975,11 +1991,12 @@ function sectionSigNow() {
|
|
|
1975
1991
|
// 导致每次 applyFromStorage 都重设 src → Edge 端也重载重播。
|
|
1976
1992
|
try { if (video.getAttribute('src') !== url) video.src = url; } catch {}
|
|
1977
1993
|
video.playbackRate = (typeof readSection().playbackRate === "number" && readSection().playbackRate >= 0.5 && readSection().playbackRate <= 2) ? readSection().playbackRate : 1;
|
|
1978
|
-
/* ①(NP-
|
|
1979
|
-
|
|
1994
|
+
/* ①(NP-4) 起播顺序:**先 muted 起播,拿到播放权之后再按设置应用 muted/volume**。
|
|
1995
|
+
旧写法在这里先 `applyVideoMute()` 再 `play()` ⇒ 那是一次"有声自动播放",
|
|
1996
|
+
没有用户手势时被浏览器直接拒(真机读数:NotAllowedError,见 npPrimePlay 的注释)
|
|
1997
|
+
⇒ 视频永远 paused、画面冻住、也没有声音。顺序是这条 bug 的全部根因。 */
|
|
1998
|
+
try { npPrimePlay(video); } catch {}
|
|
1980
1999
|
edgeDrawFrame();
|
|
1981
|
-
// ①(修正) 暂停门控:Edge canvas 渲染路径也尊重暂停,避免其余路径重播
|
|
1982
|
-
try { if (!(wallUserPaused || powPaused)) { const p = video.play(); if (p && p.catch) p.catch(() => {}); } } catch {}
|
|
1983
2000
|
// ①(修正) 首帧前先画一帧(避免白屏)——用命名函数 + removeEventListener 防泄漏
|
|
1984
2001
|
//(原来 {once:true} 在视频永不加载时会残留监听,评审指出)
|
|
1985
2002
|
const drawOnce = () => { try { video.removeEventListener("loadeddata", drawOnce); } catch {} edgeDrawFrame(); };
|
|
@@ -2038,8 +2055,9 @@ function sectionSigNow() {
|
|
|
2038
2055
|
img.style.display = "";
|
|
2039
2056
|
}
|
|
2040
2057
|
function showVideoEl(url) {
|
|
2058
|
+
npOpTrace("showVideoEl", null, String(url || "").slice(0, 48));
|
|
2041
2059
|
const { img, video, frame, canvas, wrap } = bgElements();
|
|
2042
|
-
if (!img || !video || !wrap) return;
|
|
2060
|
+
if (!img || !video || !wrap) { npOpTrace("showVideoEl", null, "no-elements"); return; }
|
|
2043
2061
|
// ①(加固) CSS 类切换:视频壁纸显示 video、隐藏 img
|
|
2044
2062
|
stopSceneAnim();
|
|
2045
2063
|
disarmSceneWatchdog(true); // ①(批次15 B1) 切到视频壁纸:解除场景看门狗并清兜底视觉
|
|
@@ -2062,21 +2080,19 @@ function sectionSigNow() {
|
|
|
2062
2080
|
// video.src → 浏览器重载媒体源 → autoplay 自动播放,绕过下方 play() 暂停门控
|
|
2063
2081
|
// (用户实测:暂停后调静音壁纸又重播、暂停按钮与实际状态脱节)。
|
|
2064
2082
|
// getAttribute('src') 返回原始的相对字符串,同 url 相等 → 不重载,彻底避免。
|
|
2065
|
-
try { if (video.getAttribute('src') !== url) video.src = url; } catch {}
|
|
2083
|
+
try { if (video.getAttribute('src') !== url) { video.src = url; npSetUserPaused(video, false) } } catch {}
|
|
2066
2084
|
// ⑳(新) 视频倍速:原生 playbackRate(0.5-2x,即时生效,不重载)
|
|
2067
2085
|
try {
|
|
2068
2086
|
const rate = readSection().playbackRate;
|
|
2069
2087
|
if (typeof rate === "number" && rate >= 0.5 && rate <= 2 && video.playbackRate !== rate) video.playbackRate = rate;
|
|
2070
2088
|
} catch {}
|
|
2071
|
-
/* ①(NP-3) 静音设置项 → 视频元素(此前只有建 DOM 时的硬编码 true,再没人改过它)。 */
|
|
2072
|
-
try { applyVideoMute(); } catch {}
|
|
2073
2089
|
video.style.display = "";
|
|
2074
2090
|
// ①(修正) 暂停状态门控:用户手动暂停(wallUserPaused)或省电暂停(powPaused)
|
|
2075
2091
|
// 时不 play——否则任何设置开关 apply(commit→applyFromStorage→showVideoEl)都
|
|
2076
2092
|
// 无条件 play(),把用户手动暂停解开(用户实测"暂停后开启效果又解开暂停")。
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
}
|
|
2093
|
+
// ①(NP-4) 门控**在 npPrimePlay 里**(它自己判 wallUserPaused/powPaused),
|
|
2094
|
+
// 并且把"静音/电平落点"挪到**起播成功之后**(顺序根因见 npPrimePlay 注释)。
|
|
2095
|
+
try { npPrimePlay(video); } catch {}
|
|
2080
2096
|
}
|
|
2081
2097
|
/**
|
|
2082
2098
|
* ①(修复 2026-09-12) 清理场景 iframe 上历史遗留的诊断开关。
|
|
@@ -2848,7 +2864,7 @@ function sectionSigNow() {
|
|
|
2848
2864
|
if (frame) frame.muted = mute;
|
|
2849
2865
|
// ①(2026-09-16 I 项) shim 通道:帧内静音由 shim 执行(不透明源读不到帧内元素)
|
|
2850
2866
|
if (frame && mpwWebShimUrl(frame.getAttribute("src"))) {
|
|
2851
|
-
webShimCall(frame, { op: "policy", muted: mute, speed:
|
|
2867
|
+
webShimCall(frame, { op: "policy", muted: mute, speed: npPolicySpeed(), volume: npWantVolume() });
|
|
2852
2868
|
}
|
|
2853
2869
|
const doc = frame && frame.contentDocument;
|
|
2854
2870
|
if (doc) {
|
|
@@ -2868,11 +2884,278 @@ function sectionSigNow() {
|
|
|
2868
2884
|
if (!video) return false;
|
|
2869
2885
|
try {
|
|
2870
2886
|
const s = readSection();
|
|
2871
|
-
const want = (s.mute !== void 0 ? !!s.mute : true) || npAudioOwns();
|
|
2887
|
+
const want = (npPrimedMuted || npSoundBlocked) ? true : ((s.mute !== void 0 ? !!s.mute : true) || npAudioOwns());
|
|
2872
2888
|
if (video.muted !== want) video.muted = want;
|
|
2873
2889
|
} catch {}
|
|
2874
2890
|
return true;
|
|
2875
2891
|
}
|
|
2892
|
+
/* ══════════════════════════════════════════════════════════════════════════
|
|
2893
|
+
①(NP-4 2026-09-19 真机修复)「壁纸声音」的电平 + 起播顺序
|
|
2894
|
+
真机读数(用户当前档 `custom|3582362359`:video + mp4 内 aac,设置 mute=false):
|
|
2895
|
+
· 加载后 `<video>` = {paused:true, muted:false, volume:1, currentTime:0}
|
|
2896
|
+
· 全程 `play()` **只被调用过一次**,`muted:false`,promise
|
|
2897
|
+
**rejected NotAllowedError**("The play method is not allowed by the user agent…"),
|
|
2898
|
+
而旧写法在 .catch 里把它吞了(lib/client.js 的 showVideoEl 尾部)
|
|
2899
|
+
⇒ 视频永远没起播:画面冻在第一帧、**也没有任何声音**,于是静音开关"开/关都听不到声音"。
|
|
2900
|
+
根因是**顺序**:旧写法先 `applyVideoMute()`(把 muted 写成 false)再 `play()`
|
|
2901
|
+
—— 那一刻这是一次"有声自动播放",没有用户手势时被浏览器直接拒;
|
|
2902
|
+
先以 muted 起播(浏览器允许)拿到播放权,再取消静音才是对的顺序。
|
|
2903
|
+
下面三件事一起做,缺一条就还剩一半:①起播顺序;②失败**不静默**(状态可查 + 首次手势重试);
|
|
2904
|
+
③**电平**落点(NP-3 只有布尔 muted,全仓没有改 volume 的地方)。
|
|
2905
|
+
══════════════════════════════════════════════════════════════════════════ */
|
|
2906
|
+
/** ①(NP-4) 传输/起播动作的**统一轨迹**(最近 32 条)。真机排障唯一能看到"动作走到哪一步"的地方;
|
|
2907
|
+
只记账,零行为影响(探针读它,见 tools/np-media-live-probe.mjs 的 I/K 组)。 */
|
|
2908
|
+
function npOpTrace(op, value, did) {
|
|
2909
|
+
try {
|
|
2910
|
+
const a = window.__mpwNpOps || (window.__mpwNpOps = []);
|
|
2911
|
+
a.push({ op: op, value: value === void 0 ? null : value, at: Date.now(), did: String(did || "") });
|
|
2912
|
+
if (a.length > 32) a.shift();
|
|
2913
|
+
} catch (e) {}
|
|
2914
|
+
}
|
|
2915
|
+
let npPlayBlocked = null; // {name,message,at,retryMuted} —— 起播被拒的可查原因(null = 没被拒)
|
|
2916
|
+
/** 起播被拒的原因**挂到 window 上**(真机探针/控制台可查;写失败不影响任何行为)。
|
|
2917
|
+
为什么必须可查:这条 bug 的修前形态就是"被拒了但没人知道"(`.catch(() => {})`)——
|
|
2918
|
+
一个只在闭包里存在的错误等于没有错误。 */
|
|
2919
|
+
function npMarkPlayBlocked(v) {
|
|
2920
|
+
npPlayBlocked = v;
|
|
2921
|
+
try { window.__mpwNpPlayBlocked = v; } catch (e) {}
|
|
2922
|
+
}
|
|
2923
|
+
let npGestureArmed = false; // 首次用户手势重试只装一次(不常驻监听)
|
|
2924
|
+
let npPrimedMuted = false; // 正在用"muted 起播"兜着(这一段里 muted 由重试持有,不被状态校准撤销)
|
|
2925
|
+
let npPrimedMutedTimer = 0; // 上面那段的到期兜底(1.5s)
|
|
2926
|
+
/* ── ①(NP-4) 浏览器自动播放策略的**第二半**(真机读数逼出来的)──────────────────
|
|
2927
|
+
第一半:没有用户手势时"有声"自动播放会被 **reject**(NotAllowedError)——见 npPrimePlay。
|
|
2928
|
+
第二半(更隐蔽):**"muted 起播成功之后再取消静音"同样不被允许** —— Firefox 在那一刻
|
|
2929
|
+
直接把元素**停住**(paused=true):既不 reject 任何 promise,也不调用 `video.pause()`
|
|
2930
|
+
(所以包 pause/load 都抓不到它)。真机读数(用户那张档,页面从未交互):
|
|
2931
|
+
ops = [showVideoEl, prime:"calling-play muted=true", prime-ok:"played"]
|
|
2932
|
+
而 video = {paused:true, currentTime:0, muted:false}
|
|
2933
|
+
⇒ 画面冻住、也没有任何声音;静音开关怎么拨都不响(因为**根本没在放**)。
|
|
2934
|
+
正确做法(浏览器规则就是这么定的,不是绕过它):
|
|
2935
|
+
· 页面还没被交互过 ⇒ 以 muted 保持画面在动,并把"声音被策略挡住"记成可查状态;
|
|
2936
|
+
· 用户**第一次**真的点了页面(pointerdown/keydown 任一)⇒ 立刻取消静音并续播(声音到位);
|
|
2937
|
+
· 这一段里 muted 由本状态持有(`npSoundBlocked` ⇒ npWantMuted() 为 true),
|
|
2938
|
+
否则"取消静音 → 被停 → 再取消静音"会自己转成一个环。 */
|
|
2939
|
+
let npWantPlay = false; // 我们**想要**它在放(用来把"被策略停住"与"用户自己暂停"分开)
|
|
2940
|
+
/* ①(NP-4) 真机回归的修法:`applyNowPlaying` 里那次"补起播"(刷新后没人再调 play 了)
|
|
2941
|
+
会把**用户刚用卡片按下的暂停**在 60ms 后解开(真机读数:点播放键 ⇒ paused false→false,
|
|
2942
|
+
因为暂停被延迟 apply 又续上了)。所以"用户按过暂停"必须是一条独立状态:
|
|
2943
|
+
· npTransport 的 pause 分支置真,play 分支置假;
|
|
2944
|
+
· npPrimePlay 在它为真时**直接返回**(和 wallUserPaused/powPaused 同一条闸);
|
|
2945
|
+
· 切壁纸/换源(applyNowPlaying 走新 section)时清零。 */
|
|
2946
|
+
/* ①(NP-4) "用户亲手按过暂停"这条状态记在**元素自己身上**(`video.__mpwUserPaused`),
|
|
2947
|
+
不记模块变量:元素是这件事的天然所有者(换源即清零),而且单元门禁里每个用例一份新夹具时
|
|
2948
|
+
模块变量会跨夹具串味(实测:上一个用例的暂停把下一个用例的静音判据带红)。 */
|
|
2949
|
+
function npUserPausedOf(video) { try { return !!(video && video.__mpwUserPaused) } catch (e) { return false } }
|
|
2950
|
+
function npSetUserPaused(video, on) { try { if (video) video.__mpwUserPaused = !!on } catch (e) {} }
|
|
2951
|
+
let npSoundBlocked = false; // 浏览器不允许"没有用户手势就出声"(已如实记录并保持画面在动)
|
|
2952
|
+
let npSoundReason = null; // 可查原因(window.__mpwNpSoundBlocked)
|
|
2953
|
+
/** 设置项 `npVolume`(0..100)→ 0..1。缺省/非法 ⇒ 1(与 elements 的默认值一致 ⇒ 零行为变化)。 */
|
|
2954
|
+
function npWantVolume() {
|
|
2955
|
+
let raw;
|
|
2956
|
+
try { raw = readSection().npVolume; } catch (e) { return 1 }
|
|
2957
|
+
const n = Number(raw);
|
|
2958
|
+
if (raw === void 0 || raw === null || !isFinite(n)) return 1;
|
|
2959
|
+
return Math.max(0, Math.min(1, n / 100));
|
|
2960
|
+
}
|
|
2961
|
+
/** 静音落点(唯一口径):设置项 `mute`,或我们的播放器正在放音(内容重复)。 */
|
|
2962
|
+
function npWantMuted() {
|
|
2963
|
+
let s = {};
|
|
2964
|
+
try { s = readSection() } catch (e) {}
|
|
2965
|
+
/* ①(NP-4) 策略挡住出声时,muted 由本状态持有(见 npSoundBlocked 的注释:不这样做,
|
|
2966
|
+
"取消静音 ⇒ 被浏览器停住 ⇒ 再取消静音"会变成一个自己转的环)。 */
|
|
2967
|
+
return npSoundBlocked || ((s.mute !== void 0 ? !!s.mute : true) || npAudioOwns());
|
|
2968
|
+
}
|
|
2969
|
+
/** ①(NP-4) 联动的唯一判据(②):开 ⇒ 传输动作同时驱动壁纸媒体;关 ⇒ 只驱动我们的播放器。 */
|
|
2970
|
+
function npLinkOn(section) {
|
|
2971
|
+
const s = section || {};
|
|
2972
|
+
return s.npLinkWallpaper !== void 0 ? !!s.npLinkWallpaper : DEFAULT_NP_LINK_WALLPAPER;
|
|
2973
|
+
}
|
|
2974
|
+
/** ①(NP-4) 「电平被别人显式设过」的唯一判据:section 里有 `npVolume` 这个键。
|
|
2975
|
+
* 为什么要有这一条(而不是"反正默认 100 等于 1,随便写"):
|
|
2976
|
+
* · web 帧那条 shim 通道(lib/web-wallpaper.js 的 `policy.volume` → setMasterVolume)
|
|
2977
|
+
* **只要字段出现就装钩子**(包 HTMLMediaElement.prototype.volume 的 setter + 包
|
|
2978
|
+
* window.Audio)——那是作者脚本能观察到的环境变化;
|
|
2979
|
+
* · 同源兼容模式直写 `els[i].volume` 会把作者自己设的音量**覆盖成 1**。
|
|
2980
|
+
* 两条都只在"用户真的动过电平"之后才允许发生 ⇒ 默认档(从没设过 npVolume)
|
|
2981
|
+
* **一个元素、一个字段都不碰**,与改动前逐字节同行为(判据见 tools/np-control-test.mjs A11/A12)。 */
|
|
2982
|
+
function npVolumeSet() {
|
|
2983
|
+
try {
|
|
2984
|
+
const raw = readSection().npVolume;
|
|
2985
|
+
return raw !== void 0 && raw !== null && isFinite(Number(raw));
|
|
2986
|
+
} catch (e) { return false }
|
|
2987
|
+
}
|
|
2988
|
+
/** ①(NP-4) 电平落点:视频元素 / 我们自己的播放器 / web 帧(同源直写 + shim 主音量)。
|
|
2989
|
+
* shim 那条通道是**既有**的(实机口径 = 作者页面内音量 × 宿主主音量,见 lib/web-wallpaper.js)。
|
|
2990
|
+
* `force` = 调用方明确要落(拖完音量条那一下);否则只在用户设过电平时才写。 */
|
|
2991
|
+
function npApplyVolume(force) {
|
|
2992
|
+
const vol = npWantVolume();
|
|
2993
|
+
const touch = force === true || npVolumeSet();
|
|
2994
|
+
if (touch) {
|
|
2995
|
+
try { const v = bgElements().video; if (v && v.volume !== vol) v.volume = vol; } catch (e) {}
|
|
2996
|
+
try { if (npAudio && npAudio.volume !== vol) npAudio.volume = vol; } catch (e) {}
|
|
2997
|
+
}
|
|
2998
|
+
try {
|
|
2999
|
+
const frame = bgElements().frame;
|
|
3000
|
+
if (frame) {
|
|
3001
|
+
if (mpwWebShimUrl(frame.getAttribute("src"))) {
|
|
3002
|
+
const msg = { op: "policy", muted: npWantMuted(), speed: npPolicySpeed() };
|
|
3003
|
+
if (touch) msg.volume = vol; /* 没设过 ⇒ 不带这个键(钩子不装,零环境变化) */
|
|
3004
|
+
webShimCall(frame, msg);
|
|
3005
|
+
}
|
|
3006
|
+
/* 兼容模式(同源):帧内元素直接写。沙箱(不透明源)读不到 ⇒ 只有上面 shim 那一条。 */
|
|
3007
|
+
const doc = touch ? frame.contentDocument : null;
|
|
3008
|
+
if (doc) {
|
|
3009
|
+
const els = doc.querySelectorAll("audio,video");
|
|
3010
|
+
for (let i = 0; i < els.length; i++) { try { if (els[i].volume !== vol) els[i].volume = vol } catch (e) {} }
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
} catch (e) {}
|
|
3014
|
+
npReportAudio();
|
|
3015
|
+
return vol;
|
|
3016
|
+
}
|
|
3017
|
+
function npPolicySpeed() {
|
|
3018
|
+
try {
|
|
3019
|
+
const r = readSection().playbackRate;
|
|
3020
|
+
return (typeof r === "number" && r >= 0.5 && r <= 2) ? r : 1;
|
|
3021
|
+
} catch (e) { return 1 }
|
|
3022
|
+
}
|
|
3023
|
+
/** ①(NP-4) 把"我们知道的音频状态"如实报到宿主 `/media-audio`(WP-1 契约)。
|
|
3024
|
+
* 节流 250ms:拖动音量时每帧都 POST 就是打自己的接口打到卡。 */
|
|
3025
|
+
let npReportAt = 0, npReportTimer = 0;
|
|
3026
|
+
function npReportAudio(now) {
|
|
3027
|
+
const send = () => {
|
|
3028
|
+
npReportAt = Date.now();
|
|
3029
|
+
try {
|
|
3030
|
+
const s = readSection();
|
|
3031
|
+
const vid = npActiveVideo(s);
|
|
3032
|
+
const muted = npWantMuted();
|
|
3033
|
+
const body = {
|
|
3034
|
+
muted: muted, volume: npWantVolume(),
|
|
3035
|
+
hasAudio: vid ? npVideoAudio(vid) : (npAudio ? true : null),
|
|
3036
|
+
source: vid ? "video" : (npAudio ? "tracks" : (bgElements().frame ? "web" : null)),
|
|
3037
|
+
};
|
|
3038
|
+
fetch(HOST_BASE + "/media-audio", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }).catch(() => {});
|
|
3039
|
+
} catch (e) {}
|
|
3040
|
+
};
|
|
3041
|
+
const since = Date.now() - npReportAt;
|
|
3042
|
+
if (now === true || since >= 250) { send(); return; }
|
|
3043
|
+
if (npReportTimer) return;
|
|
3044
|
+
try { npReportTimer = setTimeout(() => { npReportTimer = 0; send(); }, 250 - since); } catch (e) {}
|
|
3045
|
+
}
|
|
3046
|
+
/** ①(NP-4) 起播顺序的唯一入口(showVideoEl / showVideoEdge / 首帧恢复都走它)。
|
|
3047
|
+
* 规则:要起播就先 muted 起播,**拿到播放权之后**再按设置应用 muted/volume。
|
|
3048
|
+
* 失败 ⇒ 记 canPlay 的诚实状态 + 装一次性手势重试(首次点击/按键时自动补一次)。 */
|
|
3049
|
+
function npPrimePlay(video) {
|
|
3050
|
+
if (!video) { npOpTrace("prime", null, "no-video"); return false; }
|
|
3051
|
+
if (wallUserPaused || powPaused || npUserPausedOf(video)) {
|
|
3052
|
+
npApplyMute(); npApplyVolume();
|
|
3053
|
+
npOpTrace("prime", null, "gated: wallUserPaused=" + wallUserPaused + " powPaused=" + powPaused + " userPaused=" + npUserPausedOf(video));
|
|
3054
|
+
return false;
|
|
3055
|
+
}
|
|
3056
|
+
/* 已经在放 = 没有起播可偷,而且**不许在这里再校准静音**:
|
|
3057
|
+
起播那一刻的元素状态就是对的(上面那条路已经保证),而"每次 apply 都按设置重写"
|
|
3058
|
+
会踩到另一条既有竞态 —— `npTransport` 末尾那次 60ms 的延迟 apply 可能落在
|
|
3059
|
+
"设置缓存被一次更早发出的 GET /settings 的应答覆盖回旧值"之后(真机读数:
|
|
3060
|
+
localStorage=false 而 readSection()=true)⇒ 元素被按**旧值**重新静音,
|
|
3061
|
+
用户看到"开关明明关了却还是没声音"。静音/电平各自有明确入口:
|
|
3062
|
+
静音键(npTransport 的 mute 分支)与音量拖动(volume 分支)+ 切壁纸时的
|
|
3063
|
+
applyNowPlaying。这里只起播,不改状态。 */
|
|
3064
|
+
if (!video.paused) { npOpTrace("prime", null, "already-playing"); return false; }
|
|
3065
|
+
npWantPlay = true;
|
|
3066
|
+
npOpTrace("prime", null, "calling-play muted=" + video.muted);
|
|
3067
|
+
/* **第一次就用设置里的静音状态去 play()**:有用户手势(或浏览器本身允许)时这是
|
|
3068
|
+
最正确的一条 —— 元素状态从头到尾等于设置,一个字节都不偷。
|
|
3069
|
+
旧写法反过来:先无条件 muted=true 再 play(),然后指望某个回调把 muted 恢复成设置值
|
|
3070
|
+
⇒ 回调不来(promise 不被 resolve / 元素被换掉 / 那一帧整页 reload)元素就**永久停在
|
|
3071
|
+
muted=true**:设置里写着 mute=false,用户却什么都听不到("开/关都听不到声音")。
|
|
3072
|
+
⇒ 只有被拒时才退到"muted 起播"重试一次(见 npPlayRetryMuted),并且那次有兜底纠偏。 */
|
|
3073
|
+
let p = null;
|
|
3074
|
+
try { p = video.play(); } catch (e) {
|
|
3075
|
+
npMarkPlayBlocked({ name: String(e && e.name || "Error"), message: String(e && e.message || "").slice(0, 200), at: Date.now(), retryMuted: false });
|
|
3076
|
+
npArmGestureRetry();
|
|
3077
|
+
return false;
|
|
3078
|
+
}
|
|
3079
|
+
/* 起播成功只清"被拒"标记 + 落一次电平:**不在这里重写 muted** —— 起播用的就是设置里的
|
|
3080
|
+
静音状态,重写一次只会把"设置缓存被更早的 GET 应答覆盖回旧值"这条既有竞态带进来
|
|
3081
|
+
(真机读数见 npPrimePlay 里那段注释)。 */
|
|
3082
|
+
const done = () => { npMarkPlayBlocked(null); npApplyVolume(); npOpTrace("prime-ok", null, video.paused ? "resolved-but-paused" : "played"); };
|
|
3083
|
+
if (p && typeof p.then === "function") {
|
|
3084
|
+
p.then(done, (err) => { npPlayRetryMuted(video, err) });
|
|
3085
|
+
/* ①(NP-4) "promise 一直不落"也是一种结局:留一条痕,别让人对着 paused=true 猜。
|
|
3086
|
+
3s 后若既没 ok 也没 reject 的记录,就记一条 pending(只记账,不改行为)。 */
|
|
3087
|
+
try {
|
|
3088
|
+
setTimeout(() => {
|
|
3089
|
+
try {
|
|
3090
|
+
const a = window.__mpwNpOps || [];
|
|
3091
|
+
const last = a[a.length - 1] || {};
|
|
3092
|
+
if (last.did === "calling-play muted=" + false || /^calling-play/.test(String(last.did || ""))) {
|
|
3093
|
+
npOpTrace("prime-pending", null, "3s 后 promise 仍未落(paused=" + video.paused + ")");
|
|
3094
|
+
}
|
|
3095
|
+
} catch (e) {}
|
|
3096
|
+
}, 3000);
|
|
3097
|
+
} catch (e) {}
|
|
3098
|
+
} else done();
|
|
3099
|
+
return true;
|
|
3100
|
+
}
|
|
3101
|
+
/** 有声起播被浏览器拒 ⇒ **只重试一次** muted 起播(浏览器一律允许 muted 自动播放)。
|
|
3102
|
+
* muted 这一次是"元素状态 == 设置"的**唯一例外**,所以配两条兜底纠偏,缺一不可:
|
|
3103
|
+
* ① 媒体的 `playing` 事件(npWatchVideo 里接的)—— 真的出画/出声了就恢复设置值;
|
|
3104
|
+
* ② 1.5s 一次性兜底(`npPrimedMuted` 到期即清)—— 连 playing 都不来也不许一直静音。
|
|
3105
|
+
* 两条路都会把 `npPrimedMuted` 清掉,恢复之后 `npApplyMute` 才允许把 muted 写成设置值
|
|
3106
|
+
* (否则重试那一下会被"状态校准"当场撤销,等于没重试)。 */
|
|
3107
|
+
function npPlayRetryMuted(video, err) {
|
|
3108
|
+
npOpTrace("prime-reject", null, String(err && err.name || "Error"));
|
|
3109
|
+
npMarkPlayBlocked({
|
|
3110
|
+
name: String(err && err.name || "Error"),
|
|
3111
|
+
message: String(err && err.message || "").slice(0, 200), at: Date.now(), retryMuted: true,
|
|
3112
|
+
});
|
|
3113
|
+
let retried = false;
|
|
3114
|
+
try {
|
|
3115
|
+
video.muted = true;
|
|
3116
|
+
const p2 = video.play();
|
|
3117
|
+
retried = true;
|
|
3118
|
+
/* ①(NP-4) 重试**也要留结果**:不静默的判据不能只管第一次。
|
|
3119
|
+
第一次被拒时那个 promise rejection 曾被 `.catch(() => {})` 吞掉 —— 同一个坑不踩第二次。 */
|
|
3120
|
+
if (p2 && typeof p2.then === "function") {
|
|
3121
|
+
p2.then(
|
|
3122
|
+
() => { try { window.__mpwNpRetryPlay = "ok" } catch (e) {} },
|
|
3123
|
+
(e2) => { try { window.__mpwNpRetryPlay = String(e2 && e2.name || "Error") } catch (e) {} });
|
|
3124
|
+
} else { try { window.__mpwNpRetryPlay = "ok" } catch (e) {} }
|
|
3125
|
+
} catch (e) { try { window.__mpwNpRetryPlay = String(e && e.name || "Error") } catch (e2) {} }
|
|
3126
|
+
npOpTrace("prime-retry", null, retried ? "muted-play" : "throw");
|
|
3127
|
+
npPrimedMuted = retried;
|
|
3128
|
+
if (retried) {
|
|
3129
|
+
try { clearTimeout(npPrimedMutedTimer); } catch (e) {}
|
|
3130
|
+
try { npPrimedMutedTimer = setTimeout(() => { npPrimedMuted = false; npApplyMute(); npApplyVolume(); }, 1500); } catch (e) {}
|
|
3131
|
+
}
|
|
3132
|
+
try {
|
|
3133
|
+
console.warn("[dsh-mpkg-wallpaper] 壁纸视频的有声起播被浏览器拒绝(自动播放策略): "
|
|
3134
|
+
+ npPlayBlocked.name + " — " + npPlayBlocked.message
|
|
3135
|
+
+ (retried ? ";已退回 muted 起播(成功出画后恢复静音设置)" : "")
|
|
3136
|
+
+ ";首次点击/按键页面时还会再补一次。读数: "
|
|
3137
|
+
+ JSON.stringify({ muted: video.muted, volume: video.volume, readyState: video.readyState }));
|
|
3138
|
+
} catch (e) {}
|
|
3139
|
+
npArmGestureRetry();
|
|
3140
|
+
return false;
|
|
3141
|
+
}
|
|
3142
|
+
/** 起播被拒 ⇒ 装**一次性**的首次手势重试(pointerdown/keydown,capture,事件驱动、自卸)。 */
|
|
3143
|
+
function npArmGestureRetry() {
|
|
3144
|
+
if (npGestureArmed) return;
|
|
3145
|
+
npGestureArmed = true;
|
|
3146
|
+
const once = () => {
|
|
3147
|
+
try { document.removeEventListener("pointerdown", once, true); } catch (e) {}
|
|
3148
|
+
try { document.removeEventListener("keydown", once, true); } catch (e) {}
|
|
3149
|
+
npGestureArmed = false;
|
|
3150
|
+
if (!npPlayBlocked && !npSoundBlocked) return;
|
|
3151
|
+
/* ①(NP-4) 用户真的交互了 ⇒ 声音限制解除:清掉"被挡住"状态,取消静音并续播。 */
|
|
3152
|
+
npSoundBlocked = false; npSoundReason = null;
|
|
3153
|
+
try { window.__mpwNpSoundBlocked = null; window.__mpwNpSoundAllowedBy = "gesture"; } catch (e) {}
|
|
3154
|
+
try { const v = npActiveVideo(readSection()) || bgElements().video; if (v) { npApplyMute(); npApplyVolume(); npPrimePlay(v); } } catch (e) {}
|
|
3155
|
+
};
|
|
3156
|
+
try { document.addEventListener("pointerdown", once, true); } catch (e) {}
|
|
3157
|
+
try { document.addEventListener("keydown", once, true); } catch (e) {}
|
|
3158
|
+
}
|
|
2876
3159
|
/** ⑳(新) 给 web 壁纸 iframe 内的 <video> 应用倍速(含加载完成后补一次)。 */
|
|
2877
3160
|
function applyWebSpeed(frame) {
|
|
2878
3161
|
try {
|
|
@@ -2880,7 +3163,7 @@ function sectionSigNow() {
|
|
|
2880
3163
|
const rate = s.playbackRate;
|
|
2881
3164
|
if (typeof rate !== "number" || rate < 0.5 || rate > 2) return;
|
|
2882
3165
|
if (frame && mpwWebShimUrl(frame.getAttribute("src"))) {
|
|
2883
|
-
webShimCall(frame, { op: "policy", muted: s.mute !== void 0 ? !!s.mute : true, speed: rate });
|
|
3166
|
+
webShimCall(frame, { op: "policy", muted: s.mute !== void 0 ? !!s.mute : true, speed: rate, volume: npWantVolume() });
|
|
2884
3167
|
}
|
|
2885
3168
|
const doc = frame && frame.contentDocument;
|
|
2886
3169
|
if (!doc) return;
|
|
@@ -3341,7 +3624,7 @@ function sectionSigNow() {
|
|
|
3341
3624
|
function webShimPushPolicy(frame) {
|
|
3342
3625
|
try {
|
|
3343
3626
|
const s = readSection();
|
|
3344
|
-
webShimCall(frame, { op: "policy", muted: s.mute !== void 0 ? !!s.mute : true, speed:
|
|
3627
|
+
webShimCall(frame, { op: "policy", muted: s.mute !== void 0 ? !!s.mute : true, speed: npPolicySpeed(), volume: npWantVolume() });
|
|
3345
3628
|
webShimCall(frame, { op: "pause", value: !!wallUserPaused });
|
|
3346
3629
|
} catch {}
|
|
3347
3630
|
}
|
|
@@ -5748,15 +6031,39 @@ function sectionSigNow() {
|
|
|
5748
6031
|
}
|
|
5749
6032
|
return scope.ltoken ? (HOST_BASE + "/library-scene-audio?refs=0<oken=" + encodeURIComponent(scope.ltoken)) : "";
|
|
5750
6033
|
}
|
|
5751
|
-
/** 单个音轨的字节 URL
|
|
5752
|
-
|
|
6034
|
+
/** 单个音轨的字节 URL。
|
|
6035
|
+
* ①(NP-4 2026-09-19 真机修复) **子目录路径**:旧写法无条件取基名
|
|
6036
|
+
* (`path.split(/[\\/]/).pop()`)⇒ 音轨在子目录里时恒 404、在包内时恒 403。
|
|
6037
|
+
* 真机读数(宿主两条路由的实测响应,见 docs/NOW-PLAYING-DSH.md §7.8 的清单):
|
|
6038
|
+
* · `raw?custom=1&folder=3744579963&file=CH0200_MemorialLobby_1_1.ogg`
|
|
6039
|
+
* → **404**(清单里那条真实路径是 `assets/audio/CH0200_MemorialLobby_1_1.ogg`)
|
|
6040
|
+
* · `raw?...&file=assets%2Faudio%2F…ogg` → **403**(/raw 的 `file` 只接受**基名**,
|
|
6041
|
+
* 见 lib/index.js 的 `bad()` 判据:`file.includes('/')` 即拒绝)
|
|
6042
|
+
* · `custom-folder/3744579963/assets/audio/CH0200_MemorialLobby_1_1.ogg`
|
|
6043
|
+
* → **200 / audio/ogg**(prefix 路由 `/custom-folder/<folder>/<rest…>` 支持嵌套)
|
|
6044
|
+
* ⇒ 有子目录时改走 **path 式**前缀路由(与网页壁纸的相对资源同一条);
|
|
6045
|
+
* 单段路径**保持旧 URL 逐字节不变**(根目录音轨零回归:`3646392375/backgroundmuisc.mp3`、
|
|
6046
|
+
* 3580207945 那一档的 6 条都实测能放)。
|
|
6047
|
+
* 包内音频(`source==='pkg'`,文件在 `scene.pkg` 里)**没有任何字节通道**:
|
|
6048
|
+
* `/custom-folder` 404(不在磁盘上)、`/raw` 403、`/custom-media` 403、
|
|
6049
|
+
* `/custom-scene-frame` 回的是 png。⇒ 返回空串,调用方按"只列清单、不能播"如实标(不假装)。 */
|
|
6050
|
+
function npTrackUrl(scope, trackPath, src) {
|
|
5753
6051
|
if (!scope || !trackPath) return "";
|
|
5754
|
-
|
|
6052
|
+
if (String(src || "") === "pkg") return "";
|
|
6053
|
+
const segs = String(trackPath).split(/[\\/]/).filter((x) => x && x !== "." && x !== "..");
|
|
6054
|
+
const file = segs.length ? segs[segs.length - 1] : "";
|
|
5755
6055
|
if (!file) return "";
|
|
6056
|
+
const enc = segs.map(encodeURIComponent).join("/");
|
|
5756
6057
|
if (scope.mode === "custom") {
|
|
5757
|
-
|
|
6058
|
+
if (segs.length === 1) {
|
|
6059
|
+
return HOST_BASE + "/raw?custom=1&folder=" + encodeURIComponent(scope.folder) + "&file=" + encodeURIComponent(file);
|
|
6060
|
+
}
|
|
6061
|
+
return HOST_BASE + "/custom-folder/" + encodeURIComponent(scope.folder) + "/" + enc;
|
|
5758
6062
|
}
|
|
5759
|
-
|
|
6063
|
+
if (segs.length === 1) {
|
|
6064
|
+
return HOST_BASE + "/raw?ltoken=" + encodeURIComponent(scope.ltoken) + "&file=" + encodeURIComponent(file);
|
|
6065
|
+
}
|
|
6066
|
+
return HOST_BASE + "/library-web/" + encodeURIComponent(scope.ltoken) + "/" + enc;
|
|
5760
6067
|
}
|
|
5761
6068
|
|
|
5762
6069
|
/**
|
|
@@ -5802,13 +6109,13 @@ function sectionSigNow() {
|
|
|
5802
6109
|
const key = npScopeKey(scope);
|
|
5803
6110
|
const cached = npMediaCache.data;
|
|
5804
6111
|
if (!cached || npMediaCache.key !== key || !cached.tracks || !cached.tracks.length) return null;
|
|
5805
|
-
return { scope: scope, key: key, tracks: cached.tracks };
|
|
6112
|
+
return { scope: scope, key: key, tracks: cached.tracks, source: String(cached.source || "") };
|
|
5806
6113
|
}
|
|
5807
6114
|
function npCurrentTrack(section) {
|
|
5808
6115
|
const list = npTrackList(section);
|
|
5809
6116
|
if (!list) return null;
|
|
5810
6117
|
const i = Math.max(0, Math.min(list.tracks.length - 1, npTrackIdx));
|
|
5811
|
-
return { scope: list.scope, key: list.key, tracks: list.tracks, index: i, track: list.tracks[i] };
|
|
6118
|
+
return { scope: list.scope, key: list.key, tracks: list.tracks, source: list.source, index: i, track: list.tracks[i] };
|
|
5812
6119
|
}
|
|
5813
6120
|
function npAudioOwns() {
|
|
5814
6121
|
/* ①(NP-3) 我们的播放器在放音时,web 壁纸帧内的那份声音要让位(同一首会被放两遍)。 */
|
|
@@ -5827,10 +6134,14 @@ function sectionSigNow() {
|
|
|
5827
6134
|
function npResolveMedia(section) {
|
|
5828
6135
|
const s = section || {};
|
|
5829
6136
|
const id = npWallpaperIdentity(s);
|
|
6137
|
+
const link = npLinkOn(s);
|
|
6138
|
+
const vol = npWantVolume();
|
|
5830
6139
|
const out = {
|
|
5831
6140
|
kind: "none", title: "", byline: id.name, cover: "",
|
|
5832
|
-
total: null, playing: false, muted: s.mute !== void 0 ? !!s.mute : true, volume:
|
|
6141
|
+
total: null, playing: false, muted: s.mute !== void 0 ? !!s.mute : true, volume: vol,
|
|
5833
6142
|
canPlay: false, canVolume: false, canPrev: false, canNext: false, hasAudio: null, source: "",
|
|
6143
|
+
/* ①(NP-4) ②③ 新增的三个派生量(控制器据此决定键能不能按、带子能不能拖)。 */
|
|
6144
|
+
canSeek: false, link: link, listOnly: false, trackIndex: 0, trackCount: 0,
|
|
5834
6145
|
};
|
|
5835
6146
|
// 封面:当前壁纸的缩略图(真实数据;拿不到就留空 ⇒ 走渐变兜底,与上游一致)
|
|
5836
6147
|
if (s.image && /^(data:|blob:|idb:|https?:|\/)/.test(String(s.image))) out.cover = String(s.image);
|
|
@@ -5842,15 +6153,25 @@ function sectionSigNow() {
|
|
|
5842
6153
|
out.title = id.name || "Video wallpaper";
|
|
5843
6154
|
out.playing = !vid.paused;
|
|
5844
6155
|
out.muted = !!vid.muted;
|
|
5845
|
-
out.volume =
|
|
6156
|
+
out.volume = vol;
|
|
5846
6157
|
if (isFinite(vid.duration) && vid.duration > 0) out.total = Math.round(vid.duration);
|
|
5847
6158
|
const hasAudio = npVideoAudio(vid);
|
|
5848
6159
|
out.hasAudio = hasAudio;
|
|
5849
|
-
|
|
6160
|
+
/* ①(NP-4)② 联动关 ⇒ 播放/暂停**不许**碰壁纸媒体。视频档的声源就是这个视频本身
|
|
6161
|
+
⇒ 关掉联动之后没有任何可控声源:如实禁用(不假装按得动),并说清是哪一条挡住的。 */
|
|
6162
|
+
out.canPlay = link;
|
|
5850
6163
|
/* 明确知道"没有音轨"时静音键禁用(如实显示无音轨);不知道就给用户按(可能真是静音了) */
|
|
5851
6164
|
out.canVolume = hasAudio !== false;
|
|
6165
|
+
out.canSeek = link && typeof out.total === "number" && out.total > 0;
|
|
6166
|
+
/* ①(NP-4)④ 视频档的"曲目"就是容器里那一条音轨:把 1/1 如实写出来
|
|
6167
|
+
(用户看到的是"清单里只有一条",而不是"按钮莫名其妙是灰的")。
|
|
6168
|
+
1/1 说的是**媒体条数**(进度条的主人只有一个),所以 hasAudio 判不出来
|
|
6169
|
+
(Firefox 有 mozHasAudio、Chromium 那套读不到 ⇒ null)时照样成立、照样写。 */
|
|
6170
|
+
out.trackCount = hasAudio === false ? 0 : 1;
|
|
6171
|
+
out.trackIndex = out.trackCount ? 1 : 0;
|
|
5852
6172
|
out.byline = hasAudio === false ? mpwT("np.noAudio")
|
|
5853
|
-
: (
|
|
6173
|
+
: (id.name + " · " + mpwT("np.src.video") + " · 1/1");
|
|
6174
|
+
if (!link) out.byline = mpwT("np.note.linkOff");
|
|
5854
6175
|
out.canPrev = false; out.canNext = false; /* 单个媒体,没有曲目清单 */
|
|
5855
6176
|
return out;
|
|
5856
6177
|
}
|
|
@@ -5860,14 +6181,22 @@ function sectionSigNow() {
|
|
|
5860
6181
|
const n = cur.tracks.length;
|
|
5861
6182
|
const mime = cur.track && cur.track.mime ? String(cur.track.mime) : "audio";
|
|
5862
6183
|
const fname = String(cur.track && cur.track.path || "").split(/[\\/]/).filter(Boolean).pop() || id.name;
|
|
6184
|
+
/* ①(NP-4)④ 包内音频(scan 的 source==='pkg',文件在 scene.pkg 里):清单是真的,
|
|
6185
|
+
但宿主**没有任何路由**能把它的字节取出来(实测 /custom-folder 404、/raw 403、
|
|
6186
|
+
/custom-media 403)⇒ 只列清单、如实标 disabled + 副标题写清原因。 */
|
|
6187
|
+
const listOnly = String(cur.source || "") === "pkg";
|
|
5863
6188
|
out.kind = "track";
|
|
5864
6189
|
out.source = "tracks";
|
|
5865
6190
|
out.title = fname;
|
|
5866
|
-
out.
|
|
5867
|
-
out.
|
|
6191
|
+
out.listOnly = listOnly;
|
|
6192
|
+
out.trackCount = n;
|
|
6193
|
+
out.trackIndex = cur.index + 1;
|
|
6194
|
+
out.byline = id.name + " · " + mime + (n > 1 ? (" · " + (cur.index + 1) + "/" + n) : "")
|
|
6195
|
+
+ (listOnly ? (" · " + mpwT("np.note.pkgListOnly")) : "");
|
|
6196
|
+
out.canPlay = !listOnly;
|
|
5868
6197
|
out.canVolume = true;
|
|
5869
|
-
out.canPrev = n > 1;
|
|
5870
|
-
out.canNext = n > 1;
|
|
6198
|
+
out.canPrev = n > 1 && !listOnly; /* 只有一条 ⇒ 禁用(不假装有第二首) */
|
|
6199
|
+
out.canNext = n > 1 && !listOnly;
|
|
5871
6200
|
out.hasAudio = true;
|
|
5872
6201
|
if (npAudio) {
|
|
5873
6202
|
out.playing = !npAudio.paused;
|
|
@@ -5875,6 +6204,8 @@ function sectionSigNow() {
|
|
|
5875
6204
|
if (isFinite(npAudio.duration) && npAudio.duration > 0) out.total = Math.round(npAudio.duration);
|
|
5876
6205
|
else if (npAudio.readyState === 0) out.total = null;
|
|
5877
6206
|
}
|
|
6207
|
+
/* 我们自己的播放器一直归控件管(它就是"声音"本身)⇒ 与联动开关无关。 */
|
|
6208
|
+
out.canSeek = typeof out.total === "number" && out.total > 0;
|
|
5878
6209
|
return out;
|
|
5879
6210
|
}
|
|
5880
6211
|
// ③ web 壁纸(目录里没有独立音频文件):帧内那份声音只有静音这条真通道
|
|
@@ -5910,14 +6241,14 @@ function sectionSigNow() {
|
|
|
5910
6241
|
fetch(url).then((r) => (r && r.ok ? r.json() : null)).then((d) => {
|
|
5911
6242
|
if (seq !== npMediaToken) return; // 迟到的应答丢掉(壁纸已经换了)
|
|
5912
6243
|
const tracks = (d && d.tracks) || [];
|
|
5913
|
-
npMediaCache = { key: key, at: Date.now(), data: { count: d && d.count || 0, tracks: tracks } };
|
|
6244
|
+
npMediaCache = { key: key, at: Date.now(), data: { count: d && d.count || 0, tracks: tracks, source: String(d && d.source || "") } };
|
|
5914
6245
|
if (npTrackIdx >= tracks.length) npTrackIdx = 0;
|
|
5915
6246
|
/* 壁纸换了 ⇒ 我们自己的播放器必须跟着换源(否则会放着上一张壁纸的歌) */
|
|
5916
6247
|
try { if (npAudio) { npAudio.pause(); npAudio.removeAttribute("src"); } } catch (e) {}
|
|
5917
6248
|
applyNowPlaying(readSection());
|
|
5918
6249
|
}).catch(() => {
|
|
5919
6250
|
if (seq !== npMediaToken) return;
|
|
5920
|
-
npMediaCache = { key: key, at: Date.now(), data: { count: 0, tracks: [] } };
|
|
6251
|
+
npMediaCache = { key: key, at: Date.now(), data: { count: 0, tracks: [], source: "" } };
|
|
5921
6252
|
});
|
|
5922
6253
|
} catch (e) {}
|
|
5923
6254
|
}
|
|
@@ -5963,6 +6294,74 @@ function sectionSigNow() {
|
|
|
5963
6294
|
for (const ev of ["loadedmetadata", "durationchange", "play", "pause", "volumechange", "timeupdate"]) {
|
|
5964
6295
|
try { video.addEventListener(ev, sync) } catch (e) {}
|
|
5965
6296
|
}
|
|
6297
|
+
/* ①(NP-4) **谁把它停了**:真机上出现过"起播成功(promise resolve)之后又被暂停"
|
|
6298
|
+
(读数:prime-ok → paused=true)。只记账不改行为:给 pause() 包一层,把调用栈记进轨迹。
|
|
6299
|
+
包装保留原语义(返回原返回值、参数原样透传)。 */
|
|
6300
|
+
/* 同一条纪律:`load()` 也会把元素打回 paused=true —— 一起记账(谁重置的要知道)。 */
|
|
6301
|
+
try {
|
|
6302
|
+
if (!video.__mpwLoadTraced && typeof video.load === "function") {
|
|
6303
|
+
video.__mpwLoadTraced = true;
|
|
6304
|
+
const origLoad = video.load.bind(video);
|
|
6305
|
+
video.load = function () {
|
|
6306
|
+
try {
|
|
6307
|
+
const st = String(new Error().stack || "").split("\n").slice(2, 6)
|
|
6308
|
+
.map(function (x) { return (x.trim().split(" ")[1] || "?").split("/").pop() }).join("<");
|
|
6309
|
+
npOpTrace("video.load", null, st.slice(0, 180));
|
|
6310
|
+
} catch (e) {}
|
|
6311
|
+
return origLoad.apply(video, arguments);
|
|
6312
|
+
};
|
|
6313
|
+
}
|
|
6314
|
+
} catch (e) {}
|
|
6315
|
+
try {
|
|
6316
|
+
if (!video.__mpwPauseTraced && typeof video.pause === "function") {
|
|
6317
|
+
video.__mpwPauseTraced = true;
|
|
6318
|
+
const origPause = video.pause.bind(video);
|
|
6319
|
+
video.pause = function () {
|
|
6320
|
+
try {
|
|
6321
|
+
const st = String(new Error().stack || "").split("\n").slice(2, 6)
|
|
6322
|
+
.map(function (x) { return (x.trim().split(" ")[1] || "?").split("/").pop() }).join("<");
|
|
6323
|
+
npOpTrace("video.pause", null, st.slice(0, 180));
|
|
6324
|
+
} catch (e) {}
|
|
6325
|
+
return origPause.apply(video, arguments);
|
|
6326
|
+
};
|
|
6327
|
+
}
|
|
6328
|
+
} catch (e) {}
|
|
6329
|
+
/* ①(NP-4) `playing` = 真的出画/出声了。起播成功之后才把**静音与电平**落到元素上
|
|
6330
|
+
(顺序修法的收尾:muted 起播 → playing → 按设置取消静音),并且清掉"被拒"状态。 */
|
|
6331
|
+
/* ①(NP-4) **被策略停住**的识别点:`pause` 事件 + 我们本来想让它放 + 用户没按过暂停
|
|
6332
|
+
+ 还没真正放起来(currentTime 很小)⇒ 是浏览器把"没有手势的有声播放"停掉了。
|
|
6333
|
+
处理:退回 muted 让画面继续动,把原因记成可查状态,并装一次性手势重试补声音。 */
|
|
6334
|
+
try {
|
|
6335
|
+
video.addEventListener("pause", () => {
|
|
6336
|
+
try {
|
|
6337
|
+
if (wallUserPaused || powPaused) return;
|
|
6338
|
+
if (!npWantPlay) return;
|
|
6339
|
+
if (!(video.currentTime < 1.5)) return; /* 放起来之后才停(播完/切源)⇒ 不关我们的事 */
|
|
6340
|
+
if (npSoundBlocked) return;
|
|
6341
|
+
npSoundBlocked = true;
|
|
6342
|
+
npSoundReason = { at: Date.now(), currentTime: +video.currentTime.toFixed(2), muted: video.muted, hint: "没有用户手势时浏览器不允许有声播放(Firefox 会直接把它停住)" };
|
|
6343
|
+
try { window.__mpwNpSoundBlocked = npSoundReason; } catch (e) {}
|
|
6344
|
+
npOpTrace("audio-blocked", null, "退回 muted 保持画面;首次手势后补声音");
|
|
6345
|
+
try { video.muted = true; } catch (e) {}
|
|
6346
|
+
try {
|
|
6347
|
+
const p = video.play();
|
|
6348
|
+
if (p && typeof p.then === "function") {
|
|
6349
|
+
p.then(() => { npOpTrace("audio-blocked-replay", null, "ok muted=" + video.muted + " paused=" + video.paused) },
|
|
6350
|
+
(e) => { npOpTrace("audio-blocked-replay", null, "rejected:" + String(e && e.name || "Error")) });
|
|
6351
|
+
} else { npOpTrace("audio-blocked-replay", null, "no-promise") }
|
|
6352
|
+
} catch (e) { npOpTrace("audio-blocked-replay", null, "threw:" + String(e && e.name || "Error")) }
|
|
6353
|
+
npArmGestureRetry();
|
|
6354
|
+
} catch (e) {}
|
|
6355
|
+
});
|
|
6356
|
+
} catch (e) {}
|
|
6357
|
+
try {
|
|
6358
|
+
video.addEventListener("playing", () => {
|
|
6359
|
+
/* 真的出画/出声了 ⇒ "muted 起播"那一段到此结束,把静音/电平恢复成设置值。 */
|
|
6360
|
+
try { npPrimedMuted = false; clearTimeout(npPrimedMutedTimer); } catch (e) {}
|
|
6361
|
+
try { npPlayBlocked = null; npApplyMute(); npApplyVolume(); } catch (e) {}
|
|
6362
|
+
sync();
|
|
6363
|
+
});
|
|
6364
|
+
} catch (e) {}
|
|
5966
6365
|
sync();
|
|
5967
6366
|
return true;
|
|
5968
6367
|
} catch (e) { return false }
|
|
@@ -5992,7 +6391,7 @@ function sectionSigNow() {
|
|
|
5992
6391
|
let cur = null;
|
|
5993
6392
|
try { cur = npCurrentTrack(readSection()) } catch (e) { cur = null }
|
|
5994
6393
|
if (!cur || !npAudio) return;
|
|
5995
|
-
const url = npTrackUrl(cur.scope, cur.track && cur.track.path);
|
|
6394
|
+
const url = npTrackUrl(cur.scope, cur.track && cur.track.path, cur.source);
|
|
5996
6395
|
if (!url || npAudioBlobTried === url) {
|
|
5997
6396
|
try { console.warn("[dsh-mpkg-wallpaper] np: 音轨加载失败(宿主 /raw 或解码不支持): " + url) } catch (e) {}
|
|
5998
6397
|
try { if (npCtl) { npCtl.setMedia({ playing: false, canPlay: false, byline: mpwT("np.note.decode") }); npCtl.setProgress(0); } } catch (e) {}
|
|
@@ -6031,13 +6430,14 @@ function sectionSigNow() {
|
|
|
6031
6430
|
if (!cur) return false;
|
|
6032
6431
|
const a = npEnsureAudio();
|
|
6033
6432
|
if (!a) return false;
|
|
6034
|
-
const url = npTrackUrl(cur.scope, cur.track && cur.track.path);
|
|
6433
|
+
const url = npTrackUrl(cur.scope, cur.track && cur.track.path, cur.source);
|
|
6035
6434
|
if (!url) return false;
|
|
6036
6435
|
npAudioBlobTried = "";
|
|
6037
6436
|
try {
|
|
6038
6437
|
if (String(a.getAttribute("src") || "") !== url) a.setAttribute("src", url);
|
|
6039
6438
|
const st = readSection();
|
|
6040
6439
|
a.muted = !!(st.mute !== void 0 ? !!st.mute : true);
|
|
6440
|
+
if (npVolumeSet()) a.volume = npWantVolume(); /* ①(NP-4)① 用户设过电平才写(默认不碰) */
|
|
6041
6441
|
if (play) { const p = a.play(); if (p && p.catch) p.catch(() => {}); }
|
|
6042
6442
|
} catch (e) { mpwErr("npLoadTrack", e) }
|
|
6043
6443
|
npApplyFrameMute();
|
|
@@ -6050,7 +6450,7 @@ function sectionSigNow() {
|
|
|
6050
6450
|
function npApplyMute() {
|
|
6051
6451
|
const s = readSection();
|
|
6052
6452
|
const mute = s.mute !== void 0 ? !!s.mute : true;
|
|
6053
|
-
try { const v = npActiveVideo(s); if (v && v.muted !==
|
|
6453
|
+
try { const v = npActiveVideo(s); const w = (npPrimedMuted || npSoundBlocked) ? true : mute; if (v && v.muted !== w) v.muted = w; } catch (e) {}
|
|
6054
6454
|
try { if (npAudio && npAudio.muted !== mute) npAudio.muted = mute } catch (e) {}
|
|
6055
6455
|
try { applyVideoMute(); } catch (e) {}
|
|
6056
6456
|
npApplyFrameMute();
|
|
@@ -6068,47 +6468,86 @@ function sectionSigNow() {
|
|
|
6068
6468
|
* prev/next 清单里的上一条/下一条(环形)
|
|
6069
6469
|
* mute 既有设置项 `mute` + **真的落到元素**(video/audio/帧内 + 宿主 /media-audio 上报)
|
|
6070
6470
|
*/
|
|
6071
|
-
function npTransport(op, section) {
|
|
6471
|
+
function npTransport(op, section, value) {
|
|
6072
6472
|
const s = section || readSection();
|
|
6473
|
+
const link = npLinkOn(s);
|
|
6474
|
+
/* ①(NP-4) 传输动作的**可查轨迹**(最近 32 条:op / value / 有没有真的落到某个元素)。
|
|
6475
|
+
为什么要留它:真机上"点了没反应"这一类问题,界面与设置都看不出"动作到底走到哪一步"
|
|
6476
|
+
(修前那次 NotAllowedError 就是这么消失的)。它只记账,零行为影响,排障与探针都读它。 */
|
|
6477
|
+
let did = "";
|
|
6478
|
+
const trace = () => npOpTrace(op, value, did || "no-target");
|
|
6073
6479
|
try {
|
|
6074
6480
|
if (op === "play" || op === "pause") {
|
|
6075
6481
|
const vid = npActiveVideo(s);
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6482
|
+
/* ①(NP-4)② 联动开 ⇒ 壁纸媒体一起动;关 ⇒ **一个字节都不碰壁纸**(画面不受控件影响)。 */
|
|
6483
|
+
if (vid && link) {
|
|
6484
|
+
if (op === "play") { npSetUserPaused(vid, false); npPrimePlay(vid); did = "video.play"; }
|
|
6485
|
+
else { npWantPlay = false; npSetUserPaused(vid, true); vid.pause(); did = "video.pause"; }
|
|
6486
|
+
npApplyFrameMute();
|
|
6079
6487
|
} else {
|
|
6080
6488
|
const list = npTrackList(s);
|
|
6081
6489
|
if (list && list.tracks.length) {
|
|
6082
6490
|
if (op === "play") {
|
|
6083
6491
|
const a = npEnsureAudio();
|
|
6084
6492
|
/* 还没装源(清单刚到/换过壁纸)⇒ 先装当前曲目再放 */
|
|
6085
|
-
if (a && !String(a.getAttribute("src") || "")) npLoadTrack(true);
|
|
6086
|
-
else if (a) { const p = a.play(); if (p && p.catch) p.catch(() => {}); }
|
|
6087
|
-
} else if (npAudio) npAudio.pause();
|
|
6088
|
-
} else {
|
|
6493
|
+
if (a && !String(a.getAttribute("src") || "")) { npLoadTrack(true); did = "audio.load+play"; }
|
|
6494
|
+
else if (a) { const p = a.play(); if (p && p.catch) p.catch(() => {}); did = "audio.play"; }
|
|
6495
|
+
} else if (npAudio) { npAudio.pause(); did = "audio.pause"; }
|
|
6496
|
+
} else if (!vid) {
|
|
6089
6497
|
/* 清单还没到:拉一次,到位后由 applyNowPlaying 把键的状态刷新(不假装按过了) */
|
|
6090
6498
|
npFetchTracks(s);
|
|
6499
|
+
did = "fetch-tracks";
|
|
6500
|
+
} else {
|
|
6501
|
+
/* ①(NP-4) 有壁纸媒体但**联动关** ⇒ 这里就是"一个字节都不碰"的那条路。 */
|
|
6502
|
+
did = "skip(link-off)";
|
|
6091
6503
|
}
|
|
6092
6504
|
/* ①(NP-3) 播放状态变了 ⇒ 帧内静音策略要重算(我们在放音时帧内强制静音) */
|
|
6093
6505
|
npApplyFrameMute();
|
|
6094
6506
|
}
|
|
6095
6507
|
} else if (op === "next" || op === "prev") {
|
|
6096
|
-
npStepTrack(op === "next" ? 1 : -1, true);
|
|
6508
|
+
did = npStepTrack(op === "next" ? 1 : -1, true) ? "step" : "step(nolist)";
|
|
6097
6509
|
} else if (op === "restart") {
|
|
6098
6510
|
/* ①(NP-3) 保留 op(兼容旧调用/排障),但 UI 里的"回到开头"已换成上一首/下一首。 */
|
|
6099
6511
|
const vid = npActiveVideo(s);
|
|
6100
6512
|
if (vid) { try { vid.currentTime = 0 } catch (e) {} }
|
|
6101
6513
|
else if (npAudio) { try { npAudio.currentTime = 0 } catch (e) {} }
|
|
6514
|
+
} else if (op === "seek") {
|
|
6515
|
+
/* ①(NP-4)③ 拖动落点:`value` 是**相对当前媒体时长**的 0..1(控制器算好送进来)。
|
|
6516
|
+
口径(与②的联动同一条,判据见 docs/NOW-PLAYING-DSH.md §7.8):
|
|
6517
|
+
· 联动开 ⇒ **真的落到当前媒体的 currentTime**(壁纸媒体 + 我们自己的播放器,
|
|
6518
|
+
谁在放就落谁;视频档就是那个 <video>);
|
|
6519
|
+
· 联动关 ⇒ 只落到**我们自己的播放器**("只影响声音");壁纸媒体一个字节都不碰。
|
|
6520
|
+
轨道只有一个所有者(npResolveMedia 视频/曲目两条各自 return),所以
|
|
6521
|
+
这里的 ratio 不会出现"同一根带子对应两个时长"的歧义。 */
|
|
6522
|
+
const r = Math.max(0, Math.min(1, Number(value) || 0));
|
|
6523
|
+
const vid = npActiveVideo(s);
|
|
6524
|
+
if (vid && link && isFinite(vid.duration) && vid.duration > 0) {
|
|
6525
|
+
try { vid.currentTime = r * vid.duration; did = "video.seek=" + vid.currentTime.toFixed(2) } catch (e) {}
|
|
6526
|
+
} else if (vid && !link) { did = "skip(link-off)" }
|
|
6527
|
+
if (npAudio && isFinite(npAudio.duration) && npAudio.duration > 0) {
|
|
6528
|
+
try { npAudio.currentTime = r * npAudio.duration; did += (did ? "+" : "") + "audio.seek=" + npAudio.currentTime.toFixed(2) } catch (e) {}
|
|
6529
|
+
}
|
|
6530
|
+
} else if (op === "volume") {
|
|
6531
|
+
/* ①(NP-4)① 电平(0..1):写设置项 `npVolume`(0..100)并**真的落到元素**
|
|
6532
|
+
(video / 我们自己的播放器 / web 帧 + shim 主音量)。与静音键是两件事:
|
|
6533
|
+
静音键动的是既有 `mute` 开关,这里动的是电平。 */
|
|
6534
|
+
const level = Math.max(0, Math.min(1, Number(value) || 0));
|
|
6535
|
+
saveSection({ npVolume: Math.round(level * 100) });
|
|
6536
|
+
npApplyVolume(true); /* 用户明确拖了电平 ⇒ 无论默认与否都落到实处 */
|
|
6537
|
+
did = "volume=" + Math.round(level * 100);
|
|
6102
6538
|
} else if (op === "mute") {
|
|
6103
6539
|
// ①(NP-1) 静音走**既有**设置项(mute):这样视频/web 两条路径都由既有接线负责,
|
|
6104
6540
|
// 不在这里另写一套;同时把状态如实报到宿主 media-audio 契约(WP-1 新增)。
|
|
6105
6541
|
// ①(NP-3) 关键补齐:写完设置**必须真的落到元素**(下面这行 npApplyMute())。
|
|
6542
|
+
// ①(NP-4) 静音是**壁纸级**设置(设置面板里就有这一行),不受联动开关管:
|
|
6543
|
+
// 关掉联动只影响"传输键要不要驱动壁纸的播放/进度",不改变"静音"这条既有语义。
|
|
6106
6544
|
const next = !(s.mute !== void 0 ? !!s.mute : true);
|
|
6107
6545
|
saveSection({ mute: next });
|
|
6108
6546
|
npApplyMute();
|
|
6109
|
-
|
|
6547
|
+
did = "mute=" + next;
|
|
6110
6548
|
}
|
|
6111
|
-
} catch (e) { mpwErr("npTransport", e); }
|
|
6549
|
+
} catch (e) { mpwErr("npTransport", e); did = "error:" + String(e && e.message || e).slice(0, 60); }
|
|
6550
|
+
trace();
|
|
6112
6551
|
setTimeout(() => { try { applyNowPlaying(readSection()); } catch {} }, 60);
|
|
6113
6552
|
}
|
|
6114
6553
|
|
|
@@ -6127,7 +6566,7 @@ function sectionSigNow() {
|
|
|
6127
6566
|
t: (k) => { try { return mpwT(k); } catch (e) { return k; } },
|
|
6128
6567
|
doc: document, win: window,
|
|
6129
6568
|
log: console,
|
|
6130
|
-
onTransport: (op) => npTransport(op, readSection()),
|
|
6569
|
+
onTransport: (op, value) => npTransport(op, readSection(), value),
|
|
6131
6570
|
});
|
|
6132
6571
|
npCtl = mod.createController();
|
|
6133
6572
|
npSlotAction = mod.createSlotAction(npCtl);
|
|
@@ -6174,8 +6613,24 @@ function sectionSigNow() {
|
|
|
6174
6613
|
return;
|
|
6175
6614
|
}
|
|
6176
6615
|
ctl.setEnabled(true);
|
|
6177
|
-
/* ①(NP-
|
|
6178
|
-
|
|
6616
|
+
/* ①(NP-4) 换了壁纸/换了源 ⇒ "用户按过暂停"清零(新源按自动播放的规则重新起播)。 */
|
|
6617
|
+
/* ①(NP-4) 这里**不**再判"源变了没有":`video.getAttribute('src')` 是**解析后**的 URL
|
|
6618
|
+
(`/api/mpkg-wallpaper/custom-folder/…`),而 `section.image` 是 `host:?…` 形态,
|
|
6619
|
+
两者恒不相等 ⇒ 每次 apply 都会把"用户按过暂停"清掉、60ms 后又把播放续上
|
|
6620
|
+
(真机读数:`pause → video.pause` 之后紧跟 `prime: calling-play`)。
|
|
6621
|
+
换源时清零这件事由 `showVideoEl` 在**真的重设 src 那一行**做(那里才拿得到真相)。 */
|
|
6622
|
+
/* ①(NP-3) 视频壁纸:先把媒体事件接上(元数据到了要重解,否则音轨判定会一直停在旧值)。
|
|
6623
|
+
①(NP-4) 起播门控也在这里补一次:`showVideoEl` 只在**设置变化**那条路径上跑,
|
|
6624
|
+
而"刷新页面"那条路径上视频是被 `applyFromStorage` 装进来的 —— 不在这里补,
|
|
6625
|
+
刷新之后就没有人再调 play()(真机读数:全程只调用过一次 play(),还被拒)。 */
|
|
6626
|
+
try {
|
|
6627
|
+
const vid = npActiveVideo(s);
|
|
6628
|
+
if (vid) { npWatchVideo(vid); if (vid.paused && !npPlayBlocked) npPrimePlay(vid); }
|
|
6629
|
+
} catch (e) {}
|
|
6630
|
+
/* ①(NP-4)② 联动开关落点(只写一个可查询属性 + 影响媒体源的 canPlay/canSeek 派生量)。 */
|
|
6631
|
+
try { ctl.setLink(npLinkOn(s)); } catch (e) {}
|
|
6632
|
+
/* ①(NP-4)① 电平:刷新/切壁纸后把 `npVolume` 补落到真实元素上。 */
|
|
6633
|
+
try { npApplyVolume(); } catch (e) {}
|
|
6179
6634
|
ctl.setMedia(npResolveMedia(s));
|
|
6180
6635
|
/* ①(NP-3) 清单/播放器:有目录作用域才拉清单;清单到了之后把当前曲目装进播放器
|
|
6181
6636
|
(**只装源、不自动播放** —— 浏览器自动播放策略 + "不替用户按播放"两条都要求这样)。 */
|
|
@@ -6993,11 +7448,11 @@ function sectionSigNow() {
|
|
|
6993
7448
|
globalThis.__mpwNpTest = {
|
|
6994
7449
|
scope: (section) => { try { return npAudioScope(section || readSection()) } catch (e) { return { err: String(e && e.message || e) } } },
|
|
6995
7450
|
scanUrl: (section) => { try { return npScanUrl(npAudioScope(section || readSection())) } catch (e) { return "ERR " + (e && e.message || e) } },
|
|
6996
|
-
trackUrl: (section, p) => { try { return npTrackUrl(npAudioScope(section || readSection()), p) } catch (e) { return "ERR " + (e && e.message || e) } },
|
|
7451
|
+
trackUrl: (section, p, src) => { try { return npTrackUrl(npAudioScope(section || readSection()), p, src) } catch (e) { return "ERR " + (e && e.message || e) } },
|
|
6997
7452
|
activeVideo: (section) => { const v = npActiveVideo(section || readSection()); return v ? true : false },
|
|
6998
7453
|
videoAudio: (section) => npVideoAudio(npActiveVideo(section || readSection())),
|
|
6999
7454
|
resolve: (section) => { try { return npResolveMedia(section || readSection()) } catch (e) { return { err: String(e && e.message || e) } } },
|
|
7000
|
-
transport: (op, section) => { try { npTransport(op, section || readSection()); return true } catch (e) { return String(e && e.message || e) } },
|
|
7455
|
+
transport: (op, section, value) => { try { npTransport(op, section || readSection(), value); return true } catch (e) { return String(e && e.message || e) } },
|
|
7001
7456
|
audio: () => npAudio,
|
|
7002
7457
|
idx: () => npTrackIdx,
|
|
7003
7458
|
load: (i) => { try { if (typeof i === "number") npTrackIdx = Math.max(0, i); return npLoadTrack(false) } catch (e) { return String(e && e.message || e) } },
|
|
@@ -7005,10 +7460,12 @@ function sectionSigNow() {
|
|
|
7005
7460
|
ownsSound: () => npAudioOwns(),
|
|
7006
7461
|
/* 夹具:把"宿主扫描路由的应答"直接塞进缓存(等价于 npFetchTracks 的成功回调)。
|
|
7007
7462
|
入参是 **section**(不是 scope):作用域由 npAudioScope 自己算 —— 与生产同一条口径。 */
|
|
7008
|
-
seedTracks: (section, tracks) => {
|
|
7463
|
+
seedTracks: (section, tracks, source) => {
|
|
7009
7464
|
try {
|
|
7010
7465
|
const s = npAudioScope(section || readSection());
|
|
7011
|
-
|
|
7466
|
+
/* ①(NP-4)④ 第三个入参 = 宿主扫描应答里的 `source`('dir' | 'pkg'):
|
|
7467
|
+
'pkg' 表示音轨在 scene.pkg 里 ⇒ 只能列清单、不能播(判据见 A9a4/D 组)。 */
|
|
7468
|
+
npMediaCache = { key: npScopeKey(s), at: Date.now(), data: { count: (tracks || []).length, tracks: tracks || [], source: String(source || "") } };
|
|
7012
7469
|
npTrackIdx = 0;
|
|
7013
7470
|
return npMediaCache.key;
|
|
7014
7471
|
} catch (e) { return String(e && e.message || e) }
|
|
@@ -7017,6 +7474,21 @@ function sectionSigNow() {
|
|
|
7017
7474
|
/* 走**真 apply 路径**(applyNowPlaying 本身;测试/真机排障都用它手动重放一次) */
|
|
7018
7475
|
apply: (section) => { try { applyNowPlaying(section || readSection()); return true } catch (e) { return String(e && e.message || e) } },
|
|
7019
7476
|
dropAudio: () => { try { npDropAudio(); return true } catch (e) { return String(e && e.message || e) } },
|
|
7477
|
+
/* ══ ①(NP-4)① ② ③ 的新钩子(驱动的是**生产实现本身**,不是副本) ══
|
|
7478
|
+
音量 / 联动 / 起播顺序 / 拖动落点这四件事的判据全走这里,见 tools/np-control-test.mjs。 */
|
|
7479
|
+
volume: () => { try { return npWantVolume() } catch (e) { return String(e && e.message || e) } },
|
|
7480
|
+
wantMuted: () => { try { return npWantMuted() } catch (e) { return String(e && e.message || e) } },
|
|
7481
|
+
link: (section) => { try { return npLinkOn(section || readSection()) } catch (e) { return String(e && e.message || e) } },
|
|
7482
|
+
applyVolume: (force) => { try { npApplyVolume(force === true); return true } catch (e) { return String(e && e.message || e) } },
|
|
7483
|
+
volumeSet: () => { try { return npVolumeSet() } catch (e) { return String(e && e.message || e) } },
|
|
7484
|
+
playBlocked: () => npPlayBlocked,
|
|
7485
|
+
primePlay: (section) => { try { const v = npActiveVideo(section || readSection()); return v ? npPrimePlay(v) : "no-active-video" } catch (e) { return String(e && e.message || e) } },
|
|
7486
|
+
primedMuted: () => !!npPrimedMuted,
|
|
7487
|
+
userPaused: () => npUserPausedOf(npActiveVideo(readSection())),
|
|
7488
|
+
soundBlocked: () => !!npSoundBlocked,
|
|
7489
|
+
trackList: (section) => { try { const l = npTrackList(section || readSection()); return l ? { key: l.key, source: l.source, paths: l.tracks.map((t) => t.path) } : null } catch (e) { return String(e && e.message || e) } },
|
|
7490
|
+
step: (dir) => { try { return npStepTrack(dir, false) } catch (e) { return String(e && e.message || e) } },
|
|
7491
|
+
trackIdx: () => npTrackIdx,
|
|
7020
7492
|
};
|
|
7021
7493
|
globalThis.__mpwSectionTest = {
|
|
7022
7494
|
read: () => { try { const s = readSection() || {}; return { bsCompat: s.bsCompat, bsCompatUserSet: s.bsCompatUserSet === true, headerFrostUserSet: s.headerFrostUserSet === true } } catch (e) { return { err: String(e && e.message || e) } } },
|
|
@@ -7902,6 +8374,10 @@ const NP_ROOT_COLLAPSED_TOKEN = "collapsed";
|
|
|
7902
8374
|
这一层是**降级链**而不是两套实现:容器只有一个,位置只有一个来源,谁先可用就用谁。 */
|
|
7903
8375
|
const NP_SLOT_ATTR = "data-mpw-np-slot";
|
|
7904
8376
|
const NP_SLOT_SEL = "[data-mpw-np-slot]";
|
|
8377
|
+
/* ── ①(NP-4)② 联动开关("播放/暂停是否同时控制壁纸的播放/暂停")的可查询落点 ────────
|
|
8378
|
+
开关本身在设置面板里(放 Now playing 之下,NP 关掉就撤掉那一行);组件根上这个属性是
|
|
8379
|
+
真机探针/门禁读"当前是哪一档"的唯一入口(不靠读设置文件,避免两处口径)。 */
|
|
8380
|
+
const NP_LINK_ATTR = "data-mpw-np-link";
|
|
7905
8381
|
/* ── ①(NP-3) 让位:宿主同一位置已经有别的插件注入的元素时不挂/撤下 ─────────────
|
|
7906
8382
|
值就是原因串(可读、可查询):`foreign-occupant`(容器里出现了非本插件、非宿主自有的元素)。
|
|
7907
8383
|
写在我们自己的 slot div 上(宿主 slot 出口是我们唯一"永远属于自己"的节点,即使没挂载也在),
|
|
@@ -7949,6 +8425,19 @@ const HEART_D = "M12 20.4c-4.9-3.2-7.6-6.2-7.6-9.5A4.15 4.15 0 0 1 12 8.1a4.15 4
|
|
|
7949
8425
|
const VOL_MUTE_D = "M4 9.4h3.1L11.4 6v12L7.1 14.6H4z M15.2 9.6l4 4.8 M19.2 9.6l-4 4.8";
|
|
7950
8426
|
const VOL_ON_D = "M4 9.4h3.1L11.4 6v12L7.1 14.6H4z M14.6 9.2a3.9 3.9 0 0 1 0 5.6 M17 6.8a7.4 7.4 0 0 1 0 10.4";
|
|
7951
8427
|
|
|
8428
|
+
/* ── ①(NP-4) 拖动:进度 seek + 音量,共用**一条**换算 ──────────────────────────
|
|
8429
|
+
为什么两条要合一条:两者都只是"指针横坐标 → 0..1",差别只在把 0..1 交给谁。
|
|
8430
|
+
分成两份实现就会出现"一个按内边距盒算、一个按边框盒算"这种只在某一态偏几像素的
|
|
8431
|
+
分歧(上游那张样式表在别处也吃过同一种亏:同一个位置有两个意见)。
|
|
8432
|
+
`ratioOf` 是纯函数(只吃 rect + clientX)⇒ 无浏览器门禁可以直接喂矩形断言边界值。 */
|
|
8433
|
+
const NP_SCRUB_PAD = 9; /* 轨道只有 3px(RAIL_H),命中带上下各补 9px ⇒ 共 21px,够手指 */
|
|
8434
|
+
const NP_VOL_W = 84; /* 音量条的可见宽度(卡片 260 宽里时钟行放得下,见 §几何) */
|
|
8435
|
+
/* ⑤ 悬浮态放大:容器能往两侧各"借"多少像素。
|
|
8436
|
+
上限 12 而不是"能借多少借多少":借来的宽度是宿主自己的内边距(悬浮态 12px),
|
|
8437
|
+
贴到内边距盒边缘虽然不算裁切,但卡片会顶到宿主圆角卡片的边上。
|
|
8438
|
+
12 与本机实测的悬浮内边距相等 ⇒ 卡片的左右各留 1px 余量(判据见 np-control-live 的 G 组)。 */
|
|
8439
|
+
const NP_BLEED_MAX = 12;
|
|
8440
|
+
|
|
7952
8441
|
/* ── 样式:全部落在 [data-mpw-now-playing] / .mpw_np* -------------------------
|
|
7953
8442
|
上游那张样式表的原文注释保留在对应规则上。这里**故意不出现** sidebarCol / footArea /
|
|
7954
8443
|
settingsArea / .hHd-Xa_* 任何一个名字:style-scope-guard 的 SURFACES 里"侧栏"的
|
|
@@ -8225,8 +8714,133 @@ body[data-ds-dark-theme] [data-mpw-now-playing] {
|
|
|
8225
8714
|
.mpw_np_op, .mpw_np_art, .mpw_np_like { transition-duration: 1ms; }
|
|
8226
8715
|
.mpw_np_like.mpw_np_on .mpw_np_ic { animation: none; }
|
|
8227
8716
|
}
|
|
8717
|
+
/* ══ ①(NP-4) 拖动(进度 seek + 音量)══
|
|
8718
|
+
两条都是自绘的(不用 input[type=range]):本组件的每个位置都由补间逐帧写成内联样式,
|
|
8719
|
+
浏览器原生控件的高度/内边距/thum 尺寸在各家实现里都不一样,塞进来就是"第二个意见"。
|
|
8720
|
+
规则只写**材料**(颜色/圆角/命中带),位置与长度一律由 JS 内联写。 */
|
|
8721
|
+
/* the scrubber —— 轨道只有 3px,命中带上下各补 9px(NP_SCRUB_PAD)。
|
|
8722
|
+
它绝对定位在 .mpw_np_bar 里(那条是 position:absolute ⇒ 自己是包含块),
|
|
8723
|
+
**不参与** 那一列的 flex 流 ⇒ 时钟/进度条的布局一个像素都不动(放大那件事不留回归)。 */
|
|
8724
|
+
.mpw_np_scrub {
|
|
8725
|
+
position: absolute;
|
|
8726
|
+
left: 0;
|
|
8727
|
+
width: 100%;
|
|
8728
|
+
pointer-events: auto;
|
|
8729
|
+
cursor: pointer;
|
|
8730
|
+
touch-action: none;
|
|
8731
|
+
background: transparent;
|
|
8732
|
+
}
|
|
8733
|
+
/* 没有可 seek 的媒体时:命中带仍然在(判据要能量到它),但不给"能拖"的指针语义 */
|
|
8734
|
+
.mpw_np_scrub[data-mpw-np-noseek] { cursor: default; }
|
|
8735
|
+
/* the rail 的可见部分保持 3px;拖动时只把游标点亮,不改几何 */
|
|
8736
|
+
.mpw_np_rail { position: relative; }
|
|
8737
|
+
.mpw_np_scrub[data-mpw-np-drag] ~ .mpw_np_clock { color: var(--mpw-np-ink-3); }
|
|
8738
|
+
/* the level —— 卡片里时钟行的中间那一段(左时刻 / 音量 / 右剩余)。
|
|
8739
|
+
收起态整行随 late 淡出,音量条与时钟同一条约定:只在卡片里存在。 */
|
|
8740
|
+
.mpw_np_vol { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; min-width: 0; }
|
|
8741
|
+
.mpw_np_volHit {
|
|
8742
|
+
position: relative;
|
|
8743
|
+
display: block;
|
|
8744
|
+
width: 84px; /* = NP_VOL_W,两端实测一致(css-matrix 有一条对拍) */
|
|
8745
|
+
height: 12px; /* 命中带;可见轨道只有 2px,居中 */
|
|
8746
|
+
pointer-events: auto;
|
|
8747
|
+
cursor: pointer;
|
|
8748
|
+
touch-action: none;
|
|
8749
|
+
}
|
|
8750
|
+
.mpw_np_volRail {
|
|
8751
|
+
position: absolute; left: 0; right: 0; top: 50%; height: 2px; margin-top: -1px;
|
|
8752
|
+
border-radius: 999px; background: rgba(var(--mpw-np-ink-rgb), 0.16);
|
|
8753
|
+
}
|
|
8754
|
+
.mpw_np_volRun {
|
|
8755
|
+
position: absolute; left: 0; top: 50%; height: 2px; margin-top: -1px;
|
|
8756
|
+
border-radius: 999px; background: var(--mpw-np-ink);
|
|
8757
|
+
}
|
|
8758
|
+
.mpw_np_volKnob {
|
|
8759
|
+
position: absolute; top: 50%; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px;
|
|
8760
|
+
border-radius: 999px; background: var(--mpw-np-ink);
|
|
8761
|
+
}
|
|
8762
|
+
.mpw_np_volHit:focus-visible, .mpw_np_scrub:focus-visible {
|
|
8763
|
+
outline: none;
|
|
8764
|
+
box-shadow: 0 0 0 2px var(--mpw-np-card), 0 0 0 4px rgba(var(--mpw-np-ink-rgb), 0.35);
|
|
8765
|
+
}
|
|
8228
8766
|
`;
|
|
8229
8767
|
|
|
8768
|
+
|
|
8769
|
+
/* ── ①(NP-4) 拖动换算与借宽判据(**模块作用域**的纯函数) ──────────────────────
|
|
8770
|
+
为什么放在模块作用域而不是工厂里:它们一行 DOM 都不碰(只吃矩形 + 一个计算样式函数),
|
|
8771
|
+
放在这里无浏览器门禁可以直接喂矩形断言边界值(tools/np-control-test.mjs 的 F 组),
|
|
8772
|
+
不必为了测"指针横坐标 → 0..1"去装一整个控制器。工厂里只留绑定 win / NP_BLEED_MAX 的薄壳。
|
|
8773
|
+
两条拖动条(进度 seek / 音量)共用同一条换算:两者都只是"指针横坐标 → 0..1",
|
|
8774
|
+
差别只在把 0..1 交给谁。分两份实现就会出现"一个按内边距盒算、一个按边框盒算"这种
|
|
8775
|
+
只在某一态偏几像素的分歧(上游那张样式表在别处也吃过同一种亏:同一个位置有两个意见)。 */
|
|
8776
|
+
const npClamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
8777
|
+
/** 指针横坐标 → 0..1(夹住:滑出条外仍然最左/最右,不回弹、不越界)。 */
|
|
8778
|
+
function ratioOf(rect, clientX) {
|
|
8779
|
+
if (!rect) return 0;
|
|
8780
|
+
const w = Number(rect.width);
|
|
8781
|
+
if (!(w > 0)) return 0;
|
|
8782
|
+
const x = Number(clientX);
|
|
8783
|
+
if (!isFinite(x)) return 0;
|
|
8784
|
+
return npClamp((x - Number(rect.left)) / w, 0, 1);
|
|
8785
|
+
}
|
|
8786
|
+
/** 键盘步进(←/↓ 与 →/↑ 各一步、Home/End 到端点);不是这些键返回 null(不吞别的键)。 */
|
|
8787
|
+
function stepRatio(cur, key, step) {
|
|
8788
|
+
const st = Number(step) > 0 ? Number(step) : 0.02;
|
|
8789
|
+
const c = npClamp(Number(cur) || 0, 0, 1);
|
|
8790
|
+
if (key === "Home") return 0;
|
|
8791
|
+
if (key === "End") return 1;
|
|
8792
|
+
if (key === "ArrowLeft" || key === "ArrowDown") return npClamp(c - st, 0, 1);
|
|
8793
|
+
if (key === "ArrowRight" || key === "ArrowUp") return npClamp(c + st, 0, 1);
|
|
8794
|
+
return null;
|
|
8795
|
+
}
|
|
8796
|
+
/** 最近一个**会裁切**的祖先的内边距盒 = 硬边界(⑤ 的"不许被裁切"就是不许越过它)。
|
|
8797
|
+
量不到(假 DOM / 没有 getComputedStyle)⇒ null,调用方按"不放大"处理。 */
|
|
8798
|
+
function clipPadBox(el, getCS) {
|
|
8799
|
+
try {
|
|
8800
|
+
let cur = el;
|
|
8801
|
+
/* 从容器自己开始往上走(容器是我们自己的节点,overflow 是 visible ⇒ 第一轮就跳过);
|
|
8802
|
+
循环条件只判 `cur`:写成 `cur && cur.parentElement` 会把**最上面那个**祖先漏掉
|
|
8803
|
+
—— 真机上恰好是它(列元素)带 overflow:hidden,漏了就等于没有硬边界(借宽会越界)。 */
|
|
8804
|
+
while (cur) {
|
|
8805
|
+
const cs = getCS ? getCS(cur) : null;
|
|
8806
|
+
const ov = cs ? String(cs.overflow || "") + String(cs.overflowX || "") + String(cs.overflowY || "") : "";
|
|
8807
|
+
if (ov && ov.indexOf("visible") < 0) {
|
|
8808
|
+
const r = cur.getBoundingClientRect ? cur.getBoundingClientRect() : null;
|
|
8809
|
+
if (!r) return null;
|
|
8810
|
+
const pad = (v) => { const n = parseFloat(v); return isFinite(n) ? n : 0 };
|
|
8811
|
+
return {
|
|
8812
|
+
left: Number(r.left) + (cs ? pad(cs.paddingLeft) : 0),
|
|
8813
|
+
right: Number(r.right) - (cs ? pad(cs.paddingRight) : 0),
|
|
8814
|
+
node: cur,
|
|
8815
|
+
};
|
|
8816
|
+
}
|
|
8817
|
+
cur = cur.parentElement;
|
|
8818
|
+
}
|
|
8819
|
+
} catch (e) {}
|
|
8820
|
+
return null;
|
|
8821
|
+
}
|
|
8822
|
+
/** ⑤ 悬浮态放大:容器能往左右各"借"多少像素。
|
|
8823
|
+
借的是**容器到最近裁切祖先内边距盒之间那段空档**(宿主自己的内边距,本来是空的)。
|
|
8824
|
+
`applied` 是当前已经借出去的量 —— 必须减掉再算,否则量到的是加宽后的盒子、
|
|
8825
|
+
算出来只剩 1px、下一帧又缩回去:**抖动**("同一件事两个意见"在本文件里出现过一次)。
|
|
8826
|
+
借多少的硬上限是 NP_BLEED_MAX(见常量处),不贴宿主圆角卡片的边。 */
|
|
8827
|
+
function bleedFor(container, applied, maxBleed, getCS) {
|
|
8828
|
+
if (!container || !container.getBoundingClientRect) return 0;
|
|
8829
|
+
const cs = getCS || (typeof getComputedStyle === "function" ? getComputedStyle : null);
|
|
8830
|
+
const clip = clipPadBox(container, cs);
|
|
8831
|
+
if (!clip) return 0;
|
|
8832
|
+
let r = null;
|
|
8833
|
+
try { r = container.getBoundingClientRect(); } catch (e) { return 0 }
|
|
8834
|
+
if (!r) return 0;
|
|
8835
|
+
const back = Number(applied) > 0 ? Number(applied) : 0;
|
|
8836
|
+
const baseL = Number(r.left) + back, baseR = Number(r.right) - back;
|
|
8837
|
+
const budgetL = baseL - clip.left, budgetR = clip.right - baseR;
|
|
8838
|
+
const b = Math.floor(Math.min(budgetL, budgetR));
|
|
8839
|
+
if (!(b > 0)) return 0;
|
|
8840
|
+
const cap = Number(maxBleed) > 0 ? Number(maxBleed) : NP_BLEED_MAX;
|
|
8841
|
+
return Math.min(b, cap);
|
|
8842
|
+
}
|
|
8843
|
+
|
|
8230
8844
|
/* ══════════════════════════════════════════════════════════════════════════════
|
|
8231
8845
|
组件
|
|
8232
8846
|
══════════════════════════════════════════════════════════════════════════════ */
|
|
@@ -8409,6 +9023,10 @@ function createNowPlaying(deps) {
|
|
|
8409
9023
|
/* ①(NP-3) 第四个键(音量/静音)只在深半边存在(见文件头差异 3)。 */
|
|
8410
9024
|
const showVolume = late > 0;
|
|
8411
9025
|
const markCur = M.clamp(Number(q.mark) || 0, 0, 1);
|
|
9026
|
+
/* ①(NP-4) 音量(0..1)与"这条轨道能不能拖"都由控制器送进来(默认值 = 不改既有行为:
|
|
9027
|
+
音量 1、能不能拖跟"有没有已知时长"走)。 */
|
|
9028
|
+
const vol = M.clamp(Number(q.volume) >= 0 ? Number(q.volume) : (Number(media.volume) >= 0 ? Number(media.volume) : 1), 0, 1);
|
|
9029
|
+
const canSeek = q.canSeek === void 0 ? known : !!q.canSeek;
|
|
8412
9030
|
|
|
8413
9031
|
const artStyle = {
|
|
8414
9032
|
left: M.artX(), top: M.artY(), width: art, height: art,
|
|
@@ -8444,8 +9062,60 @@ function createNowPlaying(deps) {
|
|
|
8444
9062
|
className: "mpw_np_run",
|
|
8445
9063
|
style: { width: (known ? M.runPct(at) : 0) + "%" },
|
|
8446
9064
|
})),
|
|
9065
|
+
/* ①(NP-4)③ 进度条可拖动:命中带绝对定位在 bar 上(**不进**那一列的 flex 流),
|
|
9066
|
+
所以时钟与轨道的布局一个像素都不动。轨道只有 3px,命中带上下各补 NP_SCRUB_PAD。
|
|
9067
|
+
落点由控制器交给 client.js 的接线(口径见 docs/NOW-PLAYING-DSH.md §7.8):
|
|
9068
|
+
联动开 ⇒ 真的落到**当前媒体的 currentTime**;联动关 ⇒ 只落到我们自己的播放器;
|
|
9069
|
+
两样都没有 ⇒ 这条带子是只读的(data-mpw-np-noseek,不假装能拖)。 */
|
|
9070
|
+
h("span", {
|
|
9071
|
+
className: "mpw_np_scrub",
|
|
9072
|
+
"data-mpw-np-scrub": "",
|
|
9073
|
+
"data-mpw-np-noseek": canSeek ? void 0 : "",
|
|
9074
|
+
"data-mpw-np-drag": q.dragging ? "" : void 0,
|
|
9075
|
+
role: "slider",
|
|
9076
|
+
tabIndex: canSeek ? 0 : -1,
|
|
9077
|
+
"aria-label": t("np.seek"),
|
|
9078
|
+
"aria-valuemin": 0, "aria-valuemax": known ? media.total : 0,
|
|
9079
|
+
"aria-valuenow": known ? Math.round(at) : 0,
|
|
9080
|
+
"aria-disabled": canSeek ? void 0 : true,
|
|
9081
|
+
style: {
|
|
9082
|
+
/* ⚠ 这个 `top` 是**相对 `.mpw_np_bar`** 的(bar 自己已经在 `top: barTop(p)`),
|
|
9083
|
+
所以只能是 `-NP_SCRUB_PAD`。写成 `barTop(p) - NP_SCRUB_PAD` 会把偏移**加两遍**
|
|
9084
|
+
⇒ 命中带落到卡片外面(真机读数:卡片 620..787,命中带 781.95..800.53,
|
|
9085
|
+
中心点在卡片之下、被同一行的宿主元素接走 ⇒ 拖动事件根本到不了控件)。
|
|
9086
|
+
几何不变量(门禁 F12 断言):barTop(p)+RAIL_H+NP_SCRUB_PAD ≤ boxHeight(p)。 */
|
|
9087
|
+
top: -NP_SCRUB_PAD, left: 0, width: "100%",
|
|
9088
|
+
height: M.RAIL_H + NP_SCRUB_PAD * 2,
|
|
9089
|
+
},
|
|
9090
|
+
onPointerDown: (e) => { if (q.onScrubStart) q.onScrubStart(e); },
|
|
9091
|
+
onPointerMove: (e) => { if (q.onScrubMove) q.onScrubMove(e); },
|
|
9092
|
+
onPointerUp: (e) => { if (q.onScrubEnd) q.onScrubEnd(e); },
|
|
9093
|
+
onPointerCancel: (e) => { if (q.onScrubEnd) q.onScrubEnd(e); },
|
|
9094
|
+
onKeyDown: (e) => { if (q.onScrubKey) q.onScrubKey(e); },
|
|
9095
|
+
}),
|
|
8447
9096
|
h("span", { className: "mpw_np_clock", style: { opacity: late } },
|
|
8448
9097
|
h("span", null, M.clock(at)),
|
|
9098
|
+
/* ①(NP-4)① 音量:**与静音开关分开的第二件事**(用户原话:"打开之后也不能调整它
|
|
9099
|
+
开启的音量")。它只在卡片里存在 —— 与第四个键同一条 late 约定。 */
|
|
9100
|
+
showVolume ? h("span", { className: "mpw_np_vol" },
|
|
9101
|
+
h("span", {
|
|
9102
|
+
className: "mpw_np_volHit",
|
|
9103
|
+
"data-mpw-np-vol": "",
|
|
9104
|
+
role: "slider",
|
|
9105
|
+
tabIndex: media.canVolume ? 0 : -1,
|
|
9106
|
+
"aria-label": t("np.volume"),
|
|
9107
|
+
"aria-valuemin": 0, "aria-valuemax": 100,
|
|
9108
|
+
"aria-valuenow": Math.round(vol * 100),
|
|
9109
|
+
"aria-disabled": media.canVolume ? void 0 : true,
|
|
9110
|
+
onPointerDown: (e) => { if (q.onVolStart) q.onVolStart(e); },
|
|
9111
|
+
onPointerMove: (e) => { if (q.onVolMove) q.onVolMove(e); },
|
|
9112
|
+
onPointerUp: (e) => { if (q.onVolEnd) q.onVolEnd(e); },
|
|
9113
|
+
onPointerCancel: (e) => { if (q.onVolEnd) q.onVolEnd(e); },
|
|
9114
|
+
onKeyDown: (e) => { if (q.onVolKey) q.onVolKey(e); },
|
|
9115
|
+
},
|
|
9116
|
+
h("span", { className: "mpw_np_volRail" }),
|
|
9117
|
+
h("span", { className: "mpw_np_volRun", style: { width: (vol * 100) + "%" } }),
|
|
9118
|
+
h("span", { className: "mpw_np_volKnob", style: { left: (vol * 100) + "%" } }))) : null,
|
|
8449
9119
|
/* 数据源没有时长时**不假装**:显示 --:--(见 docs/NOW-PLAYING-DSH.md §4 情形②) */
|
|
8450
9120
|
h("span", null, known ? ("−" + M.clock(M.remain(at))) : "--:--"))),
|
|
8451
9121
|
h("button", {
|
|
@@ -8502,6 +9172,8 @@ function createNowPlaying(deps) {
|
|
|
8502
9172
|
kind: "none", title: "", byline: "", cover: "",
|
|
8503
9173
|
total: null, playing: false, muted: true, volume: 1,
|
|
8504
9174
|
canPlay: false, canVolume: false, canPrev: false, canNext: false, hasAudio: null,
|
|
9175
|
+
/* ①(NP-4) 空闲态也把这三项写全:谁读都不该读到 undefined(空闲态 = 不能拖、联动开)。 */
|
|
9176
|
+
canSeek: false, link: true,
|
|
8505
9177
|
};
|
|
8506
9178
|
|
|
8507
9179
|
/** ①(NP-3) 纯判据:容器里有没有**非本插件**的占用者(让位逻辑的核心,可单独测)。
|
|
@@ -8589,8 +9261,15 @@ function createNowPlaying(deps) {
|
|
|
8589
9261
|
/* 宿主**自己的**"宽/收起"信号(来自 slot 的 ownerProps.wide)。null = 这版宿主没给。
|
|
8590
9262
|
①(NP-2) 它现在只在**量不到物理宽度**时参与判据(见 shouldHide 三档与 hostSaysCollapsed)。 */
|
|
8591
9263
|
let hostWide = null;
|
|
9264
|
+
/* ── ①(NP-4) 拖动状态 ───────────────────────────────────────────────────────
|
|
9265
|
+
`drag` = 正在拖哪一条("" / "seek" / "vol")。拖动期间**媒体事件推来的进度不再回写**
|
|
9266
|
+
(否则手指按着不放,播放器每 250ms 的 timeupdate 会把游标拽回当前播放位置:
|
|
9267
|
+
真机表现就是"拖不动/拖了弹回去")。松手(或 pointercancel)立刻恢复回写。 */
|
|
9268
|
+
let drag = "";
|
|
9269
|
+
/* ⑤ 当前已经借出去的左右像素(bleedFor 要用它把"量到的盒子"还原成"没借之前的盒子")。 */
|
|
9270
|
+
let bleedNow = 0;
|
|
8592
9271
|
const counts = { resize: 0, mutation: 0, raf: 0 };
|
|
8593
|
-
const state = { open: false, playing: false, at: 0, media: Object.assign({}, IDLE_MEDIA) };
|
|
9272
|
+
const state = { open: false, playing: false, at: 0, volume: null, canSeek: false, media: Object.assign({}, IDLE_MEDIA) };
|
|
8594
9273
|
|
|
8595
9274
|
function raf(fn) {
|
|
8596
9275
|
if (win && typeof win.requestAnimationFrame === "function") return win.requestAnimationFrame(fn);
|
|
@@ -8646,6 +9325,8 @@ function createNowPlaying(deps) {
|
|
|
8646
9325
|
root.render(h(NowPlaying, {
|
|
8647
9326
|
p: pCur, mark: mCur, open: state.open, playing: state.playing, at: state.at,
|
|
8648
9327
|
media: state.media, note: note,
|
|
9328
|
+
volume: state.volume, canSeek: state.canSeek, dragging: drag === "seek",
|
|
9329
|
+
link: state.link,
|
|
8649
9330
|
onToggle: () => {
|
|
8650
9331
|
state.open = !state.open;
|
|
8651
9332
|
tweenTo(state.open ? 1 : 0);
|
|
@@ -8667,9 +9348,99 @@ function createNowPlaying(deps) {
|
|
|
8667
9348
|
if (!state.media.canVolume) { refuse("np.note.novolume"); return; }
|
|
8668
9349
|
if (deps.onTransport) deps.onTransport("mute");
|
|
8669
9350
|
},
|
|
9351
|
+
/* ①(NP-4)① 音量:0..1 交给接线落到真实元素(video / 我们的 audio / web 帧主音量)。
|
|
9352
|
+
与静音键是**两件事**:静音是开关(既有 mute 设置),这里是电平。 */
|
|
9353
|
+
onVolumeLevel: (ratio) => {
|
|
9354
|
+
if (!state.media.canVolume) { refuse("np.note.novolume"); return; }
|
|
9355
|
+
if (deps.onTransport) deps.onTransport("volume", M.clamp(Number(ratio) || 0, 0, 1));
|
|
9356
|
+
state.volume = M.clamp(Number(ratio) || 0, 0, 1);
|
|
9357
|
+
render();
|
|
9358
|
+
},
|
|
9359
|
+
/* ①(NP-4)③ 进度拖动。ratio 是**相对当前媒体时长**的 0..1;
|
|
9360
|
+
换算成秒、以及"联动了要不要同步壁纸"都在 client.js 的接线里(见 §7.8 口径)。 */
|
|
9361
|
+
onScrubStart: (e) => onDragStart("seek", e, (r) => {
|
|
9362
|
+
if (!state.canSeek) { refuse("np.note.noseek"); return; }
|
|
9363
|
+
state.at = r * (typeof state.media.total === "number" ? state.media.total : 0);
|
|
9364
|
+
if (deps.onTransport) deps.onTransport("seek", r);
|
|
9365
|
+
render();
|
|
9366
|
+
}),
|
|
9367
|
+
onScrubMove: (e) => onDragMove("seek", e, (r) => {
|
|
9368
|
+
if (!state.canSeek) return;
|
|
9369
|
+
state.at = r * (typeof state.media.total === "number" ? state.media.total : 0);
|
|
9370
|
+
if (deps.onTransport) deps.onTransport("seek", r);
|
|
9371
|
+
render();
|
|
9372
|
+
}),
|
|
9373
|
+
onScrubEnd: (e) => onDragEnd("seek", e, (r) => {
|
|
9374
|
+
if (!state.canSeek) return;
|
|
9375
|
+
if (deps.onTransport) deps.onTransport("seek", r);
|
|
9376
|
+
}),
|
|
9377
|
+
onScrubKey: (e) => onDragKey("seek", e, () => {
|
|
9378
|
+
const total = typeof state.media.total === "number" ? state.media.total : 0;
|
|
9379
|
+
return total > 0 ? state.at / total : 0;
|
|
9380
|
+
}, (r) => {
|
|
9381
|
+
if (!state.canSeek) { refuse("np.note.noseek"); return; }
|
|
9382
|
+
state.at = r * (typeof state.media.total === "number" ? state.media.total : 0);
|
|
9383
|
+
if (deps.onTransport) deps.onTransport("seek", r);
|
|
9384
|
+
render();
|
|
9385
|
+
}),
|
|
9386
|
+
onVolStart: (e) => onDragStart("vol", e, (r) => fireVol(r)),
|
|
9387
|
+
onVolMove: (e) => onDragMove("vol", e, (r) => fireVol(r)),
|
|
9388
|
+
onVolEnd: (e) => onDragEnd("vol", e, null),
|
|
9389
|
+
onVolKey: (e) => onDragKey("vol", e, () => (typeof state.volume === "number" ? state.volume : 1), (r) => fireVol(r)),
|
|
8670
9390
|
}));
|
|
8671
9391
|
} catch (err) { log.error(TAG + "render failed:", err); }
|
|
8672
9392
|
}
|
|
9393
|
+
function fireVol(r) {
|
|
9394
|
+
if (!state.media.canVolume) { refuse("np.note.novolume"); return; }
|
|
9395
|
+
if (deps.onTransport) deps.onTransport("volume", M.clamp(Number(r) || 0, 0, 1));
|
|
9396
|
+
state.volume = M.clamp(Number(r) || 0, 0, 1);
|
|
9397
|
+
render();
|
|
9398
|
+
}
|
|
9399
|
+
|
|
9400
|
+
/* ── ①(NP-4) 拖动的公共部分(seek 与音量共用;两条只是把 0..1 交给不同的落点) ──
|
|
9401
|
+
`hit` 是承载命中的那个元素(量矩形用它,不是用 e.target —— 指针落到填充/圆点上时
|
|
9402
|
+
target 会是子元素,rect 一样但语义上该以命中带为准)。捕获指针之后,手指滑出元素
|
|
9403
|
+
也还能收到 move/up(否则拖出条子就"卡住")。 */
|
|
9404
|
+
function dragHitOf(e) {
|
|
9405
|
+
try { return (e && (e.currentTarget || e.target)) || null } catch (err) { return null }
|
|
9406
|
+
}
|
|
9407
|
+
function dragRectOf(e) {
|
|
9408
|
+
const el = dragHitOf(e);
|
|
9409
|
+
try { return el && el.getBoundingClientRect ? el.getBoundingClientRect() : null } catch (err) { return null }
|
|
9410
|
+
}
|
|
9411
|
+
function dragXOf(e) {
|
|
9412
|
+
try {
|
|
9413
|
+
if (e && typeof e.clientX === "number" && isFinite(e.clientX)) return e.clientX;
|
|
9414
|
+
if (e && e.nativeEvent && typeof e.nativeEvent.clientX === "number") return e.nativeEvent.clientX;
|
|
9415
|
+
if (e && e.touches && e.touches[0]) return e.touches[0].clientX;
|
|
9416
|
+
} catch (err) {}
|
|
9417
|
+
return NaN;
|
|
9418
|
+
}
|
|
9419
|
+
function onDragStart(which, e, apply) {
|
|
9420
|
+
drag = which;
|
|
9421
|
+
try { const el = dragHitOf(e); if (el && el.setPointerCapture && e && e.pointerId !== void 0) el.setPointerCapture(e.pointerId); } catch (err) {}
|
|
9422
|
+
try { if (e && e.preventDefault) e.preventDefault(); } catch (err) {}
|
|
9423
|
+
try { apply(ratioOf(dragRectOf(e), dragXOf(e))) } catch (err) {}
|
|
9424
|
+
}
|
|
9425
|
+
function onDragMove(which, e, apply) {
|
|
9426
|
+
if (drag !== which) return; /* 没按下的移动不算拖(悬停不算) */
|
|
9427
|
+
try { if (e && e.preventDefault) e.preventDefault(); } catch (err) {}
|
|
9428
|
+
try { apply(ratioOf(dragRectOf(e), dragXOf(e))) } catch (err) {}
|
|
9429
|
+
}
|
|
9430
|
+
function onDragEnd(which, e, apply) {
|
|
9431
|
+
if (drag !== which) return;
|
|
9432
|
+
drag = "";
|
|
9433
|
+
try { if (apply) apply(ratioOf(dragRectOf(e), dragXOf(e))) } catch (err) {}
|
|
9434
|
+
render(); /* 去掉 data-mpw-np-drag */
|
|
9435
|
+
}
|
|
9436
|
+
function onDragKey(which, e, curOf, apply) {
|
|
9437
|
+
const key = String((e && e.key) || "");
|
|
9438
|
+
const cur = curOf();
|
|
9439
|
+
const next = stepRatio(cur, key, which === "vol" ? 0.05 : 0.02);
|
|
9440
|
+
if (next === null) return;
|
|
9441
|
+
try { if (e && e.preventDefault) e.preventDefault(); } catch (err) {}
|
|
9442
|
+
try { apply(next) } catch (err) {}
|
|
9443
|
+
}
|
|
8673
9444
|
|
|
8674
9445
|
/** 做不到的事:**不假装**。console 一行 + 面板上一行(用户能看到),4 秒后自己收。 */
|
|
8675
9446
|
function refuse(key) {
|
|
@@ -8754,6 +9525,33 @@ function createNowPlaying(deps) {
|
|
|
8754
9525
|
const fromContainer = !!(container && isFinite(cw) && cw > 0 && cw !== measureWidth());
|
|
8755
9526
|
return fitScale(cw, fromContainer);
|
|
8756
9527
|
}
|
|
9528
|
+
/** ⑤ 悬浮态放大:把容器往两侧各借 bleed 像素(借的是宿主自己的内边距,本来是空的)。
|
|
9529
|
+
只写**我们自己容器**的内联样式,不碰宿主任何节点:
|
|
9530
|
+
· `width: calc(100% + 2b)` + 左右各 -b 的外边距 ⇒ 盒子在原处**居中变宽**;
|
|
9531
|
+
· 硬边界是最近裁切祖先的内边距盒(bleedFor 里算),所以借满也不会被切。
|
|
9532
|
+
真机读数(悬浮态):列 256 / 内边距 12 ⇒ 内边距盒 24..256;容器原本 37..243(206),
|
|
9533
|
+
借 12 之后是 25..255(230),两边各留 1px。
|
|
9534
|
+
收敛性:bleedFor 用 `bleedNow` 把量到的盒子还原成"没借之前",所以这是一步到位的;
|
|
9535
|
+
ResizeObserver 会为这次变宽回调一次,那一次算出来的值与现在相同 ⇒ 不再改样式、不再回调。 */
|
|
9536
|
+
function applyBleed() {
|
|
9537
|
+
if (!container) return 0;
|
|
9538
|
+
const getCS = win && win.getComputedStyle ? win.getComputedStyle.bind(win) : null;
|
|
9539
|
+
const want = bleedFor(container, bleedNow, NP_BLEED_MAX, getCS);
|
|
9540
|
+
if (want === bleedNow) return bleedNow;
|
|
9541
|
+
bleedNow = want;
|
|
9542
|
+
try {
|
|
9543
|
+
if (!want) {
|
|
9544
|
+
container.style.marginLeft = "";
|
|
9545
|
+
container.style.marginRight = "";
|
|
9546
|
+
container.style.width = "";
|
|
9547
|
+
} else {
|
|
9548
|
+
container.style.marginLeft = "-" + want + "px";
|
|
9549
|
+
container.style.marginRight = "-" + want + "px";
|
|
9550
|
+
container.style.width = "calc(100% + " + (want * 2) + "px)";
|
|
9551
|
+
}
|
|
9552
|
+
} catch (e) {}
|
|
9553
|
+
return bleedNow;
|
|
9554
|
+
}
|
|
8757
9555
|
/** 一帧判据:写/撤 data-mpw-np-hidden + **让位判据**(①(NP-3))。
|
|
8758
9556
|
* 让位在**每一次**判据里都跑:挂载前(setEnabled 里单独一次)、挂载后(这里)都要判。
|
|
8759
9557
|
* 规则:容器里出现非本插件、非宿主自有的占用者 ⇒ 撤下且**不重建**(等它走了再回来)。 */
|
|
@@ -8766,6 +9564,7 @@ function createNowPlaying(deps) {
|
|
|
8766
9564
|
return hidden;
|
|
8767
9565
|
}
|
|
8768
9566
|
if (occupantNow()) { retractForOccupant(); return false; }
|
|
9567
|
+
applyBleed(); /* ⑤ 先借宽,再量可用宽度 */
|
|
8769
9568
|
const next = shouldHide(measureWidth(), hostSaysCollapsed());
|
|
8770
9569
|
if (next !== hidden) {
|
|
8771
9570
|
hidden = next;
|
|
@@ -8913,6 +9712,8 @@ function createNowPlaying(deps) {
|
|
|
8913
9712
|
try {
|
|
8914
9713
|
container = doc.createElement("div");
|
|
8915
9714
|
container.setAttribute(NP_ATTR, "");
|
|
9715
|
+
/* ①(NP-4)② 联动开关的可查询落点(值由 setLink/applyNowPlaying 维护)。 */
|
|
9716
|
+
container.setAttribute(NP_LINK_ATTR, state.link === false ? "0" : "1");
|
|
8916
9717
|
insertInto(a, true);
|
|
8917
9718
|
enabled = true;
|
|
8918
9719
|
root = (typeof deps.createRoot === "function") ? deps.createRoot(container) : null;
|
|
@@ -9002,6 +9803,7 @@ function createNowPlaying(deps) {
|
|
|
9002
9803
|
markYield(null); /* ①(NP-3) 用户自己关了 ⇒ 让位原因一起清掉(别留下过期状态) */
|
|
9003
9804
|
enabled = false; hidden = false; anchorMode = "none"; note = "";
|
|
9004
9805
|
pCur = 0; mCur = 0; state.open = false; state.playing = false;
|
|
9806
|
+
drag = ""; bleedNow = 0; /* ①(NP-4)/⑤:拖动与借宽都是"挂着才有"的状态 */
|
|
9005
9807
|
return api;
|
|
9006
9808
|
}
|
|
9007
9809
|
if (enabled) { ensureAnchored(); evaluate(); return api; } /* 重复开 = 只重新锚定,不叠节点 */
|
|
@@ -9032,6 +9834,16 @@ function createNowPlaying(deps) {
|
|
|
9032
9834
|
if (!(typeof state.media.total === "number" && state.media.total > 0)) state.at = 0;
|
|
9033
9835
|
const playing = !!state.media.playing;
|
|
9034
9836
|
if (playing !== state.playing) { state.playing = playing; tweenMark(playing); }
|
|
9837
|
+
/* ①(NP-4) 音量/可拖/联动三个派生量只在**接线送来**时才改(缺省保持"不改既有行为":
|
|
9838
|
+
音量跟随媒体元素的 volume,能不能拖跟"有没有已知时长",联动默认开)。 */
|
|
9839
|
+
if (next && next.volume !== void 0 && next.volume !== null && isFinite(Number(next.volume))) {
|
|
9840
|
+
state.volume = M.clamp(Number(next.volume), 0, 1);
|
|
9841
|
+
} else if (state.volume === null && state.media.volume !== void 0 && isFinite(Number(state.media.volume))) {
|
|
9842
|
+
state.volume = M.clamp(Number(state.media.volume), 0, 1);
|
|
9843
|
+
}
|
|
9844
|
+
if (next && next.canSeek !== void 0) state.canSeek = !!next.canSeek;
|
|
9845
|
+
else if (typeof state.media.total === "number" && state.media.total > 0) state.canSeek = true;
|
|
9846
|
+
if (next && next.link !== void 0) state.link = !!next.link;
|
|
9035
9847
|
render();
|
|
9036
9848
|
return api;
|
|
9037
9849
|
},
|
|
@@ -9041,7 +9853,24 @@ function createNowPlaying(deps) {
|
|
|
9041
9853
|
render();
|
|
9042
9854
|
return api;
|
|
9043
9855
|
},
|
|
9044
|
-
|
|
9856
|
+
/** 播放进度(媒体事件推来的)。①(NP-4):**拖动期间直接丢弃** —— 手指按着的时候
|
|
9857
|
+
播放器自己的 timeupdate 会把游标拽回播放位置(真机表现:"拖不动")。
|
|
9858
|
+
拖动那一路由 onScrubMove 自己写 state.at,所以丢弃不会让游标停住。 */
|
|
9859
|
+
setProgress(at) { if (drag) return api; state.at = Number(at) || 0; render(); return api; },
|
|
9860
|
+
/** ①(NP-4) ② 联动开关状态(client.js 的 applyNowPlaying 送)——只影响根上的可查询属性,
|
|
9861
|
+
动作口径在接线里(见 docs/NOW-PLAYING-DSH.md §7.8)。 */
|
|
9862
|
+
setLink(on) {
|
|
9863
|
+
state.link = !!on;
|
|
9864
|
+
try { if (container) container.setAttribute(NP_LINK_ATTR, state.link === false ? "0" : "1"); } catch (e) {}
|
|
9865
|
+
render();
|
|
9866
|
+
return api;
|
|
9867
|
+
},
|
|
9868
|
+
/** ①(NP-4) 音量电平(0..1,接线送;拖动时也用它回写显示)。 */
|
|
9869
|
+
setVolume(level) {
|
|
9870
|
+
const v = Number(level);
|
|
9871
|
+
if (isFinite(v)) { state.volume = M.clamp(v, 0, 1); render(); }
|
|
9872
|
+
return api;
|
|
9873
|
+
},
|
|
9045
9874
|
/** 给测试/排障看的一眼状态(不参与渲染)。 */
|
|
9046
9875
|
inspect: () => ({
|
|
9047
9876
|
enabled, hidden, anchorMode, note,
|
|
@@ -9054,6 +9883,8 @@ function createNowPlaying(deps) {
|
|
|
9054
9883
|
nextSiblingClass: container && container.nextSibling ? String(container.nextSibling.className || "") : "",
|
|
9055
9884
|
observers: { resize: counts.resize, mutation: counts.mutation, rafStarts: counts.raf, rafLive: (rafId ? 1 : 0) + (markRafId ? 1 : 0), anchorWatch: anchorWatch || null },
|
|
9056
9885
|
media: state.media, open: state.open, playing: state.playing, at: state.at, p: pCur, mark: mCur,
|
|
9886
|
+
/* ①(NP-4) 拖动/音量/联动/借宽的可查询状态(真机探针与门禁都读这里)。 */
|
|
9887
|
+
dragging: drag, volume: state.volume, canSeek: state.canSeek, link: state.link, bleed: bleedNow,
|
|
9057
9888
|
}),
|
|
9058
9889
|
/** 宿主 slot 交来的容器(我们自己的 div);null = slot 卸载了 ⇒ 回到降级链。 */
|
|
9059
9890
|
setSlotNode(node) {
|
|
@@ -9071,7 +9902,7 @@ function createNowPlaying(deps) {
|
|
|
9071
9902
|
scheduleSettle();
|
|
9072
9903
|
return api;
|
|
9073
9904
|
},
|
|
9074
|
-
inspectCollapse: () => ({ hostWide, hidden, fit: npFit(), width: measureAvailWidth(), colWidth: measureWidth(), hostCollapsed: hostSaysCollapsed(), settlePending: !!settleTimer }),
|
|
9905
|
+
inspectCollapse: () => ({ hostWide, hidden, fit: npFit(), width: measureAvailWidth(), colWidth: measureWidth(), hostCollapsed: hostSaysCollapsed(), settlePending: !!settleTimer, bleed: bleedNow }),
|
|
9075
9906
|
evaluate, ensureAnchored,
|
|
9076
9907
|
destroy() { return setEnabled(false); },
|
|
9077
9908
|
};
|
|
@@ -9112,7 +9943,9 @@ function createNowPlaying(deps) {
|
|
|
9112
9943
|
return MpwNowPlayingAction;
|
|
9113
9944
|
}
|
|
9114
9945
|
|
|
9115
|
-
return {
|
|
9946
|
+
return {
|
|
9947
|
+
NowPlaying, PlayMark, createController, createSlotAction, shouldHide, resolveAnchor, sidebarRoot, sidebarCol, occupantOf,
|
|
9948
|
+
};
|
|
9116
9949
|
}
|
|
9117
9950
|
|
|
9118
9951
|
module.exports = {
|
|
@@ -9122,7 +9955,13 @@ module.exports = {
|
|
|
9122
9955
|
NP_NOTE_ATTR,
|
|
9123
9956
|
NP_YIELD_ATTR,
|
|
9124
9957
|
NP_YIELD_FOREIGN,
|
|
9958
|
+
NP_LINK_ATTR,
|
|
9125
9959
|
NP_COLLAPSE_MAX_W,
|
|
9960
|
+
NP_SCRUB_PAD,
|
|
9961
|
+
NP_VOL_W,
|
|
9962
|
+
NP_BLEED_MAX,
|
|
9963
|
+
/* ①(NP-4) 拖动换算 + ⑤ 借宽判据(纯函数;无浏览器门禁直接断言)。 */
|
|
9964
|
+
ratioOf, stepRatio, clipPadBox, bleedFor,
|
|
9126
9965
|
NP_CSS,
|
|
9127
9966
|
NP_SIDEBAR_ROOT_SEL,
|
|
9128
9967
|
NP_SIDEBAR_ROOT_ATTR,
|
|
@@ -13240,7 +14079,8 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
13240
14079
|
glassColor: "", glassAlpha: 12, accent: "", newStyle: false, mute: true,
|
|
13241
14080
|
thinkBg: DEFAULT_THINK_BG,
|
|
13242
14081
|
bsCompat: DEFAULT_BS_COMPAT, bsFloat: false, bsFont: false, bsReveal: false, bsAlpha: false, bsAqua: false, bsBottomAvoid: false,
|
|
13243
|
-
enabled: DEFAULT_ENABLED, npNowPlaying: DEFAULT_NP_NOW_PLAYING // ①(NP-1) 恢复默认同样关掉
|
|
14082
|
+
enabled: DEFAULT_ENABLED, npNowPlaying: DEFAULT_NP_NOW_PLAYING, // ①(NP-1) 恢复默认同样关掉
|
|
14083
|
+
npLinkWallpaper: DEFAULT_NP_LINK_WALLPAPER, npVolume: DEFAULT_NP_VOLUME // ①(NP-4) 两个新键一起复位(否则"恢复默认"留下旧电平)
|
|
13244
14084
|
}), true);
|
|
13245
14085
|
setSection(readSection());
|
|
13246
14086
|
applyFromStorage();
|
|
@@ -13260,7 +14100,8 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
13260
14100
|
"brightness", "float", "playbackRate", "newStyle", "mute", "thinkBg", "enabled",
|
|
13261
14101
|
"forceEnabled", "propEdits", "clock", "clock24h", "clockSec", "clockDate", "clockPos", "clockSize",
|
|
13262
14102
|
"fpsCap", "resMax", "bsCompat", "bsFloat", "bsFont", "bsReveal", "bsRevealAlpha", "bsAlpha", "bsAqua", "bsBottomAvoid",
|
|
13263
|
-
"npNowPlaying" // ①(NP-1) Now playing 挂载开关(可随备份导出/导入)
|
|
14103
|
+
"npNowPlaying", // ①(NP-1) Now playing 挂载开关(可随备份导出/导入)
|
|
14104
|
+
"npLinkWallpaper", "npVolume" // ①(NP-4)②① 联动开关 / 音量电平(同上,随备份走)
|
|
13264
14105
|
];
|
|
13265
14106
|
const exportBackup = () => {
|
|
13266
14107
|
try {
|
|
@@ -13297,8 +14138,8 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
13297
14138
|
const patch = {};
|
|
13298
14139
|
// ①(修正) 类型校验(评审指出:原实现直接透传,`opacity:"abc"` 会污染设置):
|
|
13299
14140
|
// 数值类必须为有限数、布尔类必须为 boolean、字符串类必须为 string。
|
|
13300
|
-
const numFields = ["opacity", "blur", "zoom", "headerBlurAmount", "headerFrostAmount", "dialogAmount", "popoverAmount", "popoverAlpha", "maskAmount", "unifyAmount", "sidebarAlpha", "aquaMaskAlpha", "aquaTintStrength", "glassAlpha", "lgCssAmount", "rotateMin", "brightness", "playbackRate", "clockSize", "fpsCap", "resMax", "bsRevealAlpha", "sceneWatchdogSecs"]; // ①(批次15)
|
|
13301
|
-
const boolFields = ["sidebar", "sharp", "headerBlur", "headerBg", "rightSidebarBlur", "dialogBlur", "popoverBlur", "maskBlur", "unifyTint", "chatFollow", "sessionFollow", "aquaMask", "aquaTint", "aquaInk", "aquaTextEnhance", "todoBlur", "hybrid", "roundCompat", "rotate", "float", "newStyle", "mute", "thinkBg", "enabled", "forceEnabled", "clock", "clock24h", "clockSec", "clockDate", "bsCompat", "bsFloat", "bsFont", "bsReveal", "bsAlpha", "bsAqua", "bsBottomAvoid", "lgTest", "lgCss", "lgComposer", "lgSidebar", "lgHeader", "powPauseHidden", "powPauseBlur", "powPauseBattery", "sceneWatchdog", "npNowPlaying"]; // ①(批次15) 四处接线④:导入布尔净化;①(NP-1) 末尾追加 npNowPlaying
|
|
14141
|
+
const numFields = ["opacity", "blur", "zoom", "headerBlurAmount", "headerFrostAmount", "dialogAmount", "popoverAmount", "popoverAlpha", "maskAmount", "unifyAmount", "sidebarAlpha", "aquaMaskAlpha", "aquaTintStrength", "glassAlpha", "lgCssAmount", "rotateMin", "brightness", "playbackRate", "clockSize", "fpsCap", "resMax", "bsRevealAlpha", "sceneWatchdogSecs", "npVolume"]; // ①(批次15) 四处接线③:导入数值净化;①(NP-4)① 追加 npVolume
|
|
14142
|
+
const boolFields = ["sidebar", "sharp", "headerBlur", "headerBg", "rightSidebarBlur", "dialogBlur", "popoverBlur", "maskBlur", "unifyTint", "chatFollow", "sessionFollow", "aquaMask", "aquaTint", "aquaInk", "aquaTextEnhance", "todoBlur", "hybrid", "roundCompat", "rotate", "float", "newStyle", "mute", "thinkBg", "enabled", "forceEnabled", "clock", "clock24h", "clockSec", "clockDate", "bsCompat", "bsFloat", "bsFont", "bsReveal", "bsAlpha", "bsAqua", "bsBottomAvoid", "lgTest", "lgCss", "lgComposer", "lgSidebar", "lgHeader", "powPauseHidden", "powPauseBlur", "powPauseBattery", "sceneWatchdog", "npNowPlaying", "npLinkWallpaper"]; // ①(批次15) 四处接线④:导入布尔净化;①(NP-1) 末尾追加 npNowPlaying;①(NP-4)② 追加 npLinkWallpaper
|
|
13302
14143
|
const strFields = ["aquaColor", "aquaInkColor", "themeColor", "accent", "glassColor", "clockPos", "fontColorGrayColor", "sceneDebugParams"]; // ①(批次15) sceneDebugParams 走字符串净化(非法键/值在写入端丢弃)
|
|
13303
14144
|
for (const k of BACKUP_FIELDS) {
|
|
13304
14145
|
const v = settings[k];
|
|
@@ -13877,6 +14718,8 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
13877
14718
|
]);
|
|
13878
14719
|
};
|
|
13879
14720
|
|
|
14721
|
+
/* ①(NP-4)② ② 那一行只在 NP 开着时渲染(渲染用派生量,与持久化默认值同源)。 */
|
|
14722
|
+
const npOnNow = section.npNowPlaying !== void 0 ? !!section.npNowPlaying : DEFAULT_NP_NOW_PLAYING;
|
|
13880
14723
|
// ⑳(新) 壁纸设置 tab:计算激活的帧率上限值(缺省=0 无限制)与 ffmpeg 状态文本(渲染用)
|
|
13881
14724
|
const fpsCapVal = section.fpsCap !== void 0 && section.fpsCap !== null ? section.fpsCap : 0;
|
|
13882
14725
|
const resMaxVal = section.resMax !== void 0 && section.resMax !== null ? section.resMax : DEFAULT_RES_MAX;
|
|
@@ -14396,6 +15239,12 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
14396
15239
|
// 默认关;关 = 一行 DOM 都不注入、一个观察者都不装(见 lib/now-playing.js)。
|
|
14397
15240
|
// 左侧栏收起时组件自己隐藏(判据/阈值见 NP_COLLAPSE_MAX_W 的注释)。
|
|
14398
15241
|
toggleRow(t("npNowPlaying"), t("npNowPlaying.desc"), "npNowPlaying", DEFAULT_NP_NOW_PLAYING),
|
|
15242
|
+
/* ①(NP-4)② 「播放/暂停是否同时控制壁纸的播放/暂停」——**放在 Now playing 之下**,
|
|
15243
|
+
并且**只在 npNowPlaying 打开时显示**(NP 关掉 ⇒ 这一行跟着撤掉:一个不挂载的
|
|
15244
|
+
组件不该还留着它的第二个开关)。判据见 tools/np-control-test.mjs 的 F 组
|
|
15245
|
+
(两档:NP 开 ⇒ 这一行在;NP 关 ⇒ 这一行不在,且 DOM 里 0 个 mpw_np_* 节点)。
|
|
15246
|
+
默认值 DEFAULT_NP_LINK_WALLPAPER = **true** ⇒ 默认档与改动前逐字节同行为。 */
|
|
15247
|
+
(npOnNow ? toggleRow(t("npLinkWallpaper"), t("npLinkWallpaper.desc"), "npLinkWallpaper", DEFAULT_NP_LINK_WALLPAPER) : null),
|
|
14399
15248
|
// ①(新) 壁纸镜像翻转(Wallpaper Engine 原生基础选项)
|
|
14400
15249
|
h("div", { className: "mpw_inline" }, [
|
|
14401
15250
|
toggleRow(t("flipX"), t("flipX.desc"), "flipX", false),
|
|
@@ -15444,7 +16293,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
15444
16293
|
"scnRender.report": "场景渲染上报(排障)",
|
|
15445
16294
|
"scnRender.report.desc": "开启后壁纸每 10 秒把渲染器现场(截图 + 逐层绘制台账 + 纹理统计)写到渲染器的 reports/ 目录,用于定位“坐标/可见性”问题。仅排障时开,默认关(有磁盘写入)",
|
|
15446
16295
|
"scnRender.extUrl.ph": "http://127.0.0.1:8899/ext",
|
|
15447
|
-
"scnRender.extUrl.hint": "留空=不启用。填写后按 extbase 拼进场景渲染 iframe
|
|
16296
|
+
"scnRender.extUrl.hint": "留空=不启用。填写后按 extbase 拼进场景渲染 iframe(见渲染器仓文档 EXTENSION-HOOKS.md);点「测试」验证索引可达并显示钩子数量。",
|
|
15448
16297
|
"scnRender.extUrl.save": "保存",
|
|
15449
16298
|
"scnRender.extUrl.saved": "扩展钩子地址已保存,下次应用场景壁纸生效",
|
|
15450
16299
|
"scnRender.extUrl.cleared": "已清除扩展钩子地址",
|
|
@@ -15480,7 +16329,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
15480
16329
|
"diag.fold": "收起开关",
|
|
15481
16330
|
"diag.copy": "复制",
|
|
15482
16331
|
"diag.copied": "已复制",
|
|
15483
|
-
"diag.more": "
|
|
16332
|
+
"diag.more": "全部开关见渲染器仓文档 README-DIAGNOSTICS.md(node diag-flag-check.mjs 校验代码与文档一致)。",
|
|
15484
16333
|
"diagsend.sec": "一键诊断上报",
|
|
15485
16334
|
"diagsend.desc": "把关键子系统状态(磨砂 / 侧栏 / 时间线是否被影响、壁纸类型与路径、shim 是否注入、视频解码、表面 token、场景健康)打包发回本机宿主,落在 ~/.dsh/.dsh-mpkg-wallpaper/diag-<时间戳>.json。每个字段都带来源;读不到的字段不会消失,而是标成 null + degraded 原因。宿主不可用时自动改成下载 JSON 文件(离线也能发回来)。",
|
|
15486
16335
|
"diagsend.btn": "一键上报",
|
|
@@ -15830,6 +16679,14 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
15830
16679
|
"np.note.nolist": "当前壁纸只有一条音轨,没有上一首/下一首(不假装有列表)",
|
|
15831
16680
|
"np.note.novolume": "当前壁纸没有可控制的音量通道:只有视频/网页壁纸的声音可控",
|
|
15832
16681
|
"np.note.decode": "音轨加载失败(浏览器不支持这个格式,或宿主取字节失败)",
|
|
16682
|
+
/* ①(NP-4) 新增词的落点(zh)。 */
|
|
16683
|
+
"np.volume": "音量",
|
|
16684
|
+
"np.seek": "播放进度(可拖动)",
|
|
16685
|
+
"np.note.noseek": "这条进度现在不可拖:没有可定位的媒体(联动关掉时壁纸的进度不受控件影响)",
|
|
16686
|
+
"np.note.pkgListOnly": "音轨在包内(scene.pkg),宿主没有能取出它的字节通道 ⇒ 只能列出清单,不能播放",
|
|
16687
|
+
"np.note.linkOff": "「播放/暂停同时控制壁纸」已关:控件只驱动本插件自己的播放器,壁纸本身的播放不受影响",
|
|
16688
|
+
"npLinkWallpaper": "播放/暂停同时控制壁纸",
|
|
16689
|
+
"npLinkWallpaper.desc": "开(默认):Now playing 的播放/暂停与进度同时驱动壁纸自己的播放;关:只驱动本插件自己的播放器,壁纸的画面播放不受控件影响(壁纸媒体是唯一声源时控件如实禁用,不假装)。默认开 = 与改动前逐字节同行为(详见 docs/NOW-PLAYING-DSH.md §7.8)",
|
|
15833
16690
|
};
|
|
15834
16691
|
const en = {
|
|
15835
16692
|
"nav": "MPKG Wallpaper",
|
|
@@ -16204,7 +17061,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
16204
17061
|
"scnRender.report": "Scene render reporting (debug)",
|
|
16205
17062
|
"scnRender.report.desc": "When on, the wallpaper sends the renderer's live state (screenshot + per-layer draw ledger + texture stats) to the renderer's reports/ folder every 10s, to diagnose position/visibility bugs. Debug only; off by default (writes to disk)",
|
|
16206
17063
|
"scnRender.extUrl.ph": "http://127.0.0.1:8899/ext",
|
|
16207
|
-
"scnRender.extUrl.hint": "Empty = disabled. When set, it is passed to the scene iframe as extbase (see
|
|
17064
|
+
"scnRender.extUrl.hint": "Empty = disabled. When set, it is passed to the scene iframe as extbase (see the renderer repo docs EXTENSION-HOOKS.md); press Test to verify the index and show the hook count.",
|
|
16208
17065
|
"scnRender.extUrl.save": "Save",
|
|
16209
17066
|
"scnRender.extUrl.saved": "Extension hooks URL saved; takes effect the next time a scene wallpaper is applied",
|
|
16210
17067
|
"scnRender.extUrl.cleared": "Extension hooks URL cleared",
|
|
@@ -16240,7 +17097,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
16240
17097
|
"diag.fold": "Hide flags",
|
|
16241
17098
|
"diag.copy": "Copy",
|
|
16242
17099
|
"diag.copied": "Copied",
|
|
16243
|
-
"diag.more": "Full list:
|
|
17100
|
+
"diag.more": "Full list: the renderer repo docs README-DIAGNOSTICS.md (node diag-flag-check.mjs verifies code/doc consistency).",
|
|
16244
17101
|
"diagsend.sec": "One-click diagnostics report",
|
|
16245
17102
|
"diagsend.desc": "Packages the key subsystem state (whether frosting / sidebar / timeline rail are affected, wallpaper type and path, whether the shim was injected, video decode state, surface tokens, scene health) and sends it back to the local host, landing in ~/.dsh/.dsh-mpkg-wallpaper/diag-<timestamp>.json. Every field carries its provenance; unreadable fields are not dropped but marked null + a degraded reason. If the host is unavailable it falls back to downloading the JSON file (works offline).",
|
|
16246
17103
|
"diagsend.btn": "Send report",
|
|
@@ -16319,6 +17176,14 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
16319
17176
|
"np.note.nolist": "This wallpaper has a single audio track, so there is no previous/next (no fake playlist)",
|
|
16320
17177
|
"np.note.novolume": "This wallpaper has no controllable volume channel: only video/web wallpaper sound can be controlled",
|
|
16321
17178
|
"np.note.decode": "The audio track failed to load (unsupported format, or the host could not serve the bytes)",
|
|
17179
|
+
/* ①(NP-4) added strings (en). */
|
|
17180
|
+
"np.volume": "Volume",
|
|
17181
|
+
"np.seek": "Playback position (draggable)",
|
|
17182
|
+
"np.note.noseek": "This position is not draggable right now: there is no seekable media (with the link switch off, the wallpaper's own position is left alone)",
|
|
17183
|
+
"np.note.pkgListOnly": "The audio lives inside the package (scene.pkg) and the host has no route to serve its bytes, so the list is real but playback is not possible",
|
|
17184
|
+
"np.note.linkOff": "\"Play/pause also controls the wallpaper\" is off: the transport only drives this plugin's own player, the wallpaper's own playback is untouched",
|
|
17185
|
+
"npLinkWallpaper": "Play/pause also controls the wallpaper",
|
|
17186
|
+
"npLinkWallpaper.desc": "On (default): the Now playing transport also drives the wallpaper's own playback and position. Off: only this plugin's own player is driven and the wallpaper's playback is untouched (when the wallpaper media is the only sound source the controls are honestly disabled). On by default = byte-identical to the previous behaviour (see docs/NOW-PLAYING-DSH.md §7.8)",
|
|
16322
17187
|
};
|
|
16323
17188
|
|
|
16324
17189
|
// ═══════════════════════════════════════════════════════════════════
|