dsh-mpkg-wallpaper 3.8.0 → 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 +973 -74
- package/lib/now-playing.js +372 -5
- 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 脚本生成,勿手改);
|
|
@@ -306,6 +322,12 @@ function mpwTrace(ev, data) {
|
|
|
306
322
|
* · 源字段(image/webUrl/sceneKey)粘性 ⇒ 谁都没带就保持原样,绝不因为"这次没提"而消失。
|
|
307
323
|
* 合并结果**同时**回写两处(旧写法只写 localStorage,宿主那份半残档永远修不回来)。 */
|
|
308
324
|
function initHostSettings() {
|
|
325
|
+
// ①(2026-09-19 设置持久化轮) **一次性**:apply() 会因 RTC 重连/热重注入重跑,而每一次都会
|
|
326
|
+
// 重新 GET 一次宿主设置。旧实现里重复的 GET 只是重复合并(幂等,代价小);现在合并带
|
|
327
|
+
// "两处存储时间戳裁决 + 回写两处",重复跑会让"后到的那次 GET"拿着**更旧**的宿主快照
|
|
328
|
+
// 参与裁决(极端情况下把刚写下的本地新值判给宿主)。所以这一轮明确只跑一次。
|
|
329
|
+
if (window.__mpwHostInitOnce) return;
|
|
330
|
+
window.__mpwHostInitOnce = true;
|
|
309
331
|
try {
|
|
310
332
|
fetch(HOST_BASE + "/settings", { method: "GET" }).then(async (r) => {
|
|
311
333
|
if (!r.ok) { try { mpwBootSettle("host-get-not-ok") } catch {} return }
|
|
@@ -407,7 +429,15 @@ function sectionSigNow() {
|
|
|
407
429
|
return o;
|
|
408
430
|
}
|
|
409
431
|
function readSection() {
|
|
410
|
-
if (sectionCache)
|
|
432
|
+
if (sectionCache) {
|
|
433
|
+
// ①(2026-09-19 设置持久化轮) **读口一律不见元键**:内部缓存为了落盘会带
|
|
434
|
+
// `__mpwLocalAt` 这类元键(时间戳/自愈标记),但"用户档"的读数口(面板/buildCss/
|
|
435
|
+
// 导出/字段集合比较/测试的逐字段 diff)必须看不到它们 —— 否则元键会被当成
|
|
436
|
+
// "用户的设置项"参与比较(真机上表现为"莫名多出一个键",测试里表现为假红)。
|
|
437
|
+
const clean = mpwStripMeta(sectionCache);
|
|
438
|
+
if (clean !== sectionCache) sectionCache = clean;
|
|
439
|
+
return sectionCache;
|
|
440
|
+
}
|
|
411
441
|
sectionCache = mpwNormalizeSection(loadSection());
|
|
412
442
|
return sectionCache;
|
|
413
443
|
}
|
|
@@ -872,8 +902,26 @@ function sectionSigNow() {
|
|
|
872
902
|
(typeof mpwVal(s, "source") === "string" && mpwVal(s, "source"));
|
|
873
903
|
return !src && !!clue;
|
|
874
904
|
}
|
|
905
|
+
/** 不变量Ⅲ(半边):**补空缺、不覆盖** —— 保留 `win` 已有的每一个键的值,
|
|
906
|
+
* 只把 `lose` 里 `win` **没有**的键补进来。
|
|
907
|
+
* ⚠ 这里连踩过两次坑(都由 B1/B4 断言抓出来,故单独成函数 + 明确语义):
|
|
908
|
+
* · 用 `mpwMergeSection(lose, win)` ⇒ win 的键逐个覆盖 lose ⇒ 变成"整档赢",
|
|
909
|
+
* 等于退化成旧写法的整档覆盖语义(字段级裁决名存实亡);
|
|
910
|
+
* · 用"谁值不同就取另一份"⇒ 两边都有的键仍然由**一方**决定,同样是整档赢。
|
|
911
|
+
* 只有"以赢家为底、输家只填空缺"才同时满足:两端都有的键由时间戳裁决 + 只有一端有的键一定保留。 */
|
|
912
|
+
function mpwOverlayFill(win, lose) {
|
|
913
|
+
const out = Object.assign({}, (win && typeof win === "object") ? win : {});
|
|
914
|
+
if (!lose || typeof lose !== "object") return out;
|
|
915
|
+
for (const k of Object.keys(lose)) {
|
|
916
|
+
if (out[k] !== void 0) continue; // 赢家已有 ⇒ 一律用赢家的
|
|
917
|
+
const v = lose[k];
|
|
918
|
+
if (v === void 0 || v === null) continue; // 输家的"缺/显式删"都不该补进赢家
|
|
919
|
+
out[k] = v;
|
|
920
|
+
}
|
|
921
|
+
return out;
|
|
922
|
+
}
|
|
875
923
|
/** 不变量Ⅲ:启动时把两处存储合成一份 —— 新者优先(各自的内嵌时间戳),
|
|
876
|
-
*
|
|
924
|
+
* 输家只补赢家没有的键(`mpwOverlayFill`),源字段另有粘性保护(谁都没有时不抹)。 */
|
|
877
925
|
function mpwReconcileStores(localIn, hostIn) {
|
|
878
926
|
const local = mpwStripMeta(localIn || {});
|
|
879
927
|
const host = mpwStripMeta(hostIn || {});
|
|
@@ -894,12 +942,10 @@ function sectionSigNow() {
|
|
|
894
942
|
newer = "unknown";
|
|
895
943
|
reason = "no-stamps(local=" + localKeys.length + " keys, host=" + hostKeys.length + " keys)";
|
|
896
944
|
}
|
|
897
|
-
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
for (const k of mpwSortedKeys(B)) if (mpwVal(B, k) !== mpwVal(A, k)) patch[k] = mpwVal(B, k);
|
|
902
|
-
let merged = mpwMergeSection(A, patch);
|
|
945
|
+
/* 裁决后合成:**赢家已有的键一律用赢家的值**,输家只补赢家没有的键(见 mpwOverlayFill)。 */
|
|
946
|
+
const win = winner === "local" ? local : host;
|
|
947
|
+
const lose = winner === "local" ? host : local;
|
|
948
|
+
let merged = mpwOverlayFill(win, lose);
|
|
903
949
|
// 源字段粘性 + 一致性标记:两处都没有源、却都有来源线索 ⇒ 半残档(交给自愈)
|
|
904
950
|
merged = mpwStickySource(local, merged);
|
|
905
951
|
merged = mpwStickySource(host, merged);
|
|
@@ -1945,11 +1991,12 @@ function sectionSigNow() {
|
|
|
1945
1991
|
// 导致每次 applyFromStorage 都重设 src → Edge 端也重载重播。
|
|
1946
1992
|
try { if (video.getAttribute('src') !== url) video.src = url; } catch {}
|
|
1947
1993
|
video.playbackRate = (typeof readSection().playbackRate === "number" && readSection().playbackRate >= 0.5 && readSection().playbackRate <= 2) ? readSection().playbackRate : 1;
|
|
1948
|
-
/* ①(NP-
|
|
1949
|
-
|
|
1994
|
+
/* ①(NP-4) 起播顺序:**先 muted 起播,拿到播放权之后再按设置应用 muted/volume**。
|
|
1995
|
+
旧写法在这里先 `applyVideoMute()` 再 `play()` ⇒ 那是一次"有声自动播放",
|
|
1996
|
+
没有用户手势时被浏览器直接拒(真机读数:NotAllowedError,见 npPrimePlay 的注释)
|
|
1997
|
+
⇒ 视频永远 paused、画面冻住、也没有声音。顺序是这条 bug 的全部根因。 */
|
|
1998
|
+
try { npPrimePlay(video); } catch {}
|
|
1950
1999
|
edgeDrawFrame();
|
|
1951
|
-
// ①(修正) 暂停门控:Edge canvas 渲染路径也尊重暂停,避免其余路径重播
|
|
1952
|
-
try { if (!(wallUserPaused || powPaused)) { const p = video.play(); if (p && p.catch) p.catch(() => {}); } } catch {}
|
|
1953
2000
|
// ①(修正) 首帧前先画一帧(避免白屏)——用命名函数 + removeEventListener 防泄漏
|
|
1954
2001
|
//(原来 {once:true} 在视频永不加载时会残留监听,评审指出)
|
|
1955
2002
|
const drawOnce = () => { try { video.removeEventListener("loadeddata", drawOnce); } catch {} edgeDrawFrame(); };
|
|
@@ -2008,8 +2055,9 @@ function sectionSigNow() {
|
|
|
2008
2055
|
img.style.display = "";
|
|
2009
2056
|
}
|
|
2010
2057
|
function showVideoEl(url) {
|
|
2058
|
+
npOpTrace("showVideoEl", null, String(url || "").slice(0, 48));
|
|
2011
2059
|
const { img, video, frame, canvas, wrap } = bgElements();
|
|
2012
|
-
if (!img || !video || !wrap) return;
|
|
2060
|
+
if (!img || !video || !wrap) { npOpTrace("showVideoEl", null, "no-elements"); return; }
|
|
2013
2061
|
// ①(加固) CSS 类切换:视频壁纸显示 video、隐藏 img
|
|
2014
2062
|
stopSceneAnim();
|
|
2015
2063
|
disarmSceneWatchdog(true); // ①(批次15 B1) 切到视频壁纸:解除场景看门狗并清兜底视觉
|
|
@@ -2032,21 +2080,19 @@ function sectionSigNow() {
|
|
|
2032
2080
|
// video.src → 浏览器重载媒体源 → autoplay 自动播放,绕过下方 play() 暂停门控
|
|
2033
2081
|
// (用户实测:暂停后调静音壁纸又重播、暂停按钮与实际状态脱节)。
|
|
2034
2082
|
// getAttribute('src') 返回原始的相对字符串,同 url 相等 → 不重载,彻底避免。
|
|
2035
|
-
try { if (video.getAttribute('src') !== url) video.src = url; } catch {}
|
|
2083
|
+
try { if (video.getAttribute('src') !== url) { video.src = url; npSetUserPaused(video, false) } } catch {}
|
|
2036
2084
|
// ⑳(新) 视频倍速:原生 playbackRate(0.5-2x,即时生效,不重载)
|
|
2037
2085
|
try {
|
|
2038
2086
|
const rate = readSection().playbackRate;
|
|
2039
2087
|
if (typeof rate === "number" && rate >= 0.5 && rate <= 2 && video.playbackRate !== rate) video.playbackRate = rate;
|
|
2040
2088
|
} catch {}
|
|
2041
|
-
/* ①(NP-3) 静音设置项 → 视频元素(此前只有建 DOM 时的硬编码 true,再没人改过它)。 */
|
|
2042
|
-
try { applyVideoMute(); } catch {}
|
|
2043
2089
|
video.style.display = "";
|
|
2044
2090
|
// ①(修正) 暂停状态门控:用户手动暂停(wallUserPaused)或省电暂停(powPaused)
|
|
2045
2091
|
// 时不 play——否则任何设置开关 apply(commit→applyFromStorage→showVideoEl)都
|
|
2046
2092
|
// 无条件 play(),把用户手动暂停解开(用户实测"暂停后开启效果又解开暂停")。
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
}
|
|
2093
|
+
// ①(NP-4) 门控**在 npPrimePlay 里**(它自己判 wallUserPaused/powPaused),
|
|
2094
|
+
// 并且把"静音/电平落点"挪到**起播成功之后**(顺序根因见 npPrimePlay 注释)。
|
|
2095
|
+
try { npPrimePlay(video); } catch {}
|
|
2050
2096
|
}
|
|
2051
2097
|
/**
|
|
2052
2098
|
* ①(修复 2026-09-12) 清理场景 iframe 上历史遗留的诊断开关。
|
|
@@ -2818,7 +2864,7 @@ function sectionSigNow() {
|
|
|
2818
2864
|
if (frame) frame.muted = mute;
|
|
2819
2865
|
// ①(2026-09-16 I 项) shim 通道:帧内静音由 shim 执行(不透明源读不到帧内元素)
|
|
2820
2866
|
if (frame && mpwWebShimUrl(frame.getAttribute("src"))) {
|
|
2821
|
-
webShimCall(frame, { op: "policy", muted: mute, speed:
|
|
2867
|
+
webShimCall(frame, { op: "policy", muted: mute, speed: npPolicySpeed(), volume: npWantVolume() });
|
|
2822
2868
|
}
|
|
2823
2869
|
const doc = frame && frame.contentDocument;
|
|
2824
2870
|
if (doc) {
|
|
@@ -2838,11 +2884,278 @@ function sectionSigNow() {
|
|
|
2838
2884
|
if (!video) return false;
|
|
2839
2885
|
try {
|
|
2840
2886
|
const s = readSection();
|
|
2841
|
-
const want = (s.mute !== void 0 ? !!s.mute : true) || npAudioOwns();
|
|
2887
|
+
const want = (npPrimedMuted || npSoundBlocked) ? true : ((s.mute !== void 0 ? !!s.mute : true) || npAudioOwns());
|
|
2842
2888
|
if (video.muted !== want) video.muted = want;
|
|
2843
2889
|
} catch {}
|
|
2844
2890
|
return true;
|
|
2845
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
|
+
}
|
|
2846
3159
|
/** ⑳(新) 给 web 壁纸 iframe 内的 <video> 应用倍速(含加载完成后补一次)。 */
|
|
2847
3160
|
function applyWebSpeed(frame) {
|
|
2848
3161
|
try {
|
|
@@ -2850,7 +3163,7 @@ function sectionSigNow() {
|
|
|
2850
3163
|
const rate = s.playbackRate;
|
|
2851
3164
|
if (typeof rate !== "number" || rate < 0.5 || rate > 2) return;
|
|
2852
3165
|
if (frame && mpwWebShimUrl(frame.getAttribute("src"))) {
|
|
2853
|
-
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() });
|
|
2854
3167
|
}
|
|
2855
3168
|
const doc = frame && frame.contentDocument;
|
|
2856
3169
|
if (!doc) return;
|
|
@@ -3311,7 +3624,7 @@ function sectionSigNow() {
|
|
|
3311
3624
|
function webShimPushPolicy(frame) {
|
|
3312
3625
|
try {
|
|
3313
3626
|
const s = readSection();
|
|
3314
|
-
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() });
|
|
3315
3628
|
webShimCall(frame, { op: "pause", value: !!wallUserPaused });
|
|
3316
3629
|
} catch {}
|
|
3317
3630
|
}
|
|
@@ -5718,15 +6031,39 @@ function sectionSigNow() {
|
|
|
5718
6031
|
}
|
|
5719
6032
|
return scope.ltoken ? (HOST_BASE + "/library-scene-audio?refs=0<oken=" + encodeURIComponent(scope.ltoken)) : "";
|
|
5720
6033
|
}
|
|
5721
|
-
/** 单个音轨的字节 URL
|
|
5722
|
-
|
|
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) {
|
|
5723
6051
|
if (!scope || !trackPath) return "";
|
|
5724
|
-
|
|
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] : "";
|
|
5725
6055
|
if (!file) return "";
|
|
6056
|
+
const enc = segs.map(encodeURIComponent).join("/");
|
|
5726
6057
|
if (scope.mode === "custom") {
|
|
5727
|
-
|
|
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;
|
|
6062
|
+
}
|
|
6063
|
+
if (segs.length === 1) {
|
|
6064
|
+
return HOST_BASE + "/raw?ltoken=" + encodeURIComponent(scope.ltoken) + "&file=" + encodeURIComponent(file);
|
|
5728
6065
|
}
|
|
5729
|
-
return HOST_BASE + "/
|
|
6066
|
+
return HOST_BASE + "/library-web/" + encodeURIComponent(scope.ltoken) + "/" + enc;
|
|
5730
6067
|
}
|
|
5731
6068
|
|
|
5732
6069
|
/**
|
|
@@ -5772,13 +6109,13 @@ function sectionSigNow() {
|
|
|
5772
6109
|
const key = npScopeKey(scope);
|
|
5773
6110
|
const cached = npMediaCache.data;
|
|
5774
6111
|
if (!cached || npMediaCache.key !== key || !cached.tracks || !cached.tracks.length) return null;
|
|
5775
|
-
return { scope: scope, key: key, tracks: cached.tracks };
|
|
6112
|
+
return { scope: scope, key: key, tracks: cached.tracks, source: String(cached.source || "") };
|
|
5776
6113
|
}
|
|
5777
6114
|
function npCurrentTrack(section) {
|
|
5778
6115
|
const list = npTrackList(section);
|
|
5779
6116
|
if (!list) return null;
|
|
5780
6117
|
const i = Math.max(0, Math.min(list.tracks.length - 1, npTrackIdx));
|
|
5781
|
-
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] };
|
|
5782
6119
|
}
|
|
5783
6120
|
function npAudioOwns() {
|
|
5784
6121
|
/* ①(NP-3) 我们的播放器在放音时,web 壁纸帧内的那份声音要让位(同一首会被放两遍)。 */
|
|
@@ -5797,10 +6134,14 @@ function sectionSigNow() {
|
|
|
5797
6134
|
function npResolveMedia(section) {
|
|
5798
6135
|
const s = section || {};
|
|
5799
6136
|
const id = npWallpaperIdentity(s);
|
|
6137
|
+
const link = npLinkOn(s);
|
|
6138
|
+
const vol = npWantVolume();
|
|
5800
6139
|
const out = {
|
|
5801
6140
|
kind: "none", title: "", byline: id.name, cover: "",
|
|
5802
|
-
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,
|
|
5803
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,
|
|
5804
6145
|
};
|
|
5805
6146
|
// 封面:当前壁纸的缩略图(真实数据;拿不到就留空 ⇒ 走渐变兜底,与上游一致)
|
|
5806
6147
|
if (s.image && /^(data:|blob:|idb:|https?:|\/)/.test(String(s.image))) out.cover = String(s.image);
|
|
@@ -5812,15 +6153,25 @@ function sectionSigNow() {
|
|
|
5812
6153
|
out.title = id.name || "Video wallpaper";
|
|
5813
6154
|
out.playing = !vid.paused;
|
|
5814
6155
|
out.muted = !!vid.muted;
|
|
5815
|
-
out.volume =
|
|
6156
|
+
out.volume = vol;
|
|
5816
6157
|
if (isFinite(vid.duration) && vid.duration > 0) out.total = Math.round(vid.duration);
|
|
5817
6158
|
const hasAudio = npVideoAudio(vid);
|
|
5818
6159
|
out.hasAudio = hasAudio;
|
|
5819
|
-
|
|
6160
|
+
/* ①(NP-4)② 联动关 ⇒ 播放/暂停**不许**碰壁纸媒体。视频档的声源就是这个视频本身
|
|
6161
|
+
⇒ 关掉联动之后没有任何可控声源:如实禁用(不假装按得动),并说清是哪一条挡住的。 */
|
|
6162
|
+
out.canPlay = link;
|
|
5820
6163
|
/* 明确知道"没有音轨"时静音键禁用(如实显示无音轨);不知道就给用户按(可能真是静音了) */
|
|
5821
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;
|
|
5822
6172
|
out.byline = hasAudio === false ? mpwT("np.noAudio")
|
|
5823
|
-
: (
|
|
6173
|
+
: (id.name + " · " + mpwT("np.src.video") + " · 1/1");
|
|
6174
|
+
if (!link) out.byline = mpwT("np.note.linkOff");
|
|
5824
6175
|
out.canPrev = false; out.canNext = false; /* 单个媒体,没有曲目清单 */
|
|
5825
6176
|
return out;
|
|
5826
6177
|
}
|
|
@@ -5830,14 +6181,22 @@ function sectionSigNow() {
|
|
|
5830
6181
|
const n = cur.tracks.length;
|
|
5831
6182
|
const mime = cur.track && cur.track.mime ? String(cur.track.mime) : "audio";
|
|
5832
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";
|
|
5833
6188
|
out.kind = "track";
|
|
5834
6189
|
out.source = "tracks";
|
|
5835
6190
|
out.title = fname;
|
|
5836
|
-
out.
|
|
5837
|
-
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;
|
|
5838
6197
|
out.canVolume = true;
|
|
5839
|
-
out.canPrev = n > 1;
|
|
5840
|
-
out.canNext = n > 1;
|
|
6198
|
+
out.canPrev = n > 1 && !listOnly; /* 只有一条 ⇒ 禁用(不假装有第二首) */
|
|
6199
|
+
out.canNext = n > 1 && !listOnly;
|
|
5841
6200
|
out.hasAudio = true;
|
|
5842
6201
|
if (npAudio) {
|
|
5843
6202
|
out.playing = !npAudio.paused;
|
|
@@ -5845,6 +6204,8 @@ function sectionSigNow() {
|
|
|
5845
6204
|
if (isFinite(npAudio.duration) && npAudio.duration > 0) out.total = Math.round(npAudio.duration);
|
|
5846
6205
|
else if (npAudio.readyState === 0) out.total = null;
|
|
5847
6206
|
}
|
|
6207
|
+
/* 我们自己的播放器一直归控件管(它就是"声音"本身)⇒ 与联动开关无关。 */
|
|
6208
|
+
out.canSeek = typeof out.total === "number" && out.total > 0;
|
|
5848
6209
|
return out;
|
|
5849
6210
|
}
|
|
5850
6211
|
// ③ web 壁纸(目录里没有独立音频文件):帧内那份声音只有静音这条真通道
|
|
@@ -5880,14 +6241,14 @@ function sectionSigNow() {
|
|
|
5880
6241
|
fetch(url).then((r) => (r && r.ok ? r.json() : null)).then((d) => {
|
|
5881
6242
|
if (seq !== npMediaToken) return; // 迟到的应答丢掉(壁纸已经换了)
|
|
5882
6243
|
const tracks = (d && d.tracks) || [];
|
|
5883
|
-
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 || "") } };
|
|
5884
6245
|
if (npTrackIdx >= tracks.length) npTrackIdx = 0;
|
|
5885
6246
|
/* 壁纸换了 ⇒ 我们自己的播放器必须跟着换源(否则会放着上一张壁纸的歌) */
|
|
5886
6247
|
try { if (npAudio) { npAudio.pause(); npAudio.removeAttribute("src"); } } catch (e) {}
|
|
5887
6248
|
applyNowPlaying(readSection());
|
|
5888
6249
|
}).catch(() => {
|
|
5889
6250
|
if (seq !== npMediaToken) return;
|
|
5890
|
-
npMediaCache = { key: key, at: Date.now(), data: { count: 0, tracks: [] } };
|
|
6251
|
+
npMediaCache = { key: key, at: Date.now(), data: { count: 0, tracks: [], source: "" } };
|
|
5891
6252
|
});
|
|
5892
6253
|
} catch (e) {}
|
|
5893
6254
|
}
|
|
@@ -5933,6 +6294,74 @@ function sectionSigNow() {
|
|
|
5933
6294
|
for (const ev of ["loadedmetadata", "durationchange", "play", "pause", "volumechange", "timeupdate"]) {
|
|
5934
6295
|
try { video.addEventListener(ev, sync) } catch (e) {}
|
|
5935
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) {}
|
|
5936
6365
|
sync();
|
|
5937
6366
|
return true;
|
|
5938
6367
|
} catch (e) { return false }
|
|
@@ -5962,7 +6391,7 @@ function sectionSigNow() {
|
|
|
5962
6391
|
let cur = null;
|
|
5963
6392
|
try { cur = npCurrentTrack(readSection()) } catch (e) { cur = null }
|
|
5964
6393
|
if (!cur || !npAudio) return;
|
|
5965
|
-
const url = npTrackUrl(cur.scope, cur.track && cur.track.path);
|
|
6394
|
+
const url = npTrackUrl(cur.scope, cur.track && cur.track.path, cur.source);
|
|
5966
6395
|
if (!url || npAudioBlobTried === url) {
|
|
5967
6396
|
try { console.warn("[dsh-mpkg-wallpaper] np: 音轨加载失败(宿主 /raw 或解码不支持): " + url) } catch (e) {}
|
|
5968
6397
|
try { if (npCtl) { npCtl.setMedia({ playing: false, canPlay: false, byline: mpwT("np.note.decode") }); npCtl.setProgress(0); } } catch (e) {}
|
|
@@ -6001,13 +6430,14 @@ function sectionSigNow() {
|
|
|
6001
6430
|
if (!cur) return false;
|
|
6002
6431
|
const a = npEnsureAudio();
|
|
6003
6432
|
if (!a) return false;
|
|
6004
|
-
const url = npTrackUrl(cur.scope, cur.track && cur.track.path);
|
|
6433
|
+
const url = npTrackUrl(cur.scope, cur.track && cur.track.path, cur.source);
|
|
6005
6434
|
if (!url) return false;
|
|
6006
6435
|
npAudioBlobTried = "";
|
|
6007
6436
|
try {
|
|
6008
6437
|
if (String(a.getAttribute("src") || "") !== url) a.setAttribute("src", url);
|
|
6009
6438
|
const st = readSection();
|
|
6010
6439
|
a.muted = !!(st.mute !== void 0 ? !!st.mute : true);
|
|
6440
|
+
if (npVolumeSet()) a.volume = npWantVolume(); /* ①(NP-4)① 用户设过电平才写(默认不碰) */
|
|
6011
6441
|
if (play) { const p = a.play(); if (p && p.catch) p.catch(() => {}); }
|
|
6012
6442
|
} catch (e) { mpwErr("npLoadTrack", e) }
|
|
6013
6443
|
npApplyFrameMute();
|
|
@@ -6020,7 +6450,7 @@ function sectionSigNow() {
|
|
|
6020
6450
|
function npApplyMute() {
|
|
6021
6451
|
const s = readSection();
|
|
6022
6452
|
const mute = s.mute !== void 0 ? !!s.mute : true;
|
|
6023
|
-
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) {}
|
|
6024
6454
|
try { if (npAudio && npAudio.muted !== mute) npAudio.muted = mute } catch (e) {}
|
|
6025
6455
|
try { applyVideoMute(); } catch (e) {}
|
|
6026
6456
|
npApplyFrameMute();
|
|
@@ -6038,47 +6468,86 @@ function sectionSigNow() {
|
|
|
6038
6468
|
* prev/next 清单里的上一条/下一条(环形)
|
|
6039
6469
|
* mute 既有设置项 `mute` + **真的落到元素**(video/audio/帧内 + 宿主 /media-audio 上报)
|
|
6040
6470
|
*/
|
|
6041
|
-
function npTransport(op, section) {
|
|
6471
|
+
function npTransport(op, section, value) {
|
|
6042
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");
|
|
6043
6479
|
try {
|
|
6044
6480
|
if (op === "play" || op === "pause") {
|
|
6045
6481
|
const vid = npActiveVideo(s);
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
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();
|
|
6049
6487
|
} else {
|
|
6050
6488
|
const list = npTrackList(s);
|
|
6051
6489
|
if (list && list.tracks.length) {
|
|
6052
6490
|
if (op === "play") {
|
|
6053
6491
|
const a = npEnsureAudio();
|
|
6054
6492
|
/* 还没装源(清单刚到/换过壁纸)⇒ 先装当前曲目再放 */
|
|
6055
|
-
if (a && !String(a.getAttribute("src") || "")) npLoadTrack(true);
|
|
6056
|
-
else if (a) { const p = a.play(); if (p && p.catch) p.catch(() => {}); }
|
|
6057
|
-
} else if (npAudio) npAudio.pause();
|
|
6058
|
-
} 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) {
|
|
6059
6497
|
/* 清单还没到:拉一次,到位后由 applyNowPlaying 把键的状态刷新(不假装按过了) */
|
|
6060
6498
|
npFetchTracks(s);
|
|
6499
|
+
did = "fetch-tracks";
|
|
6500
|
+
} else {
|
|
6501
|
+
/* ①(NP-4) 有壁纸媒体但**联动关** ⇒ 这里就是"一个字节都不碰"的那条路。 */
|
|
6502
|
+
did = "skip(link-off)";
|
|
6061
6503
|
}
|
|
6062
6504
|
/* ①(NP-3) 播放状态变了 ⇒ 帧内静音策略要重算(我们在放音时帧内强制静音) */
|
|
6063
6505
|
npApplyFrameMute();
|
|
6064
6506
|
}
|
|
6065
6507
|
} else if (op === "next" || op === "prev") {
|
|
6066
|
-
npStepTrack(op === "next" ? 1 : -1, true);
|
|
6508
|
+
did = npStepTrack(op === "next" ? 1 : -1, true) ? "step" : "step(nolist)";
|
|
6067
6509
|
} else if (op === "restart") {
|
|
6068
6510
|
/* ①(NP-3) 保留 op(兼容旧调用/排障),但 UI 里的"回到开头"已换成上一首/下一首。 */
|
|
6069
6511
|
const vid = npActiveVideo(s);
|
|
6070
6512
|
if (vid) { try { vid.currentTime = 0 } catch (e) {} }
|
|
6071
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);
|
|
6072
6538
|
} else if (op === "mute") {
|
|
6073
6539
|
// ①(NP-1) 静音走**既有**设置项(mute):这样视频/web 两条路径都由既有接线负责,
|
|
6074
6540
|
// 不在这里另写一套;同时把状态如实报到宿主 media-audio 契约(WP-1 新增)。
|
|
6075
6541
|
// ①(NP-3) 关键补齐:写完设置**必须真的落到元素**(下面这行 npApplyMute())。
|
|
6542
|
+
// ①(NP-4) 静音是**壁纸级**设置(设置面板里就有这一行),不受联动开关管:
|
|
6543
|
+
// 关掉联动只影响"传输键要不要驱动壁纸的播放/进度",不改变"静音"这条既有语义。
|
|
6076
6544
|
const next = !(s.mute !== void 0 ? !!s.mute : true);
|
|
6077
6545
|
saveSection({ mute: next });
|
|
6078
6546
|
npApplyMute();
|
|
6079
|
-
|
|
6547
|
+
did = "mute=" + next;
|
|
6080
6548
|
}
|
|
6081
|
-
} catch (e) { mpwErr("npTransport", e); }
|
|
6549
|
+
} catch (e) { mpwErr("npTransport", e); did = "error:" + String(e && e.message || e).slice(0, 60); }
|
|
6550
|
+
trace();
|
|
6082
6551
|
setTimeout(() => { try { applyNowPlaying(readSection()); } catch {} }, 60);
|
|
6083
6552
|
}
|
|
6084
6553
|
|
|
@@ -6097,7 +6566,7 @@ function sectionSigNow() {
|
|
|
6097
6566
|
t: (k) => { try { return mpwT(k); } catch (e) { return k; } },
|
|
6098
6567
|
doc: document, win: window,
|
|
6099
6568
|
log: console,
|
|
6100
|
-
onTransport: (op) => npTransport(op, readSection()),
|
|
6569
|
+
onTransport: (op, value) => npTransport(op, readSection(), value),
|
|
6101
6570
|
});
|
|
6102
6571
|
npCtl = mod.createController();
|
|
6103
6572
|
npSlotAction = mod.createSlotAction(npCtl);
|
|
@@ -6144,8 +6613,24 @@ function sectionSigNow() {
|
|
|
6144
6613
|
return;
|
|
6145
6614
|
}
|
|
6146
6615
|
ctl.setEnabled(true);
|
|
6147
|
-
/* ①(NP-
|
|
6148
|
-
|
|
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) {}
|
|
6149
6634
|
ctl.setMedia(npResolveMedia(s));
|
|
6150
6635
|
/* ①(NP-3) 清单/播放器:有目录作用域才拉清单;清单到了之后把当前曲目装进播放器
|
|
6151
6636
|
(**只装源、不自动播放** —— 浏览器自动播放策略 + "不替用户按播放"两条都要求这样)。 */
|
|
@@ -6886,6 +7371,10 @@ function sectionSigNow() {
|
|
|
6886
7371
|
legacy: () => mpwPersistLegacy(),
|
|
6887
7372
|
state: () => Object.assign({}, mpwPersistState),
|
|
6888
7373
|
apply: () => { try { applyFromStorage() } catch (e) { return String((e && e.message) || e) } return "ok" },
|
|
7374
|
+
/* ①(2026-09-19) "部分档写入"的真实入口:`writeSection` 拿到**没带源字段**的 next 时,
|
|
7375
|
+
粘性护栏必须把源保住(真机上 `initHostSettings` 的宿主合成结果、`mpwMarkPersistFail`
|
|
7376
|
+
的留痕写都会经过这条路)。暴露它才能把这条护栏变成断言而不是"看代码觉得对"。 */
|
|
7377
|
+
writePartial: (next) => { try { writeSection(next || {}, true); return true } catch (e) { return String(e && e.message || e) } },
|
|
6889
7378
|
onMsg: (fn) => mpwPersistOnMsg(fn),
|
|
6890
7379
|
takePending: () => mpwPersistTakePending(),
|
|
6891
7380
|
failKey: MPW_PERSIST_FAIL_KEY,
|
|
@@ -6959,11 +7448,11 @@ function sectionSigNow() {
|
|
|
6959
7448
|
globalThis.__mpwNpTest = {
|
|
6960
7449
|
scope: (section) => { try { return npAudioScope(section || readSection()) } catch (e) { return { err: String(e && e.message || e) } } },
|
|
6961
7450
|
scanUrl: (section) => { try { return npScanUrl(npAudioScope(section || readSection())) } catch (e) { return "ERR " + (e && e.message || e) } },
|
|
6962
|
-
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) } },
|
|
6963
7452
|
activeVideo: (section) => { const v = npActiveVideo(section || readSection()); return v ? true : false },
|
|
6964
7453
|
videoAudio: (section) => npVideoAudio(npActiveVideo(section || readSection())),
|
|
6965
7454
|
resolve: (section) => { try { return npResolveMedia(section || readSection()) } catch (e) { return { err: String(e && e.message || e) } } },
|
|
6966
|
-
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) } },
|
|
6967
7456
|
audio: () => npAudio,
|
|
6968
7457
|
idx: () => npTrackIdx,
|
|
6969
7458
|
load: (i) => { try { if (typeof i === "number") npTrackIdx = Math.max(0, i); return npLoadTrack(false) } catch (e) { return String(e && e.message || e) } },
|
|
@@ -6971,10 +7460,12 @@ function sectionSigNow() {
|
|
|
6971
7460
|
ownsSound: () => npAudioOwns(),
|
|
6972
7461
|
/* 夹具:把"宿主扫描路由的应答"直接塞进缓存(等价于 npFetchTracks 的成功回调)。
|
|
6973
7462
|
入参是 **section**(不是 scope):作用域由 npAudioScope 自己算 —— 与生产同一条口径。 */
|
|
6974
|
-
seedTracks: (section, tracks) => {
|
|
7463
|
+
seedTracks: (section, tracks, source) => {
|
|
6975
7464
|
try {
|
|
6976
7465
|
const s = npAudioScope(section || readSection());
|
|
6977
|
-
|
|
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 || "") } };
|
|
6978
7469
|
npTrackIdx = 0;
|
|
6979
7470
|
return npMediaCache.key;
|
|
6980
7471
|
} catch (e) { return String(e && e.message || e) }
|
|
@@ -6983,6 +7474,21 @@ function sectionSigNow() {
|
|
|
6983
7474
|
/* 走**真 apply 路径**(applyNowPlaying 本身;测试/真机排障都用它手动重放一次) */
|
|
6984
7475
|
apply: (section) => { try { applyNowPlaying(section || readSection()); return true } catch (e) { return String(e && e.message || e) } },
|
|
6985
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,
|
|
6986
7492
|
};
|
|
6987
7493
|
globalThis.__mpwSectionTest = {
|
|
6988
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) } } },
|
|
@@ -7771,7 +8277,7 @@ module.exports = {
|
|
|
7771
8277
|
组件本体(几何/形变/那套"一个数就是全部状态"的设计)来自 **Bencho** 的 "Now playing"
|
|
7772
8278
|
(上游文件 `Sound.tsx` + 随附样式表),上游作者 Bencho,许可 **MIT**(bencho.dev/licence)。
|
|
7773
8279
|
移植方式:**逐行移植、注释原文保留**(用户原话:"它们解释了这些数字为什么是这个值,
|
|
7774
|
-
也是这份代码值得照抄而不是重写的主要原因")。归属见 `THIRD-PARTY.md` §13
|
|
8280
|
+
也是这份代码值得照抄而不是重写的主要原因")。归属见 `THIRD-PARTY.md` §6(①2026-09-19 复核更正:原先指向 §13,该节实际是别的条目);
|
|
7775
8281
|
跨仓台账见 `../docs/COPYING-RULES.md` §4。
|
|
7776
8282
|
⚠ **本文件不含任何 GPL 代码**:`we-scene-demo/demo/now-playing/**`(GPL-3.0-or-later)
|
|
7777
8283
|
只被**读来核对数学**,一行都没有拷进来(`../docs/COPYING-RULES.md` §2.2 "GPL 永不进插件")。
|
|
@@ -7868,6 +8374,10 @@ const NP_ROOT_COLLAPSED_TOKEN = "collapsed";
|
|
|
7868
8374
|
这一层是**降级链**而不是两套实现:容器只有一个,位置只有一个来源,谁先可用就用谁。 */
|
|
7869
8375
|
const NP_SLOT_ATTR = "data-mpw-np-slot";
|
|
7870
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";
|
|
7871
8381
|
/* ── ①(NP-3) 让位:宿主同一位置已经有别的插件注入的元素时不挂/撤下 ─────────────
|
|
7872
8382
|
值就是原因串(可读、可查询):`foreign-occupant`(容器里出现了非本插件、非宿主自有的元素)。
|
|
7873
8383
|
写在我们自己的 slot div 上(宿主 slot 出口是我们唯一"永远属于自己"的节点,即使没挂载也在),
|
|
@@ -7915,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
|
|
|
7915
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";
|
|
7916
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";
|
|
7917
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
|
+
|
|
7918
8441
|
/* ── 样式:全部落在 [data-mpw-now-playing] / .mpw_np* -------------------------
|
|
7919
8442
|
上游那张样式表的原文注释保留在对应规则上。这里**故意不出现** sidebarCol / footArea /
|
|
7920
8443
|
settingsArea / .hHd-Xa_* 任何一个名字:style-scope-guard 的 SURFACES 里"侧栏"的
|
|
@@ -8191,8 +8714,133 @@ body[data-ds-dark-theme] [data-mpw-now-playing] {
|
|
|
8191
8714
|
.mpw_np_op, .mpw_np_art, .mpw_np_like { transition-duration: 1ms; }
|
|
8192
8715
|
.mpw_np_like.mpw_np_on .mpw_np_ic { animation: none; }
|
|
8193
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
|
+
}
|
|
8194
8766
|
`;
|
|
8195
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
|
+
|
|
8196
8844
|
/* ══════════════════════════════════════════════════════════════════════════════
|
|
8197
8845
|
组件
|
|
8198
8846
|
══════════════════════════════════════════════════════════════════════════════ */
|
|
@@ -8375,6 +9023,10 @@ function createNowPlaying(deps) {
|
|
|
8375
9023
|
/* ①(NP-3) 第四个键(音量/静音)只在深半边存在(见文件头差异 3)。 */
|
|
8376
9024
|
const showVolume = late > 0;
|
|
8377
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;
|
|
8378
9030
|
|
|
8379
9031
|
const artStyle = {
|
|
8380
9032
|
left: M.artX(), top: M.artY(), width: art, height: art,
|
|
@@ -8410,8 +9062,60 @@ function createNowPlaying(deps) {
|
|
|
8410
9062
|
className: "mpw_np_run",
|
|
8411
9063
|
style: { width: (known ? M.runPct(at) : 0) + "%" },
|
|
8412
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
|
+
}),
|
|
8413
9096
|
h("span", { className: "mpw_np_clock", style: { opacity: late } },
|
|
8414
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,
|
|
8415
9119
|
/* 数据源没有时长时**不假装**:显示 --:--(见 docs/NOW-PLAYING-DSH.md §4 情形②) */
|
|
8416
9120
|
h("span", null, known ? ("−" + M.clock(M.remain(at))) : "--:--"))),
|
|
8417
9121
|
h("button", {
|
|
@@ -8468,6 +9172,8 @@ function createNowPlaying(deps) {
|
|
|
8468
9172
|
kind: "none", title: "", byline: "", cover: "",
|
|
8469
9173
|
total: null, playing: false, muted: true, volume: 1,
|
|
8470
9174
|
canPlay: false, canVolume: false, canPrev: false, canNext: false, hasAudio: null,
|
|
9175
|
+
/* ①(NP-4) 空闲态也把这三项写全:谁读都不该读到 undefined(空闲态 = 不能拖、联动开)。 */
|
|
9176
|
+
canSeek: false, link: true,
|
|
8471
9177
|
};
|
|
8472
9178
|
|
|
8473
9179
|
/** ①(NP-3) 纯判据:容器里有没有**非本插件**的占用者(让位逻辑的核心,可单独测)。
|
|
@@ -8555,8 +9261,15 @@ function createNowPlaying(deps) {
|
|
|
8555
9261
|
/* 宿主**自己的**"宽/收起"信号(来自 slot 的 ownerProps.wide)。null = 这版宿主没给。
|
|
8556
9262
|
①(NP-2) 它现在只在**量不到物理宽度**时参与判据(见 shouldHide 三档与 hostSaysCollapsed)。 */
|
|
8557
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;
|
|
8558
9271
|
const counts = { resize: 0, mutation: 0, raf: 0 };
|
|
8559
|
-
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) };
|
|
8560
9273
|
|
|
8561
9274
|
function raf(fn) {
|
|
8562
9275
|
if (win && typeof win.requestAnimationFrame === "function") return win.requestAnimationFrame(fn);
|
|
@@ -8612,6 +9325,8 @@ function createNowPlaying(deps) {
|
|
|
8612
9325
|
root.render(h(NowPlaying, {
|
|
8613
9326
|
p: pCur, mark: mCur, open: state.open, playing: state.playing, at: state.at,
|
|
8614
9327
|
media: state.media, note: note,
|
|
9328
|
+
volume: state.volume, canSeek: state.canSeek, dragging: drag === "seek",
|
|
9329
|
+
link: state.link,
|
|
8615
9330
|
onToggle: () => {
|
|
8616
9331
|
state.open = !state.open;
|
|
8617
9332
|
tweenTo(state.open ? 1 : 0);
|
|
@@ -8633,9 +9348,99 @@ function createNowPlaying(deps) {
|
|
|
8633
9348
|
if (!state.media.canVolume) { refuse("np.note.novolume"); return; }
|
|
8634
9349
|
if (deps.onTransport) deps.onTransport("mute");
|
|
8635
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)),
|
|
8636
9390
|
}));
|
|
8637
9391
|
} catch (err) { log.error(TAG + "render failed:", err); }
|
|
8638
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
|
+
}
|
|
8639
9444
|
|
|
8640
9445
|
/** 做不到的事:**不假装**。console 一行 + 面板上一行(用户能看到),4 秒后自己收。 */
|
|
8641
9446
|
function refuse(key) {
|
|
@@ -8720,6 +9525,33 @@ function createNowPlaying(deps) {
|
|
|
8720
9525
|
const fromContainer = !!(container && isFinite(cw) && cw > 0 && cw !== measureWidth());
|
|
8721
9526
|
return fitScale(cw, fromContainer);
|
|
8722
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
|
+
}
|
|
8723
9555
|
/** 一帧判据:写/撤 data-mpw-np-hidden + **让位判据**(①(NP-3))。
|
|
8724
9556
|
* 让位在**每一次**判据里都跑:挂载前(setEnabled 里单独一次)、挂载后(这里)都要判。
|
|
8725
9557
|
* 规则:容器里出现非本插件、非宿主自有的占用者 ⇒ 撤下且**不重建**(等它走了再回来)。 */
|
|
@@ -8732,6 +9564,7 @@ function createNowPlaying(deps) {
|
|
|
8732
9564
|
return hidden;
|
|
8733
9565
|
}
|
|
8734
9566
|
if (occupantNow()) { retractForOccupant(); return false; }
|
|
9567
|
+
applyBleed(); /* ⑤ 先借宽,再量可用宽度 */
|
|
8735
9568
|
const next = shouldHide(measureWidth(), hostSaysCollapsed());
|
|
8736
9569
|
if (next !== hidden) {
|
|
8737
9570
|
hidden = next;
|
|
@@ -8879,6 +9712,8 @@ function createNowPlaying(deps) {
|
|
|
8879
9712
|
try {
|
|
8880
9713
|
container = doc.createElement("div");
|
|
8881
9714
|
container.setAttribute(NP_ATTR, "");
|
|
9715
|
+
/* ①(NP-4)② 联动开关的可查询落点(值由 setLink/applyNowPlaying 维护)。 */
|
|
9716
|
+
container.setAttribute(NP_LINK_ATTR, state.link === false ? "0" : "1");
|
|
8882
9717
|
insertInto(a, true);
|
|
8883
9718
|
enabled = true;
|
|
8884
9719
|
root = (typeof deps.createRoot === "function") ? deps.createRoot(container) : null;
|
|
@@ -8968,6 +9803,7 @@ function createNowPlaying(deps) {
|
|
|
8968
9803
|
markYield(null); /* ①(NP-3) 用户自己关了 ⇒ 让位原因一起清掉(别留下过期状态) */
|
|
8969
9804
|
enabled = false; hidden = false; anchorMode = "none"; note = "";
|
|
8970
9805
|
pCur = 0; mCur = 0; state.open = false; state.playing = false;
|
|
9806
|
+
drag = ""; bleedNow = 0; /* ①(NP-4)/⑤:拖动与借宽都是"挂着才有"的状态 */
|
|
8971
9807
|
return api;
|
|
8972
9808
|
}
|
|
8973
9809
|
if (enabled) { ensureAnchored(); evaluate(); return api; } /* 重复开 = 只重新锚定,不叠节点 */
|
|
@@ -8998,6 +9834,16 @@ function createNowPlaying(deps) {
|
|
|
8998
9834
|
if (!(typeof state.media.total === "number" && state.media.total > 0)) state.at = 0;
|
|
8999
9835
|
const playing = !!state.media.playing;
|
|
9000
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;
|
|
9001
9847
|
render();
|
|
9002
9848
|
return api;
|
|
9003
9849
|
},
|
|
@@ -9007,7 +9853,24 @@ function createNowPlaying(deps) {
|
|
|
9007
9853
|
render();
|
|
9008
9854
|
return api;
|
|
9009
9855
|
},
|
|
9010
|
-
|
|
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
|
+
},
|
|
9011
9874
|
/** 给测试/排障看的一眼状态(不参与渲染)。 */
|
|
9012
9875
|
inspect: () => ({
|
|
9013
9876
|
enabled, hidden, anchorMode, note,
|
|
@@ -9020,6 +9883,8 @@ function createNowPlaying(deps) {
|
|
|
9020
9883
|
nextSiblingClass: container && container.nextSibling ? String(container.nextSibling.className || "") : "",
|
|
9021
9884
|
observers: { resize: counts.resize, mutation: counts.mutation, rafStarts: counts.raf, rafLive: (rafId ? 1 : 0) + (markRafId ? 1 : 0), anchorWatch: anchorWatch || null },
|
|
9022
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,
|
|
9023
9888
|
}),
|
|
9024
9889
|
/** 宿主 slot 交来的容器(我们自己的 div);null = slot 卸载了 ⇒ 回到降级链。 */
|
|
9025
9890
|
setSlotNode(node) {
|
|
@@ -9037,7 +9902,7 @@ function createNowPlaying(deps) {
|
|
|
9037
9902
|
scheduleSettle();
|
|
9038
9903
|
return api;
|
|
9039
9904
|
},
|
|
9040
|
-
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 }),
|
|
9041
9906
|
evaluate, ensureAnchored,
|
|
9042
9907
|
destroy() { return setEnabled(false); },
|
|
9043
9908
|
};
|
|
@@ -9078,7 +9943,9 @@ function createNowPlaying(deps) {
|
|
|
9078
9943
|
return MpwNowPlayingAction;
|
|
9079
9944
|
}
|
|
9080
9945
|
|
|
9081
|
-
return {
|
|
9946
|
+
return {
|
|
9947
|
+
NowPlaying, PlayMark, createController, createSlotAction, shouldHide, resolveAnchor, sidebarRoot, sidebarCol, occupantOf,
|
|
9948
|
+
};
|
|
9082
9949
|
}
|
|
9083
9950
|
|
|
9084
9951
|
module.exports = {
|
|
@@ -9088,7 +9955,13 @@ module.exports = {
|
|
|
9088
9955
|
NP_NOTE_ATTR,
|
|
9089
9956
|
NP_YIELD_ATTR,
|
|
9090
9957
|
NP_YIELD_FOREIGN,
|
|
9958
|
+
NP_LINK_ATTR,
|
|
9091
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,
|
|
9092
9965
|
NP_CSS,
|
|
9093
9966
|
NP_SIDEBAR_ROOT_SEL,
|
|
9094
9967
|
NP_SIDEBAR_ROOT_ATTR,
|
|
@@ -13206,7 +14079,8 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
13206
14079
|
glassColor: "", glassAlpha: 12, accent: "", newStyle: false, mute: true,
|
|
13207
14080
|
thinkBg: DEFAULT_THINK_BG,
|
|
13208
14081
|
bsCompat: DEFAULT_BS_COMPAT, bsFloat: false, bsFont: false, bsReveal: false, bsAlpha: false, bsAqua: false, bsBottomAvoid: false,
|
|
13209
|
-
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) 两个新键一起复位(否则"恢复默认"留下旧电平)
|
|
13210
14084
|
}), true);
|
|
13211
14085
|
setSection(readSection());
|
|
13212
14086
|
applyFromStorage();
|
|
@@ -13226,7 +14100,8 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
13226
14100
|
"brightness", "float", "playbackRate", "newStyle", "mute", "thinkBg", "enabled",
|
|
13227
14101
|
"forceEnabled", "propEdits", "clock", "clock24h", "clockSec", "clockDate", "clockPos", "clockSize",
|
|
13228
14102
|
"fpsCap", "resMax", "bsCompat", "bsFloat", "bsFont", "bsReveal", "bsRevealAlpha", "bsAlpha", "bsAqua", "bsBottomAvoid",
|
|
13229
|
-
"npNowPlaying" // ①(NP-1) Now playing 挂载开关(可随备份导出/导入)
|
|
14103
|
+
"npNowPlaying", // ①(NP-1) Now playing 挂载开关(可随备份导出/导入)
|
|
14104
|
+
"npLinkWallpaper", "npVolume" // ①(NP-4)②① 联动开关 / 音量电平(同上,随备份走)
|
|
13230
14105
|
];
|
|
13231
14106
|
const exportBackup = () => {
|
|
13232
14107
|
try {
|
|
@@ -13263,8 +14138,8 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
13263
14138
|
const patch = {};
|
|
13264
14139
|
// ①(修正) 类型校验(评审指出:原实现直接透传,`opacity:"abc"` 会污染设置):
|
|
13265
14140
|
// 数值类必须为有限数、布尔类必须为 boolean、字符串类必须为 string。
|
|
13266
|
-
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)
|
|
13267
|
-
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
|
|
13268
14143
|
const strFields = ["aquaColor", "aquaInkColor", "themeColor", "accent", "glassColor", "clockPos", "fontColorGrayColor", "sceneDebugParams"]; // ①(批次15) sceneDebugParams 走字符串净化(非法键/值在写入端丢弃)
|
|
13269
14144
|
for (const k of BACKUP_FIELDS) {
|
|
13270
14145
|
const v = settings[k];
|
|
@@ -13843,6 +14718,8 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
13843
14718
|
]);
|
|
13844
14719
|
};
|
|
13845
14720
|
|
|
14721
|
+
/* ①(NP-4)② ② 那一行只在 NP 开着时渲染(渲染用派生量,与持久化默认值同源)。 */
|
|
14722
|
+
const npOnNow = section.npNowPlaying !== void 0 ? !!section.npNowPlaying : DEFAULT_NP_NOW_PLAYING;
|
|
13846
14723
|
// ⑳(新) 壁纸设置 tab:计算激活的帧率上限值(缺省=0 无限制)与 ffmpeg 状态文本(渲染用)
|
|
13847
14724
|
const fpsCapVal = section.fpsCap !== void 0 && section.fpsCap !== null ? section.fpsCap : 0;
|
|
13848
14725
|
const resMaxVal = section.resMax !== void 0 && section.resMax !== null ? section.resMax : DEFAULT_RES_MAX;
|
|
@@ -14362,6 +15239,12 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
14362
15239
|
// 默认关;关 = 一行 DOM 都不注入、一个观察者都不装(见 lib/now-playing.js)。
|
|
14363
15240
|
// 左侧栏收起时组件自己隐藏(判据/阈值见 NP_COLLAPSE_MAX_W 的注释)。
|
|
14364
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),
|
|
14365
15248
|
// ①(新) 壁纸镜像翻转(Wallpaper Engine 原生基础选项)
|
|
14366
15249
|
h("div", { className: "mpw_inline" }, [
|
|
14367
15250
|
toggleRow(t("flipX"), t("flipX.desc"), "flipX", false),
|
|
@@ -15410,7 +16293,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
15410
16293
|
"scnRender.report": "场景渲染上报(排障)",
|
|
15411
16294
|
"scnRender.report.desc": "开启后壁纸每 10 秒把渲染器现场(截图 + 逐层绘制台账 + 纹理统计)写到渲染器的 reports/ 目录,用于定位“坐标/可见性”问题。仅排障时开,默认关(有磁盘写入)",
|
|
15412
16295
|
"scnRender.extUrl.ph": "http://127.0.0.1:8899/ext",
|
|
15413
|
-
"scnRender.extUrl.hint": "留空=不启用。填写后按 extbase 拼进场景渲染 iframe
|
|
16296
|
+
"scnRender.extUrl.hint": "留空=不启用。填写后按 extbase 拼进场景渲染 iframe(见渲染器仓文档 EXTENSION-HOOKS.md);点「测试」验证索引可达并显示钩子数量。",
|
|
15414
16297
|
"scnRender.extUrl.save": "保存",
|
|
15415
16298
|
"scnRender.extUrl.saved": "扩展钩子地址已保存,下次应用场景壁纸生效",
|
|
15416
16299
|
"scnRender.extUrl.cleared": "已清除扩展钩子地址",
|
|
@@ -15446,7 +16329,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
15446
16329
|
"diag.fold": "收起开关",
|
|
15447
16330
|
"diag.copy": "复制",
|
|
15448
16331
|
"diag.copied": "已复制",
|
|
15449
|
-
"diag.more": "
|
|
16332
|
+
"diag.more": "全部开关见渲染器仓文档 README-DIAGNOSTICS.md(node diag-flag-check.mjs 校验代码与文档一致)。",
|
|
15450
16333
|
"diagsend.sec": "一键诊断上报",
|
|
15451
16334
|
"diagsend.desc": "把关键子系统状态(磨砂 / 侧栏 / 时间线是否被影响、壁纸类型与路径、shim 是否注入、视频解码、表面 token、场景健康)打包发回本机宿主,落在 ~/.dsh/.dsh-mpkg-wallpaper/diag-<时间戳>.json。每个字段都带来源;读不到的字段不会消失,而是标成 null + degraded 原因。宿主不可用时自动改成下载 JSON 文件(离线也能发回来)。",
|
|
15452
16335
|
"diagsend.btn": "一键上报",
|
|
@@ -15796,6 +16679,14 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
15796
16679
|
"np.note.nolist": "当前壁纸只有一条音轨,没有上一首/下一首(不假装有列表)",
|
|
15797
16680
|
"np.note.novolume": "当前壁纸没有可控制的音量通道:只有视频/网页壁纸的声音可控",
|
|
15798
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)",
|
|
15799
16690
|
};
|
|
15800
16691
|
const en = {
|
|
15801
16692
|
"nav": "MPKG Wallpaper",
|
|
@@ -16170,7 +17061,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
16170
17061
|
"scnRender.report": "Scene render reporting (debug)",
|
|
16171
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)",
|
|
16172
17063
|
"scnRender.extUrl.ph": "http://127.0.0.1:8899/ext",
|
|
16173
|
-
"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.",
|
|
16174
17065
|
"scnRender.extUrl.save": "Save",
|
|
16175
17066
|
"scnRender.extUrl.saved": "Extension hooks URL saved; takes effect the next time a scene wallpaper is applied",
|
|
16176
17067
|
"scnRender.extUrl.cleared": "Extension hooks URL cleared",
|
|
@@ -16206,7 +17097,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
16206
17097
|
"diag.fold": "Hide flags",
|
|
16207
17098
|
"diag.copy": "Copy",
|
|
16208
17099
|
"diag.copied": "Copied",
|
|
16209
|
-
"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).",
|
|
16210
17101
|
"diagsend.sec": "One-click diagnostics report",
|
|
16211
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).",
|
|
16212
17103
|
"diagsend.btn": "Send report",
|
|
@@ -16285,6 +17176,14 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
16285
17176
|
"np.note.nolist": "This wallpaper has a single audio track, so there is no previous/next (no fake playlist)",
|
|
16286
17177
|
"np.note.novolume": "This wallpaper has no controllable volume channel: only video/web wallpaper sound can be controlled",
|
|
16287
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)",
|
|
16288
17187
|
};
|
|
16289
17188
|
|
|
16290
17189
|
// ═══════════════════════════════════════════════════════════════════
|