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 +1 -1
- package/README.md +1 -1
- package/lib/client.js +506 -75
- package/lib/index.js +89 -17
- package/package.json +1 -1
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
/*
|
|
1535
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
2846
|
-
setHint(t("lib.applied") + ":" +
|
|
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 =
|
|
3042
|
+
const list = rotCandidates();
|
|
2924
3043
|
if (!list.length) { showError(t("lib.empty")); return; }
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
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 =
|
|
3053
|
+
const list = rotCandidates();
|
|
2932
3054
|
if (!list.length) { showError(t("lib.empty")); return; }
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
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
|
|
2941
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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": "
|
|
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 })
|
|
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 })
|
|
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
|
-
|
|
524
|
-
|
|
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
|
-
|
|
564
|
-
|
|
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
|
|
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(
|
|
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 })
|
|
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 })
|
|
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)
|
|
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 })
|
|
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)
|
|
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)
|
|
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 })
|
|
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)
|
|
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
|
-
|
|
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