dsh-mpkg-wallpaper 3.15.1 → 3.15.3
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 +350 -79
- package/lib/index.js +101 -26
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -338,7 +338,13 @@ function mpwTrace(ev, data) {
|
|
|
338
338
|
// ①(新 2026-09-11) DSH 0.1.5 自带右侧边栏/dock 浮层虚化(兼容桥纳入旧规则作用域)
|
|
339
339
|
const DEFAULT_RIGHT_SIDEBAR_BLUR = true; // 右侧边栏/dock 虚化开关
|
|
340
340
|
const DEFAULT_RIGHT_SIDEBAR_AMOUNT = 14; // 虚化程度 px
|
|
341
|
-
|
|
341
|
+
/* ①(2026-10-03 语义翻转后复核) 值仍是 45,但语义已经是**透明度**:
|
|
342
|
+
45 ⇒ 生效不透明度 55%。为什么不跟着映射改成 55:它是"从没设过右栏那条"的兜底,
|
|
343
|
+
而右栏默认在**跟随**档(`rightT = 侧栏透明度`),这个常量几乎只在"用户关掉跟随
|
|
344
|
+
且从没动过右栏滑条"时生效;改它会平移 `--mpw-rs-alpha` 的取值 ⇒
|
|
345
|
+
`token-namespace-test` 的"同设置逐键等价"判据(before↔after)会出现 259 组漂移。
|
|
346
|
+
存量用户**显式设过**的值由 `mpwAlphaFlipValue()` 按意图搬(45→55)✓,与本常量无关。 */
|
|
347
|
+
const DEFAULT_RIGHT_SIDEBAR_ALPHA = 45; // 表面透明度 %(0=全不透明 100=全透明)
|
|
342
348
|
const DEFAULT_SIDEBAR_BLUR_AMOUNT = 14; // 侧边栏磨砂程度
|
|
343
349
|
const DEFAULT_POPOVER_BLUR = true; // 弹层虚化(菜单/提示/遮罩)开关
|
|
344
350
|
const DEFAULT_POPOVER_AMOUNT = 10; // 弹层虚化程度
|
|
@@ -624,6 +630,8 @@ function mpwTrace(ev, data) {
|
|
|
624
630
|
// localStorage 持久化(⑤:读写带缓存 + 防抖,滑杆不卡顿)
|
|
625
631
|
// ═══════════════════════════════════════════════════════════════════
|
|
626
632
|
let sectionCache = null;
|
|
633
|
+
let sectionView = null; // 用户档视图(去元键/记账键);与 sectionCache 同源时才复用
|
|
634
|
+
let sectionViewSrc = null;
|
|
627
635
|
let persistTimer = null;
|
|
628
636
|
/* ①(2026-09-19 设置持久化轮) 落盘收尾闸门:boot 的第一次 applyFromStorage() 跑在宿主
|
|
629
637
|
`GET /settings` **之前**(旧写法会在那一刻把残缺档同时写进 localStorage + PUT 给宿主,
|
|
@@ -774,6 +782,22 @@ function sectionSigNow() {
|
|
|
774
782
|
} catch {}
|
|
775
783
|
return "";
|
|
776
784
|
}
|
|
785
|
+
/* ②(2026-10-03 真 bug:"左栏还是粉色,现在右栏也被注入了粉色")
|
|
786
|
+
3.15.0 把「界面透明度」/「表面透明度」两条滑条的**数值语义翻转**了:
|
|
787
|
+
旧:数值 = 白雾**厚度/不透明**程度(0 = 全透明、100 = 全不透明)
|
|
788
|
+
新:数值 = **透明度**(0 = 全不透明、100 = 全透明) —— 用户拍板的口径
|
|
789
|
+
默认值照映射搬了(新 65 ≡ 旧 35),但**存量档里的用户值没搬**:用户当年为了"求实心"
|
|
790
|
+
把旧滑条拖到 100(= 全不透明),翻转后同一个 100 变成"全透明" ⇒ 左栏整块透出壁纸,
|
|
791
|
+
跟随档下右栏也一起透 ⇒ 真机现象就是"被注入粉色"(探针三次读数都指向"那层就是壁纸本身",
|
|
792
|
+
因为确实没刷任何采样色 —— 病根在这个没搬的值,不在渲染)。
|
|
793
|
+
处置:一次性按 `新 = 100 - 旧` 搬值(观感与旧版逐字一致),写幂等标记 `alphaSemantics = 2`
|
|
794
|
+
(登记进 MPW_META_KEYS:只随档持久化,用户档/面板/导出/诊断看不到它)。 */
|
|
795
|
+
const MPW_ALPHA_SEM_KEY = "alphaSemantics";
|
|
796
|
+
const MPW_ALPHA_SEM = 2;
|
|
797
|
+
function mpwAlphaFlipValue(v) {
|
|
798
|
+
const n = Number(v);
|
|
799
|
+
return Number.isFinite(n) ? Math.max(0, Math.min(100, 100 - n)) : void 0;
|
|
800
|
+
}
|
|
777
801
|
function mpwNormalizeSection(s0) {
|
|
778
802
|
if (!s0 || typeof s0 !== "object") return {};
|
|
779
803
|
let need = false;
|
|
@@ -793,10 +817,22 @@ function sectionSigNow() {
|
|
|
793
817
|
而不是"值是不是 false";没标记(从没设过)才迁移,动过的用户一字不动。 */
|
|
794
818
|
const phMigrate = s0.powPauseHiddenUserSet === true ? null : DEFAULT_POW_PAUSE_HIDDEN;
|
|
795
819
|
const phNeeds = phMigrate !== null && s0.powPauseHidden !== phMigrate;
|
|
796
|
-
|
|
820
|
+
/* ②(2026-10-03) 透明度语义翻转的**搬值**(口径见上面 mpwAlphaFlipValue 的注释):
|
|
821
|
+
只搬"档里真的有这个字段"的项(从没设过 ⇒ 保持新默认,不动)。 */
|
|
822
|
+
const alphaFlip = s0[MPW_ALPHA_SEM_KEY] !== MPW_ALPHA_SEM;
|
|
823
|
+
const alphaSide = alphaFlip && s0.sidebarAlpha !== void 0 ? mpwAlphaFlipValue(s0.sidebarAlpha) : void 0;
|
|
824
|
+
const alphaRight = alphaFlip && s0.rightSidebarAlpha !== void 0 ? mpwAlphaFlipValue(s0.rightSidebarAlpha) : void 0;
|
|
825
|
+
if (!need && !bsNeeds && !phNeeds && !alphaFlip) return s0;
|
|
797
826
|
const o = Object.assign({}, s0);
|
|
798
827
|
if (bsNeeds) { o.bsCompat = bsMigrate; try { console.log("[dsh-mpkg-wallpaper] bsCompat 迁移:从没显式设过 ⇒ 采用新默认 " + bsMigrate + "(手动关过的用户不受影响)") } catch (e) {} }
|
|
799
828
|
if (phNeeds) { o.powPauseHidden = phMigrate; try { console.log("[dsh-mpkg-wallpaper] powPauseHidden 迁移:从没显式设过 ⇒ 采用新默认 " + phMigrate + "(切页静音是浏览器基本礼仪;手动关过的用户不受影响)") } catch (e) {} }
|
|
829
|
+
if (alphaFlip) {
|
|
830
|
+
o[MPW_ALPHA_SEM_KEY] = MPW_ALPHA_SEM;
|
|
831
|
+
const moved = [];
|
|
832
|
+
if (alphaSide !== void 0 && alphaSide !== s0.sidebarAlpha) { o.sidebarAlpha = alphaSide; moved.push("sidebarAlpha " + s0.sidebarAlpha + "→" + alphaSide) }
|
|
833
|
+
if (alphaRight !== void 0 && alphaRight !== s0.rightSidebarAlpha) { o.rightSidebarAlpha = alphaRight; moved.push("rightSidebarAlpha " + s0.rightSidebarAlpha + "→" + alphaRight) }
|
|
834
|
+
try { console.log("[dsh-mpkg-wallpaper] 透明度语义搬值(旧=厚度 → 新=透明度,只做一次):" + (moved.length ? moved.join("、") : "档内无这两个字段,保持新默认")) } catch (e) {}
|
|
835
|
+
}
|
|
800
836
|
const bad = [];
|
|
801
837
|
for (const k of MPW_STR_KEYS) if (o[k] !== void 0 && typeof o[k] !== "string") { bad.push(k + ":" + typeof o[k]); o[k] = mpwAsStr(o[k]) }
|
|
802
838
|
try { console.warn("[dsh-mpkg-wallpaper] 已归一化异常字符串字段:", bad.join(", ")); } catch {}
|
|
@@ -808,12 +844,18 @@ function sectionSigNow() {
|
|
|
808
844
|
// `__mpwLocalAt` 这类元键(时间戳/自愈标记),但"用户档"的读数口(面板/buildCss/
|
|
809
845
|
// 导出/字段集合比较/测试的逐字段 diff)必须看不到它们 —— 否则元键会被当成
|
|
810
846
|
// "用户的设置项"参与比较(真机上表现为"莫名多出一个键",测试里表现为假红)。
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
847
|
+
// ②(2026-10-03 透明度语义搬值) 记账键同理不可见,但它**必须留在 sectionCache 里**
|
|
848
|
+
// (落盘要写、下次开机要认)⇒ 视图单独缓存,**不再回写 sectionCache**。
|
|
849
|
+
// 老写法 `sectionCache = clean` 会把记账键从缓存里抹掉 ⇒ 落盘丢标记 ⇒ 每次开机重搬值
|
|
850
|
+
// (实测:搬值日志每开一次机打一次,值被来回翻)。
|
|
851
|
+
if (sectionViewSrc === sectionCache && sectionView) return sectionView;
|
|
852
|
+
sectionView = mpwStripBook(mpwStripMeta(sectionCache));
|
|
853
|
+
sectionViewSrc = sectionCache;
|
|
854
|
+
return sectionView;
|
|
814
855
|
}
|
|
815
856
|
sectionCache = mpwNormalizeSection(loadSection());
|
|
816
|
-
|
|
857
|
+
sectionView = null; sectionViewSrc = null;
|
|
858
|
+
return readSection();
|
|
817
859
|
}
|
|
818
860
|
/** 写入内存缓存 + 防抖落盘。instant=true 立即写(文件选择/复位)。
|
|
819
861
|
* ⑤(新) host 可用时同时 PUT 到宿主端文件(跨端口/清浏览器数据不丢)。
|
|
@@ -1261,6 +1303,10 @@ function sectionSigNow() {
|
|
|
1261
1303
|
const MPW_SRC_LOST_KEY = "__mpwSrcLost";
|
|
1262
1304
|
/** 元键:只管诊断/自愈记账,**永远不进 section**(面板/apply/CSS 都看不到它们)。 */
|
|
1263
1305
|
const MPW_META_KEYS = [MPW_STAMP_LOCAL, MPW_STAMP_HOST, MPW_SRC_LOST_KEY];
|
|
1306
|
+
/* ②(2026-10-03 透明度语义搬值) **记账键**:与元键的区别是它必须活到 mpwNormalizeSection 那一趟
|
|
1307
|
+
(loadSection 只去元键),否则每次开机都重搬一次值;但用户档视图(面板/导出/CSS)同样不许看到它
|
|
1308
|
+
⇒ 只多这一层 `mpwStripBook`(见 readSection)。 */
|
|
1309
|
+
const MPW_BOOK_KEYS = [MPW_ALPHA_SEM_KEY];
|
|
1264
1310
|
/** 壁纸「源」字段:buildCss 的 `hasImage = image || webUrl` 唯一依据这三个。 */
|
|
1265
1311
|
const MPW_SRC_KEYS = ["image", "webUrl", "sceneKey"];
|
|
1266
1312
|
|
|
@@ -1285,6 +1331,18 @@ function sectionSigNow() {
|
|
|
1285
1331
|
return o;
|
|
1286
1332
|
} catch { return s0 }
|
|
1287
1333
|
}
|
|
1334
|
+
/** 去掉**记账键**(用户档视图专用;落盘/归一化那条路不经过它)。 */
|
|
1335
|
+
function mpwStripBook(s0) {
|
|
1336
|
+
if (!s0 || typeof s0 !== "object") return {};
|
|
1337
|
+
try {
|
|
1338
|
+
let need = false;
|
|
1339
|
+
for (const k of MPW_BOOK_KEYS) if (s0[k] !== void 0) { need = true; break }
|
|
1340
|
+
if (!need) return s0;
|
|
1341
|
+
const o = Object.assign({}, s0);
|
|
1342
|
+
for (const k of MPW_BOOK_KEYS) delete o[k];
|
|
1343
|
+
return o;
|
|
1344
|
+
} catch { return s0 }
|
|
1345
|
+
}
|
|
1288
1346
|
function mpwVal(s, k) {
|
|
1289
1347
|
try { return s && typeof s === "object" ? s[k] : void 0 } catch { return void 0 }
|
|
1290
1348
|
}
|
|
@@ -3569,6 +3627,11 @@ function mpwThumbFail(el, why) {
|
|
|
3569
3627
|
* state,统一经 window 事件 mpw:busy 通知组件渲染进度条。
|
|
3570
3628
|
* payload: null=隐藏 | { label, percent(null=不确定) } */
|
|
3571
3629
|
function mpwBusyEmit(payload) {
|
|
3630
|
+
/* ①(2026-10-02 用户拍板) 「转码中」条只在**用户主动配置转码**(分辨率上限/帧率上限/预缩档)
|
|
3631
|
+
或真正在转码时显示。自动降档(≥4K 临时降 1080p)属于后台行为 ⇒ 不打扰;
|
|
3632
|
+
纯外观设置(虚化程度/透明度)本来就不该触发媒体重挂,见 applyFromStorage 的 CSS-only 早退。 */
|
|
3633
|
+
try { if (payload && payload.auto === true) return } catch (e) {} // 占位静音(兼容旧调用)
|
|
3634
|
+
try { if (mpwAutoCapSilent && payload && typeof payload.label === "string" && payload.label.indexOf("tc:") === 0) return } catch (e) {} // 自动降档期间的真实转码进度也不弹
|
|
3572
3635
|
try { window.dispatchEvent(new CustomEvent("mpw:busy", { detail: payload })); } catch {}
|
|
3573
3636
|
}
|
|
3574
3637
|
/** ⓪(2026-09-17 用户第 1 条「我没用转码,为什么有个 ffmpeg 常驻转我正在放的壁纸」)
|
|
@@ -3629,11 +3692,11 @@ function mpwThumbFail(el, why) {
|
|
|
3629
3692
|
if (!d || d.phase === "idle") return;
|
|
3630
3693
|
// ①(2026-09-28 H1) `cancelled` 是**终态**(切档/宿主取消)——旧实现只认
|
|
3631
3694
|
// done/error ⇒ 已取消的任务会让进度条空转到 20min 上限。
|
|
3632
|
-
if (d.phase === "done" || d.phase === "error" || d.phase === "cancelled") { clearInterval(iv); window.__mpwTcPollKey = null; mpwBusyEmit(null); return; }
|
|
3695
|
+
if (d.phase === "done" || d.phase === "error" || d.phase === "cancelled") { clearInterval(iv); window.__mpwTcPollKey = null; try { mpwAutoCapSilence(false) } catch (e) {} mpwBusyEmit(null); return; }
|
|
3633
3696
|
mpwBusyEmit({ label: d.phase === "downloading" ? "ffdl:" + (d.percent || 0) : "tc:" + (d.percent || 0), percent: d.percent || 0 });
|
|
3634
3697
|
}).catch(() => {});
|
|
3635
3698
|
} catch { clearInterval(iv); window.__mpwTcPollKey = null; mpwBusyEmit(null); }
|
|
3636
|
-
if (++tries > 120) { clearInterval(iv); window.__mpwTcPollKey = null; mpwBusyEmit(null); } // 20min 上限
|
|
3699
|
+
if (++tries > 120) { clearInterval(iv); window.__mpwTcPollKey = null; try { mpwAutoCapSilence(false) } catch (e) {} mpwBusyEmit(null); } // 20min 上限
|
|
3637
3700
|
}, 1000);
|
|
3638
3701
|
return iv;
|
|
3639
3702
|
}
|
|
@@ -7993,7 +8056,8 @@ function mpwThumbFail(el, why) {
|
|
|
7993
8056
|
// **不走转码**(host resolveTranscodeByImage 不识别 sv=1,会 404 黑屏)。
|
|
7994
8057
|
const isSceneVideo = typeof image === "string" && image.indexOf("sv=1") >= 0;
|
|
7995
8058
|
const fpsCapN = section.fpsCap !== void 0 && section.fpsCap !== null ? Number(section.fpsCap) : 0;
|
|
7996
|
-
|
|
8059
|
+
/* ①(2026-10-02) 上限的唯一读取口:用户设过用用户的,否则可能命中 ≥4K 自动降档的**临时**覆盖 */
|
|
8060
|
+
const resMaxN = mpwResMaxEffective(section);
|
|
7997
8061
|
/* ①(2026-09-23 用户第 1 项 B) **预缩档(默认关)**:按屏幕物理尺寸 + lanczos
|
|
7998
8062
|
预缩到正好够屏,复用**既有** /transcode 通道(不新起一套)。
|
|
7999
8063
|
· 关(默认):preScaleN=0 ⇒ maxW/scale 一个都不加,URL 与行为**逐字不变**;
|
|
@@ -8438,6 +8502,7 @@ function mpwThumbFail(el, why) {
|
|
|
8438
8502
|
} catch {}
|
|
8439
8503
|
try { setupSblurObserver(); } catch {}
|
|
8440
8504
|
try { setupHeaderBlurWatch(); } catch {}
|
|
8505
|
+
try { setupPopTagWatch(); } catch {}
|
|
8441
8506
|
try { setupAquaThemeWatch(); } catch {}
|
|
8442
8507
|
// ①(新) 主题颜色:body 门控 + CSS 变量(控制侧边栏/标题栏/新会话/设置弹窗底色)
|
|
8443
8508
|
// ①(Qoder 审查 R3) 总开关关闭时还原默认外观:themeColor/accent 的
|
|
@@ -9263,7 +9328,15 @@ function mpwThumbFail(el, why) {
|
|
|
9263
9328
|
function startFrostObserver() {
|
|
9264
9329
|
try {
|
|
9265
9330
|
if (frostObserver) return;
|
|
9266
|
-
|
|
9331
|
+
/* ①(2026-10-02 用户第 7 条) 同上:帧内合并 + 动画/拖拽期间跳过(收起/展开动画时
|
|
9332
|
+
宿主每帧都改 class/style,旧写法每帧同步跑一遍 applyFrostInline)。 */
|
|
9333
|
+
let fraf = 0;
|
|
9334
|
+
const frun = () => { fraf = 0; try { applyFrostInline(); } catch (e) {} };
|
|
9335
|
+
frostObserver = new MutationObserver(() => {
|
|
9336
|
+
if (fraf) return;
|
|
9337
|
+
try { fraf = (typeof requestAnimationFrame === "function") ? requestAnimationFrame(frun) : setTimeout(frun, 16); }
|
|
9338
|
+
catch (e) { frun(); }
|
|
9339
|
+
});
|
|
9267
9340
|
frostObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] });
|
|
9268
9341
|
} catch (e) { mpwErr("startFrostObserver", e); }
|
|
9269
9342
|
}
|
|
@@ -9334,7 +9407,26 @@ function mpwThumbFail(el, why) {
|
|
|
9334
9407
|
function startCompatObserver() {
|
|
9335
9408
|
try {
|
|
9336
9409
|
if (compatObserver) return;
|
|
9337
|
-
|
|
9410
|
+
/* ①(2026-10-02 用户第 7 条「左栏收起/展开动画很卡」) **帧内合并**:
|
|
9411
|
+
宿主在动画期间每帧都会改 DOM(class/style),旧写法每个 mutation 都同步跑一遍
|
|
9412
|
+
`applyCompatBridges`(十几次 querySelectorAll + 写 style)⇒ 主线程被我们占满、
|
|
9413
|
+
动画掉帧。改成 rAF 合并:一帧最多跑一次;并且动画/拖拽期间直接跳过
|
|
9414
|
+
(`data-dsh-sidebar-dragging` 是宿主自己打的标记;动画结束后观察器还会再来一次,
|
|
9415
|
+
所以不会漏掉收尾状态)。 */
|
|
9416
|
+
let raf = 0;
|
|
9417
|
+
const run = () => {
|
|
9418
|
+
raf = 0;
|
|
9419
|
+
try {
|
|
9420
|
+
if (typeof document !== "undefined" && document.body && document.body.hasAttribute
|
|
9421
|
+
&& (document.body.hasAttribute("data-dsh-sidebar-dragging") || document.body.hasAttribute("data-dragging"))) return;
|
|
9422
|
+
applyCompatBridges(applyCompatBridges._sec);
|
|
9423
|
+
} catch (e) {}
|
|
9424
|
+
};
|
|
9425
|
+
compatObserver = new MutationObserver(() => {
|
|
9426
|
+
if (raf) return;
|
|
9427
|
+
try { raf = (typeof requestAnimationFrame === "function") ? requestAnimationFrame(run) : setTimeout(run, 16); }
|
|
9428
|
+
catch (e) { run(); }
|
|
9429
|
+
});
|
|
9338
9430
|
compatObserver.observe(document.documentElement, { childList: true, subtree: true });
|
|
9339
9431
|
} catch (e) { mpwErr("startCompatObserver", e); }
|
|
9340
9432
|
}
|
|
@@ -11462,14 +11554,21 @@ function mpwThumbFail(el, why) {
|
|
|
11462
11554
|
// - 统一虚化开 → 跟随「不透明度」条(panel/100,与侧边栏/标题栏一致);
|
|
11463
11555
|
// 聊天区壁纸模糊由壁纸层 blur 提供(chatFollow 决定归谁管,见 applyFromStorage)
|
|
11464
11556
|
// - 统一虚化关 → 0.62(原默认,跟随 dialogBlur)
|
|
11557
|
+
/* ①(2026-10-02 用户拍板) 「界面透明度 = 0%」= 完全不透明 ⇒ 主画布/标题栏/右栏的底色
|
|
11558
|
+
用**宿主主题自己的不透明底色**(这里就是"不覆盖成半透明")。此前即使是 0%,
|
|
11559
|
+
悬浮档也会把 --dsw-alias-bg-base 压到 0.35 ⇒ 标题栏/右栏出现用户报的"淡白色底"。 */
|
|
11560
|
+
const chromeOpaqueNow = unifyTint
|
|
11561
|
+
&& Math.max(0, Math.min(100, (section.sidebarAlpha !== void 0 ? section.sidebarAlpha : DEFAULT_SIDEBAR_ALPHA))) <= 0;
|
|
11465
11562
|
const baseAlphaRaw = enabled && hasImage && dialogBlur
|
|
11466
|
-
? (
|
|
11467
|
-
?
|
|
11468
|
-
:
|
|
11563
|
+
? (chromeOpaqueNow
|
|
11564
|
+
? 1
|
|
11565
|
+
: (unifyTint
|
|
11566
|
+
? Math.max(0, Math.min(1, (Math.max(50, Math.min(100, opacity)) / 100)))
|
|
11567
|
+
: 0.62))
|
|
11469
11568
|
: 1;
|
|
11470
11569
|
// A(新) 悬浮效果开启时:聊天区自动更通透(整体透出壁纸)
|
|
11471
11570
|
const floatOn2 = section.float !== void 0 ? !!section.float : DEFAULT_FLOAT;
|
|
11472
|
-
const baseAlpha = floatOn2 && enabled && hasImage ? Math.min(baseAlphaRaw, 0.35) : baseAlphaRaw;
|
|
11571
|
+
const baseAlpha = (floatOn2 && enabled && hasImage && !chromeOpaqueNow) ? Math.min(baseAlphaRaw, 0.35) : baseAlphaRaw;
|
|
11473
11572
|
const overrides = {
|
|
11474
11573
|
// 主画布背景半透明 → 壁纸透到所有区域(含输入框背后),
|
|
11475
11574
|
// 这样输入框/弹层的 backdrop-filter 才能模糊到壁纸(Aqua 生效的关键)。
|
|
@@ -16146,7 +16245,36 @@ module.exports = {
|
|
|
16146
16245
|
}
|
|
16147
16246
|
|
|
16148
16247
|
const MPW_4K_MIN_PX = 3840 * 2160; // ≥4K 的判据(像素面积,与旧提示同口径)
|
|
16149
|
-
|
|
16248
|
+
/* ①(2026-10-03) 弹层兜底底的默认不透明度:0.58(既有玻璃感、又保证字不糊)。
|
|
16249
|
+
口径:**只在宿主自己没有底色的弹层上**才刷(见 mpwTagPopoverBg),已有玻璃的弹层不动。 */
|
|
16250
|
+
const MPW_POP_SURFACE_ALPHA = 0.58;
|
|
16251
|
+
const MPW_4K_AUTOCAP_RES = 1920; // 自动降档目标 = RES_LEVELS 里的 1080p 档
|
|
16252
|
+
/* ①(2026-10-02 用户拍板:**只对当前壁纸临时生效、不写设置、不弹条**)
|
|
16253
|
+
上一版把 `resMax` 写进设置 ⇒ 之后每次改外观设置都会重新走一遍转码判断、
|
|
16254
|
+
弹出「转码中」进度条(用户第 1 条)。现在改成模块级**临时覆盖**:
|
|
16255
|
+
· key = 当前壁纸身份(mpkgKey|image|webUrl|sceneKey),换壁纸即失效;
|
|
16256
|
+
· `mpwResMaxEffective(sec)` 是唯一读取口(转码规格与 URL 都从这里取上限);
|
|
16257
|
+
· 不落盘、不打「用户设过」标记、不弹进度条(busy 事件带 auto:true 被拦掉)。 */
|
|
16258
|
+
let mpwAutoCapTransient = null; // { key, maxW } | null
|
|
16259
|
+
/* ①(2026-10-02 用户拍板"不弹条") 自动降档会触发一次真实转码,但那是**后台行为** ⇒
|
|
16260
|
+
它引起的进度事件一律不派发。静音窗口由 pollTranscodeProgress 的终态清除,
|
|
16261
|
+
另加 120s 上限兜底(即使轮询没起来也不会永久静音)。 */
|
|
16262
|
+
let mpwAutoCapSilent = false;
|
|
16263
|
+
function mpwAutoCapSilence(on) {
|
|
16264
|
+
mpwAutoCapSilent = !!on;
|
|
16265
|
+
if (on) { try { setTimeout(() => { mpwAutoCapSilent = false }, 120000) } catch (e) {} }
|
|
16266
|
+
}
|
|
16267
|
+
function mpwAutoCapKeyOf(sec) { try { return mpw4kAutoCapKey(sec || (typeof readSection === "function" ? readSection() : {}) || {}) } catch (e) { return "" } }
|
|
16268
|
+
function mpwResMaxEffective(sec) {
|
|
16269
|
+
try {
|
|
16270
|
+
const s2 = sec || {};
|
|
16271
|
+
const own = s2.resMax !== void 0 && s2.resMax !== null ? Number(s2.resMax) : 0;
|
|
16272
|
+
if (own > 0) return own; // 用户设过 ⇒ 用户优先
|
|
16273
|
+
const t = mpwAutoCapTransient;
|
|
16274
|
+
if (t && t.maxW > 0 && t.key === mpwAutoCapKeyOf(s2)) return t.maxW;
|
|
16275
|
+
} catch (e) {}
|
|
16276
|
+
return 0;
|
|
16277
|
+
}
|
|
16150
16278
|
/** 回退口 `?autocap=off` ⇒ 完全不自动降档(行为同今天)。 */
|
|
16151
16279
|
function mpwAutoCapOff() {
|
|
16152
16280
|
try { return new URLSearchParams(location.search).get("autocap") === "off" } catch (e) { return false }
|
|
@@ -16163,8 +16291,8 @@ module.exports = {
|
|
|
16163
16291
|
}
|
|
16164
16292
|
/** 纯函数:自动降档的**唯一判据**(判据出处 tools/theme-assist-test.mjs D 组)。
|
|
16165
16293
|
* why 的取值就是台账里的可观测原因:below-4k / autocap-off / not-transcodable /
|
|
16166
|
-
* dup / resmax-set / no-transcode / await-transcode-probe / applied
|
|
16167
|
-
*
|
|
16294
|
+
* dup / resmax-set / no-transcode / await-transcode-probe / applied。
|
|
16295
|
+
* ①(2026-10-02) `no-commit-path` / `applied-after-boot` 两个 why 已随"不写设置"一起删除。
|
|
16168
16296
|
* 顺位即语义:先"该不该管这个源",再看回退口与去重,最后才是"用户已设上限"与"宿主能不能转"。 */
|
|
16169
16297
|
function mpw4kAutoCapDecision(vw, vh, resMax, ffReady, autocapOff, dup, transcodable, ffInFlight) {
|
|
16170
16298
|
const W = Number(vw) || 0, H = Number(vh) || 0;
|
|
@@ -16197,26 +16325,11 @@ module.exports = {
|
|
|
16197
16325
|
const s = sec || {};
|
|
16198
16326
|
return String(s.mpkgKey || s.image || s.webUrl || s.sceneKey || "?").slice(0, 200);
|
|
16199
16327
|
}
|
|
16200
|
-
|
|
16201
|
-
|
|
16202
|
-
|
|
16203
|
-
|
|
16204
|
-
|
|
16205
|
-
* 模块级等价物**(commit 内部就是 readSection → mpwMarkUserSet → mpwSwitchPatchFull →
|
|
16206
|
-
* writeSection),否则这一项在真机上永远不生效(设置面板没开 = 静默失效)。 */
|
|
16207
|
-
function mpwAutoCapCommit(resMax) {
|
|
16208
|
-
try {
|
|
16209
|
-
const b = typeof mpwSceneUi === "function" ? mpwSceneUi() : null;
|
|
16210
|
-
if (b && typeof b.commit === "function") { b.commit({ resMax: resMax }, true, true); return true }
|
|
16211
|
-
if (typeof writeSection !== "function" || typeof readSection !== "function") return false;
|
|
16212
|
-
const cur = readSection() || {};
|
|
16213
|
-
const patch = (typeof mpwMarkUserSet === "function") ? mpwMarkUserSet({ resMax: resMax }, cur) : { resMax: resMax };
|
|
16214
|
-
const full = (typeof mpwSwitchPatchFull === "function") ? mpwSwitchPatchFull(patch) : patch;
|
|
16215
|
-
writeSection(Object.assign({}, cur, full), true);
|
|
16216
|
-
try { if (typeof mpwSectionNotify === "function" && mpwSectionNotify) mpwSectionNotify(); } catch (e) {}
|
|
16217
|
-
return true;
|
|
16218
|
-
} catch (e) { return false }
|
|
16219
|
-
}
|
|
16328
|
+
/* ①(2026-10-02 用户拍板:临时生效、不写设置、不弹条) 这里**不再有落盘口**:
|
|
16329
|
+
自动降档只写模块级临时覆盖(`mpwAutoCapTransient`),由 `mpwResMaxEffective()` 唯一读取。
|
|
16330
|
+
历史(已删除的落盘辅助)曾走"面板 commit 桥 → 模块级 writeSection 兜底",因为面板组件
|
|
16331
|
+
只在设置页打开时挂载;现在不需要了 —— 不再写盘就没有"桥拿不到"这个问题。 */
|
|
16332
|
+
|
|
16220
16333
|
/** ≥4K ⇒ 自动降档一次,返回台账片 { applied, resMax, at, why }(同时写进 __mpwPerfNote.autoCap)。
|
|
16221
16334
|
* `expectKey`:只在"等读数落地后再判"那条路径上传入 —— 期间用户换了壁纸 ⇒ 这条观察作废
|
|
16222
16335
|
* (why=source-changed:不写档、不留 seen,别拿旧源的分辨率去改新源的上限)。 */
|
|
@@ -16240,27 +16353,24 @@ module.exports = {
|
|
|
16240
16353
|
led.at = out.at; led.why = d.why;
|
|
16241
16354
|
}
|
|
16242
16355
|
if (d.act) {
|
|
16243
|
-
|
|
16244
|
-
|
|
16245
|
-
|
|
16246
|
-
|
|
16247
|
-
|
|
16248
|
-
|
|
16249
|
-
|
|
16250
|
-
|
|
16251
|
-
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
|
|
16255
|
-
|
|
16256
|
-
|
|
16257
|
-
|
|
16258
|
-
})
|
|
16356
|
+
/* ①(2026-10-02 用户拍板) **不写设置**:只设临时覆盖,然后软重挂当前壁纸
|
|
16357
|
+
(让这一次播放就走 1080p 档)。重挂期间把 busy 静音(auto:true)——
|
|
16358
|
+
≥4K 自动降档是后台行为,不该弹「转码中」条。 */
|
|
16359
|
+
mpwAutoCapTransient = { key: key, maxW: d.resMax };
|
|
16360
|
+
out.applied = true; out.resMax = d.resMax;
|
|
16361
|
+
if (led) { led.applied = true; led.resMax = d.resMax; led.mode = "transient"; }
|
|
16362
|
+
try {
|
|
16363
|
+
const sec2 = (typeof readSection === "function" ? readSection() : null) || {};
|
|
16364
|
+
const img = typeof sec2.image === "string" ? sec2.image : "";
|
|
16365
|
+
const spec2 = img ? mpwTranscodeSpec(0, d.resMax, 0, img, String(sec2.image || "").indexOf("sv=1") >= 0, HOST_URL) : null;
|
|
16366
|
+
if (spec2 && spec2.useTranscode && spec2.playUrl) {
|
|
16367
|
+
mpwBusyEmit({ auto: true }); // 静音占位(payload.auto ⇒ 不派发)
|
|
16368
|
+
if (IS_EDGE) { try { showVideoEdge(spec2.playUrl, bgElements().video) } catch (e) { showVideoEl(spec2.playUrl) } }
|
|
16369
|
+
else showVideoEl(spec2.playUrl);
|
|
16370
|
+
mpwAutoCapSilence(true);
|
|
16371
|
+
try { pollTranscodeProgress(img, spec2.fps || 30, spec2.maxW || 0, spec2.scale || "") } catch (e) { mpwAutoCapSilence(false) }
|
|
16259
16372
|
}
|
|
16260
|
-
}
|
|
16261
|
-
out.why = "no-commit-path"; // 桥与 writeSection 两条落盘口都拿不到 ⇒ 不做
|
|
16262
|
-
if (led) led.why = out.why;
|
|
16263
|
-
}
|
|
16373
|
+
} catch (e) {}
|
|
16264
16374
|
} else if (d.why === "no-transcode") {
|
|
16265
16375
|
mpwFfmpegProbeKick("4k"); // 为换壁纸/下次刷新留下读数;本次不动作
|
|
16266
16376
|
} else if (d.why === "await-transcode-probe") {
|
|
@@ -16533,7 +16643,9 @@ ${decls}
|
|
|
16533
16643
|
: `color-mix(in srgb, var(--dsw-alias-bg-base) ${U(55)}%, transparent)`);
|
|
16534
16644
|
tok("--mpw-surface-plugin-row-unify-light", `color-mix(in srgb, var(--dsw-static-neutral-bluish-00) ${U(55)}%, transparent)`);
|
|
16535
16645
|
/* 右栏全屏([data-rightbar-fullscreen])面板表面:带 fallback 的旧写法逐字保留在值里 */
|
|
16536
|
-
|
|
16646
|
+
/* ①(2026-10-02) 同一类非法值:`--mpw-chrome-bg` 现在是颜色 ⇒ 改 color-mix(真机读数:
|
|
16647
|
+
统一虚化接管时右栏/dock"有模糊没底"就是这条被丢弃造成的)。 */
|
|
16648
|
+
tok("--mpw-surface-rs-full", mpwSurfaceColor(surfacePlan.right.base, 0.55).replace(/[\d.]+%/, "calc(var(--mpw-rs-alpha, 0.45) * 55%)"));
|
|
16537
16649
|
/* 「把侧栏底色交还宿主」用的 token:无壁纸档不覆盖宿主 token,只是原样读回来。
|
|
16538
16650
|
SSOT 在 body 上,而宿主正是在 body 定义 --dsw-specific-sidebar-fill ⇒ var() 在这里能解析
|
|
16539
16651
|
(写在 :root 会 guaranteed-invalid,见 emitSurfaceTokens 注释)。 */
|
|
@@ -17344,7 +17456,7 @@ html body:has([class*="_modal"]) [data-mpw-rs] {
|
|
|
17344
17456
|
// 时"看起来还是没模糊"(这就是用户看到的"虚化消失=白纱");
|
|
17345
17457
|
// 4) 抑制条件集中在选择器里(弹窗/展开面板/拖拽/用户关开关),不再靠 JS 摘除内联;
|
|
17346
17458
|
// 5) 变量由 JS 写入 :root,改设置只改变量、不动 DOM。
|
|
17347
|
-
if (bdSupported
|
|
17459
|
+
if (bdSupported) {
|
|
17348
17460
|
// sidebarBlur 是布尔(是否启用侧栏磨砂),数值在 sidebarBlurAmount;未启用时退回统一虚化程度
|
|
17349
17461
|
// ①(2026-10-02 第3项) 跟随开关开 ⇒ 统一虚化接管(半径一律 unifyAmount,
|
|
17350
17462
|
// 不再被 sidebarBlur 的数值分走);关 ⇒ 保持历史公式(侧栏磨砂开就用它自己的数值)。
|
|
@@ -17366,11 +17478,13 @@ html body:has([class*="_modal"]) [data-mpw-rs] {
|
|
|
17366
17478
|
tok("--mpw-rs-blur", rsAmt2 + "px");
|
|
17367
17479
|
tok("--mpw-rs-alpha", rsAlpha2);
|
|
17368
17480
|
/* 侧栏(左)磨砂表面:底座色 + 透明度 → 一枚 token(原来散在规则里拼 rgba) */
|
|
17369
|
-
|
|
17481
|
+
/* ①(2026-10-02) 透明度 0(交还宿主)时这几枚"表面"token 也**不定义**:
|
|
17482
|
+
没有规则引用它们,定义着只会让判据与诊断里出现"看似还在接管"的死 token。 */
|
|
17483
|
+
if (!chromeInert) tok("--mpw-surface-side-frost", mpwSurfaceColor(surfacePlan.left.base, surfacePlan.left.opacityPct / 100));
|
|
17370
17484
|
/* 右侧栏 / dockkit 面板表面(亮色底 + 22% 档) */
|
|
17371
|
-
tok("--mpw-surface-rs-dock", mpwSurfaceColor(surfacePlan.right.base, surfacePlan.right.opacityPct / 100));
|
|
17485
|
+
if (!rightInert) tok("--mpw-surface-rs-dock", mpwSurfaceColor(surfacePlan.right.base, surfacePlan.right.opacityPct / 100));
|
|
17372
17486
|
/* 右侧栏 / dockkit 面板表面(暗色底 + 30% 档) */
|
|
17373
|
-
tok("--mpw-surface-rs-dock-dark", mpwSurfaceColor(surfacePlan.right.base, surfacePlan.right.opacityPct / 100));
|
|
17487
|
+
if (!rightInert) tok("--mpw-surface-rs-dock-dark", mpwSurfaceColor(surfacePlan.right.base, surfacePlan.right.opacityPct / 100));
|
|
17374
17488
|
/* ①(2026-10-02 第2项) 统一虚化接管时的**共同表面**:与左栏同色同厚度同半径。
|
|
17375
17489
|
三枚 token 只在接管时被下面那块 CSS 引用;不接管时它们不生效(无引用)。
|
|
17376
17490
|
厚度直接取雾模型(= 左侧边栏/标题栏透明度),0 ⇒ rgba(...,0) = 完全透明,
|
|
@@ -17415,21 +17529,21 @@ html body[data-ds-dark-theme][data-mpw-unify] [data-dsh-better-sidebar] [data-ds
|
|
|
17415
17529
|
* 作为层叠上下文:浮层被"关"在 header 内部,无法压过 header 内的文字。
|
|
17416
17530
|
修法:**header 本体彻底不加 backdrop-filter**(磨砂改由 .wSkVaW_header::before 承担,
|
|
17417
17531
|
见上方标题栏规则),这里只保留左侧栏的规则。 */
|
|
17418
|
-
html body:not([data-mpw-sblur-off]) [class*="sidebarCol"]:not([data-sidebar-right-panel]):not([data-dockkit-strip]):not([data-mpw-holds-layer]) {
|
|
17532
|
+
${chromeInert ? "" : `html body:not([data-mpw-sblur-off]) [class*="sidebarCol"]:not([data-sidebar-right-panel]):not([data-dockkit-strip]):not([data-mpw-holds-layer]) {
|
|
17419
17533
|
/* ①(2026-10-02 真机读数) 半径 0 ⇒ 写 **none**(不是 blur(0px):视觉等价但仍会建 containing block) */
|
|
17420
17534
|
-webkit-backdrop-filter: ${chromeBlurCss} !important;
|
|
17421
17535
|
backdrop-filter: ${chromeBlurCss} !important;
|
|
17422
17536
|
background-color: var(--mpw-surface-side-frost) !important;
|
|
17423
|
-
}
|
|
17537
|
+
}`}
|
|
17424
17538
|
/* 右侧栏 / dockkit(V 形:面板 + 标签条 + 表面) */
|
|
17425
|
-
html body[data-mpw-rsblur="on"] [data-sidebar-right-panel],
|
|
17539
|
+
${rightInert ? "" : `html body[data-mpw-rsblur="on"] [data-sidebar-right-panel],
|
|
17426
17540
|
html body[data-mpw-rsblur="on"] [data-dockkit-pane],
|
|
17427
17541
|
html body[data-mpw-rsblur="on"] [data-dockkit-strip],
|
|
17428
17542
|
html body[data-mpw-rsblur="on"] [data-dockkit-surface] {
|
|
17429
17543
|
-webkit-backdrop-filter: blur(var(--mpw-rs-blur)) saturate(140%) !important;
|
|
17430
17544
|
backdrop-filter: blur(var(--mpw-rs-blur)) saturate(140%) !important;
|
|
17431
17545
|
background-color: var(--mpw-surface-rs-dock) !important;
|
|
17432
|
-
}
|
|
17546
|
+
}`}
|
|
17433
17547
|
/* 抑制:弹窗/设置面板打开时(backdrop-filter 会成为 fixed 弹窗的 containing block → 必须摘除) */
|
|
17434
17548
|
html body[data-mpw-sblur-off] [class*="sidebarCol"],
|
|
17435
17549
|
html body[data-mpw-sblur-off] header[class*="_header_"],
|
|
@@ -17545,9 +17659,12 @@ html body:has([class*="_overlay"]) [class*="_floatHeader_"] {
|
|
|
17545
17659
|
html body [class*="_overlayLayer"], html body [class*="_overlay"], html body [class*="_modal"], html body [role="dialog"] {
|
|
17546
17660
|
z-index: 2000 !important;
|
|
17547
17661
|
}
|
|
17548
|
-
/* 设置/对话弹层本身不做玻璃(避免 backdrop-filter 变成下拉的 containing block
|
|
17549
|
-
|
|
17550
|
-
|
|
17662
|
+
/* 设置/对话弹层本身不做玻璃(避免 backdrop-filter 变成下拉的 containing block)。
|
|
17663
|
+
①(2026-10-02 真机读数) **收窄到"真弹层"**:旧写法把 [class*="_overlay"] 及其**所有后代**都清掉模糊,
|
|
17664
|
+
而宿主的聊天输入区锚点类名是 uV2eYG_overlayAnchor(常驻!)⇒ 挂在输入框上的那些弹层
|
|
17665
|
+
(「+」菜单 / 权限选择 / 模型与推理等级 / 上下文占用圆圈)的模糊**被永久清掉**,
|
|
17666
|
+
表现为"完全透明、文字与聊天重叠"(用户第 3 条第②④⑤⑥项)。真弹层用 _overlayLayer / _modal / role=dialog 认。 */
|
|
17667
|
+
[role="dialog"][aria-modal="true"], [class*="_overlayLayer"], [class*="_modal"] {
|
|
17551
17668
|
-webkit-backdrop-filter: none !important;
|
|
17552
17669
|
backdrop-filter: none !important;
|
|
17553
17670
|
}
|
|
@@ -17788,12 +17905,31 @@ html body[data-mpw-sblur-off] [data-mpw-rs] {
|
|
|
17788
17905
|
const n = Number(v);
|
|
17789
17906
|
return Number.isFinite(n) ? Math.max(0, Math.min(100, n)) : DEFAULT_POPOVER_ALPHA;
|
|
17790
17907
|
})();
|
|
17908
|
+
/* ①(2026-10-03) 「弹层不透明度」滑条是否被用户**显式**调过(值 ≠ 默认 94)。
|
|
17909
|
+
只有显式调过(或开了主题色遮罩/染色)才允许我们去**覆盖宿主自己画好的底**;
|
|
17910
|
+
默认状态下宿主自己的玻璃底一律不动 —— 用户第 3 条夸的就是这些。 */
|
|
17911
|
+
const popAlphaUserSet = section.popoverAlpha !== void 0 && Number(section.popoverAlpha) !== DEFAULT_POPOVER_ALPHA;
|
|
17912
|
+
const popFallbackAlpha = popAlphaUserSet ? popSurfaceAlpha / 100 : MPW_POP_SURFACE_ALPHA;
|
|
17791
17913
|
// 深色主题用深底(浅底 + 浅色文字 = 看不见字),浅色主题用白底;
|
|
17792
17914
|
// 若用户开了主题色(--mpw-theme-color)则跟随主题色,保持与整屏一致。
|
|
17793
|
-
|
|
17915
|
+
/* ①(2026-10-02 真机读数定案的**真 bug**) 这里原来拼 `rgba(var(--mpw-chrome-bg, 255,255,255), α)`,
|
|
17916
|
+
而 `--mpw-chrome-bg` 在表面模型重做后已经是**颜色**(`var(--dsw-specific-sidebar-fill)`)而不是
|
|
17917
|
+
三元组 ⇒ 解析成 `rgba(var(--dsw-specific-sidebar-fill), 0.94)` 属**非法值**,整条声明在
|
|
17918
|
+
computed-value 阶段被丢弃 ⇒ 所有弹层"只有模糊、没有底"(用户第 3 条「完全透明、文字重叠」)。
|
|
17919
|
+
改成一等公民 `color-mix()`:形态合法、运行时可调(探针在 Firefox 里逐条验过)。 */
|
|
17920
|
+
const popSurfaceRgb = ``;
|
|
17794
17921
|
/* ①(2026-09-18 §5 第1项) 弹层(面板家族)表面 → 两枚表面 token(不透明度走 --mpw-pop-alpha,
|
|
17795
17922
|
调用方可临时覆盖;默认值仍是「弹层不透明度」滑条)。 */
|
|
17796
|
-
tok("--mpw-surface-pop",
|
|
17923
|
+
tok("--mpw-surface-pop", mpwSurfaceColor(surfacePlan.left.base, popSurfaceAlpha / 100).replace(/[\d.]+%/, `calc(var(--mpw-pop-alpha, ${(popSurfaceAlpha / 100).toFixed(2)}) * 100%)`));
|
|
17924
|
+
/* ①(2026-10-03 用户:"设置背景的 token 跟那个重掉了,让他独立出来")
|
|
17925
|
+
弹层(菜单/下拉/选择器)**不再共用面板表面 token** `--mpw-surface-pop`:
|
|
17926
|
+
上一版把 0.94 的它刷到弹层上 ⇒ 用户夸过的那些"本来就有玻璃感"的弹层(标题栏里的
|
|
17927
|
+
子代理展开框、VS Code 菜单…)被我们的近不透明底盖住,**玻璃感消失**。
|
|
17928
|
+
现在独立成一对 token,并且**只在宿主自己没有底色的弹层上兜底**(由 JS 打
|
|
17929
|
+
`data-mpw-pop-bg` 标记,见 mpwTagPopoverBg):有宿主玻璃的弹层一个像素都不动。 */
|
|
17930
|
+
tok("--mpw-pop-surface", mpwSurfaceColor(surfacePlan.left.base, popFallbackAlpha).replace(/[\d.]+%/, `calc(var(--mpw-pop-surface-alpha, ${popFallbackAlpha.toFixed(2)}) * 100%)`));
|
|
17931
|
+
tok("--mpw-pop-surface-alpha", popFallbackAlpha.toFixed(2));
|
|
17932
|
+
tok("--mpw-pop-surface-dark", `rgba(18, 22, 30, var(--mpw-pop-surface-alpha, ${popFallbackAlpha.toFixed(2)}))`);
|
|
17797
17933
|
tok("--mpw-surface-pop-dark", `rgba(18, 22, 30, var(--mpw-pop-alpha, ${(popSurfaceAlpha / 100).toFixed(2)}))`);
|
|
17798
17934
|
// 弹层**表面**选择器(背景 + 模糊共用一份,避免两处写法漂移)。
|
|
17799
17935
|
// ③(关键收窄 2026-09-13) 只用 [class*="_menu"] 会连**非表面**一起命中:
|
|
@@ -17802,10 +17938,24 @@ html body[data-mpw-sblur-off] [data-mpw-rs] {
|
|
|
17802
17938
|
// `._G5b-a_menuStatus`(说明文字)都含 "_menu" ——
|
|
17803
17939
|
// 给它们加 backdrop-filter 会在界面上糊出一块方块,加底色会糊出白块。
|
|
17804
17940
|
// 这里显式排除 anchor/icon/status/open/item/row/trigger/card 等非表面后缀。
|
|
17805
|
-
const POP_NOT = `:not([role="menuitem"]):not([role="option"]):not([class*="Anchor"]):not([class*="anchor"]):not([class*="Icon"]):not([class*="icon"]):not([class*="Status"]):not([class*="status"]):not([class*="Open"]):not([class*="open"]):not([class*="_item"]):not([class*="Item"]):not([class*="_row"]):not([class*="Row"]):not([class*="_trigger"]):not([class*="Trigger"]):not([class*="_card"]):not([class*="Card"]):not([class*="_groupTitle"])`;
|
|
17806
|
-
|
|
17941
|
+
const POP_NOT = `:not([role="menuitem"]):not([role="option"]):not([class*="Anchor"]):not([class*="anchor"]):not([class*="Icon"]):not([class*="icon"]):not([class*="Status"]):not([class*="status"]):not([class*="Open"]):not([class*="open"]):not([class*="_item"]):not([class*="Item"]):not([class*="_row"]):not([class*="Row"]):not([class*="_trigger"]):not([class*="Trigger"]):not([class*="_card"]):not([class*="Card"]):not([class*="_groupTitle"]):not([class*="_viewport"])`;
|
|
17942
|
+
/* ①(2026-10-02 用户第 3 条⑥:上下文占用圆圈点开的框是**纯白**、没有玻璃感)
|
|
17943
|
+
它的真实结构是 div.JObwrW_panel[role="dialog"][aria-label="上下文已用"] ⇒ 既不在
|
|
17944
|
+
下面这串弹层选择器里(所以没有我们的底),又被"真弹层不做玻璃"那条按 role=dialog
|
|
17945
|
+
清掉了模糊 ⇒ 纯白。把它(以及同类小面板)纳入弹层表面集合。 */
|
|
17946
|
+
const popSurfaceSel = `[role="menu"]${POP_NOT}, [role="listbox"]${POP_NOT}, [role="combobox"]${POP_NOT}, [data-dsh-surface]${POP_NOT}, [class*="_menu"]${POP_NOT}, [class*="_popover"]${POP_NOT}, [class*="_dropdown"]${POP_NOT}, [class*="_denseList"]${POP_NOT}, [class*="JObwrW_panel"]${POP_NOT}`;
|
|
17807
17947
|
const popTipSel = `[role="tooltip"], [role="alert"]`;
|
|
17808
|
-
|
|
17948
|
+
/* ①(2026-10-02 用户第 3 条:「把这个好用的模糊效果独立成 token,因为还有几个地方也要实现」)
|
|
17949
|
+
弹层表面 = **底色 + 模糊半径 + 圆角** 三件套,全部由这几枚 token 定义;任何弹层只要被
|
|
17950
|
+
下面的选择器命中,就自动同一套效果(不再各处写死 rgba/blur)。
|
|
17951
|
+
取值口径:底色 = 宿主底色 × 「弹层不透明度」(默认 94%);半径 = 「虚化弹层」的
|
|
17952
|
+
popoverAmount(关掉弹层虚化时为 0 ⇒ 只留底色);圆角沿用宿主自己的(不覆盖 host radius)。 */
|
|
17953
|
+
tok("--mpw-pop-blur", ((popoverBlur && Number(popoverAmount) > 0) ? `blur(${Number(popoverAmount)}px)` : "none"));
|
|
17954
|
+
tok("--mpw-pop-radius", "var(--dsw-alias-radius-l2, 12px)");
|
|
17955
|
+
/* ②(2026-10-03 用户:"原本那种玻璃效果被修没了") **默认不再强刷**:只有用户**显式**动过
|
|
17956
|
+
「弹层不透明度」滑条、或开了主题色遮罩/染色(那时他明确要我们的表面色)才整片覆盖;
|
|
17957
|
+
默认只给"宿主自己没画底/画的是纯白小浮层"的弹层兜底(`data-mpw-pop-bg`,独立 token)。 */
|
|
17958
|
+
const popSurfaceCss = (popAlphaUserSet || menuTranslucent)
|
|
17809
17959
|
? `/* 弹层表面底色兜底(浅色/深色两套;不透明度由「弹层不透明度」控制) */
|
|
17810
17960
|
html body :is(${popSurfaceSel}) {
|
|
17811
17961
|
background-color: var(--mpw-surface-pop) !important;
|
|
@@ -17825,13 +17975,34 @@ html body[data-ds-dark-theme] :is(${popSurfaceSel}) {
|
|
|
17825
17975
|
表面选择器与上面的底色规则共用(POP_NOT 已排除 anchor/icon/status 等非表面类)。 */
|
|
17826
17976
|
${popTipSel},
|
|
17827
17977
|
${popSurfaceSel} {
|
|
17828
|
-
|
|
17829
|
-
|
|
17830
|
-
}
|
|
17978
|
+
/* ①(2026-10-02) 半径/模糊统一读自有 token(--mpw-pop-blur 是唯一来源;关掉弹层虚化时它就是 none) */
|
|
17979
|
+
backdrop-filter: var(--mpw-pop-blur, ${popFilter}) !important;
|
|
17980
|
+
-webkit-backdrop-filter: var(--mpw-pop-blur, ${popFilter}) !important;
|
|
17981
|
+
}
|
|
17982
|
+
/* ①(2026-10-03 用户:"原本那种玻璃效果被你修没了"、"背景 token 跟那个重掉了,让他独立出来")
|
|
17983
|
+
兜底底**只刷宿主自己没有底色的弹层**(JS 扫 computed backgroundColor,α=0 才打 data-mpw-pop-bg);
|
|
17984
|
+
有宿主玻璃的弹层(标题栏子代理展开框 / VS Code 菜单 / 模型选择…)一个像素都不动。
|
|
17985
|
+
独立 token:--mpw-pop-surface(-dark)(不再共用面板表面 token --mpw-surface-pop)。 */
|
|
17986
|
+
html body [data-mpw-pop-bg] { background-color: var(--mpw-pop-surface) !important; }
|
|
17987
|
+
html body[data-ds-dark-theme] [data-mpw-pop-bg] { background-color: var(--mpw-pop-surface-dark) !important; }
|
|
17831
17988
|
[role="menu"] [class*="card"],
|
|
17832
17989
|
[role="tooltip"] [class*="card"] {
|
|
17833
17990
|
backdrop-filter: none !important;
|
|
17834
17991
|
}
|
|
17992
|
+
/* ①(2026-10-02 用户第 3 条⑤) 弹层里的**分组标题条**:宿主给 div._7KE1Ra_groupTitle 之类一块
|
|
17993
|
+
不透明白底,于是"提供商标题是白色长条,而下面具体模型行是好的玻璃底"。这里把标题让成透明,
|
|
17994
|
+
透出容器的玻璃表面(与模型行一致)。POP_NOT 里原本把它排除在"表面"之外是对的(它不该有
|
|
17995
|
+
自己的底),但它自己的宿主白底必须显式清掉。 */
|
|
17996
|
+
html body [role="menu"] [class*="_groupTitle"],
|
|
17997
|
+
html body [role="listbox"] [class*="_groupTitle"],
|
|
17998
|
+
html body [class*="_menu"] [class*="_groupTitle"],
|
|
17999
|
+
html body [class*="JObwrW_panel"] [class*="_groupTitle"],
|
|
18000
|
+
html body [role="menu"] [class*="groupTitle"],
|
|
18001
|
+
html body [role="listbox"] [class*="groupTitle"],
|
|
18002
|
+
html body [class*="_menu"] [class*="groupTitle"],
|
|
18003
|
+
html body [class*="JObwrW_panel"] [class*="groupTitle"] {
|
|
18004
|
+
background-color: transparent !important;
|
|
18005
|
+
}
|
|
17835
18006
|
/* ── 弹层**表面底色**兜底(用户"浮层被压住"的根治项之一)──
|
|
17836
18007
|
背景过去只来自宿主 token(--dsw-specific-menu / -tip / -selector …),
|
|
17837
18008
|
不同组件用的 token 不一样,一旦某个链路把 token 改透明、或 backdrop 模糊在
|
|
@@ -24430,6 +24601,89 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
24430
24601
|
check();
|
|
24431
24602
|
}
|
|
24432
24603
|
|
|
24604
|
+
/* ①(2026-10-03 用户:"原本那种玻璃效果被你修没了" / "他那个设置背景的 token 好像跟那个重掉了,让他独立出来")
|
|
24605
|
+
弹层**兜底底**标记器:宿主自己把底画好了的弹层,我们**一个像素都不动**;只有下面两类才打
|
|
24606
|
+
`data-mpw-pop-bg`,由 CSS(独立 token `--mpw-pop-surface(-dark)` + `--mpw-pop-blur`)补一套玻璃:
|
|
24607
|
+
甲、宿主完全没画底(computed backgroundColor 的 α < 0.05,且近祖先也没有可见底)
|
|
24608
|
+
—— 用户第 3 条里「完全透明、文字重叠」的那几处(+ 菜单 / 权限 / 模型选择器);
|
|
24609
|
+
乙、宿主画的是**不透明纯底的小浮层**(α ≥ 0.999 且面积 < 40% 视口)
|
|
24610
|
+
—— 用户第 3⑥ 条「上下文用量圆环弹出来是纯白」那一处,换成玻璃。
|
|
24611
|
+
硬前提:**背后必须有模糊**(computed backdrop-filter 含 blur)才刷底色 ——
|
|
24612
|
+
否则半透明底 + 无模糊 = 字叠字,比原来更糟。模糊由我们(虚化弹层)或宿主提供,两者都算。
|
|
24613
|
+
开销:只在弹层插进 DOM 时(rAF 合并)扫一次,元素级 getComputedStyle 只落在命中项上。 */
|
|
24614
|
+
let popTagWatch = null;
|
|
24615
|
+
const MPW_POP_TAG_SEL = '[role="menu"], [role="listbox"], [role="combobox"], [class*="_menu"], [class*="_popover"], [class*="_dropdown"], [class*="_denseList"], [class*="JObwrW_panel"], [role="tooltip"]';
|
|
24616
|
+
const MPW_POP_TAG_NOT = /(Anchor|anchor|Icon|icon|Status|status|Item|_item|Row|_row|Trigger|_trigger|Card|_card|groupTitle|_viewport)/;
|
|
24617
|
+
/** 近祖先(≤4 层)里已经有可见底 ⇒ 真正的表面在上面,别在里面再叠一层。 */
|
|
24618
|
+
function mpwPopHasPaintedAncestor(el) {
|
|
24619
|
+
try {
|
|
24620
|
+
let p = el.parentElement, n = 0;
|
|
24621
|
+
while (p && n < 4 && p !== document.body && p !== document.documentElement) {
|
|
24622
|
+
let cs = null;
|
|
24623
|
+
try { cs = getComputedStyle(p); } catch (e) { break; }
|
|
24624
|
+
if (cs && cs.display !== "none" && mpwColorAlpha(cs.backgroundColor) >= 0.3) return true;
|
|
24625
|
+
p = p.parentElement; n++;
|
|
24626
|
+
}
|
|
24627
|
+
} catch (e) {}
|
|
24628
|
+
return false;
|
|
24629
|
+
}
|
|
24630
|
+
/** 小浮层(不是铺满视口的弹层/窗口)—— 不透明底的小面板才允许被换成玻璃。 */
|
|
24631
|
+
function mpwPopIsFloating(el) {
|
|
24632
|
+
try {
|
|
24633
|
+
const r = el.getBoundingClientRect();
|
|
24634
|
+
const vw = window.innerWidth || 1, vh = window.innerHeight || 1;
|
|
24635
|
+
if (!r || r.width <= 0 || r.height <= 0) return false;
|
|
24636
|
+
return r.width * r.height <= 0.4 * vw * vh && r.height <= vh * 0.9;
|
|
24637
|
+
} catch (e) { return false; }
|
|
24638
|
+
}
|
|
24639
|
+
function mpwTagPopoverBg() {
|
|
24640
|
+
let list = null;
|
|
24641
|
+
try { list = document.querySelectorAll(MPW_POP_TAG_SEL); } catch (e) { return; }
|
|
24642
|
+
if (!list) return;
|
|
24643
|
+
for (let i = 0; i < list.length; i++) {
|
|
24644
|
+
const el = list[i];
|
|
24645
|
+
let need = false;
|
|
24646
|
+
try {
|
|
24647
|
+
const cls = (typeof el.className === "string") ? el.className : "";
|
|
24648
|
+
if (!MPW_POP_TAG_NOT.test(cls)) {
|
|
24649
|
+
const cs = getComputedStyle(el);
|
|
24650
|
+
if (cs && cs.display !== "none" && cs.visibility !== "hidden"
|
|
24651
|
+
&& /blur\(/.test(String(cs.backdropFilter || cs.webkitBackdropFilter || ""))) {
|
|
24652
|
+
const a = mpwColorAlpha(cs.backgroundColor);
|
|
24653
|
+
if (a < 0.05) need = !mpwPopHasPaintedAncestor(el);
|
|
24654
|
+
else if (a >= 0.999) need = mpwPopIsFloating(el);
|
|
24655
|
+
}
|
|
24656
|
+
}
|
|
24657
|
+
} catch (e) { need = false; }
|
|
24658
|
+
try {
|
|
24659
|
+
if (need) { if (!el.hasAttribute("data-mpw-pop-bg")) el.setAttribute("data-mpw-pop-bg", ""); }
|
|
24660
|
+
else if (el.hasAttribute("data-mpw-pop-bg")) el.removeAttribute("data-mpw-pop-bg");
|
|
24661
|
+
} catch (e) {}
|
|
24662
|
+
}
|
|
24663
|
+
}
|
|
24664
|
+
function setupPopTagWatch() {
|
|
24665
|
+
if (popTagWatch !== null) return;
|
|
24666
|
+
let praf = 0;
|
|
24667
|
+
const prun = () => {
|
|
24668
|
+
praf = 0;
|
|
24669
|
+
try {
|
|
24670
|
+
// 左栏收起/展开与拖拽期间宿主每帧改 DOM:跳过(收尾还会再来一次)
|
|
24671
|
+
if (document.body && document.body.hasAttribute
|
|
24672
|
+
&& (document.body.hasAttribute("data-dsh-sidebar-dragging") || document.body.hasAttribute("data-dragging"))) return;
|
|
24673
|
+
mpwTagPopoverBg();
|
|
24674
|
+
} catch (e) {}
|
|
24675
|
+
};
|
|
24676
|
+
try {
|
|
24677
|
+
popTagWatch = new MutationObserver(() => {
|
|
24678
|
+
if (praf) return;
|
|
24679
|
+
try { praf = (typeof requestAnimationFrame === "function") ? requestAnimationFrame(prun) : setTimeout(prun, 16); }
|
|
24680
|
+
catch (e) { prun(); }
|
|
24681
|
+
});
|
|
24682
|
+
popTagWatch.observe(document.body, { childList: true, subtree: true });
|
|
24683
|
+
} catch { popTagWatch = null; }
|
|
24684
|
+
mpwTagPopoverBg();
|
|
24685
|
+
}
|
|
24686
|
+
|
|
24433
24687
|
// ①(新) 标题栏 backdrop blur 摘除:G 块给 header 加 blur(unifyAmount)(聊天区
|
|
24434
24688
|
// 不跟随时的标题栏虚化),但 header 内有 absolute 展开面板(子代理/后台任务,
|
|
24435
24689
|
// JObwrW_panel),backdrop-filter 会变成它们的 containing block → 面板定位错乱
|
|
@@ -24460,7 +24714,24 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
24460
24714
|
} catch {}
|
|
24461
24715
|
};
|
|
24462
24716
|
try {
|
|
24463
|
-
|
|
24717
|
+
/* ①(2026-10-02 用户第 7 条:左栏收起/展开动画卡顿;真机对照 rAF 峰值 650–684ms vs 267ms)
|
|
24718
|
+
`check()` 每次都 `querySelectorAll` + `getBoundingClientRect`(强制布局),而宿主在动画
|
|
24719
|
+
期间**每帧都改 DOM** ⇒ 旧写法等于每帧跑一次全量查询 + 布局。改成 rAF 帧内合并,
|
|
24720
|
+
并在动画/拖拽期间直接跳过(动画结束后还会再来一次,不会漏掉收尾状态)。 */
|
|
24721
|
+
let hraf = 0;
|
|
24722
|
+
const hrun = () => {
|
|
24723
|
+
hraf = 0;
|
|
24724
|
+
try {
|
|
24725
|
+
if (document.body && document.body.hasAttribute
|
|
24726
|
+
&& (document.body.hasAttribute("data-dsh-sidebar-dragging") || document.body.hasAttribute("data-dragging"))) return;
|
|
24727
|
+
check();
|
|
24728
|
+
} catch (e) {}
|
|
24729
|
+
};
|
|
24730
|
+
headerBlurWatch = new MutationObserver(() => {
|
|
24731
|
+
if (hraf) return;
|
|
24732
|
+
try { hraf = (typeof requestAnimationFrame === "function") ? requestAnimationFrame(hrun) : setTimeout(hrun, 16); }
|
|
24733
|
+
catch (e) { hrun(); }
|
|
24734
|
+
});
|
|
24464
24735
|
headerBlurWatch.observe(document.body, { childList: true, subtree: true });
|
|
24465
24736
|
} catch { headerBlurWatch = null; }
|
|
24466
24737
|
check();
|
package/lib/index.js
CHANGED
|
@@ -809,8 +809,12 @@ function normalizeScaleFlag(raw) {
|
|
|
809
809
|
* 顺序仍是"先缩放再抽帧"(编码量更小)。 */
|
|
810
810
|
function buildScaleFilter(maxW, fps, scaleFlag) {
|
|
811
811
|
const w = Number(maxW) > 0 ? Math.round(Number(maxW)) : 0;
|
|
812
|
+
/* ①(2026-10-03 ffmpeg 审计"奇数宽"条) `-2` 只保证**高**是偶数:宽是 `iw`(H.264 允许奇数显示宽,
|
|
813
|
+
靠裁剪位)或用户/屏幕推出来的 `maxW`(`Math.round(w*dpr)` 可以是奇数,如 1366×1.5=2049)。
|
|
814
|
+
一旦我们显式给了 `-pix_fmt yuv420p`(见 F2),奇数宽会直接**编码失败**;不给则 ffmpeg 可能
|
|
815
|
+
协商出非 4:2:0 格式。⇒ 宽也取偶数(`trunc(x/2)*2`),与高同一条纪律。 */
|
|
812
816
|
return w > 0
|
|
813
|
-
? "scale='min(" + w + ",iw)':-2" + (scaleFlag ? ':flags=' + scaleFlag : '') + ',fps=' + fps
|
|
817
|
+
? "scale='trunc(min(" + w + ",iw)/2)*2':-2" + (scaleFlag ? ':flags=' + scaleFlag : '') + ',fps=' + fps
|
|
814
818
|
: 'fps=' + fps;
|
|
815
819
|
}
|
|
816
820
|
/** ①(B) 转码缓存键:**只含影响播放的字段**(源标识 | mtime | fps | maxW | 缩放 flags)。
|
|
@@ -868,6 +872,13 @@ function pixFmtBitDepth(fmt) {
|
|
|
868
872
|
}
|
|
869
873
|
/** 8bit 一族(这些源**不需要**任何色彩参数 —— 实测忠实,且保持与改动前逐字节相同的产物)。 */
|
|
870
874
|
const TRANSCODE_8BIT_PIXFMT = /^(yuvj?420p|yuvj?422p|yuvj?444p|nv12|nv21|yuva420p|rgb24|bgr24|gray|pal8)$/i;
|
|
875
|
+
/** ①(2026-10-03 ffmpeg 审计 F2) **浏览器安全的白名单只有 4:2:0**(外加无色度的 gray/pal8):
|
|
876
|
+
* 8bit 的 `yuv422p` / `yuv444p` 源走旧计划是 `passthrough`(不加 `-pix_fmt`)⇒ libx264 会**顺着源**
|
|
877
|
+
* 产出 `High 4:2:2 / High 4:4:4`,而 `browserCodecGap()` 恰恰把这两种 profile 判成"浏览器吃不下"
|
|
878
|
+
* ⇒ 我们**为了救它而转码,却转出一个同样放不出的文件**,而且它是个"有效缓存"(≥1KB + ftyp)
|
|
879
|
+
* 永久命中 ⇒ 之后一直是黑屏、也不会再重转(除非删缓存)。所以:非 4:2:0 一律显式降 `yuv420p`。
|
|
880
|
+
* `rgb24/bgr24` 同理(x264 无 RGB 输入)⇒ 一起收进"要降"的一侧。 */
|
|
881
|
+
const TRANSCODE_SAFE_420 = /^(yuvj?420p|nv12|nv21|yuva420p|gray|pal8)$/i;
|
|
871
882
|
/** HDR 传输函数(PQ / HLG / 2020 系):这些 transfer 在 8bit SDR 显示链上没有正确解释。 */
|
|
872
883
|
const TRANSCODE_HDR_TRANSFER = /^(smpte2084|arib-std-b67|smpte428|bt2020-10|bt2020-12)$/i;
|
|
873
884
|
/** 探测结果 → 转码色彩计划(**纯函数**,门禁直接切片断言;不改文件、不起进程)。
|
|
@@ -886,8 +897,12 @@ function transcodeColorPlan(info) {
|
|
|
886
897
|
if (TRANSCODE_COLOR_MODE === 'force8') return { mode: 'force8', downgrade: true, deep, hdr, from, to, args: downgradeArgs, reason: 'DSH_WE_TRANSCODE_COLOR=force8:强制 8bit bt709(现场对比用)' };
|
|
887
898
|
// 不知道(探测失败/空 pix_fmt)⇒ 保持旧行为,但**记账**
|
|
888
899
|
if (!pixFmt) return { mode: 'unknown', downgrade: false, deep: false, hdr, from, to, args: [], reason: '拿不到源 pix_fmt(探测失败)⇒ 不加色彩参数(与改动前一致),台账记 unknown' };
|
|
889
|
-
|
|
890
|
-
|
|
900
|
+
/* ①(F2) 只有"8bit + SDR + 4:2:0"才允许 passthrough(与改动前逐字节相同);
|
|
901
|
+
非 4:2:0 的 8bit 源必须显式降 4:2:0(否则转出 4:2:2/4:4:4 profile = 浏览器仍放不出,
|
|
902
|
+
却被当作有效缓存永久命中)。 */
|
|
903
|
+
const chromaUnsafe = !!pixFmt && !TRANSCODE_SAFE_420.test(pixFmt);
|
|
904
|
+
if (!deep && !hdr && !chromaUnsafe) return { mode: 'passthrough', downgrade: false, deep, hdr, chroma: false, from, to, args: [], reason: '8bit SDR 4:2:0:实测转码忠实(标签/像素都不变)⇒ 不加参数,产物与改动前逐字节相同' };
|
|
905
|
+
return { mode: 'downgrade', downgrade: true, deep, hdr, chroma: chromaUnsafe, from, to, args: downgradeArgs, reason: (deep ? ('源 ' + depth + 'bit') : '') + (deep && hdr ? ' + ' : '') + (hdr ? ('HDR transfer ' + transfer) : '') + (chromaUnsafe ? ((deep || hdr ? ' + ' : '') + '源 ' + pixFmt + ' 非 4:2:0') : '') + ' ⇒ 降为 8bit 4:2:0 bt709/tv' + ((deep || hdr) ? '(无 tonemap:ffmpeg-static 无 zscale,偏暗但能播)' : '') };
|
|
891
906
|
}
|
|
892
907
|
// ═══ MPW-COLOR-PLAN-END ═══
|
|
893
908
|
/* ═══ MPW-TRANSCODE-GEN-BEGIN(H1 代际/归属/有效性;纯函数 + 三个小状态操作) ═══ */
|
|
@@ -991,10 +1006,20 @@ const BROWSER_OK_CONTAINER = /^(mov|mp4|m4a|3gp|3g2|mj2|matroska|webm)/i;
|
|
|
991
1006
|
* · h264 + opus 装进 MP4:Chromium 系不支持(Opus 在 MP4 里只有 WebM/ogg 容器被支持);
|
|
992
1007
|
* · HEVC Main10(10bit):多数平台无 10bit HEVC 解码(8bit Main 在 Safari/部分 Chromium 上可)。
|
|
993
1008
|
* 只用于"源本身就不该直读"的判断;命中即视为需要转码。 */
|
|
994
|
-
function browserCodecGap(video, audio,
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1009
|
+
function browserCodecGap(video, audio, container, profile) {
|
|
1010
|
+
/* ①(2026-10-03 ffmpeg 审计 F6) **参数拆分**:旧签名 `(video, audio, fmt)` 里 `fmt` 同时被当
|
|
1011
|
+
**容器**(h264+opus 那条)和**profile**(Main10 那条)用,而唯一调用点传的是 `videoProfile`
|
|
1012
|
+
⇒ 容器永远缺席:MKV/WebM 里的 h264+opus(本该被豁免)被判"mp4 容器里的 h264+opus"
|
|
1013
|
+
⇒ 白转一次码,`verdict.reason` 也是错的。现在容器与 profile 各是各的参数。
|
|
1014
|
+
兼容:第三个参数若是 `High 10`/`Main 10` 这类 **profile 串**(老调用点/老测试),
|
|
1015
|
+
仍按 profile 解释(容器判据退化为"未知",不误判成 mp4)。 */
|
|
1016
|
+
const looksProfile = (v) => /^\s*(high|main|baseline|constrained|profile|simple|advanced|professional)/i.test(String(v || ''));
|
|
1017
|
+
let cont = String(container || '');
|
|
1018
|
+
let prof = String(profile || '');
|
|
1019
|
+
if (prof === '' && looksProfile(cont)) { prof = cont; cont = ''; }
|
|
1020
|
+
const afmt = cont;
|
|
1021
|
+
if (video === 'h264' && audio === 'opus' && afmt !== '' && !/webm|matroska/i.test(afmt)) return 'mp4 容器里的 h264+opus(Chromium 不支持该组合)';
|
|
1022
|
+
if (video === 'hevc' && /main ?10|main10/i.test(prof)) return 'HEVC Main10(10bit,多数平台无解码器)';
|
|
998
1023
|
/* ①(2026-09-28 H2 色彩轮) **H.264 的高位深 profile**(High 10 / High 4:2:2 / High 4:4:4):
|
|
999
1024
|
主流浏览器**没有** 10bit H.264 解码器(Chromium/Firefox 只到 High 8bit;Safari/VideoToolbox
|
|
1000
1025
|
视平台),而本模块此前只认 HEVC Main10 ⇒ 一个 **H.264 High 10** 源会被判"可直读"、
|
|
@@ -1003,9 +1028,14 @@ function browserCodecGap(video, audio, fmt) {
|
|
|
1003
1028
|
仍是 `High 10 / yuv420p10le` ⇒ 位深策略(`transcodeColorPlan`)必须与这条判据配套。
|
|
1004
1029
|
置信度与既有 HEVC Main10 一条同级("多数平台无解码器"),不猜;转码若失败客户端仍有
|
|
1005
1030
|
回退原片的一次性路径(`mpwFallbackToDirect`)。 */
|
|
1006
|
-
if (video === 'h264' && /high 10|high 4:2:2|high 4:4:4|high 10 intra|high 422|high 444/i.test(
|
|
1007
|
-
return 'H.264 ' +
|
|
1031
|
+
if (video === 'h264' && /high 10|high 4:2:2|high 4:4:4|high 10 intra|high 422|high 444/i.test(prof)) {
|
|
1032
|
+
return 'H.264 ' + prof.trim() + '(10bit/高位深 profile,浏览器多数无解码器)';
|
|
1008
1033
|
}
|
|
1034
|
+
/* ①(2026-10-03 ffmpeg 审计 F2 后半) VP9 / AV1 的**高位深/高色度** profile 同样没有浏览器解码器,
|
|
1035
|
+
旧表只认 h264/hevc ⇒ 这些源被判"可直读"、直接喂浏览器(黑屏),转码这条唯一能救的路没走。
|
|
1036
|
+
ffprobe 对 VP9 报 `Profile 0/1/2/3`、对 AV1 报 `Main/High/Professional`。 */
|
|
1037
|
+
if (video === 'vp9' && /profile\s*(1|3)/i.test(prof)) return 'VP9 Profile 1/3(4:2:2 / 4:4:4,浏览器无解码器)';
|
|
1038
|
+
if (video === 'av1' && /high|professional/i.test(prof)) return 'AV1 High/Professional(4:2:2 / 4:4:4 / 12bit,浏览器无解码器)';
|
|
1009
1039
|
return '';
|
|
1010
1040
|
}
|
|
1011
1041
|
/** 给定探测结果 → { playable: true|false|null, reason }。
|
|
@@ -1020,7 +1050,8 @@ function browserPlayable(info) {
|
|
|
1020
1050
|
if (!BROWSER_OK_VIDEO.has(v)) return { playable: false, reason: '视频编码 ' + v + ' 不在浏览器白名单(h264/vp8/vp9/av1)' };
|
|
1021
1051
|
if (a && !BROWSER_OK_AUDIO.has(a)) return { playable: false, reason: '音频编码 ' + a + ' 不在浏览器白名单(aac/mp3/opus/vorbis/flac)' };
|
|
1022
1052
|
if (!BROWSER_OK_CONTAINER.test(String(fmt))) return { playable: false, reason: '容器 ' + fmt + ' 不是 mp4/webm' };
|
|
1023
|
-
|
|
1053
|
+
/* ①(F6) 容器与 profile 分开传(旧版把 profile 当容器用) */
|
|
1054
|
+
const gap = browserCodecGap(v, a, fmt, info.videoProfile);
|
|
1024
1055
|
if (gap) return { playable: false, reason: gap };
|
|
1025
1056
|
return { playable: true, reason: v + (a ? '+' + a : '') + ' / ' + fmt + '(浏览器可直读)' };
|
|
1026
1057
|
}
|
|
@@ -1967,10 +1998,17 @@ function materializeSource(src, cacheDir) {
|
|
|
1967
1998
|
while (w < n) w += writeSync(outFd, buf, w, n - w);
|
|
1968
1999
|
off += n;
|
|
1969
2000
|
}
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
|
|
2001
|
+
} catch (e) {
|
|
2002
|
+
/* ①(2026-10-03 ffmpeg 审计 F7) 拷贝中途失败(ENOSPC / 读源出错)时**必须**把半成品删掉:
|
|
2003
|
+
旧写法只有 `finally` 关句柄 ⇒ 部分写出的 `src_*.bin` 留在缓存目录里等着"1 小时后才回收",
|
|
2004
|
+
而磁盘正是满的 ⇒ 重试继续失败,用户只看到"转码失败"。 */
|
|
2005
|
+
try { closeSync(fd); } catch { /* 忽略 */ }
|
|
2006
|
+
try { closeSync(outFd); } catch { /* 忽略 */ }
|
|
2007
|
+
try { unlinkSync(tmp); } catch { /* 忽略 */ }
|
|
2008
|
+
throw e;
|
|
1973
2009
|
}
|
|
2010
|
+
try { closeSync(fd); } catch { /* 忽略 */ }
|
|
2011
|
+
try { closeSync(outFd); } catch { /* 忽略 */ }
|
|
1974
2012
|
return { file: tmp, cleanup: () => { try { unlinkSync(tmp); } catch { /* 忽略 */ } } };
|
|
1975
2013
|
}
|
|
1976
2014
|
|
|
@@ -1979,7 +2017,7 @@ function materializeSource(src, cacheDir) {
|
|
|
1979
2017
|
* (实测 0xFFFFFFEA / EPERM),故先试 detached 再试普通 spawn。
|
|
1980
2018
|
* ①(2026-09-28 H1) `jobKey` = 本子进程所属的产物路径:登记进 `ACTIVE_FFMPEG`(proc→jobKey),
|
|
1981
2019
|
* 取消时**只杀本任务**的进程(旧实现按"本请求之后启动的全部"过滤,会误杀别的档/别的源)。 */
|
|
1982
|
-
function spawnFfmpeg(ff, args, jobKey) {
|
|
2020
|
+
function spawnFfmpeg(ff, args, jobKey, deadline) {
|
|
1983
2021
|
return new Promise((resolve, reject) => {
|
|
1984
2022
|
const errLog = join(transcodeCacheDir(), 'ffmpeg-err-' + process.pid + '-' + Date.now() + '.log');
|
|
1985
2023
|
const attempts = [
|
|
@@ -2031,7 +2069,7 @@ function spawnFfmpeg(ff, args, jobKey) {
|
|
|
2031
2069
|
clearTimeout(timer);
|
|
2032
2070
|
ACTIVE_FFMPEG.delete(proc);
|
|
2033
2071
|
if (errFd) { try { closeSync(errFd); } catch { /* 忽略 */ } }
|
|
2034
|
-
let detail = msg || 'ffmpeg failed';
|
|
2072
|
+
let detail = (msg && msg.message) ? msg.message : (msg || 'ffmpeg failed');
|
|
2035
2073
|
try {
|
|
2036
2074
|
const t = readFileSync(errLog, 'utf8').trim();
|
|
2037
2075
|
if (t) detail += ' | stderr: ' + t.split('\n').slice(-4).join(' | ');
|
|
@@ -2048,12 +2086,18 @@ function spawnFfmpeg(ff, args, jobKey) {
|
|
|
2048
2086
|
if (msg) errors.push(msg);
|
|
2049
2087
|
runNext();
|
|
2050
2088
|
};
|
|
2089
|
+
/* ①(2026-10-03 ffmpeg 审计 F4) 超时按**任务剩余时间**算(`deadline` = runTranscode 给的任务级
|
|
2090
|
+
截止时刻),而不是"每次尝试各给 15 分钟"。旧写法下一次超时会被编码器循环当成"换个编码器
|
|
2091
|
+
再试"⇒ libx264 15min + libsvtav1 15min + 各自单线程再来一遍 = 最多 1 小时,全程占着
|
|
2092
|
+
`TRANSCODE_MAX_ACTIVE=1` 的唯一名额(与 docs/TRANSCODE-RESOURCE.md 的"单任务硬超时"不符)。 */
|
|
2093
|
+
const budget = Number.isFinite(deadline) ? Math.max(0, deadline - Date.now()) : TRANSCODE_TIMEOUT_MS;
|
|
2051
2094
|
const timer = setTimeout(() => {
|
|
2052
2095
|
timedOut = true;
|
|
2053
2096
|
try { proc.kill(); } catch { /* 忽略 */ }
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2097
|
+
const err = new Error(a.name + ' timed out after ' + TRANSCODE_TIMEOUT_MS + 'ms (killed)');
|
|
2098
|
+
err.taskTimedOut = true; // ← 任务级超时:runTranscode 拿到它就**不再换编码器/线程模式**
|
|
2099
|
+
finalReject(err);
|
|
2100
|
+
}, budget);
|
|
2057
2101
|
proc.on('error', (err) => {
|
|
2058
2102
|
// spawn 阶段的 'error'(如 EPERM)→ 重试下一个 attempt;执行中 error → 直接失败
|
|
2059
2103
|
if (!done && timedOut) { finalReject(a.name + ' spawn error ' + (err && err.code ? err.code + ' ' + err.message : err)); return; }
|
|
@@ -2095,6 +2139,9 @@ async function runTranscode(srcFile, out, fps, maxW, job, isCancelled, scaleFlag
|
|
|
2095
2139
|
throw new Error('ffmpeg not found (set DSH_WE_FFMPEG, install ffmpeg to PATH, or click 下载 ffmpeg → /ffmpeg-download)');
|
|
2096
2140
|
}
|
|
2097
2141
|
job.ff = ff.path;
|
|
2142
|
+
/* ①(2026-10-03 ffmpeg 审计 F4) **任务级**截止时刻:整个 runTranscode(含换编码器/换线程模式
|
|
2143
|
+
的全部尝试)共用这一个预算,超时即终态。 */
|
|
2144
|
+
const deadline = Date.now() + TRANSCODE_TIMEOUT_MS;
|
|
2098
2145
|
// ①(新) 降分辨率:`scale='min(maxW,iw)':-2` 保持宽高比、宽不超过 maxW(-2=高自动偶数)。
|
|
2099
2146
|
// 与 fps 合并到一个 -vf 链(先缩放再抽帧,编码量更小)。
|
|
2100
2147
|
// ①(2026-09-23 B) vf 链构造收敛到 buildScaleFilter(预缩档 = maxW + flags=lanczos;
|
|
@@ -2121,6 +2168,11 @@ async function runTranscode(srcFile, out, fps, maxW, job, isCancelled, scaleFlag
|
|
|
2121
2168
|
if (mode.label === 'mt' && futexSeen) continue; // 已知 futex 问题 → 跳过多线程
|
|
2122
2169
|
for (const enc of encoders) {
|
|
2123
2170
|
if (cancelled()) throw new Error('cancelled'); // ⓪ 换编码器前也要查
|
|
2171
|
+
if (Date.now() >= deadline) { // ①(F4) 预算已尽 ⇒ 不再起新进程
|
|
2172
|
+
const te = new Error('已到任务级超时(' + TRANSCODE_TIMEOUT_MS + 'ms),停止后续尝试');
|
|
2173
|
+
te.taskTimedOut = true;
|
|
2174
|
+
throw te;
|
|
2175
|
+
}
|
|
2124
2176
|
job.encoder = enc.name + (mode.label === 'st' ? ' (单线程)' : '');
|
|
2125
2177
|
// -f mp4 显式指定:临时输出路径不是 .mp4 后缀,ffmpeg 无法按扩展名选 muxer
|
|
2126
2178
|
// ①(Qoder 审查 M5) 单线程时输出侧再加 -threads 1 控制编码线程
|
|
@@ -2129,12 +2181,20 @@ async function runTranscode(srcFile, out, fps, maxW, job, isCancelled, scaleFlag
|
|
|
2129
2181
|
// ⓪(2026-09-17) x264 额外参数(默认空;见 TRANSCODE_X264_PARAMS 注释:省内存但费码率)
|
|
2130
2182
|
const x264 = (enc.name === 'libx264' && TRANSCODE_X264_PARAMS) ? ['-x264-params', TRANSCODE_X264_PARAMS] : [];
|
|
2131
2183
|
// ①(2026-09-28 H2) `colorArgs`:>8bit/HDR 源才带(8bit 源为空数组 ⇒ 产物与改动前逐字节相同)
|
|
2132
|
-
|
|
2133
|
-
|
|
2184
|
+
/* ①(2026-10-03 ffmpeg 审计 F3) 旧命令无条件 `-an`("壁纸默认静音更省")——但本插件的
|
|
2185
|
+
**壁纸声音**是一等功能(`applyVideoMute` / NOW PLAYING 音量都落在这个 `<video>` 上):
|
|
2186
|
+
用户开了声音、又设了 fpsCap/resMax/预缩(或 ≥4K 源触发静默自动降档)⇒ 转码产物**没有音轨**
|
|
2187
|
+
⇒ 壁纸突然没声,且没有任何提示。现在改为**保留音轨**(aac 128k;源本来没音轨时
|
|
2188
|
+
ffmpeg 的默认流选择不会凭空造一条,`-c:a` 也就用不上)。 */
|
|
2189
|
+
const audioArgs = ['-c:a', 'aac', '-b:a', '128k'];
|
|
2190
|
+
const full = [...mode.extra, ...base, ...enc.tail, ...x264, ...(colorArgs || []), ...(mode.label === 'st' ? ['-threads', '1'] : []), '-movflags', '+faststart', ...audioArgs, '-f', 'mp4', out];
|
|
2191
|
+
await spawnFfmpeg(ff.path, full, jobKey, deadline);
|
|
2134
2192
|
return;
|
|
2135
2193
|
} catch (err) {
|
|
2136
2194
|
lastErr = err;
|
|
2137
2195
|
try { unlinkSync(out); } catch { /* 忽略 */ }
|
|
2196
|
+
/* ①(F4) 任务级超时 = 终态:不再换编码器、不再换线程模式(旧写法把超时也当成"重试下一个")。 */
|
|
2197
|
+
if (err && err.taskTimedOut) throw err;
|
|
2138
2198
|
// ①(Qoder 审查) futex 判定用精确报错原文匹配,避免误伤:文件名/路径
|
|
2139
2199
|
// 含 "futex"(如用户 mpkg 名 futex_test.mpkg)的打开错误也会被宽松
|
|
2140
2200
|
// /futex/i 命中 → 误置 TRANSCODE_FUTEX_FALLBACK 永久单线程(性能损失)。
|
|
@@ -2315,6 +2375,13 @@ async function transcodeToFps(src, fps, maxW, isCancelled, scaleFlag) {
|
|
|
2315
2375
|
return p;
|
|
2316
2376
|
}
|
|
2317
2377
|
|
|
2378
|
+
/* ①(2026-10-03 ffmpeg 审计 F1) `maxW` 的**唯一解析点**:`/transcode`(写入键)与
|
|
2379
|
+
`/transcode-progress`(查询键)必须逐字一致 —— 两侧各写一份正是"进度永远 idle"的根因。 */
|
|
2380
|
+
function parseMaxWParam(raw) {
|
|
2381
|
+
const n = Number(raw);
|
|
2382
|
+
return Number.isFinite(n) && n > 0 ? Math.min(3840, Math.round(n)) : 0;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2318
2385
|
/** 转码/下载进度 JSON(/transcode-progress 与 /transcode 的 progress 子路径共用)。 */
|
|
2319
2386
|
function handleTranscodeProgress(req, res) {
|
|
2320
2387
|
try {
|
|
@@ -2322,10 +2389,17 @@ function handleTranscodeProgress(req, res) {
|
|
|
2322
2389
|
const file = (url.searchParams.get('file') || '').trim();
|
|
2323
2390
|
const srcParam = (url.searchParams.get('src') || '').trim();
|
|
2324
2391
|
const fps = Number(url.searchParams.get('fps')) || 0;
|
|
2325
|
-
const
|
|
2326
|
-
const maxW = Number.isFinite(maxWRaw) && maxWRaw > 0 ? Math.min(3840, Math.round(maxWRaw)) : 0;
|
|
2392
|
+
const maxW = parseMaxWParam(url.searchParams.get('maxW'));
|
|
2327
2393
|
// ①(B) 查询键与写入键一致(含缩放 flags);未给 flags ⇒ 与改动前逐字相同的键。
|
|
2328
2394
|
const scaleFlag = normalizeScaleFlag(url.searchParams.get('scale')) || '';
|
|
2395
|
+
/* ①(2026-10-03 ffmpeg 审计 F1) **查询键必须与写入键同源**:`/transcode` 没拿到 maxW 时会套
|
|
2396
|
+
`TRANSCODE_DEFAULT_MAXW`(写入键 = `srcId|fps|1920`),而这里旧写法直接用 raw maxW=0
|
|
2397
|
+
⇒ 查 `srcId|fps|0`(永远 idle)。后果有两级:①忙条读到 0% 一直到 2 分钟超时才消失;
|
|
2398
|
+
②客户端"转码完成后重新 apply"的 10s 轮询等的是 `phase==='done'`,永远等不到 ⇒ 源被判
|
|
2399
|
+
"不可直读"(HEVC Main10 / H.264 High 10 之类)时**卡在回退的原片上黑屏**,而磁盘上其实
|
|
2400
|
+
早就有转好的产物。⇒ 这里按同一条默认规则算出 effMaxW 作为首选键。 */
|
|
2401
|
+
const reqMaxW = maxW > 0 ? maxW : TRANSCODE_DEFAULT_MAXW;
|
|
2402
|
+
const effMaxW = (TRANSCODE_DEFAULT_MAXW > 0 && reqMaxW > TRANSCODE_DEFAULT_MAXW) ? TRANSCODE_DEFAULT_MAXW : reqMaxW;
|
|
2329
2403
|
let job = null;
|
|
2330
2404
|
// ②(fix) src 优先(与 /transcode 同款):客户端回退原片后轮询转码完成,
|
|
2331
2405
|
// 用原始 image 串查询(旧式 file 兼容保留)。
|
|
@@ -2335,8 +2409,10 @@ function handleTranscodeProgress(req, res) {
|
|
|
2335
2409
|
const srcId = src.type === 'mpkg' ? 'mpkg:' + src.rec.path + ':' + src.entry.index : srcPath;
|
|
2336
2410
|
// ①(Qoder 审查 R7) 查询 key 与写入一致(含 maxW)——此前写入
|
|
2337
2411
|
// srcId|fps|maxW、查询 srcId|fps,设了 maxW 的任务进度永远查不到(idle)。
|
|
2338
|
-
const
|
|
2339
|
-
job = transcodeJobs.get(
|
|
2412
|
+
const keyOf = (w) => srcId + '|' + fps + '|' + w + ((w > 0 && scaleFlag) ? '|s:' + scaleFlag : '');
|
|
2413
|
+
job = transcodeJobs.get(keyOf(effMaxW)) // ← 与 /transcode 的写入键同源(含默认降采样)
|
|
2414
|
+
|| transcodeJobs.get(keyOf(maxW)) // 兼容:显式 maxW 的老键
|
|
2415
|
+
|| transcodeJobs.get(srcId + '|' + fps + '|' + effMaxW)
|
|
2340
2416
|
|| transcodeJobs.get(srcId + '|' + fps + '|' + maxW)
|
|
2341
2417
|
|| transcodeJobs.get(srcId + '|' + fps) || null;
|
|
2342
2418
|
} else if (ffmpegDownloadProgress) {
|
|
@@ -4807,8 +4883,7 @@ function apply(ctx) {
|
|
|
4807
4883
|
const fpsRaw = Number(url.searchParams.get('fps'));
|
|
4808
4884
|
const fps = Number.isFinite(fpsRaw) && ALLOWED_FPS.includes(fpsRaw) ? fpsRaw : 0;
|
|
4809
4885
|
if (!fps) { json(res, 400, { ok: false, error: 'invalid fps (allowed: 24/30/48/60)' }); return; }
|
|
4810
|
-
const
|
|
4811
|
-
const maxW = Number.isFinite(maxWRaw) && maxWRaw > 0 ? Math.min(3840, Math.round(maxWRaw)) : 0;
|
|
4886
|
+
const maxW = parseMaxWParam(url.searchParams.get('maxW'));
|
|
4812
4887
|
// ①(2026-09-23 B) 预缩档:`scale=lanczos`(白名单外 ⇒ 400,不把任意串拼进 -vf)。
|
|
4813
4888
|
// 它只影响**缩放滤波器**;目标宽仍由 maxW 决定(客户端按屏幕物理宽算 maxW)。
|
|
4814
4889
|
const scaleFlag = normalizeScaleFlag(url.searchParams.get('scale'));
|
package/package.json
CHANGED