dsh-mpkg-wallpaper 3.1.4 → 3.1.5
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 +458 -31
- package/lib/index.js +70 -7
- 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
|
}
|
|
@@ -951,6 +963,12 @@ window.__ModuleLoader__.load({
|
|
|
951
963
|
try {
|
|
952
964
|
if (aquaOn(section)) document.body.setAttribute("data-mpw-aqua", "");
|
|
953
965
|
else document.body.removeAttribute("data-mpw-aqua");
|
|
966
|
+
// ①(修正) 标题栏跟随 mask 色只在统一雾/面板取色时开;纯自适应文字色
|
|
967
|
+
// 不刷标题栏底色(否则亮色主题下标题栏变 62% 白 → session log 白框)
|
|
968
|
+
const headerTint = (section.aquaMask !== void 0 ? !!section.aquaMask : DEFAULT_AQUA_MASK)
|
|
969
|
+
|| (section.aquaTint !== void 0 ? !!section.aquaTint : DEFAULT_AQUA_TINT);
|
|
970
|
+
if (headerTint) document.body.setAttribute("data-mpw-aqua-header", "");
|
|
971
|
+
else document.body.removeAttribute("data-mpw-aqua-header");
|
|
954
972
|
if (section.aquaTextEnhance !== void 0 ? !!section.aquaTextEnhance : DEFAULT_AQUA_TEXT_ENHANCE)
|
|
955
973
|
document.body.setAttribute("data-mpw-aqua-text", "");
|
|
956
974
|
else document.body.removeAttribute("data-mpw-aqua-text");
|
|
@@ -1080,7 +1098,11 @@ window.__ModuleLoader__.load({
|
|
|
1080
1098
|
root.style.setProperty("--mpw-aqua-brand-contrast", aquaInkContrast(aquaBrandColor(s)));
|
|
1081
1099
|
const maskOn = s.aquaMask !== void 0 ? !!s.aquaMask : DEFAULT_AQUA_MASK;
|
|
1082
1100
|
if (maskOn) {
|
|
1083
|
-
|
|
1101
|
+
let panel = Math.max(0, Math.min(100, s.aquaMaskAlpha !== void 0 ? s.aquaMaskAlpha : DEFAULT_AQUA_MASK_ALPHA)) / 100;
|
|
1102
|
+
// ①(修正) 亮色遮罩防过曝:自定义色偏亮(luma>0.6)时自动把强度
|
|
1103
|
+
// 压到 ≤55%,避免全屏亮色雾把画面洗白(用户实测:预置色 1/5 变白)
|
|
1104
|
+
const luma = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
|
|
1105
|
+
if (luma > 0.6) panel = Math.min(panel, 0.55);
|
|
1084
1106
|
el.style.background = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${panel})`;
|
|
1085
1107
|
el.style.display = "";
|
|
1086
1108
|
} else {
|
|
@@ -1618,6 +1640,25 @@ ${frostedLayers}
|
|
|
1618
1640
|
body:not([data-ds-dark-theme]) .wSkVaW_header {
|
|
1619
1641
|
background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-00) ${hdrAlpha}%, transparent) !important;
|
|
1620
1642
|
}
|
|
1643
|
+
`;
|
|
1644
|
+
// ①(修正) 标题栏内部白框:unify(透明标题栏)时 session logo 按钮 /
|
|
1645
|
+
// 「标准模式」标签等自带实心底的盒子仍显示为白色块(用户两次反馈),
|
|
1646
|
+
// 全部改透明,保留 hover 交互态。
|
|
1647
|
+
css += `
|
|
1648
|
+
.wSkVaW_header button,
|
|
1649
|
+
.wSkVaW_header [class*="button"],
|
|
1650
|
+
.wSkVaW_header [class*="logo"],
|
|
1651
|
+
.wSkVaW_header [class*="mode"],
|
|
1652
|
+
.wSkVaW_header [class*="badge"],
|
|
1653
|
+
.wSkVaW_header [class*="segmented"] {
|
|
1654
|
+
background: transparent !important;
|
|
1655
|
+
box-shadow: none !important;
|
|
1656
|
+
}
|
|
1657
|
+
.wSkVaW_header button:hover,
|
|
1658
|
+
.wSkVaW_header [class*="button"]:hover,
|
|
1659
|
+
.wSkVaW_header [class*="segmented"] button:hover {
|
|
1660
|
+
background: color-mix(in srgb, var(--dsw-alias-interactive-bg-hover) 70%, transparent) !important;
|
|
1661
|
+
}
|
|
1621
1662
|
`;
|
|
1622
1663
|
}
|
|
1623
1664
|
if (headerBg && !headerBlur) {
|
|
@@ -1975,11 +2016,18 @@ body[data-mpw-aqua] {
|
|
|
1975
2016
|
--dsw-alias-brand-text: var(--mpw-aqua-ink, inherit);
|
|
1976
2017
|
--dsw-alias-state-business-primary: var(--mpw-aqua-ink-secondary, inherit);
|
|
1977
2018
|
}
|
|
1978
|
-
/* ①(修正) 面板取色/统一雾时标题栏背景跟随 mask
|
|
1979
|
-
|
|
2019
|
+
/* ①(修正) 面板取色/统一雾时标题栏背景跟随 mask 色(用户实测只改侧边栏不改标题栏)。
|
|
2020
|
+
①(修正2) 标题栏是否刷 mask 色由 JS 打 data-mpw-aqua-header 门控:
|
|
2021
|
+
纯 aquaInk(自适应文字色)时不刷——此时遮罩色=主题中性色(亮色主题=白),
|
|
2022
|
+
会把标题栏刷成 62% 白 → session log/标准模式背后的「白框」(用户实测:
|
|
2023
|
+
统一虚化 + 自适应文字色同时开出现)。 */
|
|
2024
|
+
body[data-mpw-aqua][data-mpw-aqua-header] .wSkVaW_header {
|
|
1980
2025
|
background-color: color-mix(in srgb, rgb(var(--mpw-aqua-rgb)) 62%, transparent) !important;
|
|
1981
2026
|
backdrop-filter: none !important;
|
|
1982
2027
|
}
|
|
2028
|
+
body[data-mpw-aqua] .wSkVaW_header {
|
|
2029
|
+
backdrop-filter: none !important;
|
|
2030
|
+
}
|
|
1983
2031
|
/* ⑲(修正) Aqua 模式下弹层背景被 mask 色半透明化,必须配 backdrop blur 防文字叠压
|
|
1984
2032
|
(用户实测:加号/命令/权限/模型/上下文选择器透明但无模糊 → 文字叠在一起) */
|
|
1985
2033
|
body[data-mpw-aqua] [role="menu"],
|
|
@@ -2126,11 +2174,25 @@ body[data-ds-dark-theme] .mpw_tabBar {
|
|
|
2126
2174
|
.mpw_label { font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-secondary); }
|
|
2127
2175
|
.mpw_inline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
2128
2176
|
.mpw_input {
|
|
2129
|
-
flex: 1; min-width: 0; height:
|
|
2177
|
+
flex: 1; min-width: 0; height: 40px; padding: 0 12px; box-sizing: border-box;
|
|
2130
2178
|
border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px;
|
|
2131
2179
|
background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary);
|
|
2132
2180
|
font: inherit; font-size: 13px;
|
|
2133
2181
|
}
|
|
2182
|
+
/* ⑳(新) 自定义下拉(替代原生 select) */
|
|
2183
|
+
.mpw_selectBtn { display: flex; align-items: center; gap: 6px; cursor: pointer; }
|
|
2184
|
+
.mpw_selectMenu {
|
|
2185
|
+
background: var(--dsw-alias-bg-module-platform, #1c2230);
|
|
2186
|
+
border: 1px solid var(--dsw-alias-border-l2, #444b5c);
|
|
2187
|
+
border-radius: 10px; padding: 4px; box-shadow: var(--dsw-shadow-lv3, 0 12px 40px rgba(0,0,0,.35));
|
|
2188
|
+
}
|
|
2189
|
+
.mpw_selectOpt {
|
|
2190
|
+
display: block; width: 100%; box-sizing: border-box; text-align: left;
|
|
2191
|
+
padding: 8px 10px; border: 0; border-radius: 8px; background: transparent;
|
|
2192
|
+
color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; cursor: pointer;
|
|
2193
|
+
}
|
|
2194
|
+
.mpw_selectOpt:hover { background: color-mix(in srgb, var(--dsw-alias-interactive-bg-hover, #333a4a) 60%, transparent); }
|
|
2195
|
+
.mpw_selectOpt.mpw_on { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f8cff) 20%, transparent); }
|
|
2134
2196
|
.mpw_button {
|
|
2135
2197
|
flex: none; height: 34px; padding: 0 14px; border: 1px solid rgba(255,255,255,0.25); border-radius: 10px;
|
|
2136
2198
|
background: #3964fe !important; color: #ffffff !important;
|
|
@@ -2168,6 +2230,12 @@ body[data-ds-dark-theme] .mpw_tabBar {
|
|
|
2168
2230
|
color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px; cursor: pointer;
|
|
2169
2231
|
}
|
|
2170
2232
|
.mpw_miniBtn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
2233
|
+
/* ①(修正) 选中态:品牌色描边 + 淡品牌底(顺序/随机等按钮选中要有明确显示) */
|
|
2234
|
+
.mpw_miniBtn.mpw_on {
|
|
2235
|
+
background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f8cff) 22%, transparent);
|
|
2236
|
+
color: var(--dsw-alias-brand-primary, #4f8cff);
|
|
2237
|
+
border-color: var(--dsw-alias-brand-primary, #4f8cff);
|
|
2238
|
+
}
|
|
2171
2239
|
.mpw_hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); margin: 0; }
|
|
2172
2240
|
/* ①(新) 标题行:仓库链接(灰色、无下划线、hover 变深)+ 版本号 */
|
|
2173
2241
|
.mpw_titleRow { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
|
|
@@ -2550,6 +2618,10 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
2550
2618
|
const [updState, setUpdState] = react.useState(null); // null=未检测 / {checking}/{hasUpdate...}
|
|
2551
2619
|
const [updConfirm, setUpdConfirm] = react.useState(null); // ⑩(新) 新版本确认下载弹窗
|
|
2552
2620
|
const [webConfirm, setWebConfirm] = react.useState(null); // ①(新) 网页壁纸确认弹窗(实验性警告)
|
|
2621
|
+
const [rotModal, setRotModal] = react.useState(false); // ⑳(新) 轮播列表管理弹窗
|
|
2622
|
+
const [rotEditor, setRotEditor] = react.useState(false); // ⑳(新) 轮播列表编辑弹窗
|
|
2623
|
+
const [rotEdit, setRotEdit] = react.useState(null); // ⑳(新) 正在编辑的列表草稿
|
|
2624
|
+
const [rotFilter, setRotFilter] = react.useState("all"); // ⑳(新) 勾选界面来源过滤:all/custom/steam
|
|
2553
2625
|
const [wallList, setWallList] = react.useState([]); // ② 合并后的可选壁纸列表
|
|
2554
2626
|
const [wallIdx, setWallIdx] = react.useState(-1); // 当前在列表中的索引
|
|
2555
2627
|
const [libShow, setLibShow] = react.useState(10); // 1(新) 列表单次展示数量(可展开)
|
|
@@ -2718,6 +2790,19 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
2718
2790
|
const walls = d.ok && d.wallpapers ? d.wallpapers : [];
|
|
2719
2791
|
setLibWalls(walls);
|
|
2720
2792
|
if (d.ok && !d.installDir) showError(t("lib.noInstall"));
|
|
2793
|
+
// ⑳(新) WE 原生播放列表 → 首次自动导入为轮播列表(rotSeeded 防止删除后再播种)
|
|
2794
|
+
try {
|
|
2795
|
+
const pls = (d.playlists || []).filter((p) => p && Array.isArray(p.keys) && p.keys.length);
|
|
2796
|
+
const cur = readSection();
|
|
2797
|
+
if (pls.length && !(cur.rotGroups || []).length && !cur.rotSeeded) {
|
|
2798
|
+
const seed = pls[0];
|
|
2799
|
+
const gid = "we" + Date.now();
|
|
2800
|
+
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);
|
|
2801
|
+
setHint(t("rot.seeded") + ":" + (seed.name || ""));
|
|
2802
|
+
} else if (!pls.length && !cur.rotSeeded) {
|
|
2803
|
+
commit({ rotSeeded: true }, true);
|
|
2804
|
+
}
|
|
2805
|
+
} catch {}
|
|
2721
2806
|
// ②(新) Steam 库合并进统一列表(轮播/上下切换可用):
|
|
2722
2807
|
// 并入 video(mp4 播放)+ web(iframe)+ scene(预览图/后续静态帧),
|
|
2723
2808
|
// 用 src:"steam" 标记,next/prev 走 applyLibraryWallpaper。
|
|
@@ -2741,6 +2826,11 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
2741
2826
|
// ①(新) 检测更新(对比实际代码内容哈希,README 变更不触发)
|
|
2742
2827
|
const checkUpdate = async () => {
|
|
2743
2828
|
setUpdState({ checking: true });
|
|
2829
|
+
// ①(修正) 实时刷新当前版本(像插件市场那样打开即读最新安装版本)
|
|
2830
|
+
try {
|
|
2831
|
+
const p = await fetch(HOST_BASE + "/ping", { method: "GET" });
|
|
2832
|
+
if (p.ok) { const pd = await p.json(); if (pd && pd.version) setHostVersion(pd.version); }
|
|
2833
|
+
} catch {}
|
|
2744
2834
|
try {
|
|
2745
2835
|
const r = await fetch(HOST_BASE + "/update-check");
|
|
2746
2836
|
const d = await r.json();
|
|
@@ -2787,8 +2877,20 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
2787
2877
|
setCustomFiles(d.files || []);
|
|
2788
2878
|
// ①(新) 合并进统一列表:自定义目录文件(图片+视频)+ workshop 原始子文件夹
|
|
2789
2879
|
// (video → mp4 播放;web → iframe;scene → 预览图/后续静态帧)
|
|
2790
|
-
setWallList((
|
|
2880
|
+
setWallList((prev) => {
|
|
2881
|
+
// ①(修正) 自定义目录改为**合并**进列表(不再替换——用户误扫自定义目录后
|
|
2882
|
+
// 切不回 Steam 库的问题;现在两个来源共存,去重)
|
|
2883
|
+
const fresh = (d.files || []).map((f) => {
|
|
2791
2884
|
if (f.folder) {
|
|
2885
|
+
// ①(修正) 子文件夹里的 .mpkg(wallpaperE 角色收藏夹):独立成条
|
|
2886
|
+
if (f.folderMpkg && f.type === "mpkg") {
|
|
2887
|
+
return {
|
|
2888
|
+
key: "custom|" + f.name + "/" + f.media, title: f.title || f.media, name: f.name,
|
|
2889
|
+
type: "mpkg", folder: true, folderMpkg: true, folderName: f.name, mpkgFile: f.media,
|
|
2890
|
+
image: "host:?custom=1&folder=" + encodeURIComponent(f.name) + "&file=" + encodeURIComponent(f.media),
|
|
2891
|
+
converted: "mp4"
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2792
2894
|
if (f.type === "web") {
|
|
2793
2895
|
const pv = f.preview || "preview.jpg";
|
|
2794
2896
|
return {
|
|
@@ -2822,7 +2924,10 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
2822
2924
|
image: "host:?custom=1&file=" + encodeURIComponent(f.name),
|
|
2823
2925
|
converted: f.type === "video" ? "mp4" : (f.type === "mpkg" ? "mp4" : "gif")
|
|
2824
2926
|
};
|
|
2825
|
-
})
|
|
2927
|
+
});
|
|
2928
|
+
const keys = new Set((prev || []).map((w) => w.key));
|
|
2929
|
+
return fresh.filter((w) => !keys.has(w.key)).concat(prev || []);
|
|
2930
|
+
});
|
|
2826
2931
|
}
|
|
2827
2932
|
} catch (err) { showError(t("lib.fail") + String(err && err.message || err)); }
|
|
2828
2933
|
setLibBusy(false);
|
|
@@ -2831,19 +2936,24 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
2831
2936
|
const applyCustomMpkg = async (w) => {
|
|
2832
2937
|
setLibBusy(true);
|
|
2833
2938
|
try {
|
|
2834
|
-
|
|
2939
|
+
// ①(修正) folderMpkg:子文件夹里的 .mpkg 走 folder 参数
|
|
2940
|
+
const fq = w.folderMpkg
|
|
2941
|
+
? "folder=" + encodeURIComponent(w.folderName || w.name) + "&file=" + encodeURIComponent(w.mpkgFile || w.name)
|
|
2942
|
+
: "file=" + encodeURIComponent(w.name);
|
|
2943
|
+
const r = await fetch(HOST_BASE + "/custom-mpkg?" + fq);
|
|
2835
2944
|
const d = await r.json();
|
|
2836
|
-
|
|
2945
|
+
const display = w.title || w.name;
|
|
2946
|
+
if (!d.ok || !d.selected) { showError(t("mpkg.noAsset") + "(" + display + ")"); setLibBusy(false); return; }
|
|
2837
2947
|
const sel = d.selected;
|
|
2838
2948
|
commit({
|
|
2839
2949
|
image: "host:?token=" + d.token + "&index=" + sel.index + (sel.offset ? "&offset=" + sel.offset : ""),
|
|
2840
|
-
source: sel.name, mpkgKey: "custommpkg|" + w.
|
|
2950
|
+
source: sel.name, mpkgKey: "custommpkg|" + (w.folderMpkg ? w.folderName + "/" + w.mpkgFile : w.name), mpkgName: display,
|
|
2841
2951
|
fromMpkg: true, converted: sel.isMp4 ? "mp4" : "gif", slot: null,
|
|
2842
2952
|
info: undefined, timeVideos: undefined, timeConfig: undefined, activeSlot: null,
|
|
2843
2953
|
webUrl: undefined, sceneKey: undefined
|
|
2844
2954
|
}, true);
|
|
2845
|
-
setMpkgMeta({ name:
|
|
2846
|
-
setHint(t("lib.applied") + ":" +
|
|
2955
|
+
setMpkgMeta({ name: display, key: "custommpkg|" + (w.folderMpkg ? w.folderName + "/" + w.mpkgFile : w.name), info: null, entryName: sel.name, slot: null });
|
|
2956
|
+
setHint(t("lib.applied") + ":" + display);
|
|
2847
2957
|
if (!sel.isMp4) { setPreviewModal(true); }
|
|
2848
2958
|
} catch (err) { showError(t("lib.fail") + String(err && err.message || err)); }
|
|
2849
2959
|
setLibBusy(false);
|
|
@@ -2919,26 +3029,60 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
2919
3029
|
} catch (err) { console.error("[dsh-mpkg-wallpaper] 应用场景壁纸失败:", err); showError(t("lib.fail") + String(err && err.message || err)); }
|
|
2920
3030
|
};
|
|
2921
3031
|
// ②(新) 切换到下一个壁纸(本地库 + 自定义目录合并列表)
|
|
3032
|
+
// ⑳(新) 轮播候选:激活了列表 → 列表内的壁纸;否则全部
|
|
3033
|
+
const rotCandidates = () => {
|
|
3034
|
+
const all = wallList;
|
|
3035
|
+
if (!all.length) return [];
|
|
3036
|
+
const gid = section.rotGroupId;
|
|
3037
|
+
if (gid) {
|
|
3038
|
+
const g = (section.rotGroups || []).find((x) => x.id === gid);
|
|
3039
|
+
if (g && (g.keys || []).length) {
|
|
3040
|
+
const filtered = all.filter((w) => (g.keys || []).includes(w.key));
|
|
3041
|
+
if (filtered.length) return filtered;
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
return all;
|
|
3045
|
+
};
|
|
2922
3046
|
const nextWallpaper = () => {
|
|
2923
|
-
const list =
|
|
3047
|
+
const list = rotCandidates();
|
|
2924
3048
|
if (!list.length) { showError(t("lib.empty")); return; }
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
3049
|
+
let idx = list.findIndex((w) => w.key === (wallList[wallIdx] && wallList[wallIdx].key));
|
|
3050
|
+
if (idx < 0) idx = -1;
|
|
3051
|
+
idx = (idx + 1) % list.length;
|
|
3052
|
+
const target = list[idx];
|
|
3053
|
+
setWallIdx(wallList.findIndex((w) => w.key === target.key));
|
|
3054
|
+
applyWallFromList(target);
|
|
2928
3055
|
};
|
|
2929
3056
|
// 1(新) 切换到上一个壁纸
|
|
2930
3057
|
const prevWallpaper = () => {
|
|
2931
|
-
const list =
|
|
3058
|
+
const list = rotCandidates();
|
|
2932
3059
|
if (!list.length) { showError(t("lib.empty")); return; }
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3060
|
+
let idx = list.findIndex((w) => w.key === (wallList[wallIdx] && wallList[wallIdx].key));
|
|
3061
|
+
if (idx < 0) idx = 0;
|
|
3062
|
+
idx = (idx - 1 + list.length) % list.length;
|
|
3063
|
+
const target = list[idx];
|
|
3064
|
+
setWallIdx(wallList.findIndex((w) => w.key === target.key));
|
|
3065
|
+
applyWallFromList(target);
|
|
2936
3066
|
};
|
|
2937
|
-
// ③(新)
|
|
3067
|
+
// ③(新) 壁纸轮换:定时自动切换(⑳(新) 随机顺序的列表打乱)
|
|
2938
3068
|
const startRotation = () => {
|
|
2939
3069
|
try { if (window.__mpwRotTimer) clearInterval(window.__mpwRotTimer); } catch {}
|
|
2940
|
-
const
|
|
2941
|
-
|
|
3070
|
+
const gid = section.rotGroupId;
|
|
3071
|
+
const g = gid ? (section.rotGroups || []).find((x) => x.id === gid) : null;
|
|
3072
|
+
const sec = g && g.interval > 0 ? g.interval : (section.rotateMin > 0 ? section.rotateMin : 5);
|
|
3073
|
+
if (g && g.order === "random") {
|
|
3074
|
+
// 随机顺序:每轮洗牌后顺序播放(先随机跳一次)
|
|
3075
|
+
window.__mpwRotTimer = setInterval(() => {
|
|
3076
|
+
const list = rotCandidates();
|
|
3077
|
+
if (list.length < 2) { nextWallpaper(); return; }
|
|
3078
|
+
const r = Math.floor(Math.random() * list.length);
|
|
3079
|
+
const target = list[r];
|
|
3080
|
+
setWallIdx(wallList.findIndex((w) => w.key === target.key));
|
|
3081
|
+
applyWallFromList(target);
|
|
3082
|
+
}, sec * 60 * 1000);
|
|
3083
|
+
} else {
|
|
3084
|
+
window.__mpwRotTimer = setInterval(nextWallpaper, sec * 60 * 1000);
|
|
3085
|
+
}
|
|
2942
3086
|
};
|
|
2943
3087
|
// ③(修正) 任一弹窗打开时给 body 打标记(背后设置面板禁虚化),关闭时移除
|
|
2944
3088
|
react.useEffect(() => {
|
|
@@ -3037,6 +3181,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
3037
3181
|
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
3182
|
hybrid: DEFAULT_HYBRID, roundCompat: DEFAULT_ROUND_COMPAT,
|
|
3039
3183
|
rotate: DEFAULT_ROTATE, rotateMin: 5, customDirPath: "", brightness: DEFAULT_BRIGHTNESS, float: DEFAULT_FLOAT,
|
|
3184
|
+
playbackRate: DEFAULT_PLAYBACK_RATE, rotGroups: [], rotGroupId: "", rotSeeded: false,
|
|
3040
3185
|
thinkBg: DEFAULT_THINK_BG,
|
|
3041
3186
|
enabled: DEFAULT_ENABLED
|
|
3042
3187
|
}), true);
|
|
@@ -3330,6 +3475,93 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
3330
3475
|
const propsToShow = propsExpanded ? propsShown : [];
|
|
3331
3476
|
|
|
3332
3477
|
const h = react.createElement;
|
|
3478
|
+
// ①(修正) 弹窗遮罩 portal:渲染到 body,避开设置面板包含块/backdrop root 困局
|
|
3479
|
+
const MaskPortal = (props) => {
|
|
3480
|
+
const node = h("div", { className: "mpw_mask", onClick: props.onClick }, props.children);
|
|
3481
|
+
if (ReactDOM && ReactDOM.createPortal && typeof document !== "undefined" && document.body) {
|
|
3482
|
+
try { return ReactDOM.createPortal(node, document.body); } catch { /* 回退原位渲染 */ }
|
|
3483
|
+
}
|
|
3484
|
+
return node;
|
|
3485
|
+
};
|
|
3486
|
+
// ⑳(新) 自定义下拉(不调用原生 select——安卓原生太难看,用户实测)
|
|
3487
|
+
const [selectOpen, setSelectOpen] = react.useState("");
|
|
3488
|
+
const [selectDirUp, setSelectDirUp] = react.useState({}); // name → 是否向上展开
|
|
3489
|
+
const SelectBox = (props) => {
|
|
3490
|
+
const { value, options, onChange, placeholder } = props;
|
|
3491
|
+
const cur = options.find((o) => o.value === value);
|
|
3492
|
+
const dirUp = !!selectDirUp[props.name];
|
|
3493
|
+
const menuStyle = dirUp
|
|
3494
|
+
? { position: "absolute", left: 0, right: 0, bottom: "100%", marginBottom: 4, zIndex: 2600, maxHeight: 220, overflowY: "auto" }
|
|
3495
|
+
: { position: "absolute", left: 0, right: 0, top: "100%", marginTop: 4, zIndex: 2600, maxHeight: 220, overflowY: "auto" };
|
|
3496
|
+
return h("div", { className: "mpw_select", style: { position: "relative", flex: 1, minWidth: 0 } }, [
|
|
3497
|
+
h("button", { className: "mpw_input mpw_selectBtn", type: "button",
|
|
3498
|
+
onClick: (ev) => {
|
|
3499
|
+
if (selectOpen !== props.name) {
|
|
3500
|
+
// ①(修正) 下方空间不足时向上展开(用户实测列表底部被屏幕边缘截断)
|
|
3501
|
+
let up = false;
|
|
3502
|
+
try {
|
|
3503
|
+
const rect = ev.currentTarget.getBoundingClientRect();
|
|
3504
|
+
const est = Math.min(220, (options.length + 1) * 34);
|
|
3505
|
+
if (rect.bottom + est > (window.innerHeight || 600)) up = true;
|
|
3506
|
+
} catch {}
|
|
3507
|
+
setSelectDirUp((prev) => Object.assign({}, prev, { [props.name]: up }));
|
|
3508
|
+
}
|
|
3509
|
+
setSelectOpen(selectOpen === props.name ? "" : props.name);
|
|
3510
|
+
} },
|
|
3511
|
+
h("span", { style: { flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, cur ? cur.label : placeholder),
|
|
3512
|
+
h("span", { className: "mpw_hint" }, selectOpen === props.name ? (dirUp ? "▼" : "▲") : (dirUp ? "▲" : "▼"))
|
|
3513
|
+
),
|
|
3514
|
+
selectOpen === props.name
|
|
3515
|
+
? h("div", { className: "mpw_selectMenu", style: menuStyle },
|
|
3516
|
+
options.map((o) => h("button", { className: "mpw_selectOpt" + (o.value === value ? " mpw_on" : ""), type: "button", key: o.value,
|
|
3517
|
+
onClick: () => { setSelectOpen(""); onChange(o.value); } }, o.label)))
|
|
3518
|
+
: null
|
|
3519
|
+
]);
|
|
3520
|
+
};
|
|
3521
|
+
// ⑳(新) 视频倍速分段条:6 段竖线分隔,点击/拖拽直接定位到对应档位
|
|
3522
|
+
const SpeedBar = (props) => {
|
|
3523
|
+
const { value, onChange } = props;
|
|
3524
|
+
const ref = react.useRef(null);
|
|
3525
|
+
const dragging = react.useRef(false);
|
|
3526
|
+
const idx = Math.max(0, SPEED_LEVELS.indexOf(value));
|
|
3527
|
+
const pick = (clientX) => {
|
|
3528
|
+
const el = ref.current;
|
|
3529
|
+
if (!el) return;
|
|
3530
|
+
const rect = el.getBoundingClientRect();
|
|
3531
|
+
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
3532
|
+
const i2 = Math.min(SPEED_LEVELS.length - 1, Math.round(ratio * (SPEED_LEVELS.length - 1)));
|
|
3533
|
+
onChange(SPEED_LEVELS[i2]);
|
|
3534
|
+
};
|
|
3535
|
+
return h("div", { className: "mpw_speedBar", style: { position: "relative", height: 56, margin: "8px 0 0", touchAction: "none", cursor: "pointer" },
|
|
3536
|
+
ref,
|
|
3537
|
+
onPointerDown: (ev) => { dragging.current = true; try { ev.target.setPointerCapture && ev.target.setPointerCapture(ev.pointerId); } catch {} pick(ev.clientX); },
|
|
3538
|
+
onPointerMove: (ev) => { if (dragging.current) pick(ev.clientX); },
|
|
3539
|
+
onPointerUp: () => { dragging.current = false; },
|
|
3540
|
+
onPointerCancel: () => { dragging.current = false; },
|
|
3541
|
+
onClick: (ev) => pick(ev.clientX) },
|
|
3542
|
+
// ①(修正) 统一坐标系:左右 16px 内边距,轨道/竖线/球/进度/数字标签
|
|
3543
|
+
// 全部用 pos(i) = 16px + i/5*(100%-32px)——数字中心严格落在刻度上。
|
|
3544
|
+
// (之前 space-between 按钮宽度不同 → 中心不落在刻度;全宽又边缘被裁)
|
|
3545
|
+
// 轨道
|
|
3546
|
+
h("div", { style: { position: "absolute", left: 16, right: 16, top: 14, height: 6, borderRadius: 3, background: "color-mix(in srgb, var(--dsw-alias-interactive-bg-hover, #333a4a) 60%, transparent)" } }),
|
|
3547
|
+
// 填充进度
|
|
3548
|
+
h("div", { style: { position: "absolute", left: 16, top: 14, height: 6, borderRadius: 3, background: "var(--dsw-alias-brand-primary, #4f8cff)", width: "calc(" + (idx / (SPEED_LEVELS.length - 1) * (100 - 32)) + "% - 0px)" } }),
|
|
3549
|
+
// 分段竖线
|
|
3550
|
+
SPEED_LEVELS.map((rate, i) => h("div", { key: rate, style: {
|
|
3551
|
+
position: "absolute", top: 6, left: "calc(16px + " + (i / (SPEED_LEVELS.length - 1) * (100 - 32)) + "% - 1px)",
|
|
3552
|
+
width: 1, height: 22, background: "color-mix(in srgb, var(--dsw-alias-border-l2, #444b5c) 70%, transparent)"
|
|
3553
|
+
} })),
|
|
3554
|
+
// 当前档位球
|
|
3555
|
+
h("div", { style: { position: "absolute", top: 8, left: "calc(16px + " + (idx / (SPEED_LEVELS.length - 1) * (100 - 32)) + "% - 7px)", width: 14, height: 14, borderRadius: "50%", background: "var(--dsw-alias-brand-primary, #4f8cff)", border: "2px solid var(--dsw-alias-bg-base, #0e1420)" } }),
|
|
3556
|
+
// 数字标签也可点击
|
|
3557
|
+
SPEED_LEVELS.map((rate, i) => h("button", { key: "b" + rate, type: "button", style: {
|
|
3558
|
+
position: "absolute", top: 32, left: "calc(16px + " + (i / (SPEED_LEVELS.length - 1) * (100 - 32)) + "%)", transform: "translateX(-50%)",
|
|
3559
|
+
padding: "4px 2px", fontSize: 11, border: 0, background: "transparent", cursor: "pointer", whiteSpace: "nowrap",
|
|
3560
|
+
color: i === idx ? "var(--dsw-alias-brand-primary, #4f8cff)" : "var(--dsw-alias-label-tertiary)",
|
|
3561
|
+
fontWeight: i === idx ? 700 : 400
|
|
3562
|
+
}, onClick: () => onChange(SPEED_LEVELS[i]) }, (rate === 1 ? "1" : rate) + "x"))
|
|
3563
|
+
);
|
|
3564
|
+
};
|
|
3333
3565
|
const sliderRow = (label, field, min, max, suffix, step, def) => {
|
|
3334
3566
|
const val = section[field] !== void 0 ? section[field] : def;
|
|
3335
3567
|
const rangeRef = react.useRef(null);
|
|
@@ -3517,7 +3749,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
3517
3749
|
// 图片项用 <img>(参照 elysia395 的 preview 显示)
|
|
3518
3750
|
// ①(修正) mpkg 项用容器内 preview 图(custom-mpkg-preview 路由);视频用首帧;图片直接用
|
|
3519
3751
|
w.type === "mpkg"
|
|
3520
|
-
? h("img", { className: "mpw_thumbImg", src: HOST_BASE + "/custom-mpkg-preview?file=" + encodeURIComponent(w.name), alt: "", loading: "lazy",
|
|
3752
|
+
? 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
3753
|
onError: (ev) => { ev.target.style.display = "none"; } })
|
|
3522
3754
|
: ((w.type === "video" || w.converted === "mp4")
|
|
3523
3755
|
? h("video", { className: "mpw_thumbImg", src: resolveHostUrl(w.image), muted: true, playsInline: true, preload: "metadata",
|
|
@@ -3590,7 +3822,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
3590
3822
|
])
|
|
3591
3823
|
: libWalls && !libWalls.length ? h("p", { className: "mpw_hint" }, t("lib.empty")) : null
|
|
3592
3824
|
]),
|
|
3593
|
-
// ②③(新) 下一个壁纸 + 定时轮换
|
|
3825
|
+
// ②③(新) 下一个壁纸 + 定时轮换 + ⑳(新) 轮播列表(WE 原生播放列表风格)
|
|
3594
3826
|
h("div", { className: "mpw_field" }, [
|
|
3595
3827
|
h("label", { className: "mpw_label" }, t("lib.rotate")),
|
|
3596
3828
|
h("div", { className: "mpw_inline" }, [
|
|
@@ -3599,9 +3831,139 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
3599
3831
|
h(Toggle, { checked: section.rotate !== void 0 ? !!section.rotate : DEFAULT_ROTATE, onChange: (v) => commit({ rotate: v }) }),
|
|
3600
3832
|
h("span", { className: "mpw_hint" }, t("lib.rotateDesc"))
|
|
3601
3833
|
]),
|
|
3834
|
+
// ⑳(新) 轮播列表:选择激活列表(自定义下拉)+ 管理 + 新建
|
|
3835
|
+
h("div", { className: "mpw_inline", style: { marginTop: 4 } }, [
|
|
3836
|
+
h(SelectBox, { name: "rotGroup", value: section.rotGroupId || "", placeholder: t("rot.all"),
|
|
3837
|
+
options: [{ value: "", label: t("rot.all") }].concat((section.rotGroups || []).map((g) => ({ value: g.id, label: g.name + "(" + (g.keys || []).length + ")" }))),
|
|
3838
|
+
onChange: (v) => commit({ rotGroupId: v }, true) }),
|
|
3839
|
+
h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => setRotModal(true) }, t("rot.manage")),
|
|
3840
|
+
h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => { setRotEdit({ id: "", name: "", interval: 5, order: "sequence", keys: [] }); setRotEditor(true); } }, t("rot.new"))
|
|
3841
|
+
]),
|
|
3602
3842
|
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))
|
|
3843
|
+
sliderRow(t("lib.rotateMin"), "rotateMin", 1, 120, t("lib.minutes"), 1, 5)),
|
|
3844
|
+
// ①(修正) 选中列表后:单独设置该列表的轮换间隔(用户要求,编辑弹窗不再设间隔)
|
|
3845
|
+
(section.rotGroupId && (section.rotGroups || []).find((g) => g.id === section.rotGroupId))
|
|
3846
|
+
? h("div", { className: "mpw_inline", style: { marginTop: 4 } }, [
|
|
3847
|
+
h("span", { className: "mpw_hint" }, t("rot.groupInterval")),
|
|
3848
|
+
h("input", { type: "range", min: 1, max: 120, step: 1, style: { flex: 1, minWidth: 0 },
|
|
3849
|
+
value: (section.rotGroups || []).find((g) => g.id === section.rotGroupId).interval || 5,
|
|
3850
|
+
onChange: (ev) => {
|
|
3851
|
+
const v = Math.max(1, parseInt(ev.target.value || "5", 10));
|
|
3852
|
+
const groups = (section.rotGroups || []).map((g) => g.id === section.rotGroupId ? Object.assign({}, g, { interval: v }) : g);
|
|
3853
|
+
commit({ rotGroups: groups }, true);
|
|
3854
|
+
} }),
|
|
3855
|
+
h("span", { className: "mpw_hint" }, ((section.rotGroups || []).find((g) => g.id === section.rotGroupId).interval || 5) + t("lib.minutes"))
|
|
3856
|
+
])
|
|
3857
|
+
: null
|
|
3604
3858
|
]),
|
|
3859
|
+
// ⑳(新) 轮播列表管理弹窗(新建/编辑/删除)
|
|
3860
|
+
rotModal ? h(MaskPortal, { onClick: () => setRotModal(false) }, [
|
|
3861
|
+
h("div", { className: "mpw_dialog", style: { width: "min(560px, calc(100vw - 32px))" }, onClick: (ev) => ev.stopPropagation() }, [
|
|
3862
|
+
h("div", { className: "mpw_title" }, t("rot.title")),
|
|
3863
|
+
(section.rotGroups || []).length === 0
|
|
3864
|
+
? h("p", { className: "mpw_hint" }, t("rot.empty"))
|
|
3865
|
+
: h("div", { style: { maxHeight: 260, overflowY: "auto" } }, (section.rotGroups || []).map((g) => h("div", { className: "mpw_inline", key: g.id, style: { marginBottom: 6 } }, [
|
|
3866
|
+
h("span", { className: "mpw_propLabel", style: { flex: 1, minWidth: 0 } }, h("b", null, g.name)),
|
|
3867
|
+
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"))),
|
|
3868
|
+
h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => { setRotEdit(JSON.parse(JSON.stringify(g))); setRotModal(false); setRotEditor(true); } }, t("rot.edit")),
|
|
3869
|
+
h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => {
|
|
3870
|
+
const next = (section.rotGroups || []).filter((x) => x.id !== g.id);
|
|
3871
|
+
commit({ rotGroups: next, rotGroupId: section.rotGroupId === g.id ? "" : section.rotGroupId }, true);
|
|
3872
|
+
} }, t("rot.del"))
|
|
3873
|
+
]))),
|
|
3874
|
+
h("div", { className: "mpw_inline", style: { marginTop: 8 } }, [
|
|
3875
|
+
h("button", { className: "mpw_button", type: "button", onClick: () => { setRotEdit({ id: "", name: "", interval: 5, order: "sequence", keys: [] }); setRotModal(false); setRotEditor(true); } }, t("rot.new")),
|
|
3876
|
+
h("button", { className: "mpw_reset", type: "button", onClick: () => setRotModal(false) }, t("conflict.cancel"))
|
|
3877
|
+
])
|
|
3878
|
+
])
|
|
3879
|
+
]) : null,
|
|
3880
|
+
// ⑳(新) 轮播列表编辑弹窗(命名 + 勾选壁纸 + 间隔 + 顺序)
|
|
3881
|
+
rotEditor && rotEdit ? h(MaskPortal, { onClick: () => setRotEditor(false) }, [
|
|
3882
|
+
h("div", { className: "mpw_dialog", style: { width: "min(560px, calc(100vw - 32px))" }, onClick: (ev) => ev.stopPropagation() }, [
|
|
3883
|
+
h("div", { className: "mpw_title" }, rotEdit.id ? t("rot.editTitle") : t("rot.newTitle")),
|
|
3884
|
+
// ①(修正) 名称输入框与「图片链接」输入框同款(mpw_input;dialog 是列布局,
|
|
3885
|
+
// flex:1 会纵向拉伸 → 显式 flex:none 恢复正常高度)
|
|
3886
|
+
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 })) }),
|
|
3887
|
+
// ①(修正) 播放顺序(顺序/随机);间隔在列表选择下方单独设置
|
|
3888
|
+
h("div", { className: "mpw_inline", style: { marginBottom: 8 } }, [
|
|
3889
|
+
h("span", { className: "mpw_label", style: { fontSize: 13 } }, t("rot.order")),
|
|
3890
|
+
h("button", { className: "mpw_miniBtn" + (rotEdit.order !== "random" ? " mpw_on" : ""), type: "button", onClick: () => setRotEdit(Object.assign({}, rotEdit, { order: "sequence" })) }, t("rot.seq")),
|
|
3891
|
+
h("button", { className: "mpw_miniBtn" + (rotEdit.order === "random" ? " mpw_on" : ""), type: "button", onClick: () => setRotEdit(Object.assign({}, rotEdit, { order: "random" })) }, t("rot.random"))
|
|
3892
|
+
]),
|
|
3893
|
+
h("p", { className: "mpw_hint" }, t("rot.pickHint") + "(" + wallList.length + ")"),
|
|
3894
|
+
// ①(修正) 来源过滤 + 扫描入口:误扫自定义目录后也能切回 Steam 库(用户要求"返回")
|
|
3895
|
+
h("div", { className: "mpw_inline", style: { marginBottom: 6 } }, [
|
|
3896
|
+
h("button", { className: "mpw_miniBtn" + (rotFilter === "all" ? " mpw_on" : ""), type: "button", onClick: () => setRotFilter("all") }, t("rot.all")),
|
|
3897
|
+
h("button", { className: "mpw_miniBtn" + (rotFilter === "custom" ? " mpw_on" : ""), type: "button", onClick: () => setRotFilter("custom") }, t("rot.filterCustom")),
|
|
3898
|
+
h("button", { className: "mpw_miniBtn" + (rotFilter === "steam" ? " mpw_on" : ""), type: "button", onClick: () => setRotFilter("steam") }, t("rot.filterSteam")),
|
|
3899
|
+
h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => { if (customDir) scanCustomDir(); else showError(t("lib.dirEmpty")); } }, t("rot.scanDir")),
|
|
3900
|
+
h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: scanLibrary }, t("rot.scanLib"))
|
|
3901
|
+
]),
|
|
3902
|
+
// ①(修正) 勾选改整行点击;壁纸列表为空时给扫描入口(wallList 是运行时状态,刷新后需重扫)
|
|
3903
|
+
wallList.length === 0
|
|
3904
|
+
? h("div", { style: { padding: "12px 0" } }, [
|
|
3905
|
+
h("p", { className: "mpw_hint", style: { marginBottom: 6 } }, t("rot.noWallpapers")),
|
|
3906
|
+
h("div", { className: "mpw_inline" }, [
|
|
3907
|
+
h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => { if (customDir) scanCustomDir(); else showError(t("lib.dirEmpty")); } }, t("rot.scanDir")),
|
|
3908
|
+
h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: scanLibrary }, t("rot.scanLib"))
|
|
3909
|
+
])
|
|
3910
|
+
])
|
|
3911
|
+
: 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) => {
|
|
3912
|
+
const on = (rotEdit.keys || []).includes(w.key);
|
|
3913
|
+
// ①(修正) 缩略图:与扫描目录网格同款逻辑(mpkg preview / 视频首帧 / 静态图)
|
|
3914
|
+
let turl = "";
|
|
3915
|
+
try {
|
|
3916
|
+
if (w.type === "mpkg") turl = HOST_BASE + "/custom-mpkg-preview?" + (w.folderMpkg ? "folder=" + encodeURIComponent(w.folderName) + "&" : "") + "file=" + encodeURIComponent(w.folderMpkg ? w.mpkgFile : w.name);
|
|
3917
|
+
else if (w.image) turl = resolveHostUrl(w.image);
|
|
3918
|
+
else if (w.src === "steam" && w.ltoken) {
|
|
3919
|
+
if (w.type === "video" && w.media) turl = HOST_BASE + "/library-media?ltoken=" + encodeURIComponent(w.ltoken) + "&file=" + encodeURIComponent(String(w.media).split(/[\\/]/).pop());
|
|
3920
|
+
else turl = HOST_BASE + "/library-web?ltoken=" + encodeURIComponent(w.ltoken) + "&file=" + encodeURIComponent(String(w.preview || "").split(/[\\/]/).pop() || "preview.jpg");
|
|
3921
|
+
}
|
|
3922
|
+
} catch {}
|
|
3923
|
+
return h("div", { className: "mpw_pickCard" + (on ? " mpw_on" : ""), key: w.key,
|
|
3924
|
+
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)"),
|
|
3925
|
+
background: on ? "color-mix(in srgb, var(--dsw-alias-brand-primary, #4f8cff) 12%, transparent)" : "var(--dsw-alias-bg-module-platform, #1c2230)" },
|
|
3926
|
+
onClick: () => {
|
|
3927
|
+
const keys = (rotEdit.keys || []).slice();
|
|
3928
|
+
const i = keys.indexOf(w.key);
|
|
3929
|
+
if (i >= 0) keys.splice(i, 1); else keys.push(w.key);
|
|
3930
|
+
setRotEdit(Object.assign({}, rotEdit, { keys }));
|
|
3931
|
+
} }, [
|
|
3932
|
+
// ①(修正) 固定 64px 高度容器:**只有真正的视频项**用首帧 <video>;
|
|
3933
|
+
// mpkg 项 converted 也是 "mp4" 但缩略 URL 是预览图(图片)——
|
|
3934
|
+
// 之前误用 <video> 加载图片 → 必然失败被隐藏 → 缩略图全空白
|
|
3935
|
+
h("div", { style: { width: "100%", height: 64, position: "relative", background: "color-mix(in srgb, var(--dsw-alias-interactive-bg-hover, #333a4a) 30%, transparent)" } }, [
|
|
3936
|
+
turl && w.type === "video"
|
|
3937
|
+
? h("video", { src: turl, muted: true, playsInline: true, preload: "metadata",
|
|
3938
|
+
style: { width: "100%", height: 64, objectFit: "cover", display: "block" },
|
|
3939
|
+
onLoadedMetadata: (ev) => { try { const v = ev.target; if (v.duration && v.duration > 0.1) v.currentTime = 0.05; } catch {} },
|
|
3940
|
+
onError: (ev) => { ev.target.style.display = "none"; } })
|
|
3941
|
+
: turl
|
|
3942
|
+
? h("img", { src: turl, alt: "", loading: "lazy", style: { width: "100%", height: 64, objectFit: "cover", display: "block" }, onError: (ev) => { ev.target.style.display = "none"; } })
|
|
3943
|
+
: null
|
|
3944
|
+
]),
|
|
3945
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 6, padding: "6px 8px" } }, [
|
|
3946
|
+
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 } }, "✓"),
|
|
3947
|
+
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)
|
|
3948
|
+
])
|
|
3949
|
+
]);
|
|
3950
|
+
})),
|
|
3951
|
+
h("div", { className: "mpw_inline", style: { marginTop: 10 } }, [
|
|
3952
|
+
h("button", { className: "mpw_button", type: "button", onClick: () => {
|
|
3953
|
+
const groups = (section.rotGroups || []).slice();
|
|
3954
|
+
const keys = (rotEdit.keys || []).filter((k) => wallList.some((w) => w.key === k));
|
|
3955
|
+
if (!keys.length) { showError(t("rot.emptyKeys")); return; }
|
|
3956
|
+
// ①(修正) 新建列表默认间隔用全局 rotateMin(间隔在列表选择下方单独调)
|
|
3957
|
+
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 };
|
|
3958
|
+
const i = groups.findIndex((x) => x.id === g.id);
|
|
3959
|
+
if (i >= 0) groups[i] = g; else groups.push(g);
|
|
3960
|
+
commit({ rotGroups: groups, rotGroupId: section.rotGroupId || g.id }, true);
|
|
3961
|
+
setRotEditor(false);
|
|
3962
|
+
} }, t("rot.save")),
|
|
3963
|
+
h("button", { className: "mpw_reset", type: "button", onClick: () => setRotEditor(false) }, t("conflict.cancel"))
|
|
3964
|
+
])
|
|
3965
|
+
])
|
|
3966
|
+
]) : null,
|
|
3605
3967
|
|
|
3606
3968
|
// ③(新) 宿主端状态(proot/本机测试方法:可用 = 大文件无限制生效)
|
|
3607
3969
|
h("p", { className: "mpw_hint" },
|
|
@@ -3611,6 +3973,13 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
3611
3973
|
: "混合模式:检测宿主端中…")
|
|
3612
3974
|
: "纯浏览器模式(600MB 上限)"),
|
|
3613
3975
|
|
|
3976
|
+
// ⑳(新) 视频倍速分段条(放在 mpkg 选择上方;点击/拖拽定位档位)
|
|
3977
|
+
h("div", { className: "mpw_field" }, [
|
|
3978
|
+
h("label", { className: "mpw_label" }, t("speed") + " · " + (section.playbackRate !== void 0 ? section.playbackRate : DEFAULT_PLAYBACK_RATE) + "x"),
|
|
3979
|
+
h(SpeedBar, { value: section.playbackRate !== void 0 ? section.playbackRate : DEFAULT_PLAYBACK_RATE, onChange: (v) => commit({ playbackRate: v }, true) }),
|
|
3980
|
+
h("p", { className: "mpw_hint" }, t("speed.hint"))
|
|
3981
|
+
]),
|
|
3982
|
+
|
|
3614
3983
|
// mpkg 文件
|
|
3615
3984
|
h("div", { className: "mpw_field" }, [
|
|
3616
3985
|
h("div", { className: "mpw_inline" }, [
|
|
@@ -3995,7 +4364,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
3995
4364
|
hint ? h("p", { className: "mpw_hint" + (/^(解析失败|文件过大|背景素材过大|存储空间|内存不足|此壁纸的视频纹理|不支持)/.test(hint) ? " mpw_err" : "") }, hint) : null,
|
|
3996
4365
|
|
|
3997
4366
|
// ⑩(新) 新版本确认下载弹窗(发布<24h 提示)
|
|
3998
|
-
updConfirm ? h(
|
|
4367
|
+
updConfirm ? h(MaskPortal, { onClick: () => setUpdConfirm(null) }, [
|
|
3999
4368
|
h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
|
|
4000
4369
|
h("div", { className: "mpw_title" }, t("update.warnTitle")),
|
|
4001
4370
|
h("p", { className: "mpw_desc" }, t("update.warnBody") + updConfirm.localVersion + " → " + updConfirm.remoteVersion + ")"),
|
|
@@ -4007,7 +4376,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
4007
4376
|
]) : null,
|
|
4008
4377
|
|
|
4009
4378
|
// ①(新) 目录选择弹窗(文件夹选择器,跨平台)
|
|
4010
|
-
dirPick ? h(
|
|
4379
|
+
dirPick ? h(MaskPortal, { onClick: () => setDirPick(false) }, [
|
|
4011
4380
|
h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
|
|
4012
4381
|
h("div", { className: "mpw_title" }, t("lib.pickDir")),
|
|
4013
4382
|
h("p", { className: "mpw_hint", style: { wordBreak: "break-all" } }, t("lib.curDir") + ":" + (dirPath || "(默认)")),
|
|
@@ -4054,7 +4423,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
4054
4423
|
]) : null,
|
|
4055
4424
|
|
|
4056
4425
|
// ①(新) 通用错误弹窗(文件过大 / 无法使用 / 解析失败等)
|
|
4057
|
-
errorModal ? h(
|
|
4426
|
+
errorModal ? h(MaskPortal, { onClick: () => setErrorModal(false) }, [
|
|
4058
4427
|
h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
|
|
4059
4428
|
h("div", { className: "mpw_title" }, t("error.title")),
|
|
4060
4429
|
h("p", { className: "mpw_desc" }, errorMsg),
|
|
@@ -4065,7 +4434,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
4065
4434
|
]) : null,
|
|
4066
4435
|
|
|
4067
4436
|
// ⑤(新) 预览模式弹窗(导入后最终用 GIF/图片时提示)
|
|
4068
|
-
previewModal ? h(
|
|
4437
|
+
previewModal ? h(MaskPortal, { onClick: () => setPreviewModal(false) }, [
|
|
4069
4438
|
h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
|
|
4070
4439
|
h("div", { className: "mpw_title" }, t("preview.title")),
|
|
4071
4440
|
h("p", { className: "mpw_desc" }, t("preview.desc")),
|
|
@@ -4077,7 +4446,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
4077
4446
|
|
|
4078
4447
|
// ④ 冲突确认弹窗(自绘覆盖层)
|
|
4079
4448
|
// ①(新) 网页壁纸确认弹窗(实验性警告:可能卡顿或无法加载;预检标注风险)
|
|
4080
|
-
webConfirm ? h(
|
|
4449
|
+
webConfirm ? h(MaskPortal, { onClick: () => setWebConfirm(null) }, [
|
|
4081
4450
|
h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
|
|
4082
4451
|
h("div", { className: "mpw_title" }, t("web.confirmTitle")),
|
|
4083
4452
|
h("p", { className: "mpw_desc" }, `${t("web.confirmBody")}(${webConfirm.title || ""})`),
|
|
@@ -4099,7 +4468,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
4099
4468
|
])
|
|
4100
4469
|
])
|
|
4101
4470
|
]) : null,
|
|
4102
|
-
conflictModal ? h(
|
|
4471
|
+
conflictModal ? h(MaskPortal, { onClick: () => setConflictModal(false) }, [
|
|
4103
4472
|
h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
|
|
4104
4473
|
h("div", { className: "mpw_title" }, t("conflict.title")),
|
|
4105
4474
|
h("p", { className: "mpw_desc" }, `${t("conflict.body")}:${conflicts.join(", ")}`),
|
|
@@ -4274,6 +4643,35 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
4274
4643
|
"flipY.desc": "壁纸上下镜像(scaleY -1)",
|
|
4275
4644
|
"themeColor": "主题颜色",
|
|
4276
4645
|
"themeColor.hint": "驱动按钮/滑条/选中项/链接/发送键等品牌色(取色盘 + 预置);空 = DSH 默认蓝",
|
|
4646
|
+
"speed": "视频倍速",
|
|
4647
|
+
"speed.hint": "视频壁纸播放速度(0.5x–2x,原生播放器变速,即时生效不重载)",
|
|
4648
|
+
"rot.title": "轮播列表管理",
|
|
4649
|
+
"rot.all": "全部壁纸(不限定列表)",
|
|
4650
|
+
"rot.manage": "管理列表",
|
|
4651
|
+
"rot.empty": "还没有轮播列表。点「新建列表」从壁纸中挑选,或扫描 Steam 库自动导入 WE 播放列表。",
|
|
4652
|
+
"rot.new": "新建列表",
|
|
4653
|
+
"rot.newTitle": "新建轮播列表",
|
|
4654
|
+
"rot.edit": "编辑",
|
|
4655
|
+
"rot.editTitle": "编辑轮播列表",
|
|
4656
|
+
"rot.del": "删除",
|
|
4657
|
+
"rot.save": "保存",
|
|
4658
|
+
"rot.items": " 个壁纸",
|
|
4659
|
+
"rot.seq": "顺序",
|
|
4660
|
+
"rot.random": "随机",
|
|
4661
|
+
"rot.namePh": "列表名称",
|
|
4662
|
+
"rot.unnamed": "未命名列表",
|
|
4663
|
+
"rot.pickHint": "勾选参与轮播的壁纸",
|
|
4664
|
+
"rot.emptyKeys": "请至少勾选一个壁纸",
|
|
4665
|
+
"rot.wePlaylist": "WE 播放列表",
|
|
4666
|
+
"rot.seeded": "已导入 WE 原生播放列表",
|
|
4667
|
+
"rot.groupInterval": "该列表轮换间隔",
|
|
4668
|
+
"rot.order": "播放顺序",
|
|
4669
|
+
"rot.noWallpapers": "没有可选的壁纸(先扫描壁纸库)",
|
|
4670
|
+
"rot.scanDir": "扫描自定义目录",
|
|
4671
|
+
"rot.filterCustom": "自定义",
|
|
4672
|
+
"rot.filterSteam": "Steam库",
|
|
4673
|
+
"rot.scanLib": "扫描本地库",
|
|
4674
|
+
"rot.intervalHint": "间隔在选中列表后下方单独设置",
|
|
4277
4675
|
"themeColorReset": "默认",
|
|
4278
4676
|
"blur": "磨砂模糊",
|
|
4279
4677
|
"blur.overridden": "统一虚化 + 聊天区跟随均开启:壁纸模糊由「整屏虚化程度」接管,磨砂条暂不可调;关闭「聊天区跟随整屏虚化」后磨砂条恢复可调(此时统一虚化只管侧边栏/标题栏)",
|
|
@@ -4379,6 +4777,35 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
4379
4777
|
"lib.rotateDesc": "Automatically switch to the next wallpaper on a timer (range: custom folder + local library + Steam-discovered video wallpapers)",
|
|
4380
4778
|
"lib.rotMerged": "Playable Steam-library videos merged into the rotation: ",
|
|
4381
4779
|
"lib.rotateMin": "Rotation interval",
|
|
4780
|
+
"speed": "Video speed",
|
|
4781
|
+
"speed.hint": "Video wallpaper playback speed (0.5x–2x, native player rate, instant, no reload)",
|
|
4782
|
+
"rot.title": "Carousel list management",
|
|
4783
|
+
"rot.all": "All wallpapers (no list)",
|
|
4784
|
+
"rot.manage": "Manage lists",
|
|
4785
|
+
"rot.empty": "No carousel lists yet. Click \"New list\" to pick wallpapers, or scan the Steam library to auto-import WE playlists.",
|
|
4786
|
+
"rot.new": "New list",
|
|
4787
|
+
"rot.newTitle": "New carousel list",
|
|
4788
|
+
"rot.edit": "Edit",
|
|
4789
|
+
"rot.editTitle": "Edit carousel list",
|
|
4790
|
+
"rot.del": "Delete",
|
|
4791
|
+
"rot.save": "Save",
|
|
4792
|
+
"rot.items": " wallpapers",
|
|
4793
|
+
"rot.seq": "Sequence",
|
|
4794
|
+
"rot.random": "Random",
|
|
4795
|
+
"rot.namePh": "List name",
|
|
4796
|
+
"rot.unnamed": "Unnamed list",
|
|
4797
|
+
"rot.pickHint": "Check wallpapers to include in rotation",
|
|
4798
|
+
"rot.emptyKeys": "Select at least one wallpaper",
|
|
4799
|
+
"rot.wePlaylist": "WE playlist",
|
|
4800
|
+
"rot.seeded": "Imported WE native playlist",
|
|
4801
|
+
"rot.groupInterval": "This list rotation interval",
|
|
4802
|
+
"rot.order": "Play order",
|
|
4803
|
+
"rot.noWallpapers": "No wallpapers to pick (scan a wallpaper library first)",
|
|
4804
|
+
"rot.scanDir": "Scan custom folder",
|
|
4805
|
+
"rot.filterCustom": "Custom",
|
|
4806
|
+
"rot.filterSteam": "Steam library",
|
|
4807
|
+
"rot.scanLib": "Scan local library",
|
|
4808
|
+
"rot.intervalHint": "Interval is set separately below after selecting a list",
|
|
4382
4809
|
"lib.minutes": "min",
|
|
4383
4810
|
"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
4811
|
"conflict.detected": "Potentially conflicting plugins detected",
|
package/lib/index.js
CHANGED
|
@@ -468,6 +468,18 @@ function apply(ctx) {
|
|
|
468
468
|
else if (hasHtml) { type = 'web'; media = 'index.html'; }
|
|
469
469
|
else if (vid) { type = 'video'; media = vid; }
|
|
470
470
|
}
|
|
471
|
+
// ①(修正) 子文件夹里的 .mpkg(无 project.json/scene.pkg/html 的纯 mpkg 收藏夹,
|
|
472
|
+
// 如 wallpaperE 下的角色文件夹)→ 每个 mpkg 独立成条(folderMpkg)
|
|
473
|
+
if ((!type || !media) && e.isDirectory()) {
|
|
474
|
+
let mpkgs = [];
|
|
475
|
+
try { mpkgs = readdirSync(sub).filter((x) => x.toLowerCase().endsWith('.mpkg')); } catch {}
|
|
476
|
+
if (mpkgs.length) {
|
|
477
|
+
for (const m of mpkgs) {
|
|
478
|
+
files.push({ name: f, title: m, type: 'mpkg', media: m, folder: true, folderMpkg: true, preview: null });
|
|
479
|
+
}
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
471
483
|
if (type && media) {
|
|
472
484
|
// ①(新) 网页壁纸风险预检:Spine/L2D 骨骼动画(skel/atlas)→ 重动画
|
|
473
485
|
// 可能卡顿;引用外网 http(s) → 依赖外网可能失败(用户实测:昔涟类
|
|
@@ -520,8 +532,10 @@ function apply(ctx) {
|
|
|
520
532
|
if (!customDir) { json(res, 404, { ok: false, error: 'no dir' }); return; }
|
|
521
533
|
const url = new URL(req.url || '', 'http://localhost');
|
|
522
534
|
const file = url.searchParams.get('file') || '';
|
|
523
|
-
|
|
524
|
-
|
|
535
|
+
const folder = url.searchParams.get('folder') || '';
|
|
536
|
+
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; }
|
|
537
|
+
// ①(修正) folder= 支持子文件夹里的 .mpkg(wallpaperE 角色收藏夹)
|
|
538
|
+
const filePath = folder ? join(customDir, folder, file) : join(customDir, file);
|
|
525
539
|
if (!existsSync(filePath) || !file.toLowerCase().endsWith('.mpkg')) { json(res, 404, { ok: false, error: 'not found' }); return; }
|
|
526
540
|
const head = readFileSync(filePath).subarray(0, HEAD_BYTES);
|
|
527
541
|
const { dataStart, entries } = parseMpkgHead(head);
|
|
@@ -560,12 +574,15 @@ function apply(ctx) {
|
|
|
560
574
|
if (!customDir) { json(res, 404, { ok: false, error: 'no dir' }); return; }
|
|
561
575
|
const url = new URL(req.url || '', 'http://localhost');
|
|
562
576
|
const file = url.searchParams.get('file') || '';
|
|
563
|
-
|
|
564
|
-
|
|
577
|
+
const folder = url.searchParams.get('folder') || '';
|
|
578
|
+
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; }
|
|
579
|
+
// ①(修正) folder= 支持子文件夹里的 .mpkg
|
|
580
|
+
const filePath = folder ? join(customDir, folder, file) : join(customDir, file);
|
|
565
581
|
if (!existsSync(filePath) || !file.toLowerCase().endsWith('.mpkg')) { json(res, 404, { ok: false, error: 'not found' }); return; }
|
|
566
582
|
// ①(新) 缓存:mtime 未变 → 直接返回缓存 bytes(不再解析头部/打开大文件)
|
|
567
583
|
const st = statSync(filePath);
|
|
568
|
-
const
|
|
584
|
+
const cacheKey = (folder ? folder + '/' : '') + file;
|
|
585
|
+
const cached = mpkgPreviewGet(cacheKey);
|
|
569
586
|
if (cached && cached.mtimeMs === st.mtimeMs && cached.bytes) {
|
|
570
587
|
res.writeHead(200, { 'content-type': cached.mime, 'content-length': cached.bytes.length, 'cache-control': 'no-cache' });
|
|
571
588
|
res.end(cached.bytes);
|
|
@@ -592,7 +609,7 @@ function apply(ctx) {
|
|
|
592
609
|
if (n <= 0) break; got += n;
|
|
593
610
|
}
|
|
594
611
|
} finally { closeSync(fd); }
|
|
595
|
-
mpkgPreviewSet(
|
|
612
|
+
mpkgPreviewSet(cacheKey, { mtimeMs: st.mtimeMs, mime, bytes: buf });
|
|
596
613
|
res.writeHead(200, { 'content-type': mime, 'content-length': buf.length, 'cache-control': 'no-cache' });
|
|
597
614
|
res.end(buf);
|
|
598
615
|
return;
|
|
@@ -1003,7 +1020,53 @@ function apply(ctx) {
|
|
|
1003
1020
|
}
|
|
1004
1021
|
};
|
|
1005
1022
|
for (const root of projectsRoots) scan(root);
|
|
1006
|
-
|
|
1023
|
+
// ①(新) WE 原生播放列表(config.json → general.playlists):解析为
|
|
1024
|
+
// 轮播列表(项 = 壁纸路径 → 映射到本插件的 key:steam|ltoken / custom|目录名)
|
|
1025
|
+
const playlists = [];
|
|
1026
|
+
try {
|
|
1027
|
+
const cfgPath = join(installDir, 'config.json');
|
|
1028
|
+
if (existsSync(cfgPath)) {
|
|
1029
|
+
const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'));
|
|
1030
|
+
const byFolder = new Map(); // 文件夹名 → ltoken
|
|
1031
|
+
for (const [lt, rec] of library) {
|
|
1032
|
+
const base = String(rec.dir || '').split(/[\\/]/).filter(Boolean).pop() || '';
|
|
1033
|
+
if (base && !byFolder.has(base)) byFolder.set(base.toLowerCase(), lt);
|
|
1034
|
+
}
|
|
1035
|
+
const seen = new Set();
|
|
1036
|
+
for (const profile of Object.values(cfg || {})) {
|
|
1037
|
+
if (!profile || typeof profile !== 'object') continue;
|
|
1038
|
+
const general = profile.general || {};
|
|
1039
|
+
const rows = Array.isArray(general.playlists) ? general.playlists : [];
|
|
1040
|
+
for (const row of rows) {
|
|
1041
|
+
const items = Array.isArray(row.items) ? row.items.filter((x) => typeof x === 'string' && x.trim()) : [];
|
|
1042
|
+
if (!items.length) continue;
|
|
1043
|
+
const keys = [];
|
|
1044
|
+
for (const item of items) {
|
|
1045
|
+
// "…/workshop/content/431960/<folder>/…" → 文件夹名 → ltoken
|
|
1046
|
+
const m = /[\\/]431960[\\/]([^\\/]+)(?:[\\/]|$)/i.exec(item);
|
|
1047
|
+
const folder = m ? m[1] : /[\\/]([^\\/]+)[\\/][^\\/]+$/i.exec(item) ? /[\\/]([^\\/]+)[\\/][^\\/]+$/i.exec(item)[1] : null;
|
|
1048
|
+
if (!folder) continue;
|
|
1049
|
+
const lt = byFolder.get(folder.toLowerCase());
|
|
1050
|
+
if (lt) keys.push('steam|' + lt);
|
|
1051
|
+
else if (customDir && existsSync(join(customDir, folder))) keys.push('custom|' + folder);
|
|
1052
|
+
}
|
|
1053
|
+
if (!keys.length) continue;
|
|
1054
|
+
const name = typeof row.name === 'string' && row.name.trim() ? row.name.trim() : 'WE Playlist';
|
|
1055
|
+
const sig = name + '\u0000' + keys.join('\u0000');
|
|
1056
|
+
if (seen.has(sig)) continue;
|
|
1057
|
+
seen.add(sig);
|
|
1058
|
+
const settings = row.settings && typeof row.settings === 'object' ? row.settings : {};
|
|
1059
|
+
playlists.push({
|
|
1060
|
+
name,
|
|
1061
|
+
keys,
|
|
1062
|
+
order: settings.order === 'random' ? 'random' : 'sequence',
|
|
1063
|
+
interval: typeof settings.delay === 'number' && settings.delay > 0 ? Math.min(1440, Math.round(settings.delay)) : 5,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
} catch { /* 播放列表读取失败不阻塞 */ }
|
|
1069
|
+
json(res, 200, { ok: true, installDir, wallpapers, playlists });
|
|
1007
1070
|
} catch (err) {
|
|
1008
1071
|
json(res, 500, { ok: false, error: String(err && err.message || err) });
|
|
1009
1072
|
}
|
package/package.json
CHANGED