dsh-mpkg-wallpaper 3.1.4 → 3.1.6

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/README.en.md CHANGED
@@ -64,7 +64,7 @@ The plugin offers two partial solutions (chosen automatically by scene content):
64
64
  1. **Static-frame extraction**: parses `scene.pkg` (PKG container + LZ4 + TEX decode) and picks the main texture → **high-resolution static image** (photography/illustration scenes near-original quality; tested up to 7680×4320)
65
65
  2. **Layer compositing**: parses all `image` layers in `scene.json` (background + subject + layered character parts) and draws them **accurately composited** on a canvas using the source coordinates/sizes; time-variation scenes pick the frame for the current hour
66
66
 
67
- **Not covered**: MDL puppet characters (Kal'sit, Blue Archive characters, etc. — the body is assembled from rig parts; the flat textures are nearly empty, e.g. `girl.tex` is 0.1% opaque), shader wave/particle effects, scripted interactions. These fall back to the **official preview animation** (preview.gif).
67
+ **Not covered**: MDL puppet characters (the body is assembled from rig parts; the flat textures are nearly empty), shader wave/particle effects, scripted interactions. These fall back to the **official preview animation** (preview.gif).
68
68
 
69
69
  > For full dynamic scenes, the practical path: render externally to video → use the plugin's **video wallpaper** (Windows: official WE app screen-record; Linux: we-layerd; mobile: WE app screen-record).
70
70
 
package/README.md CHANGED
@@ -64,7 +64,7 @@
64
64
  1. **静态帧提取**:解析 `scene.pkg`(PKG 容器 + LZ4 解压 + TEX 纹理解码),从场景图选取主纹理输出**高清静态图**(摄影/插画类场景可达原图画质,实测 7680×4320)
65
65
  2. **图层合成**:解析 `scene.json` 的全部 image 图层(背景 + 主体 + 分层角色部件),按源文件坐标/尺寸在 canvas 上**精确合成完整画面**(平铺图层类场景可完整还原构图;时间变化场景按当前时段选帧)
66
66
 
67
- **无法覆盖的**:MDL 木偶人物(凯尔希、碧蓝档案等角色的身体由骨架拼装,纹理层几乎为空——如 `girl.tex` 仅 0.1% 不透明像素)、shader 波浪/粒子特效、脚本交互。这些场景回退**官方预览动图**(preview.gif,作者生成的动画预览)。
67
+ **无法覆盖的**:MDL 木偶人物(角色的身体由骨架拼装,纹理层几乎为空)、shader 波浪/粒子特效、脚本交互。这些场景回退**官方预览动图**(preview.gif,作者生成的动画预览)。
68
68
 
69
69
  > 如果你需要场景壁纸的完整动态效果,现实路径:外部渲染成视频 → 用本插件的**视频壁纸**功能(Windows 用 WE 官方版录屏、Linux 用 we-layerd 录屏、移动端用壁纸引擎 App 录屏)。
70
70
 
package/lib/client.js CHANGED
@@ -6,6 +6,11 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
 
8
8
  let react = require("react");
9
+ // ①(修正) react-dom portal:弹窗遮罩提到 document.body。
10
+ // 设置面板/backdrop-filter 容器会创建包含块/backdrop root,把 fixed 遮罩
11
+ // 困在面板内 → 遮罩只盖住右侧设置区(用户实测);portal 后盖全屏且模糊全屏。
12
+ let ReactDOM = null;
13
+ try { ReactDOM = require("react-dom"); } catch {}
9
14
 
10
15
 
11
16
 
@@ -62,6 +67,8 @@ window.__ModuleLoader__.load({
62
67
  // ⑲(新) 面板取色强度(壁纸色混合比例 0-100%,默认 45——用户实测默认太淡看不出变化)
63
68
  const DEFAULT_AQUA_TINT_STRENGTH = 45;
64
69
  const DEFAULT_ROTATE = false; // ③(新) 壁纸轮换:定时自动切换下一个壁纸
70
+ const DEFAULT_PLAYBACK_RATE = 1; // ⑳(新) 视频倍速(0.5-2x,原生 playbackRate)
71
+ const SPEED_LEVELS = [0.5, 0.75, 1, 1.25, 1.5, 2]; // ⑳(新) 倍速档位
65
72
  const DEFAULT_BRIGHTNESS = 100; // ⑧(新) 画面亮度(50-150%,滤镜)
66
73
  const DEFAULT_FLOAT = false; // ①(新) 悬浮效果:侧边栏/标题栏悬浮卡片(默认关)
67
74
  const DEFAULT_ROUND_COMPAT = false; // ②(新) 第三方插件 UI 圆角兼容开关(默认关,未完全兼容暂不打扰)
@@ -649,6 +656,11 @@ window.__ModuleLoader__.load({
649
656
  if (lastObjectUrl && lastObjectUrl !== url) { try { URL.revokeObjectURL(lastObjectUrl); } catch {} }
650
657
  lastObjectUrl = url;
651
658
  try { if (video.src !== url) video.src = url; } catch {}
659
+ // ⑳(新) 视频倍速:原生 playbackRate(0.5-2x,即时生效,不重载)
660
+ try {
661
+ const rate = readSection().playbackRate;
662
+ if (typeof rate === "number" && rate >= 0.5 && rate <= 2 && video.playbackRate !== rate) video.playbackRate = rate;
663
+ } catch {}
652
664
  video.style.display = "";
653
665
  try { const p = video.play(); if (p && p.catch) p.catch(() => {}); } catch {}
654
666
  }
@@ -936,7 +948,7 @@ window.__ModuleLoader__.load({
936
948
  } catch {}
937
949
  try { setupSblurObserver(); } catch {}
938
950
  try { setupAquaThemeWatch(); } catch {}
939
- // ①(新) 主题颜色:body 门控 + CSS 变量(默认空 = DSH 默认品牌蓝)
951
+ // ①(新) 主题颜色:body 门控 + CSS 变量(控制侧边栏/标题栏/新会话/设置弹窗底色)
940
952
  try {
941
953
  const tc = section.themeColor || "";
942
954
  if (tc && /^#[0-9a-fA-F]{6}$/.test(tc)) {
@@ -946,11 +958,30 @@ window.__ModuleLoader__.load({
946
958
  document.body.removeAttribute("data-mpw-theme");
947
959
  document.documentElement.style.removeProperty("--mpw-theme-color");
948
960
  }
961
+ // ①(新) 配色(accent):品牌交互元素(按钮/滑条/选中/链接/发送键)
962
+ const ac = section.accent || "";
963
+ if (ac && /^#[0-9a-fA-F]{6}$/.test(ac)) {
964
+ document.body.setAttribute("data-mpw-accent", "");
965
+ document.documentElement.style.setProperty("--mpw-accent-color", ac);
966
+ } else {
967
+ document.body.removeAttribute("data-mpw-accent");
968
+ document.documentElement.style.removeProperty("--mpw-accent-color");
969
+ }
949
970
  } catch {}
950
971
  // ⑲(新) Aqua 实验模式:body 门控 + 遮罩/取色刷新(默认关,不影响原功能)
951
972
  try {
952
973
  if (aquaOn(section)) document.body.setAttribute("data-mpw-aqua", "");
953
974
  else document.body.removeAttribute("data-mpw-aqua");
975
+ // ①(修正) 标题栏跟随 mask 色只在统一雾/面板取色时开;纯自适应文字色
976
+ // 不刷标题栏底色(否则亮色主题下标题栏变 62% 白 → session log 白框)
977
+ const headerTint = (section.aquaMask !== void 0 ? !!section.aquaMask : DEFAULT_AQUA_MASK)
978
+ || (section.aquaTint !== void 0 ? !!section.aquaTint : DEFAULT_AQUA_TINT);
979
+ if (headerTint) document.body.setAttribute("data-mpw-aqua-header", "");
980
+ else document.body.removeAttribute("data-mpw-aqua-header");
981
+ // ①(新) 配色(accent,品牌交互元素):与主题颜色分工
982
+ if (section.accent && /^#[0-9a-fA-F]{6}$/.test(section.accent))
983
+ document.body.setAttribute("data-mpw-accent", "");
984
+ else document.body.removeAttribute("data-mpw-accent");
954
985
  if (section.aquaTextEnhance !== void 0 ? !!section.aquaTextEnhance : DEFAULT_AQUA_TEXT_ENHANCE)
955
986
  document.body.setAttribute("data-mpw-aqua-text", "");
956
987
  else document.body.removeAttribute("data-mpw-aqua-text");
@@ -1080,7 +1111,11 @@ window.__ModuleLoader__.load({
1080
1111
  root.style.setProperty("--mpw-aqua-brand-contrast", aquaInkContrast(aquaBrandColor(s)));
1081
1112
  const maskOn = s.aquaMask !== void 0 ? !!s.aquaMask : DEFAULT_AQUA_MASK;
1082
1113
  if (maskOn) {
1083
- const panel = Math.max(0, Math.min(100, s.aquaMaskAlpha !== void 0 ? s.aquaMaskAlpha : DEFAULT_AQUA_MASK_ALPHA)) / 100;
1114
+ let panel = Math.max(0, Math.min(100, s.aquaMaskAlpha !== void 0 ? s.aquaMaskAlpha : DEFAULT_AQUA_MASK_ALPHA)) / 100;
1115
+ // ①(修正) 亮色遮罩防过曝:自定义色偏亮(luma>0.6)时自动把强度
1116
+ // 压到 ≤55%,避免全屏亮色雾把画面洗白(用户实测:预置色 1/5 变白)
1117
+ const luma = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
1118
+ if (luma > 0.6) panel = Math.min(panel, 0.55);
1084
1119
  el.style.background = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${panel})`;
1085
1120
  el.style.display = "";
1086
1121
  } else {
@@ -1531,8 +1566,9 @@ body:not([data-ds-dark-theme]) .hHd-Xa_root {
1531
1566
  background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-00) ${U(panel)}%, transparent) !important;
1532
1567
  }` : ""}
1533
1568
  .hHd-Xa_newSession {
1534
- /* ④(新) 统一虚化开:随整屏虚化(sessionFollow)或随面板不透明度;关:随面板不透明度 */
1535
- background-color: color-mix(in srgb, var(--dsw-alias-button-elevated-fill) ${unifyTint ? (section.sessionFollow !== void 0 ? !!section.sessionFollow : DEFAULT_SESSION_FOLLOW) ? panel : panel : panel}%, transparent) !important;
1569
+ /* ①(修正) 新会话按钮随 侧边栏/标题栏透明度(sidebarAlpha):统一虚化开 → U(panel)=sidebarAlpha;
1570
+ 关 → 随面板不透明度。之前直接 panel 导致不随 sidebarAlpha 变化(用户反馈) */
1571
+ background-color: color-mix(in srgb, var(--dsw-alias-button-elevated-fill) ${U(panel)}%, transparent) !important;
1536
1572
  border-color: color-mix(in srgb, var(--dsw-alias-border-l2) 65%, transparent) !important;
1537
1573
  }
1538
1574
  /* ⑬(新) 取消工作区列表底部的自带渐变 fade(用户要求直接去掉白色虚化带) */
@@ -1618,6 +1654,25 @@ ${frostedLayers}
1618
1654
  body:not([data-ds-dark-theme]) .wSkVaW_header {
1619
1655
  background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-00) ${hdrAlpha}%, transparent) !important;
1620
1656
  }
1657
+ `;
1658
+ // ①(修正) 标题栏内部白框:unify(透明标题栏)时 session logo 按钮 /
1659
+ // 「标准模式」标签等自带实心底的盒子仍显示为白色块(用户两次反馈),
1660
+ // 全部改透明,保留 hover 交互态。
1661
+ css += `
1662
+ .wSkVaW_header button,
1663
+ .wSkVaW_header [class*="button"],
1664
+ .wSkVaW_header [class*="logo"],
1665
+ .wSkVaW_header [class*="mode"],
1666
+ .wSkVaW_header [class*="badge"],
1667
+ .wSkVaW_header [class*="segmented"] {
1668
+ background: transparent !important;
1669
+ box-shadow: none !important;
1670
+ }
1671
+ .wSkVaW_header button:hover,
1672
+ .wSkVaW_header [class*="button"]:hover,
1673
+ .wSkVaW_header [class*="segmented"] button:hover {
1674
+ background: color-mix(in srgb, var(--dsw-alias-interactive-bg-hover) 70%, transparent) !important;
1675
+ }
1621
1676
  `;
1622
1677
  }
1623
1678
  if (headerBg && !headerBlur) {
@@ -1881,7 +1936,8 @@ html body:not([data-mpw-sblur-off]) [class*="wSkVaW_header"] {
1881
1936
  /* 区域1: 左上角品牌+新会话按钮(半透明,与侧边栏一致)。
1882
1937
  ③(修正) unify 开时随 sessionFollow(开=跟随不透明度条;关=面板不透明度) */
1883
1938
  button[class*="newSession"] {
1884
- background-color: color-mix(in srgb, var(--dsw-alias-button-elevated-fill) ${unifyTint ? (section.sessionFollow !== void 0 ? !!section.sessionFollow : DEFAULT_SESSION_FOLLOW) ? panel : panel : panel}%, transparent) !important;
1939
+ /* ①(修正) 收起态新会话按钮同样随 sidebarAlpha(U(panel)) */
1940
+ background-color: color-mix(in srgb, var(--dsw-alias-button-elevated-fill) ${U(panel)}%, transparent) !important;
1885
1941
  border-color: color-mix(in srgb, var(--dsw-alias-border-l2) 65%, transparent) !important;
1886
1942
  }
1887
1943
  /* 区域4: 标题栏下方 1px 横线(header::after)去掉 */
@@ -1975,11 +2031,18 @@ body[data-mpw-aqua] {
1975
2031
  --dsw-alias-brand-text: var(--mpw-aqua-ink, inherit);
1976
2032
  --dsw-alias-state-business-primary: var(--mpw-aqua-ink-secondary, inherit);
1977
2033
  }
1978
- /* ①(修正) 面板取色/统一雾时标题栏背景跟随 mask 色(用户实测只改侧边栏不改标题栏) */
1979
- body[data-mpw-aqua] .wSkVaW_header {
2034
+ /* ①(修正) 面板取色/统一雾时标题栏背景跟随 mask 色(用户实测只改侧边栏不改标题栏)。
2035
+ ①(修正2) 标题栏是否刷 mask 色由 JS 打 data-mpw-aqua-header 门控:
2036
+ 纯 aquaInk(自适应文字色)时不刷——此时遮罩色=主题中性色(亮色主题=白),
2037
+ 会把标题栏刷成 62% 白 → session log/标准模式背后的「白框」(用户实测:
2038
+ 统一虚化 + 自适应文字色同时开出现)。 */
2039
+ body[data-mpw-aqua][data-mpw-aqua-header] .wSkVaW_header {
1980
2040
  background-color: color-mix(in srgb, rgb(var(--mpw-aqua-rgb)) 62%, transparent) !important;
1981
2041
  backdrop-filter: none !important;
1982
2042
  }
2043
+ body[data-mpw-aqua] .wSkVaW_header {
2044
+ backdrop-filter: none !important;
2045
+ }
1983
2046
  /* ⑲(修正) Aqua 模式下弹层背景被 mask 色半透明化,必须配 backdrop blur 防文字叠压
1984
2047
  (用户实测:加号/命令/权限/模型/上下文选择器透明但无模糊 → 文字叠在一起) */
1985
2048
  body[data-mpw-aqua] [role="menu"],
@@ -2004,35 +2067,6 @@ body[data-mpw-aqua] .mpw_dialog .mpw_title,
2004
2067
  body[data-mpw-aqua] .mpw_dialog .mpw_label,
2005
2068
  body[data-mpw-aqua] .mpw_dialog .mpw_hint,
2006
2069
  body[data-mpw-aqua] .mpw_dialog .mpw_value { color: inherit !important; }
2007
- `;
2008
- }
2009
- // ⑲(新) 深底文字可读增强(aquaTextEnhance,默认关):
2010
- // 尝试过两种开源方案均不理想:① 双色 text-shadow 描边(浅底上像"喷墨打印机");
2011
- // ② mix-blend-mode difference(全局反色,粉色/彩色文字全被反转,用户实测不可用)。
2012
- // 最终方案:**只加白色细描边**——浅色背景上白边不可见(不破坏浅底),
2013
- // 深色/黑色背景上自动给文字勾勒白色轮廓(深底可读)。这是唯一不破坏浅色背景的近似。
2014
- // 说明:浏览器无法精确实现"只变经过深色处的字"(需逐字采样背景像素,滚动动态内容下不可靠)。
2015
- if (section.aquaTextEnhance !== void 0 ? !!section.aquaTextEnhance : false) {
2016
- css += `
2017
- /* ── 深底文字可读增强(实验,默认关):白色细描边(浅底不可见,深底提亮轮廓) ── */
2018
- body[data-mpw-aqua-text] [data-conversation-scroll] :is(p, li, h1, h2, h3, h4, td, th) {
2019
- text-shadow: 0 0 1px rgba(255,255,255,.85), 0 1px 1px rgba(255,255,255,.55) !important;
2020
- }
2021
- `;
2022
- }
2023
- // ①(新) 主题颜色(accent):按钮/滑条/选中/链接/发送键跟随自定义主题色
2024
- //(参照 elysia395 的 accent;放末尾保证覆盖 aquaInk 的品牌色)
2025
- if (section.themeColor && /^#[0-9a-fA-F]{6}$/.test(section.themeColor)) {
2026
- css += `
2027
- /* ── 主题颜色:品牌色统一为自定义主题色(可开关,空 = DSH 默认蓝) ── */
2028
- body[data-mpw-theme] {
2029
- --dsw-alias-brand-primary: var(--mpw-theme-color, #3964fe);
2030
- --dsw-alias-brand-text: var(--mpw-theme-color, #3964fe);
2031
- --dsw-alias-button-info-fill: var(--mpw-theme-color, #3964fe);
2032
- --dsw-alias-button-info-hover: var(--mpw-theme-color, #3964fe);
2033
- --dsw-alias-state-business-primary: var(--mpw-theme-color, #3964fe);
2034
- --dsw-alias-interactive-bg-selected: color-mix(in srgb, var(--mpw-theme-color, #3964fe) 16%, transparent);
2035
- }
2036
2070
  `;
2037
2071
  }
2038
2072
  // ⑲(新) 任务列表(todo 卡片)磨砂背景:收纳/展开状态都模糊(类似标题栏/侧边栏磨砂)。
@@ -2062,6 +2096,13 @@ body[data-mpw-todo-blur] [data-tool="todo_write"] * {
2062
2096
  return `
2063
2097
  /* ── 设置页 UI ── */
2064
2098
  .mpw_row { display: flex; flex-direction: column; gap: 10px; padding: 4px 0; max-width: 720px; }
2099
+ /* ①(修正) 壁纸引擎设置区(右侧)背景:半透明灰(像左侧设置栏),而不是白色区块 */
2100
+ .mpw_glassHost {
2101
+ /* ①(修正) 与左侧设置栏同款:--dsw-specific-sidebar-fill 半透明灰(能透壁纸),
2102
+ 之前用 78% 模块色太实不透(用户反馈观感不一致) */
2103
+ background: var(--dsw-specific-sidebar-fill) !important;
2104
+ border-radius: 14px; padding: 10px 12px;
2105
+ }
2065
2106
  .mpw_title { font-size: 16px; line-height: 24px; font-weight: 600; color: var(--dsw-alias-label-primary); }
2066
2107
  .mpw_desc { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-tertiary); margin: 0; }
2067
2108
  /* ⑤(重做) 分组排版:大分组标题用背景块区分(不再只是一条线,一眼分清分组) */
@@ -2119,6 +2160,7 @@ body[data-ds-dark-theme] .mpw_tabBar {
2119
2160
  }
2120
2161
  .mpw_tabRow {
2121
2162
  display: flex;
2163
+ align-items: flex-start; /* ①(修正) 列高度=内容高度(flex stretch 会让 offsetHeight 返回最高列高度 → 高度自适应失效) */
2122
2164
  transition: transform .24s cubic-bezier(.4,0,.2,1);
2123
2165
  will-change: transform;
2124
2166
  }
@@ -2126,11 +2168,25 @@ body[data-ds-dark-theme] .mpw_tabBar {
2126
2168
  .mpw_label { font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-secondary); }
2127
2169
  .mpw_inline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
2128
2170
  .mpw_input {
2129
- flex: 1; min-width: 0; height: 34px; padding: 0 10px; box-sizing: border-box;
2171
+ flex: 1; min-width: 0; height: 40px; padding: 0 12px; box-sizing: border-box;
2130
2172
  border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px;
2131
2173
  background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary);
2132
2174
  font: inherit; font-size: 13px;
2133
2175
  }
2176
+ /* ⑳(新) 自定义下拉(替代原生 select) */
2177
+ .mpw_selectBtn { display: flex; align-items: center; gap: 6px; cursor: pointer; }
2178
+ .mpw_selectMenu {
2179
+ background: var(--dsw-alias-bg-module-platform, #1c2230);
2180
+ border: 1px solid var(--dsw-alias-border-l2, #444b5c);
2181
+ border-radius: 10px; padding: 4px; box-shadow: var(--dsw-shadow-lv3, 0 12px 40px rgba(0,0,0,.35));
2182
+ }
2183
+ .mpw_selectOpt {
2184
+ display: block; width: 100%; box-sizing: border-box; text-align: left;
2185
+ padding: 8px 10px; border: 0; border-radius: 8px; background: transparent;
2186
+ color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; cursor: pointer;
2187
+ }
2188
+ .mpw_selectOpt:hover { background: color-mix(in srgb, var(--dsw-alias-interactive-bg-hover, #333a4a) 60%, transparent); }
2189
+ .mpw_selectOpt.mpw_on { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f8cff) 20%, transparent); }
2134
2190
  .mpw_button {
2135
2191
  flex: none; height: 34px; padding: 0 14px; border: 1px solid rgba(255,255,255,0.25); border-radius: 10px;
2136
2192
  background: #3964fe !important; color: #ffffff !important;
@@ -2168,6 +2224,12 @@ body[data-ds-dark-theme] .mpw_tabBar {
2168
2224
  color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px; cursor: pointer;
2169
2225
  }
2170
2226
  .mpw_miniBtn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
2227
+ /* ①(修正) 选中态:品牌色描边 + 淡品牌底(顺序/随机等按钮选中要有明确显示) */
2228
+ .mpw_miniBtn.mpw_on {
2229
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f8cff) 22%, transparent);
2230
+ color: var(--dsw-alias-brand-primary, #4f8cff);
2231
+ border-color: var(--dsw-alias-brand-primary, #4f8cff);
2232
+ }
2171
2233
  .mpw_hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); margin: 0; }
2172
2234
  /* ①(新) 标题行:仓库链接(灰色、无下划线、hover 变深)+ 版本号 */
2173
2235
  .mpw_titleRow { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
@@ -2550,6 +2612,11 @@ body[data-mpw-modal] [role="dialog"] {
2550
2612
  const [updState, setUpdState] = react.useState(null); // null=未检测 / {checking}/{hasUpdate...}
2551
2613
  const [updConfirm, setUpdConfirm] = react.useState(null); // ⑩(新) 新版本确认下载弹窗
2552
2614
  const [webConfirm, setWebConfirm] = react.useState(null); // ①(新) 网页壁纸确认弹窗(实验性警告)
2615
+ const [rotModal, setRotModal] = react.useState(false); // ⑳(新) 轮播列表管理弹窗
2616
+ const [rotEditor, setRotEditor] = react.useState(false); // ⑳(新) 轮播列表编辑弹窗
2617
+ const [rotEdit, setRotEdit] = react.useState(null); // ⑳(新) 正在编辑的列表草稿
2618
+ const [rotFilter, setRotFilter] = react.useState("all"); // ⑳(新) 勾选界面来源过滤:all/custom/steam
2619
+ const tabBodyRef = react.useRef(null); // ①(新) tab 高度自适应(跟随当前页最后按钮)
2553
2620
  const [wallList, setWallList] = react.useState([]); // ② 合并后的可选壁纸列表
2554
2621
  const [wallIdx, setWallIdx] = react.useState(-1); // 当前在列表中的索引
2555
2622
  const [libShow, setLibShow] = react.useState(10); // 1(新) 列表单次展示数量(可展开)
@@ -2718,6 +2785,19 @@ body[data-mpw-modal] [role="dialog"] {
2718
2785
  const walls = d.ok && d.wallpapers ? d.wallpapers : [];
2719
2786
  setLibWalls(walls);
2720
2787
  if (d.ok && !d.installDir) showError(t("lib.noInstall"));
2788
+ // ⑳(新) WE 原生播放列表 → 首次自动导入为轮播列表(rotSeeded 防止删除后再播种)
2789
+ try {
2790
+ const pls = (d.playlists || []).filter((p) => p && Array.isArray(p.keys) && p.keys.length);
2791
+ const cur = readSection();
2792
+ if (pls.length && !(cur.rotGroups || []).length && !cur.rotSeeded) {
2793
+ const seed = pls[0];
2794
+ const gid = "we" + Date.now();
2795
+ commit({ rotGroups: [{ id: gid, name: seed.name || t("rot.wePlaylist"), interval: seed.interval || 5, order: seed.order === "random" ? "random" : "sequence", keys: seed.keys }], rotGroupId: gid, rotSeeded: true }, true);
2796
+ setHint(t("rot.seeded") + ":" + (seed.name || ""));
2797
+ } else if (!pls.length && !cur.rotSeeded) {
2798
+ commit({ rotSeeded: true }, true);
2799
+ }
2800
+ } catch {}
2721
2801
  // ②(新) Steam 库合并进统一列表(轮播/上下切换可用):
2722
2802
  // 并入 video(mp4 播放)+ web(iframe)+ scene(预览图/后续静态帧),
2723
2803
  // 用 src:"steam" 标记,next/prev 走 applyLibraryWallpaper。
@@ -2741,6 +2821,11 @@ body[data-mpw-modal] [role="dialog"] {
2741
2821
  // ①(新) 检测更新(对比实际代码内容哈希,README 变更不触发)
2742
2822
  const checkUpdate = async () => {
2743
2823
  setUpdState({ checking: true });
2824
+ // ①(修正) 实时刷新当前版本(像插件市场那样打开即读最新安装版本)
2825
+ try {
2826
+ const p = await fetch(HOST_BASE + "/ping", { method: "GET" });
2827
+ if (p.ok) { const pd = await p.json(); if (pd && pd.version) setHostVersion(pd.version); }
2828
+ } catch {}
2744
2829
  try {
2745
2830
  const r = await fetch(HOST_BASE + "/update-check");
2746
2831
  const d = await r.json();
@@ -2787,8 +2872,20 @@ body[data-mpw-modal] [role="dialog"] {
2787
2872
  setCustomFiles(d.files || []);
2788
2873
  // ①(新) 合并进统一列表:自定义目录文件(图片+视频)+ workshop 原始子文件夹
2789
2874
  // (video → mp4 播放;web → iframe;scene → 预览图/后续静态帧)
2790
- setWallList((d.files || []).map((f) => {
2875
+ setWallList((prev) => {
2876
+ // ①(修正) 自定义目录改为**合并**进列表(不再替换——用户误扫自定义目录后
2877
+ // 切不回 Steam 库的问题;现在两个来源共存,去重)
2878
+ const fresh = (d.files || []).map((f) => {
2791
2879
  if (f.folder) {
2880
+ // ①(修正) 子文件夹里的 .mpkg(wallpaperE 角色收藏夹):独立成条
2881
+ if (f.folderMpkg && f.type === "mpkg") {
2882
+ return {
2883
+ key: "custom|" + f.name + "/" + f.media, title: f.title || f.media, name: f.name,
2884
+ type: "mpkg", folder: true, folderMpkg: true, folderName: f.name, mpkgFile: f.media,
2885
+ image: "host:?custom=1&folder=" + encodeURIComponent(f.name) + "&file=" + encodeURIComponent(f.media),
2886
+ converted: "mp4"
2887
+ };
2888
+ }
2792
2889
  if (f.type === "web") {
2793
2890
  const pv = f.preview || "preview.jpg";
2794
2891
  return {
@@ -2822,7 +2919,10 @@ body[data-mpw-modal] [role="dialog"] {
2822
2919
  image: "host:?custom=1&file=" + encodeURIComponent(f.name),
2823
2920
  converted: f.type === "video" ? "mp4" : (f.type === "mpkg" ? "mp4" : "gif")
2824
2921
  };
2825
- }));
2922
+ });
2923
+ const keys = new Set((prev || []).map((w) => w.key));
2924
+ return fresh.filter((w) => !keys.has(w.key)).concat(prev || []);
2925
+ });
2826
2926
  }
2827
2927
  } catch (err) { showError(t("lib.fail") + String(err && err.message || err)); }
2828
2928
  setLibBusy(false);
@@ -2831,19 +2931,24 @@ body[data-mpw-modal] [role="dialog"] {
2831
2931
  const applyCustomMpkg = async (w) => {
2832
2932
  setLibBusy(true);
2833
2933
  try {
2834
- const r = await fetch(HOST_BASE + "/custom-mpkg?file=" + encodeURIComponent(w.name));
2934
+ // ①(修正) folderMpkg:子文件夹里的 .mpkg 走 folder 参数
2935
+ const fq = w.folderMpkg
2936
+ ? "folder=" + encodeURIComponent(w.folderName || w.name) + "&file=" + encodeURIComponent(w.mpkgFile || w.name)
2937
+ : "file=" + encodeURIComponent(w.name);
2938
+ const r = await fetch(HOST_BASE + "/custom-mpkg?" + fq);
2835
2939
  const d = await r.json();
2836
- if (!d.ok || !d.selected) { showError(t("mpkg.noAsset") + "(" + w.name + ")"); setLibBusy(false); return; }
2940
+ const display = w.title || w.name;
2941
+ if (!d.ok || !d.selected) { showError(t("mpkg.noAsset") + "(" + display + ")"); setLibBusy(false); return; }
2837
2942
  const sel = d.selected;
2838
2943
  commit({
2839
2944
  image: "host:?token=" + d.token + "&index=" + sel.index + (sel.offset ? "&offset=" + sel.offset : ""),
2840
- source: sel.name, mpkgKey: "custommpkg|" + w.name, mpkgName: w.name,
2945
+ source: sel.name, mpkgKey: "custommpkg|" + (w.folderMpkg ? w.folderName + "/" + w.mpkgFile : w.name), mpkgName: display,
2841
2946
  fromMpkg: true, converted: sel.isMp4 ? "mp4" : "gif", slot: null,
2842
2947
  info: undefined, timeVideos: undefined, timeConfig: undefined, activeSlot: null,
2843
2948
  webUrl: undefined, sceneKey: undefined
2844
2949
  }, true);
2845
- setMpkgMeta({ name: w.name, key: "custommpkg|" + w.name, info: null, entryName: sel.name, slot: null });
2846
- setHint(t("lib.applied") + ":" + w.name);
2950
+ setMpkgMeta({ name: display, key: "custommpkg|" + (w.folderMpkg ? w.folderName + "/" + w.mpkgFile : w.name), info: null, entryName: sel.name, slot: null });
2951
+ setHint(t("lib.applied") + ":" + display);
2847
2952
  if (!sel.isMp4) { setPreviewModal(true); }
2848
2953
  } catch (err) { showError(t("lib.fail") + String(err && err.message || err)); }
2849
2954
  setLibBusy(false);
@@ -2919,27 +3024,72 @@ body[data-mpw-modal] [role="dialog"] {
2919
3024
  } catch (err) { console.error("[dsh-mpkg-wallpaper] 应用场景壁纸失败:", err); showError(t("lib.fail") + String(err && err.message || err)); }
2920
3025
  };
2921
3026
  // ②(新) 切换到下一个壁纸(本地库 + 自定义目录合并列表)
3027
+ // ⑳(新) 轮播候选:激活了列表 → 列表内的壁纸;否则全部
3028
+ const rotCandidates = () => {
3029
+ const all = wallList;
3030
+ if (!all.length) return [];
3031
+ const gid = section.rotGroupId;
3032
+ if (gid) {
3033
+ const g = (section.rotGroups || []).find((x) => x.id === gid);
3034
+ if (g && (g.keys || []).length) {
3035
+ const filtered = all.filter((w) => (g.keys || []).includes(w.key));
3036
+ if (filtered.length) return filtered;
3037
+ }
3038
+ }
3039
+ return all;
3040
+ };
2922
3041
  const nextWallpaper = () => {
2923
- const list = wallList;
3042
+ const list = rotCandidates();
2924
3043
  if (!list.length) { showError(t("lib.empty")); return; }
2925
- const idx = (wallIdx + 1) % list.length;
2926
- setWallIdx(idx);
2927
- applyWallFromList(list[idx]);
3044
+ let idx = list.findIndex((w) => w.key === (wallList[wallIdx] && wallList[wallIdx].key));
3045
+ if (idx < 0) idx = -1;
3046
+ idx = (idx + 1) % list.length;
3047
+ const target = list[idx];
3048
+ setWallIdx(wallList.findIndex((w) => w.key === target.key));
3049
+ applyWallFromList(target);
2928
3050
  };
2929
3051
  // 1(新) 切换到上一个壁纸
2930
3052
  const prevWallpaper = () => {
2931
- const list = wallList;
3053
+ const list = rotCandidates();
2932
3054
  if (!list.length) { showError(t("lib.empty")); return; }
2933
- const idx = (wallIdx - 1 + list.length) % list.length;
2934
- setWallIdx(idx);
2935
- applyWallFromList(list[idx]);
3055
+ let idx = list.findIndex((w) => w.key === (wallList[wallIdx] && wallList[wallIdx].key));
3056
+ if (idx < 0) idx = 0;
3057
+ idx = (idx - 1 + list.length) % list.length;
3058
+ const target = list[idx];
3059
+ setWallIdx(wallList.findIndex((w) => w.key === target.key));
3060
+ applyWallFromList(target);
2936
3061
  };
2937
- // ③(新) 壁纸轮换:定时自动切换
3062
+ // ③(新) 壁纸轮换:定时自动切换(⑳(新) 随机顺序的列表打乱)
2938
3063
  const startRotation = () => {
2939
3064
  try { if (window.__mpwRotTimer) clearInterval(window.__mpwRotTimer); } catch {}
2940
- const sec = section.rotateMin > 0 ? section.rotateMin : 5;
2941
- window.__mpwRotTimer = setInterval(nextWallpaper, sec * 60 * 1000);
3065
+ const gid = section.rotGroupId;
3066
+ const g = gid ? (section.rotGroups || []).find((x) => x.id === gid) : null;
3067
+ const sec = g && g.interval > 0 ? g.interval : (section.rotateMin > 0 ? section.rotateMin : 5);
3068
+ if (g && g.order === "random") {
3069
+ // 随机顺序:每轮洗牌后顺序播放(先随机跳一次)
3070
+ window.__mpwRotTimer = setInterval(() => {
3071
+ const list = rotCandidates();
3072
+ if (list.length < 2) { nextWallpaper(); return; }
3073
+ const r = Math.floor(Math.random() * list.length);
3074
+ const target = list[r];
3075
+ setWallIdx(wallList.findIndex((w) => w.key === target.key));
3076
+ applyWallFromList(target);
3077
+ }, sec * 60 * 1000);
3078
+ } else {
3079
+ window.__mpwRotTimer = setInterval(nextWallpaper, sec * 60 * 1000);
3080
+ }
2942
3081
  };
3082
+ // ①(新) tab 高度自适应:跟随当前页最后一个按钮(否则矮 tab 下方大片空白)
3083
+ react.useEffect(() => {
3084
+ try {
3085
+ const body = tabBodyRef.current;
3086
+ if (!body) return;
3087
+ const col = body.querySelector('[data-mpw-tabkey="' + settingsTab + '"]');
3088
+ if (!col) return;
3089
+ const h = col.offsetHeight;
3090
+ if (body.style.height !== h + "px") body.style.height = h + "px";
3091
+ } catch {}
3092
+ });
2943
3093
  // ③(修正) 任一弹窗打开时给 body 打标记(背后设置面板禁虚化),关闭时移除
2944
3094
  react.useEffect(() => {
2945
3095
  const anyOpen = previewModal || errorModal || dirPick || updConfirm || conflictModal;
@@ -3037,6 +3187,8 @@ body[data-mpw-modal] [role="dialog"] {
3037
3187
  aquaMask: DEFAULT_AQUA_MASK, aquaTint: DEFAULT_AQUA_TINT, aquaInk: DEFAULT_AQUA_INK, aquaColor: "", aquaInkColor: "", aquaTextEnhance: DEFAULT_AQUA_TEXT_ENHANCE, todoBlur: DEFAULT_TODO_BLUR, aquaMaskAlpha: DEFAULT_AQUA_MASK_ALPHA, aquaTintStrength: DEFAULT_AQUA_TINT_STRENGTH, themeColor: DEFAULT_THEME_COLOR,
3038
3188
  hybrid: DEFAULT_HYBRID, roundCompat: DEFAULT_ROUND_COMPAT,
3039
3189
  rotate: DEFAULT_ROTATE, rotateMin: 5, customDirPath: "", brightness: DEFAULT_BRIGHTNESS, float: DEFAULT_FLOAT,
3190
+ playbackRate: DEFAULT_PLAYBACK_RATE, rotGroups: [], rotGroupId: "", rotSeeded: false,
3191
+ glassColor: "", glassAlpha: 12, glassWindow: false, accent: "",
3040
3192
  thinkBg: DEFAULT_THINK_BG,
3041
3193
  enabled: DEFAULT_ENABLED
3042
3194
  }), true);
@@ -3330,6 +3482,64 @@ body[data-mpw-modal] [role="dialog"] {
3330
3482
  const propsToShow = propsExpanded ? propsShown : [];
3331
3483
 
3332
3484
  const h = react.createElement;
3485
+ // ①(修正) 弹窗遮罩 portal:渲染到 body,避开设置面板包含块/backdrop root 困局
3486
+ const MaskPortal = (props) => {
3487
+ const node = h("div", { className: "mpw_mask", onClick: props.onClick }, props.children);
3488
+ if (ReactDOM && ReactDOM.createPortal && typeof document !== "undefined" && document.body) {
3489
+ try { return ReactDOM.createPortal(node, document.body); } catch { /* 回退原位渲染 */ }
3490
+ }
3491
+ return node;
3492
+ };
3493
+ // ⑳(新) 自定义下拉(不调用原生 select——安卓原生太难看,用户实测)
3494
+ const [selectOpen, setSelectOpen] = react.useState("");
3495
+ const [selectDirUp, setSelectDirUp] = react.useState({}); // name → 是否向上展开
3496
+ const SelectBox = (props) => {
3497
+ const { value, options, onChange, placeholder } = props;
3498
+ const cur = options.find((o) => o.value === value);
3499
+ const dirUp = !!selectDirUp[props.name];
3500
+ const menuStyle = dirUp
3501
+ ? { position: "absolute", left: 0, right: 0, bottom: "100%", marginBottom: 4, zIndex: 2600, maxHeight: 220, overflowY: "auto" }
3502
+ : { position: "absolute", left: 0, right: 0, top: "100%", marginTop: 4, zIndex: 2600, maxHeight: 220, overflowY: "auto" };
3503
+ return h("div", { className: "mpw_select", style: { position: "relative", flex: 1, minWidth: 0 } }, [
3504
+ h("button", { className: "mpw_input mpw_selectBtn", type: "button",
3505
+ onClick: (ev) => {
3506
+ if (selectOpen !== props.name) {
3507
+ // ①(修正) 下方空间不足时向上展开(用户实测列表底部被屏幕边缘截断)
3508
+ let up = false;
3509
+ try {
3510
+ const rect = ev.currentTarget.getBoundingClientRect();
3511
+ const est = Math.min(220, (options.length + 1) * 34);
3512
+ if (rect.bottom + est > (window.innerHeight || 600)) up = true;
3513
+ } catch {}
3514
+ setSelectDirUp((prev) => Object.assign({}, prev, { [props.name]: up }));
3515
+ }
3516
+ setSelectOpen(selectOpen === props.name ? "" : props.name);
3517
+ } },
3518
+ h("span", { style: { flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, cur ? cur.label : placeholder),
3519
+ h("span", { className: "mpw_hint" }, selectOpen === props.name ? (dirUp ? "▼" : "▲") : (dirUp ? "▲" : "▼"))
3520
+ ),
3521
+ selectOpen === props.name
3522
+ ? h("div", { className: "mpw_selectMenu", style: menuStyle },
3523
+ options.map((o) => h("button", { className: "mpw_selectOpt" + (o.value === value ? " mpw_on" : ""), type: "button", key: o.value,
3524
+ onClick: () => { setSelectOpen(""); onChange(o.value); } }, o.label)))
3525
+ : null
3526
+ ]);
3527
+ };
3528
+ // ⑳(新) 视频倍速:6 档按钮(去掉进度条——条上点击/拖拽反复错档,用户要求改按钮)
3529
+ const SpeedBar = (props) => {
3530
+ const { value, onChange } = props;
3531
+ return h("div", { style: { display: "flex", gap: 4, marginTop: 4 } },
3532
+ SPEED_LEVELS.map((rate) => {
3533
+ const on = rate === value;
3534
+ return h("button", { key: rate, type: "button",
3535
+ style: { flex: "1 1 0", padding: "8px 0", fontSize: 12, borderRadius: 8, cursor: "pointer",
3536
+ border: "1px solid " + (on ? "var(--dsw-alias-brand-primary, #4f8cff)" : "var(--dsw-alias-border-l2, #444b5c)"),
3537
+ background: on ? "color-mix(in srgb, var(--dsw-alias-brand-primary, #4f8cff) 18%, transparent)" : "transparent",
3538
+ color: on ? "var(--dsw-alias-brand-primary, #4f8cff)" : "var(--dsw-alias-label-secondary)",
3539
+ fontWeight: on ? 700 : 400 },
3540
+ onClick: () => onChange(rate) }, (rate === 1 ? "1" : rate) + "x");
3541
+ }));
3542
+ };
3333
3543
  const sliderRow = (label, field, min, max, suffix, step, def) => {
3334
3544
  const val = section[field] !== void 0 ? section[field] : def;
3335
3545
  const rangeRef = react.useRef(null);
@@ -3440,7 +3650,7 @@ body[data-mpw-modal] [role="dialog"] {
3440
3650
  ]);
3441
3651
  };
3442
3652
 
3443
- return h("div", { className: "mpw_row" }, [
3653
+ return h("div", { className: "mpw_row mpw_glassHost" }, [
3444
3654
  // ①(修正) 标题/描述在 Tab 条上方
3445
3655
  h("div", { className: "mpw_titleRow" }, [
3446
3656
  h("span", { className: "mpw_title" }, t("title")),
@@ -3460,11 +3670,11 @@ body[data-mpw-modal] [role="dialog"] {
3460
3670
  h("button", { className: "mpw_tab" + (settingsTab === "other" ? " mpw_tabActive" : ""), type: "button", onClick: () => switchTab("other") }, t("sec.other")),
3461
3671
  ]),
3462
3672
  // 内容区:flex 行 + translateX 滑动(真翻页;所有 tab 内容始终挂载,hooks 稳定)
3463
- h("div", { className: "mpw_tabBody" }, [
3673
+ h("div", { className: "mpw_tabBody", ref: tabBodyRef }, [
3464
3674
  // 内层 flex 行:translateX 滑动(外层裁剪区固定,否则整体左移内容全空——用户实测)
3465
3675
  h("div", { className: "mpw_tabRow", style: { transform: "translateX(-" + TAB_ORDER.indexOf(settingsTab) * 100 + "%)" } }, [
3466
3676
  // source 组
3467
- h("div", { style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
3677
+ h("div", { "data-mpw-tabkey": "source", style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
3468
3678
  // ═══ 背景来源 ═══
3469
3679
  h("div", { className: "mpw_section" }, t("sec.source")),
3470
3680
 
@@ -3517,7 +3727,7 @@ body[data-mpw-modal] [role="dialog"] {
3517
3727
  // 图片项用 <img>(参照 elysia395 的 preview 显示)
3518
3728
  // ①(修正) mpkg 项用容器内 preview 图(custom-mpkg-preview 路由);视频用首帧;图片直接用
3519
3729
  w.type === "mpkg"
3520
- ? h("img", { className: "mpw_thumbImg", src: HOST_BASE + "/custom-mpkg-preview?file=" + encodeURIComponent(w.name), alt: "", loading: "lazy",
3730
+ ? h("img", { className: "mpw_thumbImg", src: HOST_BASE + "/custom-mpkg-preview?" + (w.folderMpkg ? "folder=" + encodeURIComponent(w.folderName) + "&" : "") + "file=" + encodeURIComponent(w.folderMpkg ? w.mpkgFile : w.name), alt: "", loading: "lazy",
3521
3731
  onError: (ev) => { ev.target.style.display = "none"; } })
3522
3732
  : ((w.type === "video" || w.converted === "mp4")
3523
3733
  ? h("video", { className: "mpw_thumbImg", src: resolveHostUrl(w.image), muted: true, playsInline: true, preload: "metadata",
@@ -3590,7 +3800,7 @@ body[data-mpw-modal] [role="dialog"] {
3590
3800
  ])
3591
3801
  : libWalls && !libWalls.length ? h("p", { className: "mpw_hint" }, t("lib.empty")) : null
3592
3802
  ]),
3593
- // ②③(新) 下一个壁纸 + 定时轮换
3803
+ // ②③(新) 下一个壁纸 + 定时轮换 + ⑳(新) 轮播列表(WE 原生播放列表风格)
3594
3804
  h("div", { className: "mpw_field" }, [
3595
3805
  h("label", { className: "mpw_label" }, t("lib.rotate")),
3596
3806
  h("div", { className: "mpw_inline" }, [
@@ -3599,9 +3809,139 @@ body[data-mpw-modal] [role="dialog"] {
3599
3809
  h(Toggle, { checked: section.rotate !== void 0 ? !!section.rotate : DEFAULT_ROTATE, onChange: (v) => commit({ rotate: v }) }),
3600
3810
  h("span", { className: "mpw_hint" }, t("lib.rotateDesc"))
3601
3811
  ]),
3812
+ // ⑳(新) 轮播列表:选择激活列表(自定义下拉)+ 管理 + 新建
3813
+ h("div", { className: "mpw_inline", style: { marginTop: 4 } }, [
3814
+ h(SelectBox, { name: "rotGroup", value: section.rotGroupId || "", placeholder: t("rot.all"),
3815
+ options: [{ value: "", label: t("rot.all") }].concat((section.rotGroups || []).map((g) => ({ value: g.id, label: g.name + "(" + (g.keys || []).length + ")" }))),
3816
+ onChange: (v) => commit({ rotGroupId: v }, true) }),
3817
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => setRotModal(true) }, t("rot.manage")),
3818
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => { setRotEdit({ id: "", name: "", interval: 5, order: "sequence", keys: [] }); setRotEditor(true); } }, t("rot.new"))
3819
+ ]),
3602
3820
  h("div", { style: { display: (section.rotate !== void 0 ? !!section.rotate : DEFAULT_ROTATE) ? "" : "none" } },
3603
- sliderRow(t("lib.rotateMin"), "rotateMin", 1, 120, t("lib.minutes"), 1, 5))
3821
+ sliderRow(t("lib.rotateMin"), "rotateMin", 1, 120, t("lib.minutes"), 1, 5)),
3822
+ // ①(修正) 选中列表后:单独设置该列表的轮换间隔(用户要求,编辑弹窗不再设间隔)
3823
+ (section.rotGroupId && (section.rotGroups || []).find((g) => g.id === section.rotGroupId))
3824
+ ? h("div", { className: "mpw_inline", style: { marginTop: 4 } }, [
3825
+ h("span", { className: "mpw_hint" }, t("rot.groupInterval")),
3826
+ h("input", { type: "range", min: 1, max: 120, step: 1, style: { flex: 1, minWidth: 0 },
3827
+ value: (section.rotGroups || []).find((g) => g.id === section.rotGroupId).interval || 5,
3828
+ onChange: (ev) => {
3829
+ const v = Math.max(1, parseInt(ev.target.value || "5", 10));
3830
+ const groups = (section.rotGroups || []).map((g) => g.id === section.rotGroupId ? Object.assign({}, g, { interval: v }) : g);
3831
+ commit({ rotGroups: groups }, true);
3832
+ } }),
3833
+ h("span", { className: "mpw_hint" }, ((section.rotGroups || []).find((g) => g.id === section.rotGroupId).interval || 5) + t("lib.minutes"))
3834
+ ])
3835
+ : null
3604
3836
  ]),
3837
+ // ⑳(新) 轮播列表管理弹窗(新建/编辑/删除)
3838
+ rotModal ? h(MaskPortal, { onClick: () => setRotModal(false) }, [
3839
+ h("div", { className: "mpw_dialog", style: { width: "min(560px, calc(100vw - 32px))" }, onClick: (ev) => ev.stopPropagation() }, [
3840
+ h("div", { className: "mpw_title" }, t("rot.title")),
3841
+ (section.rotGroups || []).length === 0
3842
+ ? h("p", { className: "mpw_hint" }, t("rot.empty"))
3843
+ : h("div", { style: { maxHeight: 260, overflowY: "auto" } }, (section.rotGroups || []).map((g) => h("div", { className: "mpw_inline", key: g.id, style: { marginBottom: 6 } }, [
3844
+ h("span", { className: "mpw_propLabel", style: { flex: 1, minWidth: 0 } }, h("b", null, g.name)),
3845
+ h("span", { className: "mpw_hint" }, (g.keys || []).length + t("rot.items") + " · " + (g.interval || 5) + t("lib.minutes") + " · " + (g.order === "random" ? t("rot.random") : t("rot.seq"))),
3846
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => { setRotEdit(JSON.parse(JSON.stringify(g))); setRotModal(false); setRotEditor(true); } }, t("rot.edit")),
3847
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => {
3848
+ const next = (section.rotGroups || []).filter((x) => x.id !== g.id);
3849
+ commit({ rotGroups: next, rotGroupId: section.rotGroupId === g.id ? "" : section.rotGroupId }, true);
3850
+ } }, t("rot.del"))
3851
+ ]))),
3852
+ h("div", { className: "mpw_inline", style: { marginTop: 8 } }, [
3853
+ h("button", { className: "mpw_button", type: "button", onClick: () => { setRotEdit({ id: "", name: "", interval: 5, order: "sequence", keys: [] }); setRotModal(false); setRotEditor(true); } }, t("rot.new")),
3854
+ h("button", { className: "mpw_reset", type: "button", onClick: () => setRotModal(false) }, t("conflict.cancel"))
3855
+ ])
3856
+ ])
3857
+ ]) : null,
3858
+ // ⑳(新) 轮播列表编辑弹窗(命名 + 勾选壁纸 + 间隔 + 顺序)
3859
+ rotEditor && rotEdit ? h(MaskPortal, { onClick: () => setRotEditor(false) }, [
3860
+ h("div", { className: "mpw_dialog", style: { width: "min(560px, calc(100vw - 32px))" }, onClick: (ev) => ev.stopPropagation() }, [
3861
+ h("div", { className: "mpw_title" }, rotEdit.id ? t("rot.editTitle") : t("rot.newTitle")),
3862
+ // ①(修正) 名称输入框与「图片链接」输入框同款(mpw_input;dialog 是列布局,
3863
+ // flex:1 会纵向拉伸 → 显式 flex:none 恢复正常高度)
3864
+ h("input", { className: "mpw_input", type: "text", style: { width: "100%", boxSizing: "border-box", flex: "none", marginBottom: 10 }, placeholder: t("rot.namePh"), value: rotEdit.name, onChange: (ev) => setRotEdit(Object.assign({}, rotEdit, { name: ev.target.value })) }),
3865
+ // ①(修正) 播放顺序(顺序/随机);间隔在列表选择下方单独设置
3866
+ h("div", { className: "mpw_inline", style: { marginBottom: 8 } }, [
3867
+ h("span", { className: "mpw_label", style: { fontSize: 13 } }, t("rot.order")),
3868
+ h("button", { className: "mpw_miniBtn" + (rotEdit.order !== "random" ? " mpw_on" : ""), type: "button", onClick: () => setRotEdit(Object.assign({}, rotEdit, { order: "sequence" })) }, t("rot.seq")),
3869
+ h("button", { className: "mpw_miniBtn" + (rotEdit.order === "random" ? " mpw_on" : ""), type: "button", onClick: () => setRotEdit(Object.assign({}, rotEdit, { order: "random" })) }, t("rot.random"))
3870
+ ]),
3871
+ h("p", { className: "mpw_hint" }, t("rot.pickHint") + "(" + wallList.length + ")"),
3872
+ // ①(修正) 来源过滤 + 扫描入口:误扫自定义目录后也能切回 Steam 库(用户要求"返回")
3873
+ h("div", { className: "mpw_inline", style: { marginBottom: 6 } }, [
3874
+ h("button", { className: "mpw_miniBtn" + (rotFilter === "all" ? " mpw_on" : ""), type: "button", onClick: () => setRotFilter("all") }, t("rot.all")),
3875
+ h("button", { className: "mpw_miniBtn" + (rotFilter === "custom" ? " mpw_on" : ""), type: "button", onClick: () => setRotFilter("custom") }, t("rot.filterCustom")),
3876
+ h("button", { className: "mpw_miniBtn" + (rotFilter === "steam" ? " mpw_on" : ""), type: "button", onClick: () => setRotFilter("steam") }, t("rot.filterSteam")),
3877
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => { if (customDir) scanCustomDir(); else showError(t("lib.dirEmpty")); } }, t("rot.scanDir")),
3878
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: scanLibrary }, t("rot.scanLib"))
3879
+ ]),
3880
+ // ①(修正) 勾选改整行点击;壁纸列表为空时给扫描入口(wallList 是运行时状态,刷新后需重扫)
3881
+ wallList.length === 0
3882
+ ? h("div", { style: { padding: "12px 0" } }, [
3883
+ h("p", { className: "mpw_hint", style: { marginBottom: 6 } }, t("rot.noWallpapers")),
3884
+ h("div", { className: "mpw_inline" }, [
3885
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => { if (customDir) scanCustomDir(); else showError(t("lib.dirEmpty")); } }, t("rot.scanDir")),
3886
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: scanLibrary }, t("rot.scanLib"))
3887
+ ])
3888
+ ])
3889
+ : h("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6, maxHeight: 220, overflowY: "auto" } }, wallList.filter((w) => rotFilter === "all" ? true : rotFilter === "steam" ? w.src === "steam" : !w.src || w.src !== "steam").map((w) => {
3890
+ const on = (rotEdit.keys || []).includes(w.key);
3891
+ // ①(修正) 缩略图:与扫描目录网格同款逻辑(mpkg preview / 视频首帧 / 静态图)
3892
+ let turl = "";
3893
+ try {
3894
+ if (w.type === "mpkg") turl = HOST_BASE + "/custom-mpkg-preview?" + (w.folderMpkg ? "folder=" + encodeURIComponent(w.folderName) + "&" : "") + "file=" + encodeURIComponent(w.folderMpkg ? w.mpkgFile : w.name);
3895
+ else if (w.image) turl = resolveHostUrl(w.image);
3896
+ else if (w.src === "steam" && w.ltoken) {
3897
+ if (w.type === "video" && w.media) turl = HOST_BASE + "/library-media?ltoken=" + encodeURIComponent(w.ltoken) + "&file=" + encodeURIComponent(String(w.media).split(/[\\/]/).pop());
3898
+ else turl = HOST_BASE + "/library-web?ltoken=" + encodeURIComponent(w.ltoken) + "&file=" + encodeURIComponent(String(w.preview || "").split(/[\\/]/).pop() || "preview.jpg");
3899
+ }
3900
+ } catch {}
3901
+ return h("div", { className: "mpw_pickCard" + (on ? " mpw_on" : ""), key: w.key,
3902
+ style: { position: "relative", borderRadius: 10, overflow: "hidden", cursor: "pointer", border: "1px solid " + (on ? "var(--dsw-alias-brand-primary, #4f8cff)" : "var(--dsw-alias-border-l2, #444b5c)"),
3903
+ background: on ? "color-mix(in srgb, var(--dsw-alias-brand-primary, #4f8cff) 12%, transparent)" : "var(--dsw-alias-bg-module-platform, #1c2230)" },
3904
+ onClick: () => {
3905
+ const keys = (rotEdit.keys || []).slice();
3906
+ const i = keys.indexOf(w.key);
3907
+ if (i >= 0) keys.splice(i, 1); else keys.push(w.key);
3908
+ setRotEdit(Object.assign({}, rotEdit, { keys }));
3909
+ } }, [
3910
+ // ①(修正) 固定 64px 高度容器:**只有真正的视频项**用首帧 <video>;
3911
+ // mpkg 项 converted 也是 "mp4" 但缩略 URL 是预览图(图片)——
3912
+ // 之前误用 <video> 加载图片 → 必然失败被隐藏 → 缩略图全空白
3913
+ h("div", { style: { width: "100%", height: 64, position: "relative", background: "color-mix(in srgb, var(--dsw-alias-interactive-bg-hover, #333a4a) 30%, transparent)" } }, [
3914
+ turl && w.type === "video"
3915
+ ? h("video", { src: turl, muted: true, playsInline: true, preload: "metadata",
3916
+ style: { width: "100%", height: 64, objectFit: "cover", display: "block" },
3917
+ onLoadedMetadata: (ev) => { try { const v = ev.target; if (v.duration && v.duration > 0.1) v.currentTime = 0.05; } catch {} },
3918
+ onError: (ev) => { ev.target.style.display = "none"; } })
3919
+ : turl
3920
+ ? h("img", { src: turl, alt: "", loading: "lazy", style: { width: "100%", height: 64, objectFit: "cover", display: "block" }, onError: (ev) => { ev.target.style.display = "none"; } })
3921
+ : null
3922
+ ]),
3923
+ h("div", { style: { display: "flex", alignItems: "center", gap: 6, padding: "6px 8px" } }, [
3924
+ h("span", { style: { flex: "none", width: 16, height: 16, borderRadius: 5, border: "1px solid " + (on ? "var(--dsw-alias-brand-primary, #4f8cff)" : "var(--dsw-alias-border-l2, #444b5c)"), display: "flex", alignItems: "center", justifyContent: "center", color: on ? "var(--dsw-alias-brand-primary, #4f8cff)" : "transparent", fontSize: 12 } }, "✓"),
3925
+ h("span", { className: "mpw_hint", style: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--dsw-alias-label-primary)", fontSize: 12 } }, w.title)
3926
+ ])
3927
+ ]);
3928
+ })),
3929
+ h("div", { className: "mpw_inline", style: { marginTop: 10 } }, [
3930
+ h("button", { className: "mpw_button", type: "button", onClick: () => {
3931
+ const groups = (section.rotGroups || []).slice();
3932
+ const keys = (rotEdit.keys || []).filter((k) => wallList.some((w) => w.key === k));
3933
+ if (!keys.length) { showError(t("rot.emptyKeys")); return; }
3934
+ // ①(修正) 新建列表默认间隔用全局 rotateMin(间隔在列表选择下方单独调)
3935
+ const g = { id: rotEdit.id || ("rot" + Date.now()), name: rotEdit.name.trim() || t("rot.unnamed"), interval: Math.max(1, rotEdit.interval || (section.rotateMin > 0 ? section.rotateMin : 5)), order: rotEdit.order === "random" ? "random" : "sequence", keys };
3936
+ const i = groups.findIndex((x) => x.id === g.id);
3937
+ if (i >= 0) groups[i] = g; else groups.push(g);
3938
+ commit({ rotGroups: groups, rotGroupId: section.rotGroupId || g.id }, true);
3939
+ setRotEditor(false);
3940
+ } }, t("rot.save")),
3941
+ h("button", { className: "mpw_reset", type: "button", onClick: () => setRotEditor(false) }, t("conflict.cancel"))
3942
+ ])
3943
+ ])
3944
+ ]) : null,
3605
3945
 
3606
3946
  // ③(新) 宿主端状态(proot/本机测试方法:可用 = 大文件无限制生效)
3607
3947
  h("p", { className: "mpw_hint" },
@@ -3611,6 +3951,13 @@ body[data-mpw-modal] [role="dialog"] {
3611
3951
  : "混合模式:检测宿主端中…")
3612
3952
  : "纯浏览器模式(600MB 上限)"),
3613
3953
 
3954
+ // ⑳(新) 视频倍速分段条(放在 mpkg 选择上方;点击/拖拽定位档位)
3955
+ h("div", { className: "mpw_field" }, [
3956
+ h("label", { className: "mpw_label" }, t("speed") + " · " + (section.playbackRate !== void 0 ? section.playbackRate : DEFAULT_PLAYBACK_RATE) + "x"),
3957
+ h(SpeedBar, { value: section.playbackRate !== void 0 ? section.playbackRate : DEFAULT_PLAYBACK_RATE, onChange: (v) => commit({ playbackRate: v }, true) }),
3958
+ h("p", { className: "mpw_hint" }, t("speed.hint"))
3959
+ ]),
3960
+
3614
3961
  // mpkg 文件
3615
3962
  h("div", { className: "mpw_field" }, [
3616
3963
  h("div", { className: "mpw_inline" }, [
@@ -3700,7 +4047,7 @@ body[data-mpw-modal] [role="dialog"] {
3700
4047
 
3701
4048
  ]),
3702
4049
  // ═══ 外观 ═══
3703
- h("div", { style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
4050
+ h("div", { "data-mpw-tabkey": "appearance", style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
3704
4051
  h("div", { className: "mpw_section" }, t("sec.appearance")),
3705
4052
  // ②(新) 外观组说明:各项作用一目了然
3706
4053
  h("p", { className: "mpw_hint" }, t("sec.appearance.desc")),
@@ -3728,6 +4075,23 @@ body[data-mpw-modal] [role="dialog"] {
3728
4075
  ]),
3729
4076
  h("p", { className: "mpw_hint" }, t("themeColor.hint"))
3730
4077
  ]),
4078
+ // ①(新) 配色(accent):品牌交互元素(按钮/滑条/选中/链接/发送键)——与主题颜色分工
4079
+ h("div", { className: "mpw_field" }, [
4080
+ h("label", { className: "mpw_label" }, t("glass.accent")),
4081
+ h("div", { className: "mpw_inline" }, [
4082
+ h("button", {
4083
+ className: "mpw_colorSwatch", "data-mpw-picker-anchor": "accent", type: "button",
4084
+ style: { background: (section.accent && /^#[0-9a-fA-F]{6}$/.test(section.accent) ? section.accent : "#3964fe") },
4085
+ onClick: () => openPicker("accent", section.accent)
4086
+ }),
4087
+ ...(AQUA_PRESETS.map((hex) => h("button", {
4088
+ className: "mpw_presetSwatch", type: "button", title: hex,
4089
+ style: { background: hex }, onClick: () => commit({ accent: hex }, true)
4090
+ }))),
4091
+ h("button", { className: "mpw_miniBtn", type: "button", onClick: () => commit({ accent: "" }, true) }, t("glass.reset"))
4092
+ ]),
4093
+ h("p", { className: "mpw_hint" }, t("glass.accent.hint"))
4094
+ ]),
3731
4095
  // ①(修正) 面板不透明度已删除:统一虚化开启时它被 sidebarAlpha 取代且无效果,
3732
4096
  // 非统一虚化下也无独立意义 → 移除滑条,保留内部默认值逻辑。
3733
4097
  // ①(修正) 磨砂模糊条:仅当「统一虚化开 + 聊天区跟随开」时被整屏虚化接管 → 禁用并提示;
@@ -3799,7 +4163,7 @@ body[data-mpw-modal] [role="dialog"] {
3799
4163
 
3800
4164
  ]),
3801
4165
  // ═══ 统一虚化(独立大标题分组)═══
3802
- h("div", { style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
4166
+ h("div", { "data-mpw-tabkey": "unify", style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
3803
4167
  h("div", { className: "mpw_section" }, t("sec.unify")),
3804
4168
  h("p", { className: "mpw_hint" }, t("sec.unify.desc")),
3805
4169
 
@@ -3825,7 +4189,7 @@ body[data-mpw-modal] [role="dialog"] {
3825
4189
 
3826
4190
  ]),
3827
4191
  // ═══ 界面虚化 ═══
3828
- h("div", { style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
4192
+ h("div", { "data-mpw-tabkey": "blur", style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
3829
4193
  h("div", { className: "mpw_section" }, t("sec.blur")),
3830
4194
  h("p", { className: "mpw_hint" }, t("sec.blur.desc")),
3831
4195
 
@@ -3861,7 +4225,7 @@ body[data-mpw-modal] [role="dialog"] {
3861
4225
 
3862
4226
  ]),
3863
4227
  // ═══ 透出壁纸 ═══
3864
- h("div", { style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
4228
+ h("div", { "data-mpw-tabkey": "show", style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
3865
4229
  h("div", { className: "mpw_section" }, t("sec.show")),
3866
4230
  h("p", { className: "mpw_hint" }, t("sec.show.desc")),
3867
4231
 
@@ -3894,7 +4258,7 @@ body[data-mpw-modal] [role="dialog"] {
3894
4258
 
3895
4259
  ]),
3896
4260
  // ═══ Aqua 实验(默认全关,不影响原功能)═══
3897
- h("div", { style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
4261
+ h("div", { "data-mpw-tabkey": "aqua", style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
3898
4262
  h("div", { className: "mpw_section" }, t("sec.aqua")),
3899
4263
  h("p", { className: "mpw_hint" }, t("sec.aqua.desc")),
3900
4264
  // ①(修正) Aqua 方案来源:跳转 Bil812 的 PR #2(致谢可视化)
@@ -3948,10 +4312,9 @@ body[data-mpw-modal] [role="dialog"] {
3948
4312
  toggleRow(t("aquaTextEnhance"), t("aquaTextEnhance.desc"), "aquaTextEnhance", DEFAULT_AQUA_TEXT_ENHANCE),
3949
4313
  // ⑲(新) 任务列表磨砂(收纳/展开统一模糊)
3950
4314
  toggleRow(t("todoBlur"), t("todoBlur.desc"), "todoBlur", DEFAULT_TODO_BLUR),
3951
-
3952
4315
  ]),
3953
4316
  // ═══ 其他 ═══
3954
- h("div", { style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
4317
+ h("div", { "data-mpw-tabkey": "other", style: { flex: "0 0 100%", minWidth: "100%", boxSizing: "border-box", paddingRight: "4px" } }, [
3955
4318
  h("div", { className: "mpw_section" }, t("sec.other")),
3956
4319
 
3957
4320
  // ⑦ 轻度锐化(可能影响 GIF 流畅度)
@@ -3995,7 +4358,7 @@ body[data-mpw-modal] [role="dialog"] {
3995
4358
  hint ? h("p", { className: "mpw_hint" + (/^(解析失败|文件过大|背景素材过大|存储空间|内存不足|此壁纸的视频纹理|不支持)/.test(hint) ? " mpw_err" : "") }, hint) : null,
3996
4359
 
3997
4360
  // ⑩(新) 新版本确认下载弹窗(发布<24h 提示)
3998
- updConfirm ? h("div", { className: "mpw_mask", onClick: () => setUpdConfirm(null) }, [
4361
+ updConfirm ? h(MaskPortal, { onClick: () => setUpdConfirm(null) }, [
3999
4362
  h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
4000
4363
  h("div", { className: "mpw_title" }, t("update.warnTitle")),
4001
4364
  h("p", { className: "mpw_desc" }, t("update.warnBody") + updConfirm.localVersion + " → " + updConfirm.remoteVersion + ")"),
@@ -4007,7 +4370,7 @@ body[data-mpw-modal] [role="dialog"] {
4007
4370
  ]) : null,
4008
4371
 
4009
4372
  // ①(新) 目录选择弹窗(文件夹选择器,跨平台)
4010
- dirPick ? h("div", { className: "mpw_mask", onClick: () => setDirPick(false) }, [
4373
+ dirPick ? h(MaskPortal, { onClick: () => setDirPick(false) }, [
4011
4374
  h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
4012
4375
  h("div", { className: "mpw_title" }, t("lib.pickDir")),
4013
4376
  h("p", { className: "mpw_hint", style: { wordBreak: "break-all" } }, t("lib.curDir") + ":" + (dirPath || "(默认)")),
@@ -4054,7 +4417,7 @@ body[data-mpw-modal] [role="dialog"] {
4054
4417
  ]) : null,
4055
4418
 
4056
4419
  // ①(新) 通用错误弹窗(文件过大 / 无法使用 / 解析失败等)
4057
- errorModal ? h("div", { className: "mpw_mask", onClick: () => setErrorModal(false) }, [
4420
+ errorModal ? h(MaskPortal, { onClick: () => setErrorModal(false) }, [
4058
4421
  h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
4059
4422
  h("div", { className: "mpw_title" }, t("error.title")),
4060
4423
  h("p", { className: "mpw_desc" }, errorMsg),
@@ -4065,7 +4428,7 @@ body[data-mpw-modal] [role="dialog"] {
4065
4428
  ]) : null,
4066
4429
 
4067
4430
  // ⑤(新) 预览模式弹窗(导入后最终用 GIF/图片时提示)
4068
- previewModal ? h("div", { className: "mpw_mask", onClick: () => setPreviewModal(false) }, [
4431
+ previewModal ? h(MaskPortal, { onClick: () => setPreviewModal(false) }, [
4069
4432
  h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
4070
4433
  h("div", { className: "mpw_title" }, t("preview.title")),
4071
4434
  h("p", { className: "mpw_desc" }, t("preview.desc")),
@@ -4077,7 +4440,7 @@ body[data-mpw-modal] [role="dialog"] {
4077
4440
 
4078
4441
  // ④ 冲突确认弹窗(自绘覆盖层)
4079
4442
  // ①(新) 网页壁纸确认弹窗(实验性警告:可能卡顿或无法加载;预检标注风险)
4080
- webConfirm ? h("div", { className: "mpw_mask", onClick: () => setWebConfirm(null) }, [
4443
+ webConfirm ? h(MaskPortal, { onClick: () => setWebConfirm(null) }, [
4081
4444
  h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
4082
4445
  h("div", { className: "mpw_title" }, t("web.confirmTitle")),
4083
4446
  h("p", { className: "mpw_desc" }, `${t("web.confirmBody")}(${webConfirm.title || ""})`),
@@ -4099,7 +4462,7 @@ body[data-mpw-modal] [role="dialog"] {
4099
4462
  ])
4100
4463
  ])
4101
4464
  ]) : null,
4102
- conflictModal ? h("div", { className: "mpw_mask", onClick: () => setConflictModal(false) }, [
4465
+ conflictModal ? h(MaskPortal, { onClick: () => setConflictModal(false) }, [
4103
4466
  h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
4104
4467
  h("div", { className: "mpw_title" }, t("conflict.title")),
4105
4468
  h("p", { className: "mpw_desc" }, `${t("conflict.body")}:${conflicts.join(", ")}`),
@@ -4211,6 +4574,16 @@ body[data-mpw-modal] [role="dialog"] {
4211
4574
  "aquaTextEnhance": "深底文字可读增强",
4212
4575
  "aquaTextEnhance.desc": "给聊天区文字加双色描边,黑色/深色背景上也能看清(近似方案:无法精确到'只变经过深色处的字';默认关)",
4213
4576
  "todoBlur": "任务列表磨砂",
4577
+ "glass.title": "液态玻璃(elysia395 方案)",
4578
+ "glass.desc": "设置卡片/弹窗玻璃化:玻璃底色 + 白叠层强度 + 总开关(与 Aqua 其他功能独立,默认关)",
4579
+ "glassWindow": "设置窗口液态玻璃",
4580
+ "glassWindow.desc": "整个设置卡片/弹窗玻璃化(半透明 + 模糊 + 底色 tint)",
4581
+ "glass.accent": "配色(accent)",
4582
+ "glass.accent.hint": "驱动按钮/滑条/选中/链接/发送键等品牌交互元素(与主题颜色分工:它管整体底色)",
4583
+ "glass.color": "玻璃颜色",
4584
+ "glass.color.hint": "玻璃底色 tint(默认亮=白/暗=深夜蓝;两主题统一用所选色)",
4585
+ "glass.alpha": "玻璃透明度",
4586
+ "glass.reset": "默认",
4214
4587
  "todoBlur.desc": "对话中列出的任务(todo 卡片)背景模糊,收纳/展开状态统一(类似标题栏/侧边栏磨砂;默认关)",
4215
4588
  "aquaInk.desc": "文字颜色随遮罩亮度自适应(亮底深字/暗底浅字),并把品牌蓝等硬编码色统一为墨色衍生色",
4216
4589
  "mpkg.pick": "选择 .mpkg 文件",
@@ -4273,7 +4646,36 @@ body[data-mpw-modal] [role="dialog"] {
4273
4646
  "flipY": "垂直翻转(镜像)",
4274
4647
  "flipY.desc": "壁纸上下镜像(scaleY -1)",
4275
4648
  "themeColor": "主题颜色",
4276
- "themeColor.hint": "驱动按钮/滑条/选中项/链接/发送键等品牌色(取色盘 + 预置);空 = DSH 默认蓝",
4649
+ "themeColor.hint": "控制侧边栏 / 标题栏 / 新会话按钮 / 设置弹窗的整体底色 tint(取色盘 + 预置);品牌交互元素(按钮/选中/链接)由 Aqua 区的「配色」控制;空 = 不启用",
4650
+ "speed": "视频倍速",
4651
+ "speed.hint": "视频壁纸播放速度(0.5x–2x,原生播放器变速,即时生效不重载)",
4652
+ "rot.title": "轮播列表管理",
4653
+ "rot.all": "全部壁纸(不限定列表)",
4654
+ "rot.manage": "管理列表",
4655
+ "rot.empty": "还没有轮播列表。点「新建列表」从壁纸中挑选,或扫描 Steam 库自动导入 WE 播放列表。",
4656
+ "rot.new": "新建列表",
4657
+ "rot.newTitle": "新建轮播列表",
4658
+ "rot.edit": "编辑",
4659
+ "rot.editTitle": "编辑轮播列表",
4660
+ "rot.del": "删除",
4661
+ "rot.save": "保存",
4662
+ "rot.items": " 个壁纸",
4663
+ "rot.seq": "顺序",
4664
+ "rot.random": "随机",
4665
+ "rot.namePh": "列表名称",
4666
+ "rot.unnamed": "未命名列表",
4667
+ "rot.pickHint": "勾选参与轮播的壁纸",
4668
+ "rot.emptyKeys": "请至少勾选一个壁纸",
4669
+ "rot.wePlaylist": "WE 播放列表",
4670
+ "rot.seeded": "已导入 WE 原生播放列表",
4671
+ "rot.groupInterval": "该列表轮换间隔",
4672
+ "rot.order": "播放顺序",
4673
+ "rot.noWallpapers": "没有可选的壁纸(先扫描壁纸库)",
4674
+ "rot.scanDir": "扫描自定义目录",
4675
+ "rot.filterCustom": "自定义",
4676
+ "rot.filterSteam": "Steam库",
4677
+ "rot.scanLib": "扫描本地库",
4678
+ "rot.intervalHint": "间隔在选中列表后下方单独设置",
4277
4679
  "themeColorReset": "默认",
4278
4680
  "blur": "磨砂模糊",
4279
4681
  "blur.overridden": "统一虚化 + 聊天区跟随均开启:壁纸模糊由「整屏虚化程度」接管,磨砂条暂不可调;关闭「聊天区跟随整屏虚化」后磨砂条恢复可调(此时统一虚化只管侧边栏/标题栏)",
@@ -4379,6 +4781,35 @@ body[data-mpw-modal] [role="dialog"] {
4379
4781
  "lib.rotateDesc": "Automatically switch to the next wallpaper on a timer (range: custom folder + local library + Steam-discovered video wallpapers)",
4380
4782
  "lib.rotMerged": "Playable Steam-library videos merged into the rotation: ",
4381
4783
  "lib.rotateMin": "Rotation interval",
4784
+ "speed": "Video speed",
4785
+ "speed.hint": "Video wallpaper playback speed (0.5x–2x, native player rate, instant, no reload)",
4786
+ "rot.title": "Carousel list management",
4787
+ "rot.all": "All wallpapers (no list)",
4788
+ "rot.manage": "Manage lists",
4789
+ "rot.empty": "No carousel lists yet. Click \"New list\" to pick wallpapers, or scan the Steam library to auto-import WE playlists.",
4790
+ "rot.new": "New list",
4791
+ "rot.newTitle": "New carousel list",
4792
+ "rot.edit": "Edit",
4793
+ "rot.editTitle": "Edit carousel list",
4794
+ "rot.del": "Delete",
4795
+ "rot.save": "Save",
4796
+ "rot.items": " wallpapers",
4797
+ "rot.seq": "Sequence",
4798
+ "rot.random": "Random",
4799
+ "rot.namePh": "List name",
4800
+ "rot.unnamed": "Unnamed list",
4801
+ "rot.pickHint": "Check wallpapers to include in rotation",
4802
+ "rot.emptyKeys": "Select at least one wallpaper",
4803
+ "rot.wePlaylist": "WE playlist",
4804
+ "rot.seeded": "Imported WE native playlist",
4805
+ "rot.groupInterval": "This list rotation interval",
4806
+ "rot.order": "Play order",
4807
+ "rot.noWallpapers": "No wallpapers to pick (scan a wallpaper library first)",
4808
+ "rot.scanDir": "Scan custom folder",
4809
+ "rot.filterCustom": "Custom",
4810
+ "rot.filterSteam": "Steam library",
4811
+ "rot.scanLib": "Scan local library",
4812
+ "rot.intervalHint": "Interval is set separately below after selecting a list",
4382
4813
  "lib.minutes": "min",
4383
4814
  "hybrid.desc": "On: mpkg is uploaded to the DSH host and streamed (supports files >600MB; restart dsh web for the host half). Off: pure browser mode (600MB cap)",
4384
4815
  "conflict.detected": "Potentially conflicting plugins detected",
package/lib/index.js CHANGED
@@ -164,6 +164,15 @@ function parseMpkgHead(buf) {
164
164
  return { dataStart: pos, entries };
165
165
  }
166
166
 
167
+ /** ①(修正) 流式响应统一清理(issue-26 同类隐患:客户端中途断开时
168
+ * createReadStream 只 unpipe 不销毁 → fd 泄漏;Windows 上会锁文件无法删除)。
169
+ * close(客户端断开)/ error 都 destroy 源流;正常结束由 pipe 自行处理。 */
170
+ function serveStream(res, stream) {
171
+ stream.on('error', () => { try { res.destroy(); } catch { /* 忽略 */ } });
172
+ res.on('close', () => { try { stream.destroy(); } catch { /* 忽略 */ } });
173
+ stream.pipe(res);
174
+ }
175
+
167
176
  function json(res, code, obj) {
168
177
  res.writeHead(code, { 'content-type': 'application/json' });
169
178
  res.end(JSON.stringify(obj));
@@ -340,10 +349,10 @@ function apply(ctx) {
340
349
  'content-range': `bytes ${s - start}-${e - start}/${entry.size}`,
341
350
  'content-length': e - s + 1,
342
351
  });
343
- createReadStream(rec.path, { start: s, end: e }).pipe(res);
352
+ serveStream(res, createReadStream(rec.path, { start: s, end: e }));
344
353
  } else {
345
354
  res.writeHead(200, { 'content-type': mime, 'accept-ranges': 'bytes', 'content-length': entry.size });
346
- createReadStream(rec.path, { start, end }).pipe(res);
355
+ serveStream(res, createReadStream(rec.path, { start, end }));
347
356
  }
348
357
  } catch (err) {
349
358
  json(res, 500, { ok: false, error: String(err && err.message || err) });
@@ -468,6 +477,18 @@ function apply(ctx) {
468
477
  else if (hasHtml) { type = 'web'; media = 'index.html'; }
469
478
  else if (vid) { type = 'video'; media = vid; }
470
479
  }
480
+ // ①(修正) 子文件夹里的 .mpkg(无 project.json/scene.pkg/html 的纯 mpkg 收藏夹,
481
+ // 如 wallpaperE 下的角色文件夹)→ 每个 mpkg 独立成条(folderMpkg)
482
+ if ((!type || !media) && e.isDirectory()) {
483
+ let mpkgs = [];
484
+ try { mpkgs = readdirSync(sub).filter((x) => x.toLowerCase().endsWith('.mpkg')); } catch {}
485
+ if (mpkgs.length) {
486
+ for (const m of mpkgs) {
487
+ files.push({ name: f, title: m, type: 'mpkg', media: m, folder: true, folderMpkg: true, preview: null });
488
+ }
489
+ continue;
490
+ }
491
+ }
471
492
  if (type && media) {
472
493
  // ①(新) 网页壁纸风险预检:Spine/L2D 骨骼动画(skel/atlas)→ 重动画
473
494
  // 可能卡顿;引用外网 http(s) → 依赖外网可能失败(用户实测:昔涟类
@@ -520,8 +541,10 @@ function apply(ctx) {
520
541
  if (!customDir) { json(res, 404, { ok: false, error: 'no dir' }); return; }
521
542
  const url = new URL(req.url || '', 'http://localhost');
522
543
  const file = url.searchParams.get('file') || '';
523
- if (!file || file.includes('..') || file.includes('/') || file.includes('\\')) { json(res, 403, { ok: false, error: 'forbidden' }); return; }
524
- const filePath = join(customDir, file);
544
+ const folder = url.searchParams.get('folder') || '';
545
+ if (!file || file.includes('..') || file.includes('/') || file.includes('\\') || (folder && (folder.includes('..') || folder.includes('/') || folder.includes('\\') || folder.startsWith('.')))) { json(res, 403, { ok: false, error: 'forbidden' }); return; }
546
+ // ①(修正) folder= 支持子文件夹里的 .mpkg(wallpaperE 角色收藏夹)
547
+ const filePath = folder ? join(customDir, folder, file) : join(customDir, file);
525
548
  if (!existsSync(filePath) || !file.toLowerCase().endsWith('.mpkg')) { json(res, 404, { ok: false, error: 'not found' }); return; }
526
549
  const head = readFileSync(filePath).subarray(0, HEAD_BYTES);
527
550
  const { dataStart, entries } = parseMpkgHead(head);
@@ -560,12 +583,15 @@ function apply(ctx) {
560
583
  if (!customDir) { json(res, 404, { ok: false, error: 'no dir' }); return; }
561
584
  const url = new URL(req.url || '', 'http://localhost');
562
585
  const file = url.searchParams.get('file') || '';
563
- if (!file || file.includes('..') || file.includes('/') || file.includes('\\')) { json(res, 403, { ok: false, error: 'forbidden' }); return; }
564
- const filePath = join(customDir, file);
586
+ const folder = url.searchParams.get('folder') || '';
587
+ if (!file || file.includes('..') || file.includes('/') || file.includes('\\') || (folder && (folder.includes('..') || folder.includes('/') || folder.includes('\\') || folder.startsWith('.')))) { json(res, 403, { ok: false, error: 'forbidden' }); return; }
588
+ // ①(修正) folder= 支持子文件夹里的 .mpkg
589
+ const filePath = folder ? join(customDir, folder, file) : join(customDir, file);
565
590
  if (!existsSync(filePath) || !file.toLowerCase().endsWith('.mpkg')) { json(res, 404, { ok: false, error: 'not found' }); return; }
566
591
  // ①(新) 缓存:mtime 未变 → 直接返回缓存 bytes(不再解析头部/打开大文件)
567
592
  const st = statSync(filePath);
568
- const cached = mpkgPreviewGet(file);
593
+ const cacheKey = (folder ? folder + '/' : '') + file;
594
+ const cached = mpkgPreviewGet(cacheKey);
569
595
  if (cached && cached.mtimeMs === st.mtimeMs && cached.bytes) {
570
596
  res.writeHead(200, { 'content-type': cached.mime, 'content-length': cached.bytes.length, 'cache-control': 'no-cache' });
571
597
  res.end(cached.bytes);
@@ -592,14 +618,14 @@ function apply(ctx) {
592
618
  if (n <= 0) break; got += n;
593
619
  }
594
620
  } finally { closeSync(fd); }
595
- mpkgPreviewSet(file, { mtimeMs: st.mtimeMs, mime, bytes: buf });
621
+ mpkgPreviewSet(cacheKey, { mtimeMs: st.mtimeMs, mime, bytes: buf });
596
622
  res.writeHead(200, { 'content-type': mime, 'content-length': buf.length, 'cache-control': 'no-cache' });
597
623
  res.end(buf);
598
624
  return;
599
625
  }
600
626
  // 超大 preview:流式读(不缓存)
601
627
  res.writeHead(200, { 'content-type': mime, 'content-length': e.size, 'cache-control': 'no-cache' });
602
- createReadStream(filePath, { start: dataStart + e.index, end: dataStart + e.index + e.size - 1 }).pipe(res);
628
+ serveStream(res, createReadStream(filePath, { start: dataStart + e.index, end: dataStart + e.index + e.size - 1 }));
603
629
  } catch (err) { json(res, 500, { ok: false, error: String(err && err.message || err) }); }
604
630
  },
605
631
  });
@@ -631,10 +657,10 @@ function apply(ctx) {
631
657
  const s = Math.max(0, rs); const e = Math.min(st.size - 1, re);
632
658
  if (s > e) { res.writeHead(416, { 'content-range': `bytes */${st.size}` }); res.end(); return; }
633
659
  res.writeHead(206, { 'content-type': mime, 'accept-ranges': 'bytes', 'content-range': `bytes ${s}-${e}/${st.size}`, 'content-length': e - s + 1 });
634
- createReadStream(filePath, { start: s, end: e }).pipe(res);
660
+ serveStream(res, createReadStream(filePath, { start: s, end: e }));
635
661
  } else {
636
662
  res.writeHead(200, { 'content-type': mime, 'accept-ranges': 'bytes', 'content-length': st.size });
637
- createReadStream(filePath).pipe(res);
663
+ serveStream(res, createReadStream(filePath));
638
664
  }
639
665
  } catch (err) { json(res, 500, { ok: false, error: String(err && err.message || err) }); }
640
666
  },
@@ -676,10 +702,10 @@ function apply(ctx) {
676
702
  const s = Math.max(0, rs); const e = Math.min(st.size - 1, re);
677
703
  if (s > e) { res.writeHead(416, { 'content-range': `bytes */${st.size}` }); res.end(); return; }
678
704
  res.writeHead(206, { 'content-type': mime, 'accept-ranges': 'bytes', 'content-range': `bytes ${s}-${e}/${st.size}`, 'content-length': e - s + 1 });
679
- createReadStream(filePath, { start: s, end: e }).pipe(res);
705
+ serveStream(res, createReadStream(filePath, { start: s, end: e }));
680
706
  } else {
681
707
  res.writeHead(200, { 'content-type': mime, 'accept-ranges': 'bytes', 'content-length': st.size });
682
- createReadStream(filePath).pipe(res);
708
+ serveStream(res, createReadStream(filePath));
683
709
  }
684
710
  };
685
711
  // ①(新) 自定义目录 workshop 子文件夹资源:**prefix 路由 + path 式 URL**
@@ -752,7 +778,7 @@ function apply(ctx) {
752
778
  : name.endsWith('.webp') ? 'image/webp' : 'image/jpeg';
753
779
  const st = statSync(pv);
754
780
  res.writeHead(200, { 'content-type': mime, 'content-length': st.size, 'cache-control': 'no-cache' });
755
- createReadStream(pv).pipe(res);
781
+ serveStream(res, createReadStream(pv));
756
782
  return;
757
783
  }
758
784
  json(res, 404, { ok: false, error: 'no frame' });
@@ -932,10 +958,10 @@ function apply(ctx) {
932
958
  'content-type': mime, 'accept-ranges': 'bytes',
933
959
  'content-range': `bytes ${s}-${e}/${st.size}`, 'content-length': e - s + 1,
934
960
  });
935
- createReadStream(filePath, { start: s, end: e }).pipe(res);
961
+ serveStream(res, createReadStream(filePath, { start: s, end: e }));
936
962
  } else {
937
963
  res.writeHead(200, { 'content-type': mime, 'accept-ranges': 'bytes', 'content-length': st.size });
938
- createReadStream(filePath).pipe(res);
964
+ serveStream(res, createReadStream(filePath));
939
965
  }
940
966
  } catch (err) {
941
967
  json(res, 500, { ok: false, error: String(err && err.message || err) });
@@ -1003,7 +1029,53 @@ function apply(ctx) {
1003
1029
  }
1004
1030
  };
1005
1031
  for (const root of projectsRoots) scan(root);
1006
- json(res, 200, { ok: true, installDir, wallpapers });
1032
+ // ①(新) WE 原生播放列表(config.json → general.playlists):解析为
1033
+ // 轮播列表(项 = 壁纸路径 → 映射到本插件的 key:steam|ltoken / custom|目录名)
1034
+ const playlists = [];
1035
+ try {
1036
+ const cfgPath = join(installDir, 'config.json');
1037
+ if (existsSync(cfgPath)) {
1038
+ const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'));
1039
+ const byFolder = new Map(); // 文件夹名 → ltoken
1040
+ for (const [lt, rec] of library) {
1041
+ const base = String(rec.dir || '').split(/[\\/]/).filter(Boolean).pop() || '';
1042
+ if (base && !byFolder.has(base)) byFolder.set(base.toLowerCase(), lt);
1043
+ }
1044
+ const seen = new Set();
1045
+ for (const profile of Object.values(cfg || {})) {
1046
+ if (!profile || typeof profile !== 'object') continue;
1047
+ const general = profile.general || {};
1048
+ const rows = Array.isArray(general.playlists) ? general.playlists : [];
1049
+ for (const row of rows) {
1050
+ const items = Array.isArray(row.items) ? row.items.filter((x) => typeof x === 'string' && x.trim()) : [];
1051
+ if (!items.length) continue;
1052
+ const keys = [];
1053
+ for (const item of items) {
1054
+ // "…/workshop/content/431960/<folder>/…" → 文件夹名 → ltoken
1055
+ const m = /[\\/]431960[\\/]([^\\/]+)(?:[\\/]|$)/i.exec(item);
1056
+ const folder = m ? m[1] : /[\\/]([^\\/]+)[\\/][^\\/]+$/i.exec(item) ? /[\\/]([^\\/]+)[\\/][^\\/]+$/i.exec(item)[1] : null;
1057
+ if (!folder) continue;
1058
+ const lt = byFolder.get(folder.toLowerCase());
1059
+ if (lt) keys.push('steam|' + lt);
1060
+ else if (customDir && existsSync(join(customDir, folder))) keys.push('custom|' + folder);
1061
+ }
1062
+ if (!keys.length) continue;
1063
+ const name = typeof row.name === 'string' && row.name.trim() ? row.name.trim() : 'WE Playlist';
1064
+ const sig = name + '\u0000' + keys.join('\u0000');
1065
+ if (seen.has(sig)) continue;
1066
+ seen.add(sig);
1067
+ const settings = row.settings && typeof row.settings === 'object' ? row.settings : {};
1068
+ playlists.push({
1069
+ name,
1070
+ keys,
1071
+ order: settings.order === 'random' ? 'random' : 'sequence',
1072
+ interval: typeof settings.delay === 'number' && settings.delay > 0 ? Math.min(1440, Math.round(settings.delay)) : 5,
1073
+ });
1074
+ }
1075
+ }
1076
+ }
1077
+ } catch { /* 播放列表读取失败不阻塞 */ }
1078
+ json(res, 200, { ok: true, installDir, wallpapers, playlists });
1007
1079
  } catch (err) {
1008
1080
  json(res, 500, { ok: false, error: String(err && err.message || err) });
1009
1081
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mpkg-wallpaper",
3
- "version": "3.1.4",
3
+ "version": "3.1.6",
4
4
  "description": "DSH Web 壁纸引擎 Wallpaper Engine mpkg 背景插件:浏览器内直接解析 .mpkg(preview.gif 动态背景/内嵌 mp4 视频/多时段切换),整屏统一虚化/对话框/弹层/遮罩独立虚化、镜头缩放平移、时钟、冲突检测。",
5
5
  "private": false,
6
6
  "type": "module",