dsh-mpkg-wallpaper 3.5.2 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lib/client.js +275 -116
  2. package/lib/index.js +54 -13
  3. package/package.json +5 -2
package/lib/client.js CHANGED
@@ -98,6 +98,11 @@ window.__ModuleLoader__.load({
98
98
  let persistTimer = null;
99
99
  let hostSettingsOk = false; // ⑤(新) host /settings 是否可用(持久化到宿主端文件)
100
100
  let sectionDirty = false; // ⑤(修正) 本次启动后用户是否已改过设置(防 host GET 竞态覆盖)
101
+ // ①(修正) 用户手动暂停/播放壁纸(停住画面)。wallUserPaused 是**唯一**权威事实来源,
102
+ // 只由 toggleWallPause(source='user')修改并执行暂停/恢复。
103
+ // ①(修正) 声明**上移**到模块级顶部(原在下方函数区):确保任何 video play/pause 事件
104
+ // 回调(ensureBgDom 阶段挂载)引用它时已初始化,避免 TDZ 报错。
105
+ let wallUserPaused = false;
101
106
  // ⑤(修正) 宿主端只存"设置"(外观类),不含大体积 image dataURL/临时 info——
102
107
  // 既避免每次滑块拖动 PUT 1MB+,也让恢复时壁纸选择(image 等)回落到 localStorage。
103
108
  // ①(修正) webUrl 只是短 URL(几十字节),不该在 SKIP 里——它不持久化到 host 会导致
@@ -666,7 +671,10 @@ window.__ModuleLoader__.load({
666
671
  const video = document.createElement("video");
667
672
  video.id = BG_VIDEO_ID;
668
673
  video.className = "mpw-bgVideo";
669
- video.autoplay = true;
674
+ // ①(修正) 不用 autoplay 属性——只要 autoplay 在,src 一重载浏览器就自动播放,
675
+ // 会**绕过** showVideoEl 里基于 (wallUserPaused||powPaused) 的 play() 暂停门控。
676
+ // 改为靠那个门控的 play() 统一播放:暂停时任何 src 重载都不重播。
677
+ // video.autoplay = true;
670
678
  video.loop = true;
671
679
  video.muted = true;
672
680
  video.playsInline = true;
@@ -681,6 +689,12 @@ window.__ModuleLoader__.load({
681
689
  console.warn("[dsh-mpkg-wallpaper] 视频背景加载失败(编码可能不被浏览器支持):", video.src);
682
690
  try { window.__mpwVideoFailed = true; } catch {}
683
691
  });
692
+ // ①(修正) 暂停按钮**实时**同步:监听 video 实际 play/pause 事件,刷新模块级
693
+ // wallPausedByUser 标志并派发自定义事件给设置页按钮(否则用户暂停后,任何
694
+ // 非本按钮路径(如切换壁纸、省电恢复、重载重播)导致的实际播放变化,按钮文案
695
+ // 不会跟着变 — 用户实测「暂停键不是实时检测」)。
696
+ video.addEventListener("play", () => { try { setWallPausedByUserState(false); } catch {} });
697
+ video.addEventListener("pause", () => { try { setWallPausedByUserState(true); } catch {} });
684
698
  wrap.appendChild(img);
685
699
  wrap.appendChild(video);
686
700
  // ①(新) 网页壁纸 iframe(web wallpaper):独立沙箱层,覆盖整屏。
@@ -875,10 +889,13 @@ window.__ModuleLoader__.load({
875
889
  edgeDraw = { ctx: g, video, wrap };
876
890
  if (edgeResizeObs) { try { edgeResizeObs.disconnect(); } catch {} }
877
891
  try { edgeResizeObs = new ResizeObserver(() => edgeDrawFrame()); edgeResizeObs.observe(canvas); } catch {}
878
- try { if (video.src !== url) video.src = url; } catch {}
892
+ // ①(修正) showVideoEl:用 getAttribute('src') 判等,避免 video.src(绝对)与 url(相对)恒不等
893
+ // 导致每次 applyFromStorage 都重设 src → Edge 端也重载重播。
894
+ try { if (video.getAttribute('src') !== url) video.src = url; } catch {}
879
895
  video.playbackRate = (typeof readSection().playbackRate === "number" && readSection().playbackRate >= 0.5 && readSection().playbackRate <= 2) ? readSection().playbackRate : 1;
880
896
  edgeDrawFrame();
881
- try { const p = video.play(); if (p && p.catch) p.catch(() => {}); } catch {}
897
+ // (修正) 暂停门控:Edge canvas 渲染路径也尊重暂停,避免其余路径重播
898
+ try { if (!(wallUserPaused || powPaused)) { const p = video.play(); if (p && p.catch) p.catch(() => {}); } } catch {}
882
899
  // ①(修正) 首帧前先画一帧(避免白屏)——用命名函数 + removeEventListener 防泄漏
883
900
  //(原来 {once:true} 在视频永不加载时会残留监听,评审指出)
884
901
  const drawOnce = () => { try { video.removeEventListener("loadeddata", drawOnce); } catch {} edgeDrawFrame(); };
@@ -945,14 +962,25 @@ window.__ModuleLoader__.load({
945
962
  if (canvas) canvas.style.display = "none";
946
963
  if (lastObjectUrl && lastObjectUrl !== url) { try { URL.revokeObjectURL(lastObjectUrl); } catch {} }
947
964
  lastObjectUrl = url;
948
- try { if (video.src !== url) video.src = url; } catch {}
965
+ // ①(修正) getAttribute('src')(原始相对串)比较,**别用 video.src**——
966
+ // video.src 读取返回**绝对** URL(http://host/api/...),而 url 是相对路径,
967
+ // 两者恒不相等 → 每次 applyFromStorage(调静音/亮度等**无关**设置)都重赋值
968
+ // video.src → 浏览器重载媒体源 → autoplay 自动播放,绕过下方 play() 暂停门控
969
+ // (用户实测:暂停后调静音壁纸又重播、暂停按钮与实际状态脱节)。
970
+ // getAttribute('src') 返回原始的相对字符串,同 url 相等 → 不重载,彻底避免。
971
+ try { if (video.getAttribute('src') !== url) video.src = url; } catch {}
949
972
  // ⑳(新) 视频倍速:原生 playbackRate(0.5-2x,即时生效,不重载)
950
973
  try {
951
974
  const rate = readSection().playbackRate;
952
975
  if (typeof rate === "number" && rate >= 0.5 && rate <= 2 && video.playbackRate !== rate) video.playbackRate = rate;
953
976
  } catch {}
954
977
  video.style.display = "";
955
- try { const p = video.play(); if (p && p.catch) p.catch(() => {}); } catch {}
978
+ // (修正) 暂停状态门控:用户手动暂停(wallUserPaused)或省电暂停(powPaused)
979
+ // 时不 play——否则任何设置开关 apply(commit→applyFromStorage→showVideoEl)都
980
+ // 无条件 play(),把用户手动暂停解开(用户实测"暂停后开启效果又解开暂停")。
981
+ if (!(wallUserPaused || powPaused)) {
982
+ try { const p = video.play(); if (p && p.catch) p.catch(() => {}); } catch {}
983
+ }
956
984
  }
957
985
  /** ①(新) 网页壁纸:iframe 全屏显示(隐藏 img/video)。 */
958
986
  function showWebEl(url) {
@@ -984,10 +1012,12 @@ window.__ModuleLoader__.load({
984
1012
  wrap.classList.add("mpw-web");
985
1013
  img.style.display = "none";
986
1014
  video.style.display = "none";
987
- try { if (frame.src !== url) frame.src = url; } catch {}
988
- // ①(修正) 同 URL 重入(如调 opacity 滑块 → applyFromStorage → showWebEl 同 URL):
1015
+ try { if (frame.getAttribute('src') !== url) frame.src = url; } catch {}
1016
+ // ①(修正) 同 URL 重入(如调 opacity/静音滑块 → applyFromStorage → showWebEl 同 URL):
989
1017
  // src 未变 → onload 不触发 → observer 不会重挂(评审发现的回归:旧 observer 已
990
1018
  // 被上面 disposeWebFrame 断开)。此时手动补挂,面板/媒体保障不丢。
1019
+ // ①(修正) 用 getAttribute('src') 判等(与上行一致)——frame.src 读回绝对 URL,
1020
+ // 与相对 url 恒不等会让每次 applyFromStorage 都重设 src → web 壁纸无关设置也重载。
991
1021
  try {
992
1022
  if (frame.getAttribute("src") === url) {
993
1023
  webMediaObserve(frame);
@@ -1365,7 +1395,11 @@ window.__ModuleLoader__.load({
1365
1395
  if (useTranscode) console.log("[dsh-mpkg-wallpaper] 转码播放:", playUrl);
1366
1396
  // ①(修正) 重启竞态兜底:video 也要 404 重试(否则 host 晚就绪 → 视频壁纸永久空白)
1367
1397
  const vid = bgElements().video;
1368
- if (vid && !vid.__mpwHostRetryVideo) {
1398
+ // (修正) 监听守卫:原 `!vid.__mpwHostRetryVideo` 在 error 发生前恒为 falsy
1399
+ // → 每次 applyFromStorage 都 addEventListener("error") 累积监听器(拖滑块
1400
+ // 100 次 = 100 个闭包空转)。改用独立布尔 __mpwErrWired,只加一次。
1401
+ if (vid && !vid.__mpwErrWired) {
1402
+ vid.__mpwErrWired = true;
1369
1403
  vid.__mpwHostTries = 0;
1370
1404
  vid.__mpwAutoTranscoded = false; // ①(新) 编码不支持 → 自动转码降级(只做一次)
1371
1405
  vid.addEventListener("error", () => {
@@ -1388,7 +1422,9 @@ window.__ModuleLoader__.load({
1388
1422
  vid.removeAttribute("src");
1389
1423
  vid.src = tcUrl;
1390
1424
  vid.load();
1391
- try { const pp = vid.play(); if (pp && pp.catch) pp.catch(() => {}); } catch {}
1425
+ // (修正) 暂停门控:自动转码重试也尊重暂停,否则用户暂停后
1426
+ // 编码报错自动切转码会重播(绕过 showVideoEl 的门控)。
1427
+ try { if (!(wallUserPaused || powPaused)) { const pp = vid.play(); if (pp && pp.catch) pp.catch(() => {}); } } catch {}
1392
1428
  } catch {}
1393
1429
  return;
1394
1430
  }
@@ -1407,7 +1443,8 @@ window.__ModuleLoader__.load({
1407
1443
  vid.removeAttribute("src");
1408
1444
  vid.src = cur;
1409
1445
  vid.load();
1410
- try { const pp = vid.play(); if (pp && pp.catch) pp.catch(() => {}); } catch {}
1446
+ // (修正) 暂停门控:404 重试不重播(同上,尊重暂停状态)
1447
+ try { if (!(wallUserPaused || powPaused)) { const pp = vid.play(); if (pp && pp.catch) pp.catch(() => {}); } } catch {}
1411
1448
  }, 800 * vid.__mpwHostTries);
1412
1449
  });
1413
1450
  }
@@ -1473,6 +1510,9 @@ window.__ModuleLoader__.load({
1473
1510
  } else if (typeof image === "string" && image.indexOf("idb:") === 0) {
1474
1511
  idbGet("bg").then((v) => { if (gen !== bgGen) return; if (v && img.src !== v) { img.src = v; showImageEl(); } }).catch(() => {});
1475
1512
  } else {
1513
+ // ①(修正) 内存:置 null 前 revoke 旧 Blob URL——否则清壁纸/切换到图片时
1514
+ // 上次的 blob(最大 600MB 视频)URL 仍存活在 registry,不可回收,只增不减。
1515
+ if (lastBgSig && lastBgSig.url) { try { URL.revokeObjectURL(lastBgSig.url); } catch {} }
1476
1516
  lastBgSig = null;
1477
1517
  // ①(修正) 非 web 壁纸:无条件卸载残留的 web iframe(否则帧泄漏,
1478
1518
  // 星野/瞬这类 web 壁纸清除后 iframe 仍存活,内存只增不减)。
@@ -1594,6 +1634,9 @@ window.__ModuleLoader__.load({
1594
1634
  try { updateClock(); } catch {}
1595
1635
  // ①(新) 液态玻璃叠加层(lgComposer/lgSidebar/lgHeader 任一开启时启动)
1596
1636
  try { applyLiquidGlass(section); } catch {}
1637
+ // ①(修正) 省电:每次 apply 后同步暂停状态——否则勾选"省电"开关后不立即生效
1638
+ //(原只有事件触发才 updatePowerPause,开关开了要等下次事件才暂停)
1639
+ try { updatePowerPause(); } catch {}
1597
1640
  }
1598
1641
 
1599
1642
  // ═══════════════════════════════════════════════════════════════════
@@ -1604,36 +1647,16 @@ window.__ModuleLoader__.load({
1604
1647
  let lgModule = null; // { LiquidGlassWebGLV2 }
1605
1648
  let lgStatus = ""; // 调试状态(显示在 tab 里)
1606
1649
  function setLgStatus(s) { lgStatus = s; try { notifySectionChanged(); } catch {} }
1607
- let lgGlass = null; // 实例
1608
- let lgCanvas = null; // 全屏玻璃 canvas
1609
- let lgResizeObs = null;
1610
- let lgBgVideoHooked = false;
1611
- const LG_EL_KEYS = [['lgComposer', '.wSkVaW_scrollBody, [data-slot*="composer"] [class*="card"], [data-composer-card]'], ['lgSidebar', '[class*="sidebarCol"], [data-dsh-better-sidebar] [class*="_panel"], [data-dsh-better-sidebar] [class*="_bottomPanel"]'], ['lgHeader', '.wSkVaW_header, [class*="wSkVaW_header"]']];
1612
- async function ensureLgModule() {
1613
- if (lgModule) return lgModule;
1614
- // ①(修正) 用 host 路由动态加载(不再内联 107KB 源码——内联的模板字符串
1615
- // 含反引号/${,会触发 DSH client-modules 打包器异常 → 插件 bundle 损坏
1616
- // → loadSection is not defined + 连带官方插件 bundle 失败,dsh 启动崩溃)。
1617
- // host 路由(/lg)在 index.js 注册,加载失败仅 console 警告,不影响主功能。
1618
- try {
1619
- const mod = await import(HOST_BASE + "/lg/v2.js");
1620
- lgModule = mod;
1621
- setLgStatus("✓ 模块加载成功");
1622
- return mod;
1623
- } catch (err) { console.warn("[dsh-mpkg-wallpaper] 液态玻璃模块加载失败:", err); setLgStatus("✗ 模块加载失败: " + (err && err.message || err)); return null; }
1624
- }
1650
+ const LG_EL_KEYS = [['lgComposer', '[data-composer-card], [data-slot*="composer"] [class*="card"], .wSkVaW_scrollBody'], ['lgSidebar', '[class*="sidebarCol"], [data-dsh-better-sidebar] [class*="_panel"], [data-dsh-better-sidebar] [class*="_bottomPanel"]'], ['lgHeader', '.wSkVaW_header, [class*="wSkVaW_header"]']];
1651
+ // ①(重做) 液态玻璃 bundle **base64 内联**(LG_BUNDLE_B64 由 tools/inline-lg-b64.mjs
1625
1652
  function lgEnabled(section) {
1626
1653
  return !!(section && ((section.lgComposer !== void 0 ? !!section.lgComposer : false)
1627
1654
  || (section.lgSidebar !== void 0 ? !!section.lgSidebar : false)
1628
1655
  || (section.lgHeader !== void 0 ? !!section.lgHeader : false)));
1629
1656
  }
1630
1657
  function destroyLiquidGlass() {
1631
- try { if (lgResizeObs) { lgResizeObs.disconnect(); lgResizeObs = null; } } catch {}
1632
- try { if (lgGlass && typeof lgGlass.stop === "function") lgGlass.stop(); } catch {}
1633
- lgGlass = null;
1634
- try { if (lgCanvas) { lgCanvas.remove(); } } catch {}
1635
- lgCanvas = null;
1636
- lgBgVideoHooked = false;
1658
+ // (修正) CSS 版:只用 lgClearClasses 清除标记即可(WebGL 残留已删)
1659
+ lgClearClasses();
1637
1660
  }
1638
1661
  /** 收集当前启用的玻璃目标元素(按开关),返回 { el, shape } 列表。 */
1639
1662
  function lgTargets(section) {
@@ -1647,84 +1670,143 @@ window.__ModuleLoader__.load({
1647
1670
  }
1648
1671
  return out;
1649
1672
  }
1673
+ // ①(重做) 液态玻璃改为 **CSS 版**(方案1:稳定可靠,不崩)。
1674
+ // 之前用 WebGL 库(setBackdrop + 喂 DSH 元素坐标)bug:setBackdrop 用残留壁纸帧
1675
+ // 透出"已清除的壁纸"、overlay 模式取代目标区域内容(用户 4 张截图确认)。
1676
+ // CSS 版:给目标元素加 backdrop-filter(磨砂)+ 半透明背景,模拟液态玻璃——
1677
+ // 不覆盖、不透壁纸(backdrop-filter 只模糊元素背后,不画壁纸帧)、不取代内容。
1678
+ const LG_CSS_ON = 'data-mpw-lg-css';
1679
+ function lgClearClasses() {
1680
+ try { document.querySelectorAll('[' + LG_CSS_ON + ']').forEach((el) => { try { el.removeAttribute(LG_CSS_ON); } catch {} }); } catch {}
1681
+ }
1650
1682
  async function applyLiquidGlass(section) {
1651
1683
  try {
1652
- if (!lgEnabled(section)) { destroyLiquidGlass(); return; }
1653
- const mod = await ensureLgModule();
1654
- if (!mod || !mod.LiquidGlassWebGLV2) { setLgStatus("✗ 模块加载失败(检查 /lg 路由)"); return; }
1655
- setLgStatus("✓ 模块加载成功");
1684
+ if (!lgEnabled(section)) { lgClearClasses(); destroyLiquidGlass(); return; }
1685
+ lgClearClasses();
1656
1686
  const targets = lgTargets(section);
1657
- if (!targets.length) { setLgStatus("✗ 未匹配到玻璃目标元素(检查开关与 DSH 元素)"); destroyLiquidGlass(); return; }
1658
- setLgStatus("✓ 匹配 " + targets.length + " 个元素");
1659
- if (!lgCanvas) {
1660
- lgCanvas = document.createElement("canvas");
1661
- lgCanvas.id = "mpw-lg-canvas";
1662
- lgCanvas.style.cssText = "position:fixed;inset:0;width:100%;height:100%;z-index:9999;pointer-events:none;";
1663
- (document.body || document.documentElement).appendChild(lgCanvas);
1664
- lgGlass = new mod.LiquidGlassWebGLV2(lgCanvas, { preserveDrawingBuffer: true, compositeMode: "overlay" });
1665
- // 背景:优先当前壁纸 video(live),否则静态渐变
1666
- try {
1667
- const vid = bgElements().video;
1668
- if (vid && vid.readyState >= 2 && vid.videoWidth > 0) {
1669
- lgGlass.setBackdrop(vid, { update: "live", autoStart: true });
1670
- lgBgVideoHooked = true;
1671
- } else {
1672
- lgGlass.setBackdrop(makeLgFallback(), { update: "static" });
1673
- }
1674
- } catch { try { lgGlass.setBackdrop(makeLgFallback(), { update: "static" }); } catch {} }
1687
+ if (!targets.length) { setLgStatus("✗ 未匹配到玻璃目标元素"); destroyLiquidGlass(); return; }
1688
+ setLgStatus("✓ CSS 玻璃作用于 " + targets.length + " 个元素");
1689
+ // 给每个目标元素标记,buildCss 的 lg 规则会让它变玻璃(backdrop-filter 磨砂)
1690
+ targets.forEach((t) => { try { t.el.setAttribute(LG_CSS_ON, ""); } catch {} });
1691
+ } catch (err) { console.warn("[dsh-mpkg-wallpaper] 液态玻璃 CSS 应用失败:", err); setLgStatus("✗ " + (err && err.message || err)); }
1692
+ }
1693
+ /** 静态渐变兜底背景(视频不可用时)。 */
1694
+
1695
+ // ═══════════════════════════════════════════════════════════════════
1696
+ // 🔋(新) 省电(遮挡暂停三档,借鉴 elysia395):页面最小化/切页、窗口失焦、
1697
+ // 电池供电时自动暂停壁纸视频(解码归零),回到界面/接通电源自动继续。
1698
+ // 三档独立开关(powPauseHidden/powPauseBlur/powPauseBattery),持久保存。
1699
+ // ═══════════════════════════════════════════════════════════════════
1700
+ let powBattery = null; // BatteryManager(getBattery 探测)
1701
+ let powBatteryAsked = false;
1702
+ let powPaused = false; // 当前是否被省电暂停
1703
+ let powHiddenNow = false; // 页面当前隐藏
1704
+ let powFocusedNow = true; // 窗口当前聚焦
1705
+ /** 当前壁纸视频暂停(停住画面)。 */
1706
+ /** web 壁纸 iframe 内所有 video/audio 暂停(省电/暂停按钮对 web 生效)。 */
1707
+ function pauseWebFrame() {
1708
+ try {
1709
+ const { frame } = bgElements();
1710
+ if (!frame) return;
1711
+ const doc = frame.contentDocument;
1712
+ if (doc) {
1713
+ const els = doc.querySelectorAll("video,audio");
1714
+ for (let i = 0; i < els.length; i++) { try { els[i].pause(); } catch {} }
1675
1715
  }
1676
- // 元素几何对齐
1677
- const els = targets.map((t, i) => {
1678
- const r = t.el.getBoundingClientRect();
1679
- return { id: "lg" + i, shape: "rect", x: r.left, y: r.top, width: r.width, height: r.height };
1680
- });
1681
- try { lgGlass.setElements(els); } catch {}
1682
- if (!lgResizeObs) {
1683
- try {
1684
- lgResizeObs = new ResizeObserver(() => {
1685
- try {
1686
- if (!lgGlass) return;
1687
- // 元素可能移动/缩放:重算几何后重渲染
1688
- const els2 = targets.map((t, i) => {
1689
- const r = t.el.getBoundingClientRect();
1690
- return { id: "lg" + i, shape: "rect", x: r.left, y: r.top, width: r.width, height: r.height };
1691
- });
1692
- lgGlass.setElements(els2);
1693
- lgGlass.render();
1694
- } catch {}
1695
- });
1696
- targets.forEach((t) => { try { lgResizeObs.observe(t.el); } catch {} });
1697
- } catch {}
1698
- } else {
1699
- // 已有 observer:确保覆盖新目标
1700
- try { targets.forEach((t) => { try { lgResizeObs.observe(t.el); } catch {} }); } catch {}
1716
+ } catch {}
1717
+ }
1718
+ /** web 壁纸 iframe 内所有 video/audio 恢复播放。 */
1719
+ function resumeWebFrame() {
1720
+ try {
1721
+ const { frame } = bgElements();
1722
+ if (!frame) return;
1723
+ const doc = frame.contentDocument;
1724
+ if (doc) {
1725
+ const els = doc.querySelectorAll("video,audio");
1726
+ for (let i = 0; i < els.length; i++) { try { const p = els[i].play(); if (p && p.catch) p.catch(() => {}); } catch {} }
1701
1727
  }
1702
- // 视频就绪后切换 live 背景
1703
- if (!lgBgVideoHooked) {
1704
- try {
1705
- const vid = bgElements().video;
1706
- if (vid) {
1707
- vid.addEventListener("loadeddata", () => {
1708
- try { if (lgGlass && vid.videoWidth > 0) { lgGlass.setBackdrop(vid, { update: "live", autoStart: true }); lgBgVideoHooked = true; } } catch {}
1709
- }, { once: true });
1710
- }
1711
- } catch {}
1728
+ } catch {}
1729
+ }
1730
+ function pauseWallpaperVideo() {
1731
+ try {
1732
+ const { video, frame } = bgElements();
1733
+ if (video && !video.paused) video.pause();
1734
+ if (frame && frame.style.display !== "none") pauseWebFrame();
1735
+ } catch {}
1736
+ }
1737
+ /** 当前壁纸视频恢复播放(用户手动暂停时恢复——由调用方判断, 见 toggleWallPause)。 */
1738
+ function resumeWallpaperVideo() {
1739
+ try {
1740
+ const s = readSection();
1741
+ if (!(s.enabled !== void 0 ? !!s.enabled : true)) return;
1742
+ const { video, frame } = bgElements();
1743
+ if (video && video.paused && video.src) {
1744
+ try { const p = video.play(); if (p && p.catch) p.catch(() => {}); } catch {}
1712
1745
  }
1713
- } catch (err) { console.warn("[dsh-mpkg-wallpaper] 液态玻璃叠加失败:", err); }
1746
+ if (frame && frame.style.display !== "none") resumeWebFrame();
1747
+ } catch {}
1714
1748
  }
1715
- /** 静态渐变兜底背景(视频不可用时)。 */
1716
- function makeLgFallback() {
1749
+ /** 按三档状态决定暂停/恢复(任一档触发即暂停,全恢复才继续)。 */
1750
+ function updatePowerPause() {
1717
1751
  try {
1718
- const c = document.createElement("canvas");
1719
- c.width = 800; c.height = 600;
1720
- const g = c.getContext("2d");
1721
- if (!g) return c;
1722
- const grad = g.createRadialGradient(300, 200, 50, 400, 300, 600);
1723
- grad.addColorStop(0, "#4a6cf7"); grad.addColorStop(0.5, "#8a4ad8"); grad.addColorStop(1, "#123456");
1724
- g.fillStyle = grad; g.fillRect(0, 0, 800, 600);
1725
- for (let i = 0; i < 8; i++) { g.beginPath(); g.arc(Math.random() * 800, Math.random() * 600, 20 + Math.random() * 50, 0, Math.PI * 2); g.fillStyle = "rgba(255,255,255," + (0.05 + Math.random() * 0.1) + ")"; g.fill(); }
1726
- return c;
1727
- } catch { return null; }
1752
+ const s = readSection();
1753
+ const hiddenOn = s.powPauseHidden !== void 0 ? !!s.powPauseHidden : false;
1754
+ const blurOn = s.powPauseBlur !== void 0 ? !!s.powPauseBlur : false;
1755
+ const battOn = s.powPauseBattery !== void 0 ? !!s.powPauseBattery : false;
1756
+ const shouldPause = (hiddenOn && powHiddenNow) || (blurOn && !powFocusedNow) || (battOn && powBattery && powBattery.charging === false);
1757
+ if (shouldPause && !powPaused) { powPaused = true; pauseWallpaperVideo(); }
1758
+ else if (!shouldPause && powPaused) { powPaused = false; resumeWallpaperVideo(); }
1759
+ } catch {}
1760
+ }
1761
+ /** 注册省电监听(一次性)。 */
1762
+ function setupPowerSave() {
1763
+ if (window.__mpwPowerWired) return;
1764
+ window.__mpwPowerWired = true;
1765
+ try {
1766
+ document.addEventListener("visibilitychange", () => {
1767
+ powHiddenNow = document.hidden === true;
1768
+ updatePowerPause();
1769
+ });
1770
+ } catch {}
1771
+ try {
1772
+ window.addEventListener("blur", () => { powFocusedNow = false; updatePowerPause(); });
1773
+ window.addEventListener("focus", () => { powFocusedNow = true; updatePowerPause(); });
1774
+ } catch {}
1775
+ // 电池状态(非标准 API,存在则用,不存在静默跳过)
1776
+ try {
1777
+ if (typeof navigator !== "undefined" && navigator.getBattery && !powBatteryAsked) {
1778
+ powBatteryAsked = true;
1779
+ navigator.getBattery().then((b) => {
1780
+ powBattery = b;
1781
+ b.addEventListener("chargingchange", () => updatePowerPause());
1782
+ b.addEventListener("levelchange", () => updatePowerPause());
1783
+ updatePowerPause();
1784
+ }).catch(() => {});
1785
+ }
1786
+ } catch {}
1787
+ // 初始化一次
1788
+ try { powHiddenNow = !!(document && document.hidden); updatePowerPause(); } catch {}
1789
+ }
1790
+ /** ①(修正) 设置页暂停标志更新 + 派发 mpw:wallpaused 事件,供按钮实时刷新。
1791
+ * - source='user':用户点按钮 → 改权威 wallUserPaused + 执行暂停/恢复。
1792
+ * - source='video':视频实际 play/pause 事件 → **只派发展示事件**,不改权威值。
1793
+ * (否则 video 加载新 src 时的瞬态 pause 会把「未暂停」误置「已暂停」,跳过门控。)
1794
+ * - source='state':设置页内部同步显示(不改权威)。 */
1795
+ function setWallPausedByUserState(paused, source) {
1796
+ try {
1797
+ if (source === "user") {
1798
+ wallUserPaused = paused;
1799
+ if (paused) pauseWallpaperVideo();
1800
+ else resumeWallpaperVideo();
1801
+ // ①(修正) notifySectionChanged 只在用户操作时触发(改权威值后刷新设置页);
1802
+ // video 实际 play/pause 事件(source 非 user)不触发,避免每帧瞬态多余通知。
1803
+ try { notifySectionChanged(); } catch {}
1804
+ }
1805
+ window.dispatchEvent(new CustomEvent("mpw:wallpaused", { detail: { paused: wallUserPaused } }));
1806
+ } catch {}
1807
+ }
1808
+ function toggleWallPause() {
1809
+ setWallPausedByUserState(!wallUserPaused, "user");
1728
1810
  }
1729
1811
 
1730
1812
  // ═══════════════════════════════════════════════════════════════════
@@ -2188,10 +2270,10 @@ window.__ModuleLoader__.load({
2188
2270
  headerBlur: false, headerBlurAmount: 0, headerBg: false, aquaMask: false, aquaTint: false,
2189
2271
  aquaInk: false, aquaTextEnhance: false, todoBlur: false, clock: false, clock24h: false,
2190
2272
  clockSec: false, clockDate: false, themeColor: "", accent: "", glassWindow: false,
2191
- // ①(修正) 侧边栏保持透出壁纸(sidebar:true):lgTest 是纯净环境测玻璃,
2192
- // 玻璃折射需要侧边栏透明透出壁纸才有意义;原 sidebar:false 让侧边栏
2193
- // 变不透明 → 用户实测"侧边栏不变透明、没玻璃效果"。
2194
- sidebar: true, sharp: false,
2273
+ // ①(修正) lgTest 测试模式下侧边栏**不透明**(sidebar:false):用户实测
2274
+ // 测试模式侧边栏透壁纸,界面花、看不清内容。测试模式要的是干净对比界面
2275
+ // (壁纸在右侧聊天区可测玻璃),侧边栏不透明便于看清。
2276
+ sidebar: false, sharp: false,
2195
2277
  });
2196
2278
  }
2197
2279
  // ①(新) 悬浮效果开关(buildCss 内声明,之前误加在 applyFromStorage 里导致 ReferenceError)
@@ -2956,6 +3038,40 @@ body[data-mpw-todo-blur] [data-tool="todo_write"] * {
2956
3038
  }
2957
3039
  `;
2958
3040
  }
3041
+ // ①(新) 液态玻璃(CSS 版):给标记 [data-mpw-lg-css] 的目标元素加磨砂玻璃
3042
+ // 材质(半透明 + backdrop-filter 模糊背后内容)。不清壁纸、不覆盖内容、不崩。
3043
+ // 不同元素不同 blur 半径:输入框/侧边栏/标题栏各自可调(复用 lg 开关)。
3044
+ css += `
3045
+ /* ── 液态玻璃(CSS 版)──
3046
+ ①(修正) **侧边栏玻璃不能用 backdrop-filter**:sidebarCol 是 DSH 设置弹窗的祖先
3047
+ (弹窗渲染在 sidebarCol > settingsArea 内),backdrop-filter 会创建 containing block
3048
+ → 设置弹窗被"困"进侧边栏压缩成窄条(用户实测;与之前 Via 设置被压缩同根因)。
3049
+ 故 sidebarCol 的玻璃**只半透明 + 边缘高光**(无 blur),磨砂由壁纸层 blur 提供
3050
+ (见 G/E 块)。输入框/标题栏不是设置弹窗祖先,可用 backdrop-filter。 */
3051
+ /* 通用玻璃材质(输入框/标题栏等):半透明 + backdrop blur */
3052
+ body:not([data-ds-dark-theme]) [data-mpw-lg-css] {
3053
+ background-color: color-mix(in srgb, #eef1f7 45%, transparent) !important;
3054
+ }
3055
+ body[data-ds-dark-theme] [data-mpw-lg-css] {
3056
+ background-color: color-mix(in srgb, #10141f 45%, transparent) !important;
3057
+ }
3058
+ /* backdrop blur 仅用于**非侧边栏**目标(输入框/标题栏);sidebarCol 排除 */
3059
+ body:not([data-ds-dark-theme]) [data-mpw-lg-css]:not([class*="sidebarCol"]):not([class*="wSkVaW_header"]) {
3060
+ backdrop-filter: blur(${(() => { const v = Number(section.lgBlur); return Number.isFinite(v) ? Math.max(4, Math.min(40, v)) : 18; })()}px) saturate(1.4) !important;
3061
+ -webkit-backdrop-filter: blur(${(() => { const v = Number(section.lgBlur); return Number.isFinite(v) ? Math.max(4, Math.min(40, v)) : 18; })()}px) saturate(1.4) !important;
3062
+ }
3063
+ body[data-ds-dark-theme] [data-mpw-lg-css]:not([class*="sidebarCol"]):not([class*="wSkVaW_header"]) {
3064
+ backdrop-filter: blur(${(() => { const v = Number(section.lgBlur); return Number.isFinite(v) ? Math.max(4, Math.min(40, v)) : 18; })()}px) saturate(1.4) !important;
3065
+ -webkit-backdrop-filter: blur(${(() => { const v = Number(section.lgBlur); return Number.isFinite(v) ? Math.max(4, Math.min(40, v)) : 18; })()}px) saturate(1.4) !important;
3066
+ }
3067
+ /* 玻璃边缘高光(液态玻璃感):顶部一条细亮边 */
3068
+ [data-mpw-lg-css] { position: relative; }
3069
+ [data-mpw-lg-css]:before {
3070
+ content: ""; position: absolute; top: 0; left: 0; right: 0; height: 1px;
3071
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.35) 25%, rgba(255,255,255,0.35) 75%, transparent);
3072
+ pointer-events: none; z-index: 1;
3073
+ }
3074
+ `;
2959
3075
  return css + buildUiCss(section, dlgFrosted);
2960
3076
  }
2961
3077
 
@@ -3725,6 +3841,7 @@ body[data-mpw-modal] [role="dialog"] {
3725
3841
  setSettingsTab(id);
3726
3842
  };
3727
3843
  const [mpkgMeta, setMpkgMeta] = react.useState(initMeta); // { name, key, info, entryName, slot }
3844
+ const [wallPausedByUser, setWallPausedByUser] = react.useState(wallUserPaused); // ①(新) 用户手动暂停壁纸(初值取权威 wallUserPaused,避免重开设置页显示错)
3728
3845
  const [busy, setBusy] = react.useState(false);
3729
3846
  // ①(修正) hint 自动超时清空(5 秒):用户反馈「恢复所有默认设置」下方一直挂着
3730
3847
  // 「已应用壁纸:xxx」——hint 是操作结果提示,常驻会让用户误以为是与恢复默认相关
@@ -3733,6 +3850,13 @@ body[data-mpw-modal] [role="dialog"] {
3733
3850
  const hintTimerRef = react.useRef(null);
3734
3851
  // ①(修正) 卸载时清理 hint 计时器(防对已卸载组件 setState)
3735
3852
  react.useEffect(() => () => { try { if (hintTimerRef.current) clearTimeout(hintTimerRef.current); } catch {} }, []);
3853
+ // ①(修正) 暂停按钮**实时**同步:监听 mpw:wallpaused 事件(视频实际 play/pause 变化
3854
+ // 或用户点按钮都会派发),把 wallPausedByUser 同步为权威 wallUserPaused 的值。
3855
+ react.useEffect(() => {
3856
+ const onWallPaused = (e) => { try { setWallPausedByUser(!!(e && e.detail && e.detail.paused)); } catch {} };
3857
+ try { window.addEventListener("mpw:wallpaused", onWallPaused); } catch {}
3858
+ return () => { try { window.removeEventListener("mpw:wallpaused", onWallPaused); } catch {} };
3859
+ }, []);
3736
3860
  const setHint = (msg) => {
3737
3861
  _setHintRaw(msg);
3738
3862
  if (hintTimerRef.current) { try { clearTimeout(hintTimerRef.current); } catch {} }
@@ -4712,7 +4836,7 @@ body[data-mpw-modal] [role="dialog"] {
4712
4836
  // ①(修正) 类型校验(评审指出:原实现直接透传,`opacity:"abc"` 会污染设置):
4713
4837
  // 数值类必须为有限数、布尔类必须为 boolean、字符串类必须为 string。
4714
4838
  const numFields = ["opacity", "blur", "zoom", "headerBlurAmount", "dialogAmount", "popoverAmount", "maskAmount", "unifyAmount", "sidebarAlpha", "aquaMaskAlpha", "aquaTintStrength", "glassAlpha", "rotateMin", "brightness", "playbackRate", "clockSize", "fpsCap", "resMax", "bsRevealAlpha"];
4715
- const boolFields = ["sidebar", "sharp", "headerBlur", "headerBg", "dialogBlur", "popoverBlur", "maskBlur", "unifyTint", "chatFollow", "sessionFollow", "aquaMask", "aquaTint", "aquaInk", "aquaTextEnhance", "todoBlur", "hybrid", "roundCompat", "rotate", "float", "newStyle", "mute", "thinkBg", "enabled", "forceEnabled", "clock", "clock24h", "clockSec", "clockDate", "glassWindow", "bsCompat", "bsFloat", "bsFont", "bsReveal", "bsAlpha", "bsAqua", "bsBottomAvoid", "lgTest", "lgComposer", "lgSidebar", "lgHeader"];
4839
+ const boolFields = ["sidebar", "sharp", "headerBlur", "headerBg", "dialogBlur", "popoverBlur", "maskBlur", "unifyTint", "chatFollow", "sessionFollow", "aquaMask", "aquaTint", "aquaInk", "aquaTextEnhance", "todoBlur", "hybrid", "roundCompat", "rotate", "float", "newStyle", "mute", "thinkBg", "enabled", "forceEnabled", "clock", "clock24h", "clockSec", "clockDate", "glassWindow", "bsCompat", "bsFloat", "bsFont", "bsReveal", "bsAlpha", "bsAqua", "bsBottomAvoid", "lgTest", "lgComposer", "lgSidebar", "lgHeader", "powPauseHidden", "powPauseBlur", "powPauseBattery"];
4716
4840
  const strFields = ["aquaColor", "aquaInkColor", "themeColor", "accent", "glassColor", "clockPos", "fontColorGrayColor"];
4717
4841
  for (const k of BACKUP_FIELDS) {
4718
4842
  const v = settings[k];
@@ -4786,6 +4910,8 @@ body[data-mpw-modal] [role="dialog"] {
4786
4910
  }
4787
4911
  if (s.image === "idb:blob") {
4788
4912
  // idb 视频/动图:强制重建 ObjectURL 重缓冲
4913
+ // ①(修正) 内存:先 revoke 旧 blob URL 再清引用,防泄漏
4914
+ if (lastBgSig && lastBgSig.url) { try { URL.revokeObjectURL(lastBgSig.url); } catch {} }
4789
4915
  lastBgSig = null;
4790
4916
  applyFromStorage();
4791
4917
  setHint(t("refresh.done"));
@@ -5003,6 +5129,8 @@ body[data-mpw-modal] [role="dialog"] {
5003
5129
  }, true);
5004
5130
  setMpkgMeta(meta);
5005
5131
  // ①(修正) 视频→图片切换:强制清掉背景内容缓存,确保新壁纸立即显示
5132
+ // ①(修正) 内存:先 revoke 旧 blob URL 再清引用,防切换泄漏
5133
+ if (lastBgSig && lastBgSig.url) { try { URL.revokeObjectURL(lastBgSig.url); } catch {} }
5006
5134
  try { lastBgSig = null; } catch {}
5007
5135
  // ①(修正) 仅当最终显示的是图片/GIF(非 mp4)才提示预览模式;
5008
5136
  // 独立 mp4 视频壁纸(洛茜_01 等)不弹窗、不显示 GIF 提示
@@ -5362,6 +5490,10 @@ body[data-mpw-modal] [role="dialog"] {
5362
5490
  ])
5363
5491
  : null,
5364
5492
  h("div", { className: "mpw_inline" }, [
5493
+ // ①(新) 暂停/播放:视频/web 类壁纸可暂停(停住画面),再点恢复。
5494
+ // ①(修正) 只调 toggleWallPause()——它内部更新权威 wallUserPaused + 派发
5495
+ // mpw:wallpaused 事件,组件监听后实时刷新 wallPausedByUser,不必手动翻转。
5496
+ (section.converted === "mp4" || section.converted === "web") ? h("button", { className: "mpw_reset", type: "button", onClick: () => { toggleWallPause(); } }, wallPausedByUser ? t("pause.play") : t("pause.pause")) : null,
5365
5497
  h("button", { className: "mpw_reset", type: "button", onClick: refreshBg }, t("refresh.bg")),
5366
5498
  h("button", { className: "mpw_reset", type: "button", onClick: clearBg }, t("clear.bg"))
5367
5499
  ])
@@ -6182,6 +6314,11 @@ body[data-mpw-modal] [role="dialog"] {
6182
6314
  // ①(新) 第三方 UI 圆角兼容开关(其他组)
6183
6315
  toggleRow(t("roundCompat"), t("roundCompat.desc"), "roundCompat", DEFAULT_ROUND_COMPAT),
6184
6316
 
6317
+ // ①(新) 省电(遮挡暂停三档,借鉴 elysia395):页面隐藏/失焦/电池供电时暂停壁纸
6318
+ toggleRow(t("powPauseHidden"), t("powPauseHidden.desc"), "powPauseHidden", false),
6319
+ toggleRow(t("powPauseBlur"), t("powPauseBlur.desc"), "powPauseBlur", false),
6320
+ toggleRow(t("powPauseBattery"), t("powPauseBattery.desc"), "powPauseBattery", false),
6321
+
6185
6322
  // ①(新) 检测更新 / 一键热更新
6186
6323
  h("div", { className: "mpw_field" }, [
6187
6324
  h("label", { className: "mpw_label" }, t("update.title")),
@@ -6429,6 +6566,10 @@ body[data-mpw-modal] [role="dialog"] {
6429
6566
  "nav": "壁纸引擎背景",
6430
6567
  "master": "启用壁纸引擎背景功能",
6431
6568
  "clear.bg": "清除背景",
6569
+ "pause.pause": "暂停壁纸",
6570
+ "pause.play": "播放壁纸",
6571
+ "pause.pause": "暂停壁纸",
6572
+ "pause.play": "播放壁纸",
6432
6573
  "refresh.bg": "刷新壁纸",
6433
6574
  "refresh.done": "壁纸已刷新",
6434
6575
  "master.desc": "开启后应用所选背景",
@@ -6647,6 +6788,12 @@ body[data-mpw-modal] [role="dialog"] {
6647
6788
  "update.chooseBody": "推荐先到插件市场更新(semver 检测与市场一致)。确认仍用本插件的直接更新(拉取 GitHub 代码写回本地)吗?版本 ",
6648
6789
  "update.confirmSelf": "直接用本插件更新",
6649
6790
  "roundCompat.desc": "给其他插件注入界面的矩形容器补圆角(不覆盖插件自己的样式);如与某插件冲突可关闭",
6791
+ "powPauseHidden": "省电·页面隐藏/切页暂停",
6792
+ "powPauseHidden.desc": "最小化/切页时暂停壁纸视频(解码归零),回来自动继续",
6793
+ "powPauseBlur": "省电·失焦暂停",
6794
+ "powPauseBlur.desc": "窗口失去焦点(切到其他窗口)时暂停壁纸,回来继续",
6795
+ "powPauseBattery": "省电·电池供电暂停",
6796
+ "powPauseBattery.desc": "使用电池供电时不播放壁纸(省电),接通电源自动恢复",
6650
6797
  "error.title": "导入失败",
6651
6798
  "preview.title": "预览模式",
6652
6799
  "preview.desc": "该壁纸当前以预览图(GIF/图片)显示,浏览器无法播放其动态内容(Live2D 场景/高清视频需壁纸引擎 App 渲染)。",
@@ -6767,6 +6914,8 @@ body[data-mpw-modal] [role="dialog"] {
6767
6914
  "nav": "MPKG Wallpaper",
6768
6915
  "master": "Enable mpkg background",
6769
6916
  "clear.bg": "Clear background",
6917
+ "pause.pause": "Pause wallpaper",
6918
+ "pause.play": "Play wallpaper",
6770
6919
  "refresh.bg": "Refresh wallpaper",
6771
6920
  "refresh.done": "Wallpaper refreshed",
6772
6921
  "master.desc": "On applies the chosen background",
@@ -7005,6 +7154,12 @@ body[data-mpw-modal] [role="dialog"] {
7005
7154
  "update.chooseBody": "Prefer updating from the plugin market (semver check matches the market). Still update directly via this plugin (pull GitHub code and write it back locally)? v",
7006
7155
  "update.confirmSelf": "Update via this plugin",
7007
7156
  "roundCompat.desc": "Adds border-radius to rectangular containers injected by other plugins (does not override their own styles); turn off if it conflicts",
7157
+ "powPauseHidden": "Power save · pause when hidden",
7158
+ "powPauseHidden.desc": "Pause the wallpaper video when the page is hidden/in a background tab (decoder to zero), resume on return",
7159
+ "powPauseBlur": "Power save · pause on blur",
7160
+ "powPauseBlur.desc": "Pause the wallpaper when the window loses focus, resume when refocused",
7161
+ "powPauseBattery": "Power save · pause on battery",
7162
+ "powPauseBattery.desc": "Do not play the wallpaper when on battery power; resume when plugged in",
7008
7163
  "error.title": "Import failed",
7009
7164
  "preview.title": "Preview mode",
7010
7165
  "preview.desc": "This wallpaper is currently shown as a preview image (GIF/picture); the browser cannot play its dynamic content (Live2D scene / HD video requires the Wallpaper Engine app).",
@@ -7298,15 +7453,19 @@ body[data-mpw-modal] [role="dialog"] {
7298
7453
  // 按 timeSrc 按需提取,单槽峰值 ~50MB;④(新) timeOverride 手动锁定时暂停自动切换)
7299
7454
  try {
7300
7455
  setInterval(() => {
7301
- const s = readSection();
7302
- if (!s.timeVideos || !s.timeConfig || !s.timeConfig.enabled) return;
7303
- // () 手动锁定时段:不再随时间自动切换,直到点「自动」
7304
- if (s.timeOverride) return;
7305
- const slot = slotForTime(s.timeConfig, new Date());
7306
- if (slot === s.activeSlot) return;
7307
- swapTimeSlot(slot);
7456
+ try {
7457
+ const s = readSection();
7458
+ if (!s.timeVideos || !s.timeConfig || !s.timeConfig.enabled) return;
7459
+ // () 手动锁定时段:不再随时间自动切换,直到点「自动」
7460
+ if (s.timeOverride) return;
7461
+ const slot = slotForTime(s.timeConfig, new Date());
7462
+ if (slot === s.activeSlot) return;
7463
+ swapTimeSlot(slot);
7464
+ } catch { /* 时段配置畸形等异常不得成为未捕获定时器错误 */ }
7308
7465
  }, 60000);
7309
7466
  } catch {}
7467
+ // ①(新) 省电(遮挡暂停三档):页面隐藏/失焦/电池供电时暂停壁纸,回来自动继续
7468
+ try { setupPowerSave(); } catch {}
7310
7469
  }
7311
7470
  const sectionInjected = () => ({
7312
7471
  commit: () => { applyFromStorage(); }
package/lib/index.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import { createWriteStream, createReadStream, mkdirSync, existsSync, statSync, readFileSync, readdirSync, writeFileSync, openSync, readSync, closeSync, renameSync, unlinkSync, chmodSync, writeSync } from 'node:fs';
8
8
  import { join, resolve, sep, isAbsolute } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
+ import os from 'node:os';
10
11
  // tmpdir 不再使用(持久目录用 DATA_DIR)
11
12
  import crypto from 'node:crypto';
12
13
  import { execFileSync, spawn } from 'node:child_process';
@@ -41,7 +42,10 @@ const STEAM_PROBE_DIRS = [
41
42
  /** ①(修正) 持久数据目录:放 DSH_HOME/HOME 下而非 tmpdir —— Termux/proot 环境
42
43
  * /tmp 每次重启清空 → 重启后 mpkg 与 customDir 全部丢失(用户实测)。
43
44
  * 用持久目录:上传的 mpkg、custom-dir.json、token 映射重启后都能恢复。 */
44
- const DATA_DIR = join(process.env.DSH_HOME || process.env.HOME || '.', '.dsh-mpkg-wallpaper');
45
+ // ①(修正) os.homedir() 兜底:Windows 默认**无 process.env.HOME**(原回退 '.' 相对路径
46
+ // → cwd 不可写时 /settings、/upload、customDir、上传 mpkg 全部静默丢失)。os.homedir()
47
+ // 跨平台正确(Windows/ macOS/Linux/WSL)。DSH_HOME 优先,其次 os.homedir()。
48
+ const DATA_DIR = join(process.env.DSH_HOME || os.homedir() || '.', '.dsh-mpkg-wallpaper');
45
49
 
46
50
  /** token → { path, size, dataStart, entries } */
47
51
  const files = new Map();
@@ -237,12 +241,20 @@ function steamPathFromRegistry() {
237
241
  } catch { return null; }
238
242
  }
239
243
 
240
- /** ③ 自动发现:定位壁纸引擎安装目录(Steam libraryfolders.vdf + 常见路径探测)。 */
244
+ /** ③ 自动发现:定位壁纸引擎安装目录(Steam libraryfolders.vdf + 常见路径探测)。
245
+ * ①(修正) 补非 Windows 平台探测:macOS/Linux/WSL 也各自有 Steam 安装路径。
246
+ * 路径是纯字符串,平台用不到时 existsSync 自然为 false,无副作用。 */
247
+ const STEAM_PROBE_DIRS_NONWIN = [
248
+ ...(process.platform === 'darwin' ? [join(os.homedir(), 'Library', 'Application Support', 'Steam')] : []),
249
+ ...(process.platform === 'linux' || process.platform === 'android' ? [join(os.homedir(), '.local', 'share', 'Steam')] : []),
250
+ '/mnt/c/Program Files (x86)/Steam', // WSL
251
+ '/mnt/c/Program Files/Steam', // WSL
252
+ ];
241
253
  function locateWallpaperEngine() {
242
254
  const probes = [];
243
255
  const reg = steamPathFromRegistry();
244
256
  if (reg) probes.push(reg);
245
- probes.push(...STEAM_PROBE_DIRS);
257
+ probes.push(...STEAM_PROBE_DIRS, ...STEAM_PROBE_DIRS_NONWIN);
246
258
  const libraries = [];
247
259
  for (const probe of probes) {
248
260
  const vdf = join(probe, 'steamapps', 'libraryfolders.vdf');
@@ -957,14 +969,27 @@ function apply(ctx) {
957
969
  mkdirSync(dir, { recursive: true });
958
970
  const filePath = join(dir, token + '.mpkg');
959
971
  const out = createWriteStream(filePath);
972
+ // ①(修正) 给写入流挂 error 监听:原无监听——写失败(磁盘满/权限 EACCES/
973
+ // Windows 文件被占用/杀软锁文件)时 EventEmitter 直接 throw → uncaught
974
+ // exception → 宿主进程崩溃(用户实测风险)。error 转成 reject → 被外层
975
+ // catch 接住返回 500 而非崩宿主进程。
976
+ let writeErr = null;
977
+ out.on('error', (e) => { writeErr = e; });
960
978
  let head = Buffer.alloc(0);
961
979
  let size = 0;
962
- for await (const chunk of req) {
963
- size += chunk.length;
964
- if (head.length < HEAD_BYTES) head = Buffer.concat([head, chunk]);
965
- if (!out.write(chunk)) await new Promise((r) => out.once('drain', r));
980
+ try {
981
+ for await (const chunk of req) {
982
+ size += chunk.length;
983
+ if (head.length < HEAD_BYTES) head = Buffer.concat([head, chunk]);
984
+ if (writeErr) throw writeErr;
985
+ if (!out.write(chunk)) await new Promise((r) => { out.once('drain', r); if (writeErr) r(); });
986
+ }
987
+ await new Promise((r) => { out.on('error', r); out.on('finish', r); if (writeErr) r(); out.end(); });
988
+ if (writeErr) throw writeErr;
989
+ } catch (e) {
990
+ try { out.destroy(); } catch {}
991
+ throw e;
966
992
  }
967
- await new Promise((r) => { out.end(r); });
968
993
  const { dataStart, entries } = parseMpkgHead(head);
969
994
  files.set(token, { path: filePath, size, dataStart, entries });
970
995
  json(res, 200, { ok: true, token, size, entries });
@@ -1074,7 +1099,19 @@ function apply(ctx) {
1074
1099
  const client = await fetchRaw('lib/client.js');
1075
1100
  const index = await fetchRaw('lib/index.js');
1076
1101
  if (!client.includes('dsh-mpkg-wallpaper') || !index.includes('dsh-mpkg-wallpaper')) { json(res, 400, { ok: false, error: 'invalid payload' }); return; }
1077
- writeFileSync(new URL('./client.js', dir), client, 'utf8');
1102
+ // ①(修正) id 适配:仓库 client.js 注册 id=dsh-mpkg-wallpaper;本地 @local 安装
1103
+ // 时 cordis 以 @local/dsh-mpkg-wallpaper 引用,DSH 期望注册同名 id——直接覆盖
1104
+ // 会导致 loaded without registering → dsh 启动崩溃(用户实测:点"从本插件更新"
1105
+ // 后崩溃)。写回前按**当前 client.js 的 id** 调整(本地是 @local 就替换成 @local)。
1106
+ let clientOut = client;
1107
+ try {
1108
+ const curSrc = readFileSync(new URL('./client.js', dir), 'utf8');
1109
+ const curId = /id:\s*"([^"]+)"/.exec(curSrc);
1110
+ if (curId && curId[1] && curId[1] !== 'dsh-mpkg-wallpaper') {
1111
+ clientOut = clientOut.replace(/id:\s*"dsh-mpkg-wallpaper"/, 'id: "' + curId[1] + '"');
1112
+ }
1113
+ } catch { /* 读当前 id 失败则保持仓库 id */ }
1114
+ writeFileSync(new URL('./client.js', dir), clientOut, 'utf8');
1078
1115
  writeFileSync(new URL('./index.js', dir), index, 'utf8');
1079
1116
  // 同步更新 package.json 版本号(让"更新到 vX"名副其实)
1080
1117
  try {
@@ -1099,13 +1136,15 @@ function apply(ctx) {
1099
1136
  const url = new URL(req.url || '', 'http://localhost');
1100
1137
  const p = (url.searchParams.get('path') || '').trim();
1101
1138
  // ③(修正) 默认起始路径:Windows → C:\;其他平台 → 当前用户 HOME(proot 的 /root 等),
1102
- // 而不是直接开根目录(用户反馈打开的是安卓根目录、看不到自己环境)
1103
- const base = p || (process.platform === 'win32' ? 'C:\\' : (process.env.HOME || '/'));
1139
+ // 而不是直接开根目录(用户反馈打开的是安卓根目录、看不到自己环境)。
1140
+ // ①(修正) os.homedir() 兜底(Windows process.env.HOME;os.homedir() 跨平台正确)。
1141
+ const home = os.homedir() || '/';
1142
+ const base = p || (process.platform === 'win32' ? 'C:\\' : home);
1104
1143
  if (!existsSync(base) || !statSync(base).isDirectory()) { json(res, 400, { ok: false, error: 'invalid dir' }); return; }
1105
1144
  const subdirs = readdirSync(base, { withFileTypes: true })
1106
1145
  .filter((d) => d.isDirectory() && !d.name.startsWith('.'))
1107
1146
  .map((d) => d.name);
1108
- json(res, 200, { ok: true, dir: base, subdirs, home: process.env.HOME || '/', platform: process.platform });
1147
+ json(res, 200, { ok: true, dir: base, subdirs, home, platform: process.platform });
1109
1148
  } catch (err) { json(res, 500, { ok: false, error: String(err && err.message || err) }); }
1110
1149
  },
1111
1150
  });
@@ -1720,7 +1759,9 @@ function apply(ctx) {
1720
1759
  const wallpapers = [];
1721
1760
  const scan = (root) => {
1722
1761
  if (!existsSync(root)) return;
1723
- for (const dir of readdirSync(root)) {
1762
+ let dirs = [];
1763
+ try { dirs = readdirSync(root); } catch { return; } // ①(修正) 目录不可读则整体跳过,不崩
1764
+ for (const dir of dirs) {
1724
1765
  const p = join(root, dir);
1725
1766
  const proj = join(p, 'project.json');
1726
1767
  if (!existsSync(proj)) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mpkg-wallpaper",
3
- "version": "3.5.2",
3
+ "version": "3.6.0",
4
4
  "description": "DSH Web 壁纸引擎 Wallpaper Engine mpkg 背景插件:浏览器内直接解析 .mpkg(preview.gif 动态背景/内嵌 mp4 视频/多时段切换),整屏统一虚化/对话框/弹层/遮罩独立虚化、镜头缩放平移、时钟、冲突检测。",
5
5
  "private": false,
6
6
  "type": "module",
@@ -51,5 +51,8 @@
51
51
  "homepage": "https://github.com/XHR666/dsh-mpkg-wallpaper",
52
52
  "bugs": {
53
53
  "url": "https://github.com/XHR666/dsh-mpkg-wallpaper/issues"
54
+ },
55
+ "devDependencies": {
56
+ "playwright": "^1.62.1"
54
57
  }
55
- }
58
+ }