dsh-mpkg-wallpaper 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,3477 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@local/dsh-mpkg-wallpaper",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+
8
+ let react = require("react");
9
+
10
+ // ═══════════════════════════════════════════════════════════════════
11
+ // 常量
12
+ // ═══════════════════════════════════════════════════════════════════
13
+ const ROW_ID = "mpkg-wallpaper";
14
+ const NS = "ui-mpkg-wallpaper";
15
+ const STORE_KEY = "dsh.mpkg-wallpaper.v2";
16
+ const DEFAULT_OPACITY = 82;
17
+ const DEFAULT_BLUR = 12;
18
+ const DEFAULT_ZOOM = 100;
19
+ const DEFAULT_SIDEBAR = true;
20
+ const DEFAULT_SHARP = true;
21
+ const DEFAULT_HEADER = true;
22
+ const DEFAULT_HEADER_BG = true;
23
+ const DEFAULT_HEADER_BLUR_AMOUNT = 80; // 标题栏磨砂程度 0-100%(白雾厚度,独立于整屏虚化)
24
+ const DEFAULT_DIALOG_BLUR = true;
25
+ const DEFAULT_DIALOG_AMOUNT = 14;
26
+ // ①(新) 虚化对话框拆三类:通用居中窗口 / 设置面板 / 下载·确认弹窗,各自独立开关+程度
27
+ const DEFAULT_SETTINGS_BLUR = true; // 虚化设置面板
28
+ const DEFAULT_SETTINGS_AMOUNT = 14; // 设置面板虚化程度
29
+ const DEFAULT_CONFIRM_BLUR = true; // 虚化下载/确认弹窗
30
+ const DEFAULT_CONFIRM_AMOUNT = 12; // 下载/确认弹窗虚化程度
31
+ // ①(新) 侧边栏磨砂(Aqua 方案:sidebarCol 自身 backdrop-filter;弹窗打开时 :has() 摘除)
32
+ const DEFAULT_SIDEBAR_BLUR = false; // 侧边栏磨砂开关(默认关=壁纸层方案;开=Aqua 玻璃)
33
+ const DEFAULT_SIDEBAR_BLUR_AMOUNT = 14; // 侧边栏磨砂程度
34
+ const DEFAULT_POPOVER_BLUR = true; // 弹层虚化(菜单/提示/遮罩)开关
35
+ const DEFAULT_POPOVER_AMOUNT = 10; // 弹层虚化程度
36
+ const DEFAULT_MASK_BLUR = true; // 遮罩虚化(设置/弹层打开时全屏背景遮罩)
37
+ const DEFAULT_MASK_AMOUNT = 8; // 遮罩虚化程度
38
+ const DEFAULT_UNIFY_TINT = true; // 统一虚化:整屏模糊感由一个独立条控制
39
+ const DEFAULT_UNIFY_AMOUNT = 30; // 统一虚化程度 0-40:0=清晰,40=强模糊(控制壁纸层 blur)
40
+ const DEFAULT_CHAT_FOLLOW = true; // ①(新) 统一虚化是否接管聊天区壁纸(开=整屏统一;关=磨砂条独立控制聊天区,统一虚化只管侧边栏/标题栏)
41
+ const DEFAULT_SIDEBAR_ALPHA = 55; // ①(新) 统一虚化下侧边栏/标题栏白雾厚度 0-100(独立于不透明度条)
42
+ const DEFAULT_SESSION_FOLLOW = true; // 统一虚化下,「新会话」按钮是否随整屏虚化(关=随面板不透明度)
43
+ const DEFAULT_HYBRID = true; // ①(新) 大文件混合模式:上传到 DSH 宿主流式播放(>600MB 无限制);关=纯浏览器模式
44
+ const DEFAULT_ROTATE = false; // ③(新) 壁纸轮换:定时自动切换下一个壁纸
45
+ const DEFAULT_BRIGHTNESS = 100; // ⑧(新) 画面亮度(50-150%,滤镜)
46
+ const DEFAULT_FLOAT = false; // ①(新) 悬浮效果:侧边栏/标题栏悬浮卡片(默认关)
47
+ const DEFAULT_ROUND_COMPAT = false; // ②(新) 第三方插件 UI 圆角兼容开关(默认关,未完全兼容暂不打扰)
48
+ const HOST_BASE = "/api/mpkg-wallpaper";
49
+ const DEFAULT_THINK_BG = false; // Deep diving 背景方框:默认取消(透明)
50
+ const DEFAULT_ENABLED = true;
51
+ const DEFAULT_CLOCK = false;
52
+ const DEFAULT_CLOCK_24H = true;
53
+ const DEFAULT_CLOCK_SEC = false;
54
+ const DEFAULT_CLOCK_DATE = false;
55
+ const DEFAULT_CLOCK_POS = "tr";
56
+ const DEFAULT_CLOCK_SIZE = 40;
57
+ const BG_WRAP_ID = "mpw-bgWrap";
58
+ const BG_IMG_ID = "mpw-bgImg";
59
+ const BG_VIDEO_ID = "mpw-bgVideo";
60
+ /** ⑥ 设置导航图标(中性"风景画"图标,非壁纸引擎商标,无侵权风险)。 */
61
+ const NAV_ICON = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDIwIDIwIj4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0ic2t5IiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+CjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiM3YWEyZmYiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiM0YjZmZDQiLz4KPC9saW5lYXJHcmFkaWVudD4KPC9kZWZzPgo8cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHJ4PSIzIiBmaWxsPSJ1cmwoI3NreSkiLz4KPGNpcmNsZSBjeD0iMTQiIGN5PSI2LjUiIHI9IjEuOCIgZmlsbD0iI2ZmZDc2ZSIvPgo8cGF0aCBkPSJNMiAxNCBMNyA5IEwxMC41IDEyLjUgTDEzIDEwIEwxOCAxNC41IEwxOCAxNSBRMTggMTYgMTcgMTYgTDMgMTYgUTIgMTYgMiAxNSBaIiBmaWxsPSIjMmYzZDU3Ii8+CjxwYXRoIGQ9Ik03IDkgTDUuNSA3LjUgTDQgOSBaIiBmaWxsPSIjM2U1ZjRmIi8+CjxyZWN0IHg9IjIiIHk9IjIiIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgcng9IjMiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZmZmZjY2IiBzdHJva2Utd2lkdGg9IjEiLz4KPC9zdmc+";
62
+
63
+ // ═══════════════════════════════════════════════════════════════════
64
+ // localStorage 持久化(⑤:读写带缓存 + 防抖,滑杆不卡顿)
65
+ // ═══════════════════════════════════════════════════════════════════
66
+ let sectionCache = null;
67
+ let persistTimer = null;
68
+ function loadSection() {
69
+ try {
70
+ const raw = localStorage.getItem(STORE_KEY);
71
+ if (!raw) return {};
72
+ const parsed = JSON.parse(raw);
73
+ return parsed && typeof parsed === "object" ? parsed : {};
74
+ } catch {
75
+ return {};
76
+ }
77
+ }
78
+ /** 带缓存的读取:避免每次滑杆事件都解析大 JSON(背景图 data URL 有 1MB+)。 */
79
+ function readSection() {
80
+ if (sectionCache) return sectionCache;
81
+ sectionCache = loadSection();
82
+ return sectionCache;
83
+ }
84
+ /** 写入内存缓存 + 防抖落盘。instant=true 立即写(文件选择/复位)。 */
85
+ function writeSection(next, instant) {
86
+ sectionCache = next;
87
+ if (persistTimer) clearTimeout(persistTimer);
88
+ persistTimer = setTimeout(() => {
89
+ try {
90
+ localStorage.setItem(STORE_KEY, JSON.stringify(sectionCache));
91
+ } catch {
92
+ /* 存储满时静默失败 */
93
+ }
94
+ }, instant ? 0 : 250);
95
+ }
96
+ function saveSection(patch) {
97
+ const next = Object.assign({}, readSection(), patch);
98
+ writeSection(next, true);
99
+ return next;
100
+ }
101
+ function clearSection() {
102
+ sectionCache = null;
103
+ if (persistTimer) clearTimeout(persistTimer);
104
+ try {
105
+ localStorage.removeItem(STORE_KEY);
106
+ } catch {}
107
+ }
108
+
109
+ // ═══════════════════════════════════════════════════════════════════
110
+ // IndexedDB 大图存储(③:高清 GIF/大图超过 localStorage 上限时用)
111
+ // ═══════════════════════════════════════════════════════════════════
112
+ let idbDb = null;
113
+ function idbOpen() {
114
+ if (idbDb) return Promise.resolve(idbDb);
115
+ return new Promise((resolve, reject) => {
116
+ try {
117
+ if (typeof indexedDB === "undefined") { reject(new Error("no idb")); return; }
118
+ const req = indexedDB.open("dsh-mpkg-wallpaper", 1);
119
+ req.onupgradeneeded = () => { req.result.createObjectStore("images"); };
120
+ req.onsuccess = () => { idbDb = req.result; resolve(idbDb); };
121
+ req.onerror = () => reject(req.error);
122
+ } catch (e) { reject(e); }
123
+ });
124
+ }
125
+ function idbPut(key, value) {
126
+ return idbOpen().then((db) => new Promise((resolve, reject) => {
127
+ try {
128
+ const tx = db.transaction("images", "readwrite");
129
+ tx.objectStore("images").put(value, key);
130
+ tx.oncomplete = () => resolve();
131
+ tx.onerror = () => reject(tx.error);
132
+ } catch (e) { reject(e); }
133
+ }));
134
+ }
135
+ function idbGet(key) {
136
+ return idbOpen().then((db) => new Promise((resolve, reject) => {
137
+ try {
138
+ const tx = db.transaction("images", "readonly");
139
+ const req = tx.objectStore("images").get(key);
140
+ req.onsuccess = () => resolve(req.result);
141
+ req.onerror = () => reject(req.error);
142
+ } catch (e) { reject(e); }
143
+ }));
144
+ }
145
+ function idbDel(key) {
146
+ return idbOpen().then((db) => new Promise((resolve, reject) => {
147
+ try {
148
+ const tx = db.transaction("images", "readwrite");
149
+ tx.objectStore("images").delete(key);
150
+ tx.oncomplete = () => resolve();
151
+ tx.onerror = () => reject(tx.error);
152
+ } catch (e) { reject(e); }
153
+ })).catch(() => {});
154
+ }
155
+ /** dataUrl 太大时写入 IndexedDB,返回 "idb:bg" 标记;否则原样返回。 */
156
+ function storeImage(dataUrl) {
157
+ if (dataUrl.length <= 2 * 1024 * 1024) return Promise.resolve(dataUrl);
158
+ return idbPut("bg", dataUrl).then(() => "idb:bg").catch(() => dataUrl);
159
+ }
160
+ /** 视频 Blob 存入 IndexedDB(①:外部渲染成视频后作为动态背景)。 */
161
+ function storeVideoBlob(blob) {
162
+ return idbPut("bg", blob).then(() => "idb:blob");
163
+ }
164
+ /** ③(新) 大图片 Blob 直接存 IndexedDB(不走 dataURL,避免 base64 膨胀 1.37 倍爆内存),
165
+ * 返回 "idb:img" 标记;小图走 dataURL 直接内联。 */
166
+ function storeImageBlob(blob) {
167
+ if (blob.size <= 2 * 1024 * 1024) {
168
+ return blobToDataUrl(blob).then((d) => (d.length > 2 * 1024 * 1024 ? idbPut("bg", d).then(() => "idb:img") : d));
169
+ }
170
+ return idbPut("bg", blob).then(() => "idb:img");
171
+ }
172
+
173
+ // ═══════════════════════════════════════════════════════════════════
174
+ // mpkg 解析(Wallpaper Engine 手机版 .mpkg 容器)
175
+ // 布局:头部 + 全部条目头(在前),随后是全部文件数据(连续)
176
+ // header : version_length(u32 LE) + version + file_total(u32 LE)
177
+ // entry : name_length(u32) + name + index(u32) + size(u32)
178
+ // data : 第 i 个文件位于 dataStart + entries[i].index,长度 size
179
+ // ═══════════════════════════════════════════════════════════════════
180
+ function parseMpkg(buffer) {
181
+ const dv = new DataView(buffer);
182
+ const decoder = new TextDecoder();
183
+ let pos = 0;
184
+ const versionLength = dv.getUint32(pos, true); pos += 4;
185
+ const version = decoder.decode(new Uint8Array(buffer, pos, versionLength)); pos += versionLength;
186
+ const fileTotal = dv.getUint32(pos, true); pos += 4;
187
+ const entries = [];
188
+ for (let i = 0; i < fileTotal; i++) {
189
+ const nameLength = dv.getUint32(pos, true); pos += 4;
190
+ const name = decoder.decode(new Uint8Array(buffer, pos, nameLength)); pos += nameLength;
191
+ const index = dv.getUint32(pos, true); pos += 4;
192
+ const size = dv.getUint32(pos, true); pos += 4;
193
+ entries.push({ name, index, size });
194
+ }
195
+ return { version, entries, dataStart: pos };
196
+ }
197
+
198
+ function guessMime(name) {
199
+ const n = name.toLowerCase();
200
+ if (n.endsWith(".gif")) return "image/gif";
201
+ if (n.endsWith(".png")) return "image/png";
202
+ if (n.endsWith(".jpg") || n.endsWith(".jpeg")) return "image/jpeg";
203
+ if (n.endsWith(".webp")) return "image/webp";
204
+ if (n.endsWith(".mp4")) return "video/mp4";
205
+ if (n.endsWith(".webm")) return "video/webm";
206
+ if (n.endsWith(".mov")) return "video/quicktime";
207
+ if (n.endsWith(".json")) return "application/json";
208
+ return "application/octet-stream";
209
+ }
210
+
211
+ function entryBytes(buffer, entry, dataStart) {
212
+ return new Uint8Array(buffer, dataStart + entry.index, entry.size);
213
+ }
214
+
215
+ /** 当前时段(用于按时间选择素材,⑨)。 */
216
+ function timeSlotKey(date) {
217
+ const h = date.getHours();
218
+ if (h >= 5 && h < 8) return "morning";
219
+ if (h >= 8 && h < 17) return "day";
220
+ if (h >= 17 && h < 19) return "dusk";
221
+ return "night";
222
+ }
223
+
224
+ /** 按当前时间挑背景素材:优先 preview_{时段}.gif / {时段}.gif 等;否则回退任意图片。 */
225
+ function pickBackgroundEntry(entries, date) {
226
+ const slot = timeSlotKey(date);
227
+ const suffixes = [slot, "day", "night", "dusk", "morning"];
228
+ for (const suf of suffixes) {
229
+ for (const ext of ["gif", "png", "jpg", "jpeg", "webp"]) {
230
+ const hit = entries.find((e) => {
231
+ const n = e.name.toLowerCase();
232
+ return n === `preview_${suf}.${ext}` || n === `preview-${suf}.${ext}` || n === `${suf}.${ext}`;
233
+ });
234
+ if (hit) return { entry: hit, slot: suf === slot ? slot : null };
235
+ }
236
+ }
237
+ // ① 内嵌 mp4(视频类壁纸)优先:比 preview.gif 清晰得多
238
+ const vid = entries.find((e) => /\.(mp4|webm|mov)$/i.test(e.name));
239
+ if (vid) return { entry: vid, slot: null };
240
+ const any = entries.find((e) => /\.(gif|png|jpe?g|webp)$/i.test(e.name)) || null;
241
+ return { entry: any, slot: null };
242
+ }
243
+
244
+ function blobToDataUrl(blob) {
245
+ return new Promise((resolve, reject) => {
246
+ const reader = new FileReader();
247
+ reader.onload = () => resolve(reader.result);
248
+ reader.onerror = () => reject(reader.error);
249
+ reader.readAsDataURL(blob);
250
+ });
251
+ }
252
+
253
+ /** 确保 GIF 无限循环(有的预览图可能只循环 N 次;⑪)。返回新的 Uint8Array。 */
254
+ function ensureInfiniteGif(bytes) {
255
+ if (bytes.length < 13) return bytes;
256
+ const packed = bytes[10];
257
+ let pos = 13;
258
+ if (packed & 0x80) pos += 3 * (1 << ((packed & 0x07) + 1));
259
+ const needle = [0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30]; // NETSCAPE2.0
260
+ for (let p = pos; p <= bytes.length - 20; p++) {
261
+ if (bytes[p] === 0x21 && bytes[p + 1] === 0xff && bytes[p + 2] === 0x0b) {
262
+ let match = true;
263
+ for (let k = 0; k < needle.length; k++) {
264
+ if (bytes[p + 3 + k] !== needle[k]) { match = false; break; }
265
+ }
266
+ if (match) {
267
+ // 结构: 21 FF 0B NETSCAPE2.0 03 01 [lo] [hi] 00
268
+ if (bytes[p + 14] === 0x03 && bytes[p + 15] === 0x01) {
269
+ bytes[p + 16] = 0;
270
+ bytes[p + 17] = 0;
271
+ }
272
+ return bytes;
273
+ }
274
+ }
275
+ }
276
+ // 没有 NETSCAPE 扩展 → 插入一个(无限循环)
277
+ const ext = [0x21, 0xff, 0x0b, 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30, 0x03, 0x01, 0x00, 0x00, 0x00];
278
+ const out = new Uint8Array(bytes.length + ext.length);
279
+ out.set(bytes.subarray(0, pos), 0);
280
+ out.set(ext, pos);
281
+ out.set(bytes.subarray(pos), pos + ext.length);
282
+ return out;
283
+ }
284
+
285
+ /** 处理视频纹理壁纸:提取 MP4、按时间选时段、存入 IndexedDB。
286
+ * ③(重做) readEntry(entry, limit?) 按需读取,424MB 大包不整体进内存。 */
287
+ async function handleVideoTexes(mpkg, vtex, file, t, showError, readEntry) {
288
+ try {
289
+ // 提取每个视频纹理的 MP4 并识别时间槽
290
+ const list = [];
291
+ let cfg = { enabled: true, morning: 4, day: 9, dusk: 17, night: 20 };
292
+ let propsMap = null;
293
+ const proj = mpkg.entries.find((e) => e.name === "project.json");
294
+ if (proj) {
295
+ try {
296
+ const pj = JSON.parse(new TextDecoder().decode(await readEntry(proj)));
297
+ propsMap = (pj.general && pj.general.properties) || null;
298
+ } catch {}
299
+ }
300
+ const key2 = file.name + "|" + file.size;
301
+ const propEdits = (readSection().propEdits && readSection().propEdits[key2]) || {};
302
+ if (propsMap) cfg = timeConfigFromProps(propsMap, propEdits);
303
+ for (let i = 0; i < vtex.length; i++) {
304
+ const e = vtex[i];
305
+ // ③(新) 内存防护:超大视频纹理在移动端直接 OOM(页面崩溃),前置跳过
306
+ if (e.size > 250 * 1024 * 1024) continue;
307
+ // ⑤(新) 分段读取:先读 tex 头 4KB 找 ftyp 偏移,再只读 mp4 段
308
+ // (避免整个大 tex 进内存:峰值 = mp4 大小,而非 tex 大小)。
309
+ const texHead = await readEntry(e, 4096);
310
+ const mp4Off = extractTexVideoOffset(texHead);
311
+ let mp4 = null;
312
+ if (mp4Off !== null) {
313
+ mp4 = await readEntry(e, e.size - mp4Off, mp4Off);
314
+ } else {
315
+ const bytes = await readEntry(e);
316
+ mp4 = extractTexVideo(bytes);
317
+ }
318
+ if (!mp4 || !mp4.length) continue;
319
+ const slot = slotFromName(e.name);
320
+ const skey = slot || "v" + i;
321
+ const blob = new Blob([mp4], { type: "video/mp4" });
322
+ if (blob.size > 250 * 1024 * 1024) continue;
323
+ await idbPut("bg-" + skey, blob);
324
+ list.push({ slot: slot || skey, name: e.name, key: skey, size: blob.size });
325
+ }
326
+ if (!list.length) {
327
+ // ⑨(重做) 视频纹理都无法提取时返回 false,让 onMpkg 回退到 preview.gif
328
+ //(用户至少能看到预览,而不是静默无反应)
329
+ const anyBig = vtex.some((e) => e.size > 250 * 1024 * 1024);
330
+ showError(anyBig ? t("mpkg.vtexBig") : t("mpkg.noAsset"));
331
+ return false;
332
+ }
333
+ // 选当前时段
334
+ let active = null;
335
+ if (cfg.enabled && list.some((l) => l.slot === "morning" || l.slot === "day" || l.slot === "dusk" || l.slot === "night")) {
336
+ const slot = slotForTime(cfg, new Date());
337
+ active = list.find((l) => l.slot === slot) || list[0];
338
+ } else {
339
+ active = list[0];
340
+ }
341
+ const activeBlob = await idbGet("bg-" + active.key);
342
+ if (activeBlob) await idbPut("bg", activeBlob);
343
+ const info = await extractProjectInfoAsync(mpkg.entries, readEntry);
344
+ const lensDef = lensDefaultsFromProps(propsMap);
345
+ // ③ 标题用真实文件名;⑤⑥ 镜头默认值(用户未改过才生效)
346
+ const cur = readSection();
347
+ const lensPatch = {};
348
+ if (lensDef) {
349
+ if (cur.zoom === void 0) lensPatch.zoom = lensDef.zoom;
350
+ if (cur.lensX === void 0) lensPatch.lensX = lensDef.x;
351
+ if (cur.lensY === void 0) lensPatch.lensY = lensDef.y;
352
+ }
353
+ writeSection(Object.assign({}, cur, {
354
+ image: "idb:blob", source: "视频纹理:" + active.name,
355
+ fromMpkg: true, converted: "mp4",
356
+ timeVideos: list, timeConfig: cfg, activeSlot: active.slot || active.key,
357
+ mpkgKey: key2, mpkgName: file.name,
358
+ info: { title: info ? info.title : "", properties: info ? info.properties : [] },
359
+ slot: null
360
+ }, lensPatch), true);
361
+ applyFromStorage();
362
+ setHint(t("time.picked") + ":" + t("time." + (active.slot || "day")));
363
+ return true;
364
+ } catch (err) {
365
+ console.error("[dsh-mpkg-wallpaper] handleVideoTexes 失败:", err);
366
+ showError(t("mpkg.fail") + String(err && err.message || err));
367
+ return false;
368
+ }
369
+ }
370
+ /** ⑥ 从壁纸属性读镜头默认值(镜头大小/位置X/Y,按文本匹配 镜头/lens)。 */
371
+ function lensDefaultsFromProps(propsMap) {
372
+ if (!propsMap) return null;
373
+ let zoom = 100, x = 0, y = 0;
374
+ for (const k of Object.keys(propsMap)) {
375
+ const p = propsMap[k];
376
+ const txt = ((p.text || "") + " " + k).toLowerCase();
377
+ const v = Number(p && p.value);
378
+ if (isNaN(v)) continue;
379
+ if (/镜头大小|lens\s*size/.test(txt)) zoom = Math.max(10, Math.min(2000, v <= 1 ? v * 100 : v));
380
+ else if (/镜头位置\s*x|lens\s*position\s*x/.test(txt)) x = Math.max(-2000, Math.min(2000, v));
381
+ else if (/镜头位置\s*y|lens\s*position\s*y/.test(txt)) y = Math.max(-2000, Math.min(2000, v));
382
+ }
383
+ if (zoom === 100 && x === 0 && y === 0) return null;
384
+ return { zoom, x, y };
385
+ }
386
+
387
+ /** 清理属性标签:去 HTML 标签 / 实体 / 多余空白(⑤)。 */
388
+ function cleanLabel(text) {
389
+ return (text || "")
390
+ .replace(/<[^>]*>/g, "")
391
+ .replace(/&nbsp;/gi, " ")
392
+ .replace(/&amp;/g, "&")
393
+ .replace(/&lt;/g, "<")
394
+ .replace(/&gt;/g, ">")
395
+ .replace(/&quot;/g, '"')
396
+ .replace(/&#39;|&apos;/g, "'")
397
+ .replace(/\s+/g, " ")
398
+ .trim();
399
+ }
400
+
401
+ /** 解析 project.json 文本,提取标题 + 可调参数信息。 */
402
+ function extractProjectInfo(jsonText) {
403
+ try {
404
+ const json = JSON.parse(jsonText);
405
+ const props = json.general && json.general.properties;
406
+ const list = [];
407
+ if (props) {
408
+ for (const key of Object.keys(props)) {
409
+ if (!safePropKey(key)) continue;
410
+ const p = props[key];
411
+ if (!p || typeof p !== "object") continue;
412
+ const label = cleanLabel(p.text);
413
+ if (!label) continue;
414
+ let value = p.value;
415
+ if (Array.isArray(p.options) && p.options.length) {
416
+ const opt = p.options.find((o) => String(o.value) === String(value));
417
+ if (opt) value = opt.label;
418
+ }
419
+ // type "text" 是纯展示性条目(作者信息/说明),不给输入框(⑥)
420
+ const displayOnly = p.type === "text";
421
+ // ① 关键开关识别(entry animation 开场动画 / prompt box 提示框)
422
+ const keyText = (label + " " + (p.text || "")).toLowerCase();
423
+ const important = /entry\s*animation|开场动画|入场动画|prompt\s*box|提示框|水印|盗版|防盗|开始时间|随现实时间|timevarying|morningtime|daytime|dusktime|nighttime|时间变化|时间段/.test(keyText);
424
+ list.push({ key, label, value: value === void 0 ? "" : value, type: p.type, options: p.options || null, displayOnly, important });
425
+ }
426
+ }
427
+ return { title: json.title || "", properties: list };
428
+ } catch (e) {
429
+ return null;
430
+ }
431
+ }
432
+ /** ③(重做) 异步版:project.json 按需读取(424MB 大包不整体进内存)。 */
433
+ async function extractProjectInfoAsync(entries, readEntry) {
434
+ const proj = entries.find((e) => e.name === "project.json");
435
+ if (!proj) return null;
436
+ try {
437
+ const buf = await readEntry(proj);
438
+ return extractProjectInfo(new TextDecoder().decode(buf));
439
+ } catch (e) {
440
+ return null;
441
+ }
442
+ }
443
+
444
+
445
+ // ═══════════════════════════════════════════════════════════════════
446
+ // 背景 DOM(一个固定 img 层;用 <img> 保证 GIF 动画可靠播放,⑪)
447
+ // ═══════════════════════════════════════════════════════════════════
448
+ function ensureBgDom() {
449
+ let wrap = document.getElementById(BG_WRAP_ID);
450
+ if (!wrap) {
451
+ wrap = document.createElement("div");
452
+ wrap.id = BG_WRAP_ID;
453
+ wrap.className = "mpw-bgWrap";
454
+ const img = document.createElement("img");
455
+ img.id = BG_IMG_ID;
456
+ img.className = "mpw-bgImg";
457
+ img.alt = "";
458
+ img.draggable = false;
459
+ img.referrerPolicy = "no-referrer";
460
+ img.crossOrigin = "anonymous";
461
+ img.style.display = "none";
462
+ const video = document.createElement("video");
463
+ video.id = BG_VIDEO_ID;
464
+ video.className = "mpw-bgVideo";
465
+ video.autoplay = true;
466
+ video.loop = true;
467
+ video.muted = true;
468
+ video.playsInline = true;
469
+ video.referrerPolicy = "no-referrer";
470
+ video.style.display = "none";
471
+ // ②(新) 视频播放失败检测:编码不支持/加载失败时不再静默(写 console + 全局标记)
472
+ video.addEventListener("error", () => {
473
+ console.warn("[dsh-mpkg-wallpaper] 视频背景加载失败(编码可能不被浏览器支持):", video.src);
474
+ try { window.__mpwVideoFailed = true; } catch {}
475
+ });
476
+ wrap.appendChild(img);
477
+ wrap.appendChild(video);
478
+ (document.body || document.documentElement).appendChild(wrap);
479
+ }
480
+ return wrap;
481
+ }
482
+ function bgElements() {
483
+ return {
484
+ img: document.getElementById(BG_IMG_ID),
485
+ video: document.getElementById(BG_VIDEO_ID),
486
+ wrap: document.getElementById(BG_WRAP_ID)
487
+ };
488
+ }
489
+ let clockEl = null;
490
+ let clockTimer = null;
491
+ function ensureClockEl() {
492
+ if (clockEl) return clockEl;
493
+ clockEl = document.createElement("div");
494
+ clockEl.id = "mpw-clock";
495
+ clockEl.style.cssText = "position:fixed;z-index:2000;pointer-events:none;font-family:ui-monospace,SFMono-Regular,monospace;font-variant-numeric:tabular-nums;text-shadow:0 1px 6px rgba(0,0,0,.55);color:var(--dsw-alias-label-primary,#e8eaf0);";
496
+ (document.body || document.documentElement).appendChild(clockEl);
497
+ return clockEl;
498
+ }
499
+ function clockText() {
500
+ const s = readSection();
501
+ const now = new Date();
502
+ let h = now.getHours();
503
+ const use24 = s.clock24h !== void 0 ? !!s.clock24h : DEFAULT_CLOCK_24H;
504
+ const hs = use24 ? String(h).padStart(2, "0") : String(((h % 12) || 12)).padStart(2, "0");
505
+ const sec = s.clockSec !== void 0 ? !!s.clockSec : DEFAULT_CLOCK_SEC;
506
+ const date = s.clockDate !== void 0 ? !!s.clockDate : DEFAULT_CLOCK_DATE;
507
+ const t = `${hs}:${String(now.getMinutes()).padStart(2, "0")}${sec ? ":" + String(now.getSeconds()).padStart(2, "0") : ""}`;
508
+ return date ? `${now.getMonth() + 1}月${now.getDate()}日 ${t}` : t;
509
+ }
510
+ function tickClock() {
511
+ const s = readSection();
512
+ const on = s.clock !== void 0 ? !!s.clock : DEFAULT_CLOCK;
513
+ if (!on) {
514
+ if (clockEl) clockEl.style.display = "none";
515
+ if (clockTimer) { clearInterval(clockTimer); clockTimer = null; }
516
+ return;
517
+ }
518
+ const el = ensureClockEl();
519
+ el.style.display = "";
520
+ el.textContent = clockText();
521
+ }
522
+ function updateClock() {
523
+ const s = readSection();
524
+ const on = s.clock !== void 0 ? !!s.clock : DEFAULT_CLOCK;
525
+ if (!on) { tickClock(); return; }
526
+ const el = ensureClockEl();
527
+ const pos = s.clockPos !== void 0 ? s.clockPos : DEFAULT_CLOCK_POS;
528
+ const size = s.clockSize !== void 0 ? s.clockSize : DEFAULT_CLOCK_SIZE;
529
+ el.style.fontSize = size + "px";
530
+ el.style.top = /t/.test(pos) ? "18px" : "auto";
531
+ el.style.bottom = /b/.test(pos) ? "18px" : "auto";
532
+ el.style.left = /l/.test(pos) ? "18px" : "auto";
533
+ el.style.right = /r/.test(pos) ? "18px" : "auto";
534
+ tickClock();
535
+ if (!clockTimer) clockTimer = setInterval(tickClock, 1000);
536
+ }
537
+
538
+ let lastObjectUrl = null;
539
+ // P0②(优化) 背景内容签名:仅当壁纸源真正变化时才重建 img/video 层。
540
+ // 拖动模糊/透明度等滑块时 applyFromStorage 会反复进入,若无此缓存
541
+ // idb:blob 每次都会新建 ObjectURL → 视频反复重缓冲、GIF 重播。
542
+ // IndexedDB 每次 get 返回新 Blob 实例(结构化克隆),故用 size+type 做内容签名。
543
+ let lastBgSig = null;
544
+ // ①(修正) 背景应用代数:applyFromStorage 每次调用递增;异步 IndexedDB 回调
545
+ // 若在更新的应用之后才返回(竞态),会被忽略——修复"视频壁纸→GIF 壁纸
546
+ // 切换后背景仍显示旧视频"的问题(旧视频的 idbGet 回调晚到覆盖新背景)。
547
+ let bgGen = 0;
548
+ function showImageEl() {
549
+ const { img, video, wrap } = bgElements();
550
+ if (!img || !video || !wrap) return;
551
+ try { video.pause(); } catch {}
552
+ if (lastObjectUrl) { try { URL.revokeObjectURL(lastObjectUrl); } catch {} lastObjectUrl = null; }
553
+ try { video.removeAttribute("src"); } catch {}
554
+ // ①(加固) 用 CSS 类切换 img/video 显示(比 inline style 更可靠)
555
+ wrap.classList.remove("mpw-video");
556
+ wrap.classList.add("mpw-img");
557
+ video.style.display = "none";
558
+ img.style.display = "";
559
+ }
560
+ function showVideoEl(url) {
561
+ const { img, video, wrap } = bgElements();
562
+ if (!img || !video || !wrap) return;
563
+ // ①(加固) CSS 类切换:视频壁纸显示 video、隐藏 img
564
+ wrap.classList.remove("mpw-img");
565
+ wrap.classList.add("mpw-video");
566
+ img.style.display = "none";
567
+ if (lastObjectUrl && lastObjectUrl !== url) { try { URL.revokeObjectURL(lastObjectUrl); } catch {} }
568
+ lastObjectUrl = url;
569
+ try { if (video.src !== url) video.src = url; } catch {}
570
+ video.style.display = "";
571
+ try { const p = video.play(); if (p && p.catch) p.catch(() => {}); } catch {}
572
+ }
573
+
574
+ /** 从 localStorage 状态渲染背景 DOM + 样式。 */
575
+ function applyFromStorage() {
576
+ bgGen++;
577
+ const gen = bgGen;
578
+ // ⑭ backdrop-filter 支持:用 CSS.supports 判断即可(用户实测 Via/Firefox 都支持)。
579
+ // 之前用"屏幕外测试元素 + getComputedStyle"检测会误判 false(屏幕外/透明
580
+ // 元素 computed backdropFilter 可能为空),导致 blur 被跳过只剩半透明=白纱。
581
+ try {
582
+ window.__mpwBackdropRendered = !!(typeof CSS !== "undefined" && !!CSS.supports && CSS.supports("backdrop-filter", "blur(1px)"));
583
+ } catch { window.__mpwBackdropRendered = true; }
584
+ const sectionRaw = readSection();
585
+ const enabled = sectionRaw.enabled !== void 0 ? !!sectionRaw.enabled : DEFAULT_ENABLED;
586
+ // ④ 总开关关闭 → 视为无背景(恢复默认外观)
587
+ const section = enabled ? sectionRaw : Object.assign({}, sectionRaw, { image: "" });
588
+ const { img, video, wrap } = bgElements();
589
+ if (!img || !video || !wrap) return;
590
+ const image = section.image || "";
591
+ if (typeof image === "string" && image.indexOf("host:") === 0) {
592
+ // ①(新) hybrid 模式:media URL(HTTP Range 流式),不经 IndexedDB
593
+ const q = image.slice(5); // "host:" 是 5 字符
594
+ // ④(新) 本地壁纸库 URL(ltoken=)走 /library-media;mpkg 条目走 /media;
595
+ // ①(新) 自定义目录(custom=)走 /custom-media
596
+ let hostUrl;
597
+ if (q.indexOf("ltoken=") >= 0) hostUrl = HOST_BASE + "/library-media" + q;
598
+ else if (q.indexOf("custom=") >= 0) hostUrl = HOST_BASE + "/custom-media" + q.replace("custom=1&", "");
599
+ else hostUrl = HOST_BASE + "/media" + q;
600
+ console.log("[dsh-mpkg-wallpaper] hybrid 背景:", section.converted, hostUrl);
601
+ if (section.converted === "mp4") {
602
+ // ①(修正) 重启竞态兜底:video 也要 404 重试(否则 host 晚就绪 → 视频壁纸永久空白)
603
+ const vid = bgElements().video;
604
+ if (vid && !vid.__mpwHostRetryVideo) {
605
+ vid.__mpwHostTries = 0;
606
+ vid.addEventListener("error", () => {
607
+ if (gen !== bgGen) return;
608
+ if ((vid.__mpwHostTries || 0) >= 5) { vid.__mpwHostTries = 0; return; }
609
+ vid.__mpwHostTries = (vid.__mpwHostTries || 0) + 1;
610
+ vid.__mpwHostRetryVideo = setTimeout(() => {
611
+ if (gen !== bgGen) return;
612
+ console.log("[dsh-mpkg-wallpaper] host video 404 → 重试", vid.__mpwHostTries);
613
+ const cur = vid.src;
614
+ vid.removeAttribute("src");
615
+ vid.src = cur;
616
+ vid.load();
617
+ try { const pp = vid.play(); if (pp && pp.catch) pp.catch(() => {}); } catch {}
618
+ }, 800 * vid.__mpwHostTries);
619
+ });
620
+ }
621
+ showVideoEl(hostUrl);
622
+ } else {
623
+ // ①(修正) 重启竞态兜底:dsh 重启后浏览器可能先于 host 就绪请求 media → 404;
624
+ // 挂 onerror 自动重试(最多 5 次、间隔递增),host 恢复后壁纸自动回来。
625
+ if (img.__mpwHostRetry) { try { clearTimeout(img.__mpwHostRetry); } catch {} }
626
+ img.__mpwHostTries = 0;
627
+ img.onerror = () => {
628
+ if (gen !== bgGen) return;
629
+ if ((img.__mpwHostTries || 0) >= 5) { img.__mpwHostTries = 0; return; }
630
+ img.__mpwHostTries = (img.__mpwHostTries || 0) + 1;
631
+ img.__mpwHostRetry = setTimeout(() => {
632
+ if (gen !== bgGen) return;
633
+ console.log("[dsh-mpkg-wallpaper] host media 404 → 重试", img.__mpwHostTries);
634
+ const cur = img.src;
635
+ img.src = "";
636
+ img.src = cur;
637
+ }, 800 * img.__mpwHostTries);
638
+ };
639
+ if (img.src !== hostUrl) img.src = hostUrl;
640
+ showImageEl();
641
+ }
642
+ } else if (image === "idb:blob") {
643
+ idbGet("bg").then((v) => {
644
+ if (gen !== bgGen) return; // 过期回调(已有更新的应用)
645
+ if (!(v instanceof Blob)) return;
646
+ const sig = v.size + ":" + v.type;
647
+ if (lastBgSig && lastBgSig.sig === sig && lastBgSig.url) {
648
+ // 内容未变:复用已有 ObjectURL,不重建 video(避免重缓冲)
649
+ showVideoEl(lastBgSig.url);
650
+ return;
651
+ }
652
+ if (lastBgSig && lastBgSig.url) { try { URL.revokeObjectURL(lastBgSig.url); } catch {} }
653
+ const url = URL.createObjectURL(v);
654
+ lastBgSig = { sig, url };
655
+ showVideoEl(url);
656
+ }).catch(() => {});
657
+ } else if (image === "idb:img") {
658
+ // ③(新) 大图片 Blob 路径:ObjectURL 显示,不走 dataURL(防内存膨胀)
659
+ idbGet("bg").then((v) => {
660
+ if (gen !== bgGen) return; // 过期回调
661
+ if (!(v instanceof Blob)) return;
662
+ const sig = v.size + ":" + v.type;
663
+ if (lastBgSig && lastBgSig.sig === sig && lastBgSig.url) {
664
+ if (img.src !== lastBgSig.url) img.src = lastBgSig.url;
665
+ showImageEl();
666
+ return;
667
+ }
668
+ if (lastBgSig && lastBgSig.url) { try { URL.revokeObjectURL(lastBgSig.url); } catch {} }
669
+ const url = URL.createObjectURL(v);
670
+ lastBgSig = { sig, url };
671
+ if (img.src !== url) img.src = url;
672
+ showImageEl();
673
+ }).catch(() => {});
674
+ } else if (typeof image === "string" && image.indexOf("idb:") === 0) {
675
+ idbGet("bg").then((v) => { if (gen !== bgGen) return; if (v && img.src !== v) { img.src = v; showImageEl(); } }).catch(() => {});
676
+ } else {
677
+ lastBgSig = null;
678
+ if (img.src !== image) {
679
+ console.log("[dsh-mpkg-wallpaper] 背景切换: img.src →", String(image).slice(0, 40) + "…");
680
+ img.src = image;
681
+ showImageEl();
682
+ } else {
683
+ console.log("[dsh-mpkg-wallpaper] 背景切换: img.src 未变(可能是重复应用)", String(image).slice(0, 40) + "…");
684
+ }
685
+ }
686
+ if (image) {
687
+ const zoom = section.zoom !== void 0 ? section.zoom : DEFAULT_ZOOM;
688
+ wrap.style.setProperty("--mpw-zoom", String(zoom / 100));
689
+ wrap.style.setProperty("--mpw-lensX", String(section.lensX !== void 0 ? section.lensX : 0) + "px");
690
+ wrap.style.setProperty("--mpw-lensY", String(section.lensY !== void 0 ? section.lensY : 0) + "px");
691
+ wrap.classList.toggle("mpw-sharp", section.sharp !== void 0 ? !!section.sharp : DEFAULT_SHARP);
692
+ // ⑯ 磨砂模糊条 → 壁纸层 blur(0 = 完全清晰)。
693
+ // ①(修正) 壁纸 blur 归谁管:
694
+ // - 统一虚化开 + chatFollow 开 → unifyAmount 接管(整屏统一模糊)
695
+ // - 统一虚化开 + chatFollow 关 → 磨砂条控制壁纸 blur;侧边栏/标题栏毛玻璃
696
+ // 由壁纸层 blur + 表面半透明提供(见 frostedLayers 注释)
697
+ // - 统一虚化关 → 磨砂条控制
698
+ const unifyTintApply = section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT;
699
+ const chatFollowApply = section.chatFollow !== void 0 ? !!section.chatFollow : DEFAULT_CHAT_FOLLOW;
700
+ const unifyAmt = section.unifyAmount !== void 0 ? section.unifyAmount : DEFAULT_UNIFY_AMOUNT;
701
+ const frostBlur = section.blur !== void 0 ? section.blur : DEFAULT_BLUR;
702
+ const bgBlur = (unifyTintApply && chatFollowApply) ? unifyAmt : frostBlur;
703
+ wrap.style.setProperty("--mpw-bg-blur", bgBlur > 0 ? `blur(${bgBlur}px)` : "none");
704
+ // ⑧(新) 画面亮度
705
+ const brightness = section.brightness !== void 0 ? section.brightness : DEFAULT_BRIGHTNESS;
706
+ wrap.style.setProperty("--mpw-brightness", String(brightness / 100));
707
+ }
708
+ // ①(新) 悬浮效果:控制 body 属性门控(需在 buildCss 前读 section.float)
709
+ const floatOn = section.float !== void 0 ? !!section.float : DEFAULT_FLOAT;
710
+ // ①(新) 悬浮效果:控制 body 属性门控
711
+ try {
712
+ if (floatOn) document.body.setAttribute("data-mpw-float", "");
713
+ else document.body.removeAttribute("data-mpw-float");
714
+ } catch {}
715
+ // ①(新) 侧边栏磨砂(Aqua 方案):body 属性门控;弹窗打开时 JS 打 data-mpw-sblur-off 摘除
716
+ try {
717
+ const sidebarBlurOn = section.sidebarBlur !== void 0 ? !!section.sidebarBlur : DEFAULT_SIDEBAR_BLUR;
718
+ if (sidebarBlurOn) document.body.setAttribute("data-mpw-sblur", "");
719
+ else document.body.removeAttribute("data-mpw-sblur");
720
+ } catch {}
721
+ try { setupSblurObserver(); } catch {}
722
+ // 2(修正) seam 标记 sidebar root(解除宽度用,参考 seam-stamper)
723
+ try {
724
+ const sr = document.querySelector('[class*="sidebarCol"] [class*="root"]');
725
+ if (sr && !sr.hasAttribute("data-mpw-sidebar-root")) sr.setAttribute("data-mpw-sidebar-root", "");
726
+ } catch {}
727
+ const styleEl = getStyleEl();
728
+ if (styleEl) {
729
+ try { styleEl.textContent = buildCss(section); }
730
+ catch (err) { console.error("[dsh-mpkg-wallpaper] buildCss failed:", err); }
731
+ }
732
+ // ⑭ token override:弹层/主画布/侧边栏半透明(对话框虚化核心)
733
+ try { applyTokenOverrides(pluginCtx, section); } catch {}
734
+ try { applyDialogInline(section); } catch {}
735
+ try { updateClock(); } catch {}
736
+ }
737
+
738
+ /**
739
+ * ⑭ 用 token override 实现对话框/弹层半透明(参考 ui-theme-background-custom 方案)。
740
+ * DSH 的弹层/菜单/对话框背景由 --dsw-alias-bg-overlay 驱动;主画布由
741
+ * --dsw-alias-bg-base 驱动、侧边栏由 --dsw-specific-sidebar-fill 驱动。
742
+ * overrideTokens 从 token 层面让这些表面半透明,弹层自动跟随(不猜类名),
743
+ * 背景壁纸在 z-index:-1 层透过半透明表面显示 → 对话框虚化跟随背景位置颜色。
744
+ */
745
+ function applyTokenOverrides(ctx, section) {
746
+ if (!ctx || !ctx.theme || typeof ctx.theme.overrideTokens !== "function") return;
747
+ const enabled = section.enabled !== void 0 ? !!section.enabled : DEFAULT_ENABLED;
748
+ const hasImage = !!section.image;
749
+ const dialogBlur = section.dialogBlur !== void 0 ? !!section.dialogBlur : DEFAULT_DIALOG_BLUR;
750
+ const opacity = section.opacity !== void 0 ? section.opacity : DEFAULT_OPACITY;
751
+ // ③(新) 统一虚化:主画布底透明度跟随 opacity(白雾厚度与侧边栏一致)
752
+ const unifyTint = section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT;
753
+ // 弹层/菜单/设置面板背景保持 DSH 原色,通过 token 变半透明
754
+ //(颜色跟随主题,不覆盖背景;配合 buildCss 的 backdrop-filter blur)。
755
+ // 透明度:虚化开 → 60-75% 半透明;关/无背景 → 还原。
756
+ let a = 1;
757
+ const bdOk = window.__mpwBackdropRendered !== false;
758
+ if (enabled && hasImage && dialogBlur && bdOk) {
759
+ const panel = Math.max(50, Math.min(100, opacity)) / 100;
760
+ a = Math.min(0.78, 0.60 + 0.15 * (1 - panel));
761
+ }
762
+ const toAlpha = (staticVar, alpha) => `color-mix(in srgb, var(${staticVar}) ${Math.round(alpha * 100)}%, transparent)`;
763
+ // 主画布背景(聊天区)白雾:
764
+ // - 统一虚化开 → 跟随「不透明度」条(panel/100,与侧边栏/标题栏一致);
765
+ // 聊天区壁纸模糊由壁纸层 blur 提供(chatFollow 决定归谁管,见 applyFromStorage)
766
+ // - 统一虚化关 → 0.62(原默认,跟随 dialogBlur)
767
+ const baseAlphaRaw = enabled && hasImage && dialogBlur
768
+ ? (unifyTint
769
+ ? Math.max(0, Math.min(1, (Math.max(50, Math.min(100, opacity)) / 100)))
770
+ : 0.62)
771
+ : 1;
772
+ // A(新) 悬浮效果开启时:聊天区自动更通透(整体透出壁纸)
773
+ const floatOn2 = section.float !== void 0 ? !!section.float : DEFAULT_FLOAT;
774
+ const baseAlpha = floatOn2 && enabled && hasImage ? Math.min(baseAlphaRaw, 0.35) : baseAlphaRaw;
775
+ const overrides = {
776
+ // 主画布背景半透明 → 壁纸透到所有区域(含输入框背后),
777
+ // 这样输入框/弹层的 backdrop-filter 才能模糊到壁纸(Aqua 生效的关键)。
778
+ // 透明度 0.62:壁纸可见且文字可读。
779
+ "--dsw-alias-bg-base": {
780
+ light: toAlpha("--dsw-static-neutral-bluish-00", baseAlpha),
781
+ dark: toAlpha("--dsw-static-neutral-bluish-950", baseAlpha)
782
+ },
783
+ "--dsw-alias-bg-overlay": {
784
+ light: toAlpha("--dsw-static-neutral-bluish-150", a),
785
+ dark: toAlpha("--dsw-static-neutral-bluish-700", a)
786
+ },
787
+ "--dsw-alias-bg-layer-2": {
788
+ light: toAlpha("--dsw-static-neutral-bluish-50", a),
789
+ dark: toAlpha("--dsw-static-neutral-bluish-850", a)
790
+ },
791
+ // 注意:input-major(composer 输入框)和 bubble(聊天气泡)不再由
792
+ // token 控制——buildCss 的 CSS 已直接给 [data-composer-card] 设背景,
793
+ // 双重半透明会导致颜色过深/发白。
794
+ // ⑥(修正) layer-1 不再全局透明:该 token 被第三方插件 UI(如 dshmarket
795
+ // 的分类卡片)使用,全透明会让他们显示异常(矩形区域透出壁纸)。
796
+ // Deep diving 思考框背景由 buildCss 的 thinkBg 分支用 CSS 精确处理。
797
+ "--dsw-alias-bg-layer-1": {
798
+ light: toAlpha("--dsw-static-neutral-bluish-50", a),
799
+ dark: toAlpha("--dsw-static-neutral-bluish-850", a)
800
+ }
801
+ };
802
+ if (!(enabled && hasImage)) {
803
+ try { if (tokenDisposer) { tokenDisposer(); tokenDisposer = null; } } catch {}
804
+ return;
805
+ }
806
+ try {
807
+ if (tokenDisposer) { tokenDisposer(); tokenDisposer = null; }
808
+ tokenDisposer = ctx.theme.overrideTokens("@local/dsh-mpkg-wallpaper-tokens", overrides);
809
+ } catch (err) { console.error("[dsh-mpkg-wallpaper] overrideTokens failed:", err); }
810
+ } /**
811
+ * ⑭ 内联应用对话框虚化 + fade 修复(绕过 CSS 选择器匹配问题)。
812
+ * CSS 选择器可能因 hash 类名/结构变化失配,这里直接用 JS 找到元素
813
+ * 并内联 backdrop-filter / 渐变,确保生效。
814
+ */
815
+ function applyDialogInline(section) {
816
+ const enabled = section.enabled !== void 0 ? !!section.enabled : DEFAULT_ENABLED;
817
+ if (!enabled) return;
818
+ const dialogBlur = section.dialogBlur !== void 0 ? !!section.dialogBlur : DEFAULT_DIALOG_BLUR;
819
+ const dialogAmount = section.dialogAmount !== void 0 ? section.dialogAmount : DEFAULT_DIALOG_AMOUNT;
820
+ const thinkBg = section.thinkBg !== void 0 ? !!section.thinkBg : DEFAULT_THINK_BG;
821
+ const floatOn = section.float !== void 0 ? !!section.float : DEFAULT_FLOAT;
822
+ const panel = Math.max(50, Math.min(100, section.opacity !== void 0 ? section.opacity : DEFAULT_OPACITY));
823
+ // 对话框虚化:完全由 buildCss 的 CSS 控制。
824
+ // 这里只处理 Deep diving 背景方框(见下方)
825
+ // Deep diving 背景方框:开关控制(默认关 = 透明 + 文字可见色)
826
+ const turnStatuses = document.querySelectorAll('[role="status"]');
827
+ const thinkRows = document.querySelectorAll('[data-variant="think"]');
828
+ if (thinkBg) {
829
+ thinkRows.forEach((el) => { el.style.background = ""; });
830
+ turnStatuses.forEach((el) => {
831
+ el.style.background = "";
832
+ el.style.color = "";
833
+ el.style.webkitTextFillColor = "";
834
+ el.style.webkitBackgroundClip = "";
835
+ el.style.backgroundClip = "";
836
+ el.style.backdropFilter = "";
837
+ el.style.webkitBackdropFilter = "";
838
+ });
839
+ } else {
840
+ thinkRows.forEach((el) => { el.style.background = "transparent"; });
841
+ turnStatuses.forEach((el) => {
842
+ el.style.background = "transparent";
843
+ el.style.color = "var(--dsw-alias-label-secondary, #9aa4b2)";
844
+ el.style.webkitTextFillColor = "var(--dsw-alias-label-secondary, #9aa4b2)";
845
+ el.style.webkitBackgroundClip = "border-box";
846
+ el.style.backgroundClip = "border-box";
847
+ el.style.backdropFilter = "none";
848
+ el.style.webkitBackdropFilter = "none";
849
+ });
850
+ }
851
+ // fade 修复:用户要求直接取消列表底部的白色渐变(不要半透明,直接隐藏)
852
+ document.querySelectorAll('[class*="fade"]').forEach((el) => {
853
+ if (!el.className || el.className.indexOf("fade") < 0) return;
854
+ // 优先匹配 workspace/session 列表区域;否则匹配绝对定位在底部的渐变 fade
855
+ const p = el.closest('[data-slot*="workspaces"], [data-slot*="session"], [class*="regionArea"], [class*="sidebarCol"]');
856
+ let isListFade = !!p;
857
+ if (!isListFade) {
858
+ try {
859
+ const cs = getComputedStyle(el);
860
+ isListFade = cs.position === "absolute" && (cs.bottom === "0px" || parseFloat(cs.bottom) <= 1);
861
+ } catch {}
862
+ }
863
+ if (isListFade) {
864
+ el.style.display = "none";
865
+ }
866
+ });
867
+ }
868
+ /** MutationObserver 持续应用内联样式(React 重挂载时补打)。 */
869
+ function startInlineWatcher(sectionRef) {
870
+ let timer = null;
871
+ const obs = new MutationObserver(() => {
872
+ if (timer) return;
873
+ timer = setTimeout(() => {
874
+ timer = null;
875
+ try {
876
+ const sec = readSection();
877
+ applyDialogInline(sec);
878
+ // ①(修正) 悬浮开启时持续标记 sidebar root:刷新/路由切换后 React
879
+ // 重渲染会重建 sidebar root 节点,属性丢失 → width:100% 失效 →
880
+ // 内容被 overflow:hidden 切掉。observer 每次变更都补打。
881
+ try {
882
+ if (sec && sec.float !== void 0 ? !!sec.float : DEFAULT_FLOAT) {
883
+ const sr = document.querySelector('[class*="sidebarCol"] [class*="root"]');
884
+ if (sr && !sr.hasAttribute("data-mpw-sidebar-root")) sr.setAttribute("data-mpw-sidebar-root", "");
885
+ }
886
+ } catch {}
887
+ } catch {}
888
+ }, 120);
889
+ });
890
+ obs.observe(document.documentElement, { childList: true, subtree: true });
891
+ // 1(修正) 持续标记 sidebar root(刷新/路由切换后 DSH 重渲染也能找到)
892
+ try {
893
+ const sr = document.querySelector('[class*="sidebarCol"] [class*="root"]');
894
+ if (sr && !sr.hasAttribute("data-mpw-sidebar-root")) sr.setAttribute("data-mpw-sidebar-root", "");
895
+ } catch {}
896
+ return () => obs.disconnect();
897
+ }
898
+
899
+ /** 仅更新 CSS 的预览(滑杆拖动时调用,避免 React 重渲染卡顿,③)。 */
900
+ function previewCss(patch) {
901
+ const merged = Object.assign({}, readSection(), patch);
902
+ const el = getStyleEl();
903
+ if (el) { try { el.textContent = buildCss(merged); } catch (err) { console.error("[dsh-mpkg-wallpaper] previewCss failed:", err); } }
904
+ // 内联 backdrop-filter 同步(否则内联旧值覆盖 CSS 新值,虚化程度拉条失效)
905
+ try { applyDialogInline(merged); } catch {}
906
+ }
907
+ /** buildCss 里 _noBlur=true 时临时关闭 backdrop-filter(拖动时降低重绘开销,①)。 */
908
+ function effBlur(section, def) {
909
+ return section._noBlur ? 0 : def;
910
+ }
911
+
912
+ // ═══════════════════════════════════════════════════════════════════
913
+ // CSS 生成
914
+ // 注意:backdrop-filter 会为 fixed 定位子元素创建包含块,绝不能加在
915
+ // 侧边栏/聊天区的根元素上(否则设置弹窗会被"关"在侧边栏里,①),
916
+ // 只加在内部滚动容器上(regionArea / scrollBody / details body)。
917
+ // ═══════════════════════════════════════════════════════════════════
918
+ function buildCss(section) {
919
+ // ③(修正) 总开关关闭时统一视为无背景:所有路径(applyFromStorage/previewCss)
920
+ // 都走同一逻辑,避免拖动滑块时 previewCss 绕过 enabled 检查把壁纸又显示出来
921
+ if (section && section.enabled === false) {
922
+ section = Object.assign({}, section, { image: "" });
923
+ }
924
+ // ①(新) 悬浮效果开关(buildCss 内声明,之前误加在 applyFromStorage 里导致 ReferenceError)
925
+ const floatOn = section.float !== void 0 ? !!section.float : DEFAULT_FLOAT;
926
+ const hasImage = !!(section && section.image);
927
+ const opacity = section.opacity !== void 0 ? section.opacity : DEFAULT_OPACITY;
928
+ // 磨砂条 blur:不走 effBlur(_noBlur 拖动时临时关掉会闪),壁纸层 blur 实时
929
+ const blur = section.blur !== void 0 ? section.blur : DEFAULT_BLUR;
930
+ const sidebar = section.sidebar !== void 0 ? !!section.sidebar : DEFAULT_SIDEBAR;
931
+ const headerBlur = section.headerBlur !== void 0 ? !!section.headerBlur : DEFAULT_HEADER;
932
+ const headerBg = section.headerBg !== void 0 ? !!section.headerBg : DEFAULT_HEADER_BG;
933
+ const dialogBlur = section.dialogBlur !== void 0 ? !!section.dialogBlur : DEFAULT_DIALOG_BLUR;
934
+ const dialogAmount = section.dialogAmount !== void 0 ? section.dialogAmount : DEFAULT_DIALOG_AMOUNT;
935
+ // ①(新) 虚化对话框拆三类:设置面板 / 下载确认弹窗 各自独立开关+程度
936
+ const settingsBlur = section.settingsBlur !== void 0 ? !!section.settingsBlur : DEFAULT_SETTINGS_BLUR;
937
+ const settingsAmount = section.settingsAmount !== void 0 ? section.settingsAmount : DEFAULT_SETTINGS_AMOUNT;
938
+ const confirmBlur = section.confirmBlur !== void 0 ? !!section.confirmBlur : DEFAULT_CONFIRM_BLUR;
939
+ const confirmAmount = section.confirmAmount !== void 0 ? section.confirmAmount : DEFAULT_CONFIRM_AMOUNT;
940
+ // ①(新) 侧边栏磨砂(Aqua 方案:sidebarCol 自身 backdrop-filter)
941
+ const sidebarBlur = section.sidebarBlur !== void 0 ? !!section.sidebarBlur : DEFAULT_SIDEBAR_BLUR;
942
+ const sidebarBlurAmount = section.sidebarBlurAmount !== void 0 ? section.sidebarBlurAmount : DEFAULT_SIDEBAR_BLUR_AMOUNT;
943
+ // ②(修正) chatFollow(统一虚化开时聊天区是否跟随整屏虚化):
944
+ // 关 → 聊天区壁纸由磨砂条控制,侧边栏/标题栏背后仍需跟随整屏虚化度
945
+ const chatFollowInCss = section.chatFollow !== void 0 ? !!section.chatFollow : DEFAULT_CHAT_FOLLOW;
946
+ // ②(新) 弹层虚化(菜单/提示/遮罩)独立于对话框虚化
947
+ const popoverBlur = section.popoverBlur !== void 0 ? !!section.popoverBlur : DEFAULT_POPOVER_BLUR;
948
+ const popoverAmount = section.popoverAmount !== void 0 ? section.popoverAmount : DEFAULT_POPOVER_AMOUNT;
949
+ // ②(重做) 遮罩虚化(设置/弹层打开时的全屏背景)独立于弹层虚化
950
+ const maskBlur = section.maskBlur !== void 0 ? !!section.maskBlur : DEFAULT_MASK_BLUR;
951
+ const maskAmount = section.maskAmount !== void 0 ? section.maskAmount : DEFAULT_MASK_AMOUNT;
952
+ const thinkBg = section.thinkBg !== void 0 ? !!section.thinkBg : DEFAULT_THINK_BG;
953
+ const panel = Math.max(50, Math.min(100, opacity));
954
+ const details = Math.min(100, panel + 3);
955
+ const hideBg = hasImage && panel >= 100;
956
+ // ③(新) 统一虚化:所有表面白雾厚度一致 = panel%(跟随外观-不透明度条)。
957
+ // unifyTint=true 时各区域不再用各自写死的百分比,全部用 U(原值)=panel,
958
+ // 视觉上侧边栏/聊天区/标题栏/输入框不再"一块白一块透"地分裂背景。
959
+ const unifyTint = section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT;
960
+ // ①(重做) 统一虚化:unifyTint 开时 unifyAmount 控制**壁纸层真实 blur**(虚化程度):
961
+ // 0px = 壁纸清晰,40px = 强模糊。表面白雾统一由「不透明度」条(panel)控制
962
+ //(不再把 unifyAmount 映射成透明度——那只是"改白雾厚度",不是"改虚化程度")。
963
+ const unifyAmount = section.unifyAmount !== void 0 ? section.unifyAmount : DEFAULT_UNIFY_AMOUNT;
964
+ // 侧边栏/标题栏白雾厚度:统一虚化开 → sidebarAlpha 独立条;关 → 各区域原值
965
+ const uAlpha = Math.max(0, Math.min(100, (section.sidebarAlpha !== void 0 ? section.sidebarAlpha : DEFAULT_SIDEBAR_ALPHA)));
966
+ const U = (v) => unifyTint ? uAlpha : v;
967
+ // 区域 backdrop-filter:unify 开 → none(壁纸层 blur 接管);关 → 磨砂条
968
+ const blurFilter = unifyTint ? "none" : (blur > 0 ? `blur(${blur}px)` : "none");
969
+ // ①(修正) 设置界面污染 bug 根治:**彻底移除**所有对侧边栏/标题栏的
970
+ // backdrop-filter 与 z-index 干预(历史多次实测均引发弹窗被盖/图标被盖/
971
+ // 分离模糊块问题)。毛玻璃统一走**壁纸层 filter blur**(--mpw-bg-blur)+
972
+ // 表面 sidebarAlpha 半透明透出 —— 零 z-index、零 backdrop-filter、
973
+ // 不困弹窗、不盖内容、设置界面 100% 干净。
974
+ const frostedLayers = "";
975
+ let css = `/* ── 背景插件 @local/dsh-mpkg-wallpaper v3 生成的样式 ── */\n`;
976
+ if (!hasImage) {
977
+ // ② 未设置背景时:不显示壁纸层,面板恢复不透明(默认外观)
978
+ css += `
979
+ html, body { background: transparent !important; }
980
+ .mpw-bgWrap { display: none !important; }
981
+ .pI_x6G_frame { background-color: var(--dsw-alias-bg-base) !important; }
982
+ .pI_x6G_sidebarCol,
983
+ .hHd-Xa_root { background-color: var(--dsw-specific-sidebar-fill) !important; }
984
+ .wSkVaW_root { background-color: var(--dsw-alias-bg-base) !important; }
985
+ .ydkMvW_root { background-color: var(--dsw-alias-bg-base) !important; }
986
+ `;
987
+ return css + buildUiCss(section, false);
988
+ }
989
+ css += `
990
+ html, body {
991
+ background: transparent !important;
992
+ }
993
+ .mpw-bgWrap {
994
+ position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none;
995
+ /* 缩放 <100% 时边缘露出底色(③),避免透明露白 */
996
+ background-color: var(--dsw-alias-bg-base, #0e1420);
997
+ }
998
+ /* ①(加固) img/video 显示由 CSS 类控制(避免切换残留) */
999
+ .mpw-bgWrap.mpw-img video { display: none !important; }
1000
+ .mpw-bgWrap.mpw-video img { display: none !important; }
1001
+ .mpw-bgWrap img,
1002
+ .mpw-bgWrap video {
1003
+ width: 100%; height: 100%; object-fit: cover;
1004
+ /* ④(修正) 镜头:translate 在前、scale 在后 → 平移量不被缩放影响
1005
+ (原 scale 在前导致 zoom>100 时平移被放大、zoom<100 时平移不够,
1006
+ 用户反馈"只能渲染整个屏幕的位置")。transform 列表从左到右复合,
1007
+ translate() scale() = 先缩放后平移(平移是原始像素)。 */
1008
+ transform: translate(var(--mpw-lensX, 0), var(--mpw-lensY, 0)) scale(var(--mpw-zoom, 1));
1009
+ transform-origin: center center;
1010
+ /* ⑯ 磨砂模糊条:壁纸层自身 blur,拖到 0 = 完全清晰 */
1011
+ filter: var(--mpw-bg-blur, none) brightness(var(--mpw-brightness, 1)) ${section.sharp !== void 0 && !section.sharp ? "" : "contrast(1.06) saturate(1.12)"};
1012
+ }
1013
+ .mpw-bgWrap.mpw-sharp {
1014
+ /* ②(修正) sharp 类不能覆盖掉 brightness(原来只写 contrast/saturate → 亮度失效) */
1015
+ filter: brightness(var(--mpw-brightness, 1)) contrast(1.06) saturate(1.12);
1016
+ }
1017
+ .pI_x6G_frame {
1018
+ background-color: transparent !important;
1019
+ }
1020
+ /* 侧边栏根(半透明,不设 backdrop-filter)。
1021
+ ③(新) 取色统一:unify 开时用主题静态色 panel%(与聊天区 bg-base 同色系同厚度,
1022
+ 不再用 sidebar-fill 导致侧边栏与聊天区颜色不同、背景被"分裂") */
1023
+ .pI_x6G_sidebarCol,
1024
+ .hHd-Xa_root {
1025
+ background-color: ${unifyTint
1026
+ ? `color-mix(in srgb, var(--dsw-static-neutral-bluish-950) ${U(panel)}%, transparent)`
1027
+ : `color-mix(in srgb, var(--dsw-specific-sidebar-fill) ${panel}%, transparent)`} !important;
1028
+ }
1029
+ ${unifyTint ? `body:not([data-ds-dark-theme]) .pI_x6G_sidebarCol,
1030
+ body:not([data-ds-dark-theme]) .hHd-Xa_root {
1031
+ background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-00) ${U(panel)}%, transparent) !important;
1032
+ }` : ""}
1033
+ .hHd-Xa_newSession {
1034
+ /* ④(新) 统一虚化开:随整屏虚化(sessionFollow)或随面板不透明度;关:随面板不透明度 */
1035
+ background-color: color-mix(in srgb, var(--dsw-alias-button-elevated-fill) ${unifyTint ? (section.sessionFollow !== void 0 ? !!section.sessionFollow : DEFAULT_SESSION_FOLLOW) ? panel : panel : panel}%, transparent) !important;
1036
+ border-color: color-mix(in srgb, var(--dsw-alias-border-l2) 65%, transparent) !important;
1037
+ }
1038
+ /* ⑬(新) 取消工作区列表底部的自带渐变 fade(用户要求直接去掉白色虚化带) */
1039
+ .hHd-Xa_regionArea [class*="fade"],
1040
+ [data-slot*="workspaces"] [class*="fade"],
1041
+ [data-slot*="session"] [class*="fade"] {
1042
+ display: none !important;
1043
+ }
1044
+ /* 聊天区 / 详情面板根:背景透明,由 token override 的 --dsw-alias-bg-base
1045
+ 控制半透明(避免双重透明叠加) */
1046
+ .wSkVaW_root {
1047
+ background-color: transparent !important;
1048
+ }
1049
+ /* ⑤(修正) 输入框座(composerSeat):去掉白色渐变特效(用户反馈拉高不透明度时
1050
+ 输入框附近出现白渐变、有边界、收起也延伸)。保持透明透出壁纸。 */
1051
+ .wSkVaW_root[data-phase="active"] .wSkVaW_composerSeat {
1052
+ background: transparent !important;
1053
+ }
1054
+ .ydkMvW_root {
1055
+ /* ③(新) unify 开 → 与聊天区同色系(static 色);关 → 原 bg-base 混色 */
1056
+ background-color: ${unifyTint
1057
+ ? `color-mix(in srgb, var(--dsw-static-neutral-bluish-950) ${U(details)}%, transparent)`
1058
+ : `color-mix(in srgb, var(--dsw-alias-bg-base) ${U(details)}%, transparent)`} !important;
1059
+ }
1060
+ ${unifyTint ? `body:not([data-ds-dark-theme]) .ydkMvW_root {
1061
+ background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-00) ${U(details)}%, transparent) !important;
1062
+ }` : ""}
1063
+ /* 磨砂 blur:壁纸层自身 filter blur(磨砂条控制,见 .mpw-bgWrap img/video)。
1064
+ 聊天区 scrollBody:不加 backdrop-filter(避免与输入框虚化嵌套冲突,
1065
+ Firefox 嵌套会隔离输入框 blur),半透明背景透出(模糊的)壁纸。
1066
+ 输入框虚化由输入框自己的 backdrop-filter 处理(虚化开关控制)。 */
1067
+ .hHd-Xa_regionArea {
1068
+ /* ①(修正) 不给侧边栏任何容器加 backdrop-filter(会困设置弹窗 / 形成分离模糊块);
1069
+ 毛玻璃 = 壁纸层 blur + 表面半透明(见 frostedLayers 注释)。unify 开 → none。 */
1070
+ backdrop-filter: ${blurFilter} !important;
1071
+ }
1072
+ ${frostedLayers}
1073
+ /* ①(修正) 左上角(logoRow/品牌/新会话) + 左下角(设置/底部):
1074
+ 不加 backdrop-filter(settingsArea/footArea 是设置弹层 fixed 遮罩的祖先,
1075
+ backdrop-filter 会创建包含块把弹层"关"进侧边栏!),只清背景透明,
1076
+ 磨砂跟随由 root 半透明 + 壁纸层 filter blur 提供(透出模糊壁纸,效果等价)。 */
1077
+ [data-slot="sidebar"] [class*="logoRow"],
1078
+ [data-slot="sidebar"] [class*="footArea"],
1079
+ [data-slot="sidebar"] [class*="footerActions"],
1080
+ [data-slot="sidebar"] [class*="settingsArea"] {
1081
+ background-color: transparent !important;
1082
+ }
1083
+ /* ⑩(新) 第三方插件注入侧边栏的内容(DSH-better-sidebar/account-balance 等):
1084
+ 兜底 45% 雾底保证在壁纸上可读;不加 !important → 插件自己的背景样式优先 */
1085
+ [data-slot="sidebar"] [data-plugin] {
1086
+ background-color: color-mix(in srgb, var(--dsw-alias-bg-base) 45%, transparent);
1087
+ border-radius: 8px;
1088
+ }
1089
+ .wSkVaW_scrollBody {
1090
+ backdrop-filter: none !important;
1091
+ /* 统一虚化开 → 透明:底色完全由 bg-base(panel%)提供,与侧边栏白雾厚度一致,
1092
+ 避免 scrollBody 与 bg-base 双层叠加导致聊天区比侧边栏更白(背景分裂)。
1093
+ 悬浮开 → 也透明:悬浮卡片风格下聊天区整体透出壁纸(不再切出直角白雾矩形)。
1094
+ 关 → 保持 68% 半透明底。 */
1095
+ background-color: ${(unifyTint || floatOn) ? "transparent" : `color-mix(in srgb, var(--dsw-alias-bg-base) ${U(68)}%, transparent)`} !important;
1096
+ }
1097
+ .ydkMvW_body {
1098
+ backdrop-filter: ${blurFilter} !important;
1099
+ }
1100
+ `;
1101
+ if (headerBlur && headerBg) {
1102
+ // ①(修正) 标题栏磨砂。**绝不能给 header 本体加 backdrop-filter**——
1103
+ // header 有 fixed 子元素(子代理展开面板/后台任务条),backdrop-filter 会成为
1104
+ // 它们的 containing block,导致面板定位错乱(用户实测 bug)。
1105
+ // ①(修正) 标题栏雾底:统一虚化开 → 跟随 sidebarAlpha(标题栏磨砂程度条被接管);
1106
+ // 关 → 由「标题栏磨砂程度」条控制。headerBg/headerBlur 开关仍独立(关=纯白)。
1107
+ const hblurAmt = section.headerBlurAmount !== void 0 ? section.headerBlurAmount : DEFAULT_HEADER_BLUR_AMOUNT;
1108
+ const hdrAlpha = unifyTint ? uAlpha : Math.max(0, Math.min(100, hblurAmt));
1109
+ const hdrBg = `color-mix(in srgb, var(--dsw-static-neutral-bluish-950) ${hdrAlpha}%, transparent)`;
1110
+ css += `
1111
+ .wSkVaW_header {
1112
+ background-color: ${hdrBg} !important;
1113
+ backdrop-filter: none !important;
1114
+ border-bottom: 1px solid transparent !important;
1115
+ }
1116
+ body:not([data-ds-dark-theme]) .wSkVaW_header {
1117
+ background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-00) ${hdrAlpha}%, transparent) !important;
1118
+ }
1119
+ `;
1120
+ }
1121
+ if (headerBg && !headerBlur) {
1122
+ // ①(新) 标题栏透出壁纸但不磨砂:实心主题色(无 backdrop-filter)
1123
+ css += `
1124
+ .wSkVaW_header {
1125
+ background-color: var(--dsw-static-neutral-bluish-00) !important;
1126
+ backdrop-filter: none !important;
1127
+ }
1128
+ body[data-ds-dark-theme] .wSkVaW_header {
1129
+ background-color: var(--dsw-static-neutral-bluish-950) !important;
1130
+ }
1131
+ `;
1132
+ }
1133
+ if (!headerBg) {
1134
+ // ①④(修正) 标题栏不透出壁纸:白色不透明(用户要求"关=一片白色的不透明状态")。
1135
+ // 不能用 var(--dsw-alias-bg-base)——它已被 token override 成半透明会透出壁纸。
1136
+ // 用主题静态色:亮色=白、暗色=深色(跟随 data-ds-dark-theme)。
1137
+ css += `
1138
+ .wSkVaW_header {
1139
+ background-color: var(--dsw-static-neutral-bluish-00) !important;
1140
+ backdrop-filter: none !important;
1141
+ }
1142
+ body[data-ds-dark-theme] .wSkVaW_header {
1143
+ background-color: var(--dsw-static-neutral-bluish-950) !important;
1144
+ }
1145
+ `;
1146
+ }
1147
+ if (!sidebar) {
1148
+ // ⑥ 侧边栏不透出壁纸:恢复为不透明,保持与聊天区区分
1149
+ css += `
1150
+ .pI_x6G_sidebarCol,
1151
+ .hHd-Xa_root {
1152
+ background-color: var(--dsw-specific-sidebar-fill) !important;
1153
+ }
1154
+ .hHd-Xa_newSession {
1155
+ background-color: var(--dsw-alias-button-elevated-fill) !important;
1156
+ }
1157
+ .hHd-Xa_regionArea { backdrop-filter: none !important; }
1158
+ .hHd-Xa_regionArea [class*="fade"],
1159
+ [data-slot*="workspaces"] [class*="fade"],
1160
+ [data-slot*="session"] [class*="fade"] {
1161
+ display: none !important;
1162
+ }
1163
+ `;
1164
+ }
1165
+ if (hideBg) {
1166
+ // ④ 面板 100% 不透明时:隐藏壁纸层,帧底色用主题色,避免中缝透出背景
1167
+ css += `
1168
+ .mpw-bgWrap { display: none !important; }
1169
+ .pI_x6G_frame { background-color: var(--dsw-alias-bg-base) !important; }
1170
+ `;
1171
+ } // ⑫(重做) ② 虚化分块:
1172
+ // - 对话框虚化(dialogBlur)→ 通用居中窗口 [role=dialog]/[role=alertdialog] + 聊天输入框
1173
+ // - 设置面板虚化(settingsBlur)→ DSH 设置面板([class*="settingsArea"] 内的居中窗口)
1174
+ // - 确认弹窗虚化(confirmBlur)→ 本插件的下载/确认弹窗(.mpw_dialog)
1175
+ // - 侧边栏磨砂(sidebarBlur)→ Aqua 方案:sidebarCol 自身 backdrop-filter,弹窗打开时 :has() 摘除
1176
+ // scrollBody 已无 backdrop-filter(避免嵌套冲突),所以输入框的独立 blur 能生效。
1177
+ const bdSupported = window.__mpwBackdropRendered !== false;
1178
+ // 磨砂玻璃 = 半透明背景 + backdrop blur,两者缺一不可(原理见下方 CSS 注释)。
1179
+ const dlgFrosted = dialogBlur && dialogAmount > 0 && bdSupported;
1180
+ const settingsFrosted = settingsBlur && settingsAmount > 0 && bdSupported;
1181
+ const confirmFrosted = confirmBlur && confirmAmount > 0 && bdSupported;
1182
+ const sidebarFrosted = sidebarBlur && sidebarBlurAmount > 0 && bdSupported;
1183
+ // ── A. 不透明兜底(对应开关关时防 token 半透明透出聊天内容)──
1184
+ // 设置面板/通用对话框是 role=dialog:token override 把 bg-base 变半透明后
1185
+ // 面板会透出背后聊天标题/按钮(用户实测"聊天内容显示在设置界面")。
1186
+ if (!dlgFrosted) css += `
1187
+ /* 对话框虚化关 → 通用居中窗口不透明兜底(防透出) */
1188
+ [role="dialog"],
1189
+ [role="alertdialog"] {
1190
+ background-color: var(--dsw-static-neutral-bluish-00) !important;
1191
+ }
1192
+ body[data-ds-dark-theme] [role="dialog"],
1193
+ body[data-ds-dark-theme] [role="alertdialog"] {
1194
+ background-color: var(--dsw-static-neutral-bluish-950) !important;
1195
+ }
1196
+ `;
1197
+ if (!settingsFrosted) css += `
1198
+ /* 设置面板虚化关 → 设置面板不透明兜底(防透出) */
1199
+ [class*="settingsArea"] [role="dialog"] {
1200
+ background-color: var(--dsw-static-neutral-bluish-00) !important;
1201
+ }
1202
+ body[data-ds-dark-theme] [class*="settingsArea"] [role="dialog"] {
1203
+ background-color: var(--dsw-static-neutral-bluish-950) !important;
1204
+ }
1205
+ `;
1206
+ if (!confirmFrosted) css += `
1207
+ /* 下载/确认弹窗虚化关 → 弹窗不透明兜底(防透出壁纸) */
1208
+ .mpw_dialog {
1209
+ background: var(--dsw-static-neutral-bluish-950) !important;
1210
+ }
1211
+ body:not([data-ds-dark-theme]) .mpw_dialog {
1212
+ background: var(--dsw-static-neutral-bluish-00) !important;
1213
+ }
1214
+ `;
1215
+ // ── B. 通用对话框磨砂(dialogBlur)──
1216
+ if (dlgFrosted) {
1217
+ const dlgFilter = `blur(${dialogAmount}px)`;
1218
+ css += `
1219
+ /* ── 对话框虚化(通用居中窗口 + 聊天输入框;设置面板由 settingsBlur 单独覆盖) ── */
1220
+ [role="dialog"],
1221
+ [role="alertdialog"],
1222
+ [data-composer-card] {
1223
+ backdrop-filter: ${dlgFilter} !important;
1224
+ -webkit-backdrop-filter: ${dlgFilter} !important;
1225
+ }
1226
+ /* 磨砂玻璃原理:半透明背景 + backdrop blur 缺一不可。
1227
+ 全不透明背景会把 blur 完全遮住 → 纯白无模糊(用户实测);
1228
+ 半透明但无 blur 会透出清晰聊天文字 → 设置界面污染(旧 bug)。
1229
+ 80% 半透明 + blur:背后文字变糊影不可读,模糊的壁纸色调明显透出
1230
+ (90% 时磨砂感几乎不可见,用户实测仍像纯白)。 */
1231
+ [role="dialog"],
1232
+ [role="alertdialog"] {
1233
+ background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-00) 80%, transparent) !important;
1234
+ }
1235
+ body[data-ds-dark-theme] [role="dialog"],
1236
+ body[data-ds-dark-theme] [role="alertdialog"] {
1237
+ background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-950) 80%, transparent) !important;
1238
+ }
1239
+ [role="dialog"] [class*="card"] {
1240
+ backdrop-filter: none !important;
1241
+ }
1242
+ /* 提问输入框(composer):跟随「虚化对话框」开关(聊天框不随统一虚化)。
1243
+ 虚化开 = 半透明背景,透出模糊壁纸/经过的字。 */
1244
+ [data-composer-card] {
1245
+ background-color: color-mix(in srgb, #1b2233 42%, transparent) !important;
1246
+ }
1247
+ body:not([data-ds-dark-theme]) [data-composer-card] {
1248
+ background-color: color-mix(in srgb, #dde3ee 55%, transparent) !important;
1249
+ }
1250
+ /* 输入框背后区域(composerStack/seat)背景透明,让壁纸透到输入框背后 */
1251
+ [data-slot*="composer"] [class*="stack"],
1252
+ [data-slot*="composer"] [class*="Stack"],
1253
+ [data-slot*="composer"] [class*="seat"],
1254
+ [data-slot*="composer"] [class*="Seat"],
1255
+ [class*="composerStack"],
1256
+ [class*="composerSeat"] {
1257
+ background: transparent !important;
1258
+ }
1259
+ /* ⑤(修正) 输入框外围 seat:去掉白色渐变特效。保持透明透出壁纸。 */
1260
+ [data-slot*="composer"] [class*="seat"],
1261
+ [data-slot*="composer"] [class*="Seat"] {
1262
+ backdrop-filter: none !important;
1263
+ background: transparent !important;
1264
+ }
1265
+ `;
1266
+ }
1267
+ // ── C. 设置面板磨砂(settingsBlur,覆盖通用 blur 值)──
1268
+ if (settingsFrosted) {
1269
+ const setFilter = `blur(${settingsAmount}px)`;
1270
+ css += `
1271
+ /* ── 设置面板虚化(覆盖通用对话框值,独立可调) ── */
1272
+ [class*="settingsArea"] [role="dialog"] {
1273
+ backdrop-filter: ${setFilter} !important;
1274
+ -webkit-backdrop-filter: ${setFilter} !important;
1275
+ background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-00) 80%, transparent) !important;
1276
+ }
1277
+ body[data-ds-dark-theme] [class*="settingsArea"] [role="dialog"] {
1278
+ background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-950) 80%, transparent) !important;
1279
+ }
1280
+ `;
1281
+ }
1282
+ // ── D. 下载/确认弹窗磨砂(confirmBlur)──
1283
+ if (confirmFrosted) {
1284
+ const cnfFilter = `blur(${confirmAmount}px)`;
1285
+ css += `
1286
+ /* ── 下载/确认弹窗虚化(本插件 .mpw_dialog) ── */
1287
+ .mpw_dialog {
1288
+ backdrop-filter: ${cnfFilter} !important;
1289
+ -webkit-backdrop-filter: ${cnfFilter} !important;
1290
+ background: color-mix(in srgb, var(--dsw-static-neutral-bluish-950) 80%, transparent) !important;
1291
+ }
1292
+ body:not([data-ds-dark-theme]) .mpw_dialog {
1293
+ background: color-mix(in srgb, var(--dsw-static-neutral-bluish-00) 80%, transparent) !important;
1294
+ }
1295
+ `;
1296
+ }
1297
+ // ── E. 侧边栏磨砂(sidebarBlur,Aqua 方案)──
1298
+ // unify 开时由整屏虚化(壁纸层 blur)接管,不输出自身 backdrop-filter(避免双重模糊)
1299
+ if (sidebarFrosted && !unifyTint) {
1300
+ const sblFilter = `blur(${sidebarBlurAmount}px)`;
1301
+ css += `
1302
+ /* ── 侧边栏磨砂(Aqua 方案:sidebarCol 自身 backdrop-filter) ──
1303
+ backdrop-filter 会把 sidebarCol 变成 fixed 弹窗的 containing block(困住弹窗)。
1304
+ 弹窗打开时 JS 在 body 打 data-mpw-sblur-off 摘除模糊(见 setupSblurObserver)。
1305
+ 不用 :has() —— Firefox 对 :has()+backdrop-filter 的层叠计算有 bug:
1306
+ 两条规则共存时 :has() 规则不覆盖基础规则(实测);body 属性方案全浏览器可靠。
1307
+ html body 前缀提高特异性,确保摘除规则无条件赢过基础规则。 */
1308
+ body[data-mpw-sblur] .pI_x6G_sidebarCol,
1309
+ body[data-mpw-sblur] [class*="sidebarCol"] {
1310
+ backdrop-filter: ${sblFilter} !important;
1311
+ -webkit-backdrop-filter: ${sblFilter} !important;
1312
+ }
1313
+ html body[data-mpw-sblur][data-mpw-sblur-off] .pI_x6G_sidebarCol,
1314
+ html body[data-mpw-sblur][data-mpw-sblur-off] [class*="sidebarCol"] {
1315
+ backdrop-filter: none !important;
1316
+ -webkit-backdrop-filter: none !important;
1317
+ }
1318
+ `;
1319
+ }
1320
+ // ── F. 统一虚化 + 聊天区不跟随:侧边栏/标题栏背后跟随整屏虚化度 ──
1321
+ // chatFollow 关 → 壁纸层 blur = 磨砂条(frostBlur)供聊天区;
1322
+ // 侧边栏/标题栏再叠 backdrop blur(unifyAmt),调整「整屏虚化程度」即可改变
1323
+ // 侧边栏/标题栏背后的虚化(用户实测:当前关了 chatFollow 后 unifyAmount 失效)。
1324
+ // 弹窗打开时 body[data-mpw-sblur-off] 摘除(复用侧边栏磨砂的摘除机制,
1325
+ // backdrop-filter 会困住 fixed 弹窗)。unify 关时不输出。
1326
+ if (unifyTint && !chatFollowInCss && unifyAmount > 0 && bdSupported) {
1327
+ css += `
1328
+ /* ── 统一虚化 + 聊天区不跟随:侧边栏/标题栏背后叠加整屏虚化度 ── */
1329
+ html body:not([data-mpw-sblur-off]) .pI_x6G_sidebarCol,
1330
+ html body:not([data-mpw-sblur-off]) [class*="sidebarCol"],
1331
+ html body:not([data-mpw-sblur-off]) [class*="wSkVaW_header"] {
1332
+ backdrop-filter: blur(${unifyAmount}px) !important;
1333
+ -webkit-backdrop-filter: blur(${unifyAmount}px) !important;
1334
+ }
1335
+ `;
1336
+ }
1337
+ // ②(新) 弹层虚化(菜单/提示/data-surface,独立于对话框)
1338
+ if (popoverBlur && popoverAmount > 0 && bdSupported) {
1339
+ const popFilter = `blur(${popoverAmount}px)`;
1340
+ css += `
1341
+ /* ── 弹层虚化(菜单/下拉/提示/列表/悬浮面板,非居中窗口) ── */
1342
+ [role="menu"],
1343
+ [role="tooltip"],
1344
+ [role="alert"],
1345
+ [role="listbox"],
1346
+ [role="combobox"],
1347
+ [data-dsh-surface] {
1348
+ backdrop-filter: ${popFilter} !important;
1349
+ -webkit-backdrop-filter: ${popFilter} !important;
1350
+ }
1351
+ [role="menu"] [class*="card"],
1352
+ [role="tooltip"] [class*="card"] {
1353
+ backdrop-filter: none !important;
1354
+ }
1355
+ `;
1356
+ }
1357
+ // ⑯ 虚化关 → 输入框纯白不透明(用户要求:关闭虚化则输入框不透明)
1358
+ if (hasImage && !(dialogBlur && dialogAmount > 0)) {
1359
+ css += `
1360
+ [data-composer-card] {
1361
+ background-color: var(--dsw-alias-bg-base) !important;
1362
+ backdrop-filter: none !important;
1363
+ -webkit-backdrop-filter: none !important;
1364
+ }
1365
+ [data-slot*="composer"] [class*="seat"],
1366
+ [data-slot*="composer"] [class*="Seat"] {
1367
+ background: transparent !important;
1368
+ backdrop-filter: none !important;
1369
+ }
1370
+ `;
1371
+ }
1372
+ // ⑯(新) 5 处未虚化区域补透明(DOM 研究确认的选择器)
1373
+ if (hasImage) {
1374
+ css += `
1375
+ /* ── 5 处未虚化区域:背景透明透出(模糊的)壁纸 ── */
1376
+ /* 区域1: 左上角品牌+新会话按钮(半透明,与侧边栏一致)。
1377
+ ③(修正) unify 开时随 sessionFollow(开=跟随不透明度条;关=面板不透明度) */
1378
+ button[class*="newSession"] {
1379
+ background-color: color-mix(in srgb, var(--dsw-alias-button-elevated-fill) ${unifyTint ? (section.sessionFollow !== void 0 ? !!section.sessionFollow : DEFAULT_SESSION_FOLLOW) ? panel : panel : panel}%, transparent) !important;
1380
+ border-color: color-mix(in srgb, var(--dsw-alias-border-l2) 65%, transparent) !important;
1381
+ }
1382
+ /* 区域4: 标题栏下方 1px 横线(header::after)去掉 */
1383
+ .wSkVaW_header:after { display: none !important; }
1384
+
1385
+ /* 区域3: Cordis 面板(设置上方状态条,无 role=dialog)补虚化 + 半透明。
1386
+ ③(新) unify 开 → 与聊天区同色系(static 色),backdrop-filter 取消(整屏磨砂接管) */
1387
+ [data-cordis-panel] {
1388
+ backdrop-filter: ${unifyTint ? "none" : blurFilter} !important;
1389
+ -webkit-backdrop-filter: ${unifyTint ? "none" : blurFilter} !important;
1390
+ background-color: ${unifyTint
1391
+ ? `color-mix(in srgb, var(--dsw-static-neutral-bluish-950) ${U(62)}%, transparent)`
1392
+ : `color-mix(in srgb, var(--dsw-alias-bg-base) ${U(62)}%, transparent)`} !important;
1393
+ }
1394
+ ${unifyTint ? `body:not([data-ds-dark-theme]) [data-cordis-panel] {
1395
+ background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-00) ${U(62)}%, transparent) !important;
1396
+ }` : ""}
1397
+ [data-cordis-panel] [class*="row"] {
1398
+ background-color: ${unifyTint
1399
+ ? `color-mix(in srgb, var(--dsw-static-neutral-bluish-950) ${U(55)}%, transparent)`
1400
+ : `color-mix(in srgb, var(--dsw-alias-bg-base) ${U(55)}%, transparent)`} !important;
1401
+ }
1402
+ ${unifyTint ? `body:not([data-ds-dark-theme]) [data-cordis-panel] [class*="row"] {
1403
+ background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-00) ${U(55)}%, transparent) !important;
1404
+ }` : ""}
1405
+ /* 区域5: 侧边栏收起后 rail 右缘 1px 接缝透明 */
1406
+ [data-sidebar-collapsed] .pI_x6G_sidebarCol { border-right-color: transparent !important; }
1407
+ [data-sidebar-collapsed] [class*="sidebarCol"] { border-right-color: transparent !important; }
1408
+ /* 区域5b: 收起后 rail 内部面(logoRow/newSession/footArea)透出模糊壁纸:
1409
+ 同样不加 backdrop-filter(root 是弹层祖先,会困住 fixed 弹层) */
1410
+ [data-sidebar-collapsed] [class*="root"] {
1411
+ background-color: transparent !important;
1412
+ }
1413
+ /* ②(重做) 遮罩虚化独立(maskBlur/maskAmount):设置/弹层打开时全屏背景遮罩的虚化,
1414
+ 与对话框虚化、弹层虚化分开控制。匹配所有 [class*="mask"]。 */
1415
+ [class*="mask"] {
1416
+ backdrop-filter: ${(maskBlur && maskAmount > 0 && bdSupported) ? `blur(${maskAmount}px)` : "none"} !important;
1417
+ -webkit-backdrop-filter: ${(maskBlur && maskAmount > 0 && bdSupported) ? `blur(${maskAmount}px)` : "none"} !important;
1418
+ }
1419
+ `;
1420
+ }
1421
+ if (thinkBg) {
1422
+ // 开关开:恢复 Deep diving 背景方框 + 虚化 + 蓝色文字
1423
+ // (方框=渐变背景,浏览器未生效 background-clip:text 时显示为方框;
1424
+ // 生效时渐变只在文字上=蓝色文字;backdrop-filter 虚化方框背后的壁纸)
1425
+ const dlgFilter2 = dialogBlur && dialogAmount > 0 ? `blur(${dialogAmount}px)` : "blur(12px)";
1426
+ css += `
1427
+ /* Deep diving 背景方框(开关:开)→ 方框 + 虚化 + 蓝字 */
1428
+ [data-variant="think"] { background: var(--dsw-alias-bg-base) !important; }
1429
+ [role="status"] {
1430
+ background: linear-gradient(90deg, var(--dsw-static-deepseek-500) 0%, var(--dsw-static-deepseek-500) 40%, var(--dsw-static-deepseek-200) 50%, var(--dsw-static-deepseek-500) 60%, var(--dsw-static-deepseek-500) 100%) !important;
1431
+ -webkit-background-clip: text !important; background-clip: text !important;
1432
+ color: transparent !important; -webkit-text-fill-color: transparent !important;
1433
+ backdrop-filter: ${dlgFilter2} !important;
1434
+ -webkit-backdrop-filter: ${dlgFilter2} !important;
1435
+ }
1436
+ `;
1437
+ } else {
1438
+ // 开关关(默认):取消 Deep diving 背景方框,文字显示在壁纸上
1439
+ css += `
1440
+ /* Deep diving 背景方框(开关:关)→ 透明 + 文字可见色 */
1441
+ [data-variant="think"] { background: transparent !important; }
1442
+ [data-variant="think"] [class*="thinkBody"] { background: transparent !important; }
1443
+ [role="status"] {
1444
+ background: transparent !important;
1445
+ backdrop-filter: none !important;
1446
+ -webkit-backdrop-filter: none !important;
1447
+ color: var(--dsw-alias-label-secondary, #9aa4b2) !important;
1448
+ -webkit-text-fill-color: var(--dsw-alias-label-secondary, #9aa4b2) !important;
1449
+ -webkit-background-clip: border-box !important;
1450
+ background-clip: border-box !important;
1451
+ }
1452
+ `;
1453
+ }
1454
+ css += `
1455
+ @supports not (backdrop-filter: blur(1px)) {
1456
+ .hHd-Xa_regionArea, .wSkVaW_scrollBody, .ydkMvW_body { backdrop-filter: none !important; }
1457
+ }
1458
+ `;
1459
+ return css + buildUiCss(section, dlgFrosted);
1460
+ }
1461
+
1462
+ /** 设置页 UI 样式(与背景模式无关,公共部分)。 */
1463
+ function buildUiCss(section, dlgFrosted) {
1464
+ const roundCompat = section && section.roundCompat !== void 0 ? !!section.roundCompat : DEFAULT_ROUND_COMPAT;
1465
+ const floatOn = section && section.float !== void 0 ? !!section.float : DEFAULT_FLOAT;
1466
+ return `
1467
+ /* ── 设置页 UI ── */
1468
+ .mpw_row { display: flex; flex-direction: column; gap: 10px; padding: 4px 0; max-width: 720px; }
1469
+ .mpw_title { font-size: 16px; line-height: 24px; font-weight: 600; color: var(--dsw-alias-label-primary); }
1470
+ .mpw_desc { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-tertiary); margin: 0; }
1471
+ /* ⑤(重做) 分组排版:大分组标题用背景块区分(不再只是一条线,一眼分清分组) */
1472
+ .mpw_section {
1473
+ margin: 16px 0 6px; padding: 7px 12px;
1474
+ border-radius: 10px;
1475
+ background: color-mix(in srgb, var(--dsw-alias-label-primary) 7%, transparent);
1476
+ font-size: 13px; line-height: 20px; font-weight: 600;
1477
+ color: var(--dsw-alias-label-primary); letter-spacing: 0.02em;
1478
+ }
1479
+ .mpw_section:first-of-type { margin-top: 10px; }
1480
+ .mpw_field { display: flex; flex-direction: column; gap: 6px; padding: 8px 4px; border-top: 1px solid color-mix(in srgb, var(--dsw-alias-border-l2) 45%, transparent); }
1481
+ .mpw_label { font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-secondary); }
1482
+ .mpw_inline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
1483
+ .mpw_input {
1484
+ flex: 1; min-width: 0; height: 34px; padding: 0 10px; box-sizing: border-box;
1485
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px;
1486
+ background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary);
1487
+ font: inherit; font-size: 13px;
1488
+ }
1489
+ .mpw_button {
1490
+ flex: none; height: 34px; padding: 0 14px; border: 1px solid rgba(255,255,255,0.25); border-radius: 10px;
1491
+ background: #3964fe !important; color: #ffffff !important;
1492
+ font: inherit; font-size: 13px; cursor: pointer; outline: none;
1493
+ }
1494
+ .mpw_button:hover { filter: brightness(1.08); }
1495
+ .mpw_button:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #7aa2ff); outline-offset: 2px; }
1496
+ .mpw_fileBtn, .mpw_reset {
1497
+ align-self: flex-start; height: 32px; padding: 0 14px; border-radius: 10px;
1498
+ border: 1px solid var(--dsw-alias-border-l2); background: transparent;
1499
+ color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; cursor: pointer;
1500
+ }
1501
+ .mpw_fileBtn:hover, .mpw_reset:hover { background: var(--dsw-alias-interactive-bg-hover); }
1502
+ .mpw_slider { flex: 1; min-width: 0; }
1503
+ .mpw_numInput {
1504
+ flex: none; width: 76px; height: 30px; padding: 0 8px; box-sizing: border-box;
1505
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px;
1506
+ background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary);
1507
+ font: inherit; font-size: 12px; text-align: right;
1508
+ }
1509
+ /* ④(修正) 输入框聚焦:去掉浏览器默认黄/蓝 outline,灰框→主题深色框(与设置页其他输入框一致) */
1510
+ .mpw_input:focus, .mpw_numInput:focus {
1511
+ outline: none !important;
1512
+ border-color: var(--dsw-alias-label-primary) !important;
1513
+ box-shadow: 0 0 0 1px var(--dsw-alias-label-primary);
1514
+ }
1515
+ .mpw_value {
1516
+ flex: none; text-align: right; font-size: 13px; padding-left: 2px;
1517
+ color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums;
1518
+ }
1519
+ /* ③(新) 滑条旁「默认」小按钮 */
1520
+ .mpw_miniBtn {
1521
+ flex: none; height: 28px; padding: 0 10px; border-radius: 8px;
1522
+ border: 1px solid var(--dsw-alias-border-l2); background: transparent;
1523
+ color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px; cursor: pointer;
1524
+ }
1525
+ .mpw_miniBtn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
1526
+ .mpw_hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); margin: 0; }
1527
+ /* ④(新) 导入失败/错误提示:红色醒目,不再一闪而过看不清 */
1528
+ .mpw_hint.mpw_err { color: #ff6b6b; font-weight: 500; }
1529
+ .mpw_info { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); margin: 0; }
1530
+ .mpw_props { display: flex; flex-direction: column; gap: 6px; padding: 8px; border-radius: 8px; background: var(--dsw-alias-bg-module-platform); }
1531
+ .mpw_prop { display: flex; flex-direction: column; gap: 4px; padding: 6px 0; }
1532
+ .mpw_propLabel { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
1533
+ .mpw_propLabel b { color: var(--dsw-alias-label-secondary); font-weight: 500; }
1534
+ .mpw_check { display: flex; align-items: center; gap: 8px; }
1535
+ .mpw_check input { accent-color: var(--dsw-alias-brand-primary, #3964fe); }
1536
+ /* ② 左右滑动式开关 */
1537
+ .mpw_switch {
1538
+ flex: none; width: 40px; height: 22px; padding: 0; border-radius: 11px;
1539
+ border: 1px solid color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent);
1540
+ background: color-mix(in srgb, var(--dsw-alias-label-primary) 16%, transparent);
1541
+ position: relative; cursor: pointer; transition: background .15s ease; outline: none;
1542
+ }
1543
+ .mpw_switch::after {
1544
+ content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
1545
+ border-radius: 50%; background: var(--dsw-alias-label-primary);
1546
+ box-shadow: 0 1px 2px rgba(0,0,0,0.35); transition: left .15s ease;
1547
+ }
1548
+ .mpw_switch.mpw_on { background: var(--dsw-alias-brand-primary, #3964fe); border-color: transparent; }
1549
+ .mpw_switch.mpw_on::after { left: 20px; background: #fff; }
1550
+ .mpw_switch:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #7aa2ff); outline-offset: 2px; }
1551
+ /* ⑥ 纯展示性条目 */
1552
+ .mpw_static { padding: 2px 0; }
1553
+ .mpw_important { border-left: 2px solid var(--dsw-alias-brand-primary, #3964fe); padding-left: 8px; }
1554
+ .mpw_static .mpw_propLabel { color: var(--dsw-alias-label-secondary); }
1555
+ /* ④ 展开/收起 */
1556
+ .mpw_moreBtn { align-self: flex-start; }
1557
+ .mpw_propInput {
1558
+ height: 30px; padding: 0 8px; box-sizing: border-box; max-width: 260px;
1559
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px;
1560
+ background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary);
1561
+ font: inherit; font-size: 12px;
1562
+ }
1563
+ .mpw_propSlider { flex: 1; min-width: 120px; max-width: 260px; }
1564
+ .mpw_tag { font-size: 11px; line-height: 16px; padding: 1px 6px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.15)); color: var(--dsw-alias-label-tertiary); }
1565
+ /* ⑥ 设置导航图标:隐藏默认齿轮,显示自定义图标 */
1566
+ .VOzbGW_navList button:has(.mpw_navIconImg) .VOzbGW_navIcon { display: none !important; }
1567
+ .mpw_navIconImg {
1568
+ width: 16px; height: 16px; flex: none; vertical-align: -3px; margin-right: 8px;
1569
+ border-radius: 4px; object-fit: cover;
1570
+ background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15);
1571
+ }
1572
+ ${roundCompat ? `/* ②(新) 第三方插件 UI 圆角兼容:给有背景的"矩形容器"统一补圆角。
1573
+ 用 :where 零优先级 → 插件/DSH 自己的圆角样式永远优先,只补"完全没有圆角"的元素。
1574
+ 命中常见容器类名子串(wrap/panel/box/section/container),不限某个插件。 */
1575
+ :where([class*="wrap"], [class*="panel"], [class*="box"], [class*="section"], [class*="container"]) {
1576
+ border-radius: 10px;
1577
+ }` : ""}
1578
+ /* 4(修正) 悬浮效果移到 buildUiCss:背景清除(无图)时也能用(原来只在 hasImage 分支 → 清背景后悬浮失效) */
1579
+ ${floatOn ? `/* ①(修正) 悬浮效果(默认关):侧边栏/标题栏悬浮卡片。
1580
+ 注意:绝不给 sidebarCol/header 加 backdrop-filter(会困住 fixed 设置弹窗/子代理面板)。
1581
+ 参考 DSH-Transparent-UI-Plugin:四周 margin + 全圆角 = 悬浮卡片;sidebar root 解除宽度。
1582
+ ②(修正) 标题栏下边保留描边(跟随圆角):headerBg 规则的 border-bottom transparent
1583
+ 用 !important 会取消下边线 → 这里用 !important 覆盖回来,圆角描边完整。 */
1584
+ body[data-mpw-float] .wSkVaW_header {
1585
+ margin: 10px 16px 0;
1586
+ padding: 8px 16px 8px;
1587
+ border: 1px solid rgba(19,45,83,0.26);
1588
+ border-bottom-color: rgba(19,45,83,0.26) !important;
1589
+ border-radius: 20px;
1590
+ }
1591
+ body[data-mpw-float] body[data-ds-dark-theme] .wSkVaW_header,
1592
+ body[data-ds-dark-theme] body[data-mpw-float] .wSkVaW_header {
1593
+ border-color: rgba(148,180,220,0.32);
1594
+ border-bottom-color: rgba(148,180,220,0.32) !important;
1595
+ }
1596
+ /* ②(修正) 悬浮时隐藏 header 底部的 1px 分隔线(::after 直线会横穿圆角下方,与圆角有空隙) */
1597
+ body[data-mpw-float] .wSkVaW_header:after { display: none !important; }
1598
+ body[data-mpw-float] .pI_x6G_sidebarCol {
1599
+ margin: 12px;
1600
+ padding: 10px 12px 14px;
1601
+ border: 1px solid rgba(19,45,83,0.26);
1602
+ border-radius: 20px;
1603
+ overflow: hidden;
1604
+ /* ④(修正) 收起/展开动画平滑:margin/padding/圆角过渡(与收起态规则配合) */
1605
+ transition: margin 0.18s ease, padding 0.18s ease, border-radius 0.18s ease, border-color 0.18s ease;
1606
+ }
1607
+ body[data-mpw-float] body[data-ds-dark-theme] .pI_x6G_sidebarCol,
1608
+ body[data-ds-dark-theme] body[data-mpw-float] .pI_x6G_sidebarCol {
1609
+ border-color: rgba(148,180,220,0.32);
1610
+ }
1611
+ body[data-mpw-float] [data-mpw-sidebar-root] {
1612
+ width: 100% !important;
1613
+ background: transparent !important;
1614
+ border-radius: 20px;
1615
+ }
1616
+ /* ①(修正) 侧边栏收起态也有悬浮效果:保留圆角卡片 + 边框 + 半透明。
1617
+ ②(修正) 收起 rail 内容(logo/切换/会话图标 36px)按 DSH 原 rail 宽布局:
1618
+ 绝不能加左右 padding 或 overflow:hidden 把它压进更窄的卡片 —— 内容会被裁掉
1619
+ (用户实测:收起态上方 UI 被切,只剩底部设置)。悬浮只做外壳:
1620
+ margin + 圆角 + 边框,padding 归零、overflow 可见。
1621
+ ③(修正) 收起/展开动画流畅度(用户实测:收起态贴屏幕边缘、动画卡顿):
1622
+ 左/上/下 margin 与展开态一致(12px)→ 收起时左缘不跳变、不贴边;
1623
+ 右 margin 用 -12px 补回宽度(宽 = 父宽,内容完整),右缘与 rail 区对齐;
1624
+ transition 平滑 padding/border-radius/margin 过渡。 */
1625
+ body[data-mpw-float] [data-sidebar-collapsed] .pI_x6G_sidebarCol,
1626
+ body[data-mpw-float] [data-sidebar-collapsed] [class*="sidebarCol"] {
1627
+ margin: 12px -12px 12px 12px;
1628
+ padding: 0;
1629
+ border-radius: 12px;
1630
+ border: 1px solid rgba(19,45,83,0.26);
1631
+ overflow: visible;
1632
+ position: relative;
1633
+ z-index: 5;
1634
+ transition: margin 0.18s ease, padding 0.18s ease, border-radius 0.18s ease, border-color 0.18s ease;
1635
+ }
1636
+ body[data-mpw-float] body[data-ds-dark-theme] [data-sidebar-collapsed] .pI_x6G_sidebarCol,
1637
+ body[data-mpw-float] body[data-ds-dark-theme] [data-sidebar-collapsed] [class*="sidebarCol"] {
1638
+ border-color: rgba(148,180,220,0.32);
1639
+ }` : ""}
1640
+ /* ③(修正) 弹窗打开时:背后的设置面板只去掉 backdrop-filter(不再让字晕染),
1641
+ 背景保持原来的半透明(透出壁纸)——绝不整体改成深色(会导致设置界面变黑)。
1642
+ 不透明兜底与磨砂覆盖已全部移到 buildCss(A 块:dialogBlur/settingsBlur/confirmBlur
1643
+ 各自开关关时兜底不透明;B/C/D 块:开关开时磨砂覆盖),这里不再重复以免覆盖磨砂。 */
1644
+ body[data-mpw-modal] [role="dialog"] {
1645
+ backdrop-filter: none !important;
1646
+ -webkit-backdrop-filter: none !important;
1647
+ }
1648
+ /* ④ 冲突确认弹窗(背景不透明兜底在 buildCss A 块;此处为结构样式) */
1649
+ .mpw_mask {
1650
+ position: fixed; inset: 0; z-index: 3000;
1651
+ background: rgba(0,0,0,0.45); display: flex; align-items: center; justify-content: center;
1652
+ }
1653
+ .mpw_dialog {
1654
+ width: min(420px, calc(100vw - 48px)); box-sizing: border-box;
1655
+ border: 1px solid var(--dsw-alias-border-l2);
1656
+ border-radius: 16px; padding: 20px; box-shadow: var(--dsw-shadow-lv3, 0 12px 40px rgba(0,0,0,.35));
1657
+ display: flex; flex-direction: column; gap: 12px;
1658
+ }
1659
+ `;
1660
+ }
1661
+
1662
+ // ═══════════════════════════════════════════════════════════════════
1663
+ // 冲突检测(④:同时装了其他壁纸/主题插件时自动关闭本功能)
1664
+ // ═══════════════════════════════════════════════════════════════════
1665
+ const CONFLICT_IDS = [
1666
+ "@local/dsh-bg-image", "dsh-bg-image", "dsh-skin", "dsh-dream-skin",
1667
+ "dsh-wallpaper-rotator", "dsh_web_client_theme_switcher",
1668
+ "@local/dsh-ui-preset-enhance", "ui-theme-switcher", "theme-switcher",
1669
+ "@deepseek-ai/dsh-client-ui-aqua", "dsh-client-ui-aqua", "ui-aqua"
1670
+ ];
1671
+ // P1④(新) 皮肤类插件的 body 属性签名(各 skin.json 的 bodyAttr):
1672
+ // 皮肤启用后在 <body> 打 data-* 属性,比扫 [data-plugin] 更直接
1673
+ const CONFLICT_BODY_ATTRS = [
1674
+ "data-dsh-aurora", "data-dsh-whale-song", "data-dsh-skin",
1675
+ "data-dsh-ui-skin", "data-dsh-theme", "data-skin"
1676
+ ];
1677
+ function detectConflicts() {
1678
+ const found = [];
1679
+ try {
1680
+ const els = document.querySelectorAll ? document.querySelectorAll("[data-plugin]") : [];
1681
+ els.forEach((el) => {
1682
+ const id = (el.getAttribute && el.getAttribute("data-plugin")) || "";
1683
+ if (id && CONFLICT_IDS.some((c) => id.indexOf(c) >= 0 || c.indexOf(id) >= 0)) {
1684
+ if (found.indexOf(id) < 0) found.push(id);
1685
+ }
1686
+ });
1687
+ } catch {}
1688
+ // ⑪(新) 运行时检测:其他插件往 body/html 设了背景图,或存在其他全屏背景层
1689
+ try {
1690
+ const bodyBg = document.body ? getComputedStyle(document.body).backgroundImage : "none";
1691
+ if (bodyBg && bodyBg !== "none") found.push("body-background");
1692
+ const htmlBg = getComputedStyle(document.documentElement).backgroundImage;
1693
+ if (htmlBg && htmlBg !== "none") found.push("html-background");
1694
+ } catch {}
1695
+ // P1④(新) 皮肤类插件的 body 属性签名检测(data-dsh-aurora 等)
1696
+ try {
1697
+ if (document.body) {
1698
+ CONFLICT_BODY_ATTRS.forEach((attr) => {
1699
+ if (document.body.hasAttribute(attr)) found.push(attr);
1700
+ });
1701
+ }
1702
+ } catch {}
1703
+ try {
1704
+ document.querySelectorAll("body > *").forEach((el) => {
1705
+ if (el.classList && el.classList.contains("mpw-bgWrap")) return; // 自己的壁纸层
1706
+ const cs = getComputedStyle(el);
1707
+ const z = Number(cs.zIndex);
1708
+ if (cs.position === "fixed" && isFinite(z) && z < 0) {
1709
+ found.push("fullscreen-bg:" + ((el.className && String(el.className)) || el.tagName));
1710
+ }
1711
+ });
1712
+ } catch {}
1713
+ return Array.from(new Set(found));
1714
+ }
1715
+
1716
+ // ═══════════════════════════════════════════════════════════════════
1717
+ // 视频纹理(tex 内嵌 MP4):提取 + 时间槽识别
1718
+ // ═══════════════════════════════════════════════════════════════════
1719
+ /** 检查 tex 前 4KB 是否有 MP4 魔数(ftyp)。 */
1720
+ function texHasVideo(bytes) {
1721
+ const n = Math.min(65536, bytes.length - 4);
1722
+ for (let i = 0; i < n; i++) {
1723
+ if (bytes[i] === 0x66 && bytes[i+1] === 0x74 && bytes[i+2] === 0x79 && bytes[i+3] === 0x70) return true;
1724
+ }
1725
+ return false;
1726
+ }
1727
+ /** 从 tex 前部找内嵌 MP4 的偏移(ftyp-4);找不到返回 null。 */
1728
+ function extractTexVideoOffset(bytes) {
1729
+ const n = Math.min(65536, bytes.length - 4);
1730
+ for (let i = 0; i < n; i++) {
1731
+ if (bytes[i] === 0x66 && bytes[i+1] === 0x74 && bytes[i+2] === 0x79 && bytes[i+3] === 0x70) {
1732
+ const start = i - 4;
1733
+ return start >= 0 ? start : null;
1734
+ }
1735
+ }
1736
+ return null;
1737
+ }
1738
+ /** 从 tex 提取内嵌 MP4(从 ftyp-4 到结尾)。 */
1739
+ function extractTexVideo(bytes) {
1740
+ const off = extractTexVideoOffset(bytes);
1741
+ return off === null ? null : bytes.slice(off);
1742
+ }
1743
+ /** 由纹理名识别时间槽:清晨/白天/黄昏/夜晚 或 morning/day/dusk/night。 */
1744
+ function slotFromName(name) {
1745
+ const n = name.toLowerCase();
1746
+ if (/清晨|morning/.test(n)) return "morning";
1747
+ if (/白天|^day|day[^n]/.test(n)) return "day";
1748
+ if (/黄昏|dusk/.test(n)) return "dusk";
1749
+ if (/夜晚|night/.test(n)) return "night";
1750
+ return null;
1751
+ }
1752
+ /** 从 project.json 属性读时间配置(含已编辑值)。 */
1753
+ function timeConfigFromProps(props, propEdits) {
1754
+ const get = (k, def) => {
1755
+ if (propEdits && propEdits[k] !== void 0) return Number(propEdits[k]);
1756
+ if (props && props[k] && props[k].value !== void 0) return Number(props[k].value);
1757
+ return def;
1758
+ };
1759
+ return {
1760
+ enabled: get("timevarying", 1) !== 0,
1761
+ morning: get("morningtime", 4),
1762
+ day: get("daytime", 9),
1763
+ dusk: get("dusktime", 17),
1764
+ night: get("nighttime", 20)
1765
+ };
1766
+ }
1767
+ /** 按时间配置 + 当前时间算时间槽。 */
1768
+ function slotForTime(cfg, date) {
1769
+ const h = date.getHours();
1770
+ if (h >= cfg.morning && h < cfg.day) return "morning";
1771
+ if (h >= cfg.day && h < cfg.dusk) return "day";
1772
+ if (h >= cfg.dusk && h < cfg.night) return "dusk";
1773
+ return "night";
1774
+ }
1775
+
1776
+ // ═══════════════════════════════════════════════════════════════════
1777
+ // 安全加固(③):文件类型嗅探 + URL 协议白名单 + 属性键过滤
1778
+ // ═══════════════════════════════════════════════════════════════════
1779
+ /** 读文件头判断真实类型;未知/可疑(SVG/HTML/脚本等)返回 null。 */
1780
+ function sniffFileType(file) {
1781
+ return new Promise((resolve) => {
1782
+ try {
1783
+ const reader = new FileReader();
1784
+ reader.onload = () => {
1785
+ try {
1786
+ const bytes = new Uint8Array(reader.result, 0, 16);
1787
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(" ");
1788
+ const ascii = String.fromCharCode(...bytes.slice(0, 8));
1789
+ if (hex.indexOf("89 50 4e 47") === 0) return resolve("png");
1790
+ if (ascii.indexOf("GIF8") === 0) return resolve("gif");
1791
+ if (hex.indexOf("ff d8 ff") === 0) return resolve("jpeg");
1792
+ if (ascii.indexOf("RIFF") === 0 && ascii.indexOf("WEBP") > 0) return resolve("webp");
1793
+ if (bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) return resolve("mp4"); // ....ftyp
1794
+ if (hex.indexOf("1a 45 df a3") === 0) return resolve("webm"); // Matroska/WebM
1795
+ // mpkg:头部 version_length(u32 LE) + "PKGM"
1796
+ const vl = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24);
1797
+ if (vl > 0 && vl < 64 && ascii.indexOf("PKGM") === 4) return resolve("mpkg");
1798
+ resolve(null);
1799
+ } catch { resolve(null); }
1800
+ };
1801
+ reader.onerror = () => resolve(null);
1802
+ reader.readAsArrayBuffer(file.slice(0, 16));
1803
+ } catch { resolve(null); }
1804
+ });
1805
+ }
1806
+ /** URL 协议白名单:仅 http(s) 与 data:image。 */
1807
+ function sanitizeImageUrl(value) {
1808
+ const v = String(value || "").trim();
1809
+ if (/^https?:\/\//i.test(v)) return v;
1810
+ if (/^data:image\//i.test(v)) return v;
1811
+ return null;
1812
+ }
1813
+ /** 属性键安全:禁止原型污染相关键。 */
1814
+ function safePropKey(key) {
1815
+ return key !== "__proto__" && key !== "constructor" && key !== "prototype";
1816
+ }
1817
+
1818
+ // ═══════════════════════════════════════════════════════════════════
1819
+ // 设置页组件(settings.section:出现在设置左侧导航,⑤)
1820
+ // ═══════════════════════════════════════════════════════════════════
1821
+ /** 左右滑动式开关(②:不用打勾的 checkbox)。 */
1822
+ function Toggle({ checked, onChange, disabled }) {
1823
+ const h = react.createElement;
1824
+ return h("button", {
1825
+ type: "button",
1826
+ role: "switch",
1827
+ "aria-checked": !!checked,
1828
+ disabled: !!disabled,
1829
+ className: "mpw_switch" + (checked ? " mpw_on" : ""),
1830
+ onClick: () => onChange(!checked)
1831
+ });
1832
+ }
1833
+
1834
+ function MpkgSection(props) {
1835
+ try {
1836
+ const { t } = props;
1837
+ const initMeta = (() => {
1838
+ const s = readSection();
1839
+ if (s.fromMpkg && s.mpkgKey && s.info) return { name: s.mpkgName, key: s.mpkgKey, info: s.info, entryName: s.source || "preview.gif", slot: s.slot };
1840
+ return null;
1841
+ })();
1842
+ const [section, setSection] = react.useState(readSection());
1843
+ const [mpkgMeta, setMpkgMeta] = react.useState(initMeta); // { name, key, info, entryName, slot }
1844
+ const [busy, setBusy] = react.useState(false);
1845
+ const [hint, setHint] = react.useState("");
1846
+ const [url, setUrl] = react.useState("");
1847
+ const [propsExpanded, setPropsExpanded] = react.useState(false);
1848
+ const [conflictModal, setConflictModal] = react.useState(false);
1849
+ const [previewModal, setPreviewModal] = react.useState(false);
1850
+ const [errorModal, setErrorModal] = react.useState(false);
1851
+ const [errorMsg, setErrorMsg] = react.useState("");
1852
+ const [conflicts, setConflicts] = react.useState([]);
1853
+ const [hostOk, setHostOk] = react.useState(null); // null=未检测 true=可用 false=不可用
1854
+ const [libWalls, setLibWalls] = react.useState(null); // null=未扫描 [] = 空列表
1855
+ const [libBusy, setLibBusy] = react.useState(false);
1856
+ const [customDir, setCustomDirState] = react.useState(readSection().customDirPath || "");
1857
+ const [customFiles, setCustomFiles] = react.useState(null); // null=未扫描
1858
+ const [dirPick, setDirPick] = react.useState(false); // 目录选择弹窗
1859
+ const [dirPath, setDirPath] = react.useState("");
1860
+ const [dirSubs, setDirSubs] = react.useState([]);
1861
+ const [dirHome, setDirHome] = react.useState("");
1862
+ const [dirPlatform, setDirPlatform] = react.useState("");
1863
+ const [updState, setUpdState] = react.useState(null); // null=未检测 / {checking}/{hasUpdate...}
1864
+ const [updConfirm, setUpdConfirm] = react.useState(null); // ⑩(新) 新版本确认下载弹窗
1865
+ const [wallList, setWallList] = react.useState([]); // ② 合并后的可选壁纸列表
1866
+ const [wallIdx, setWallIdx] = react.useState(-1); // 当前在列表中的索引
1867
+ const [libShow, setLibShow] = react.useState(10); // 1(新) 列表单次展示数量(可展开)
1868
+ const [libOpen, setLibOpen] = react.useState(true); // 2(新) 壁纸列表展开/收起
1869
+ // ①(新) 统一错误弹窗:导入失败/文件过大/无法使用等提示全部弹窗体现
1870
+ const showError = (msg) => { setErrorMsg(String(msg)); setErrorModal(true); setHint(String(msg)); };
1871
+ const mpkgRef = react.useRef(null);
1872
+ const imgRef = react.useRef(null);
1873
+
1874
+ // ④ 初始冲突检测(设置页打开时其他插件都已加载)
1875
+ react.useEffect(() => {
1876
+ setConflicts(detectConflicts());
1877
+ // ③(新) 检测宿主端可用性(大文件混合模式是否生效)
1878
+ fetch(HOST_BASE + "/ping", { method: "GET" }).then((r) => setHostOk(!!r.ok)).catch(() => setHostOk(false));
1879
+ }, []);
1880
+
1881
+ // ④ 总开关:开启时若有冲突 → 弹窗确认
1882
+ const onMaster = (v) => {
1883
+ if (v && conflicts.length && !section.forceEnabled) {
1884
+ setConflictModal(true);
1885
+ return;
1886
+ }
1887
+ commit({ enabled: v }, true);
1888
+ };
1889
+ const confirmEnable = () => {
1890
+ setConflictModal(false);
1891
+ commit({ enabled: true, forceEnabled: true }, true);
1892
+ setConflicts(detectConflicts());
1893
+ };
1894
+
1895
+ const commit = (patch, instant) => {
1896
+ const next = Object.assign({}, readSection(), patch);
1897
+ writeSection(next, instant);
1898
+ setSection(next);
1899
+ applyFromStorage();
1900
+ return next;
1901
+ };
1902
+ // ④(新) 扫描本地壁纸库(Steam 自动发现,host 提供)
1903
+ const scanLibrary = async () => {
1904
+ if (!hostOk) { showError(t("lib.noHost")); return; }
1905
+ setLibBusy(true);
1906
+ try {
1907
+ const r = await fetch(HOST_BASE + "/steam-inventory");
1908
+ const d = await r.json();
1909
+ const walls = d.ok && d.wallpapers ? d.wallpapers : [];
1910
+ setLibWalls(walls);
1911
+ if (d.ok && !d.installDir) showError(t("lib.noInstall"));
1912
+ // ②(新) Steam 库合并进统一列表(轮播/上下切换可用):
1913
+ // 只并入能实际播放的 video(scene 只有预览图、web 未实现),
1914
+ // 用 src:"steam" 标记,next/prev 走 applyLibraryWallpaper。
1915
+ const steamPlayable = walls.filter((wp) => wp.type === "video" && wp.media);
1916
+ if (steamPlayable.length) {
1917
+ setWallList((prev) => {
1918
+ const merged = steamPlayable.map((wp) => ({
1919
+ key: "steam|" + wp.ltoken, title: wp.title || wp.ltoken,
1920
+ name: wp.title || wp.ltoken, type: "video", src: "steam",
1921
+ ltoken: wp.ltoken, media: wp.media
1922
+ }));
1923
+ const existing = prev || [];
1924
+ const keys = new Set(existing.map((w) => w.key));
1925
+ return existing.concat(merged.filter((w) => !keys.has(w.key)));
1926
+ });
1927
+ setHint(t("lib.rotMerged") + steamPlayable.length);
1928
+ }
1929
+ } catch (err) { showError(t("lib.fail") + String(err && err.message || err)); }
1930
+ setLibBusy(false);
1931
+ };
1932
+ // ①(新) 检测更新(对比实际代码内容哈希,README 变更不触发)
1933
+ const checkUpdate = async () => {
1934
+ setUpdState({ checking: true });
1935
+ try {
1936
+ const r = await fetch(HOST_BASE + "/update-check");
1937
+ const d = await r.json();
1938
+ setUpdState(d.ok ? d : { error: String(d.error || "fail") });
1939
+ } catch (err) { setUpdState({ error: String(err && err.message || err) }); }
1940
+ };
1941
+ // ①(新) 一键热更新(host 从 GitHub 下载最新代码写回)
1942
+ const applyUpdate = async () => {
1943
+ setUpdState({ applying: true });
1944
+ try {
1945
+ const r = await fetch(HOST_BASE + "/update-apply", { method: "POST" });
1946
+ const d = await r.json();
1947
+ setUpdState(d.ok ? { applied: true } : { error: String(d.error || "fail") });
1948
+ } catch (err) { setUpdState({ error: String(err && err.message || err) }); }
1949
+ };
1950
+ // ①(新) 目录选择器:打开并列出指定路径的子目录(跨平台,host 浏览)
1951
+ const openDirPicker = async (path) => {
1952
+ setDirPick(true); // ⑦(修正) 打开弹窗(之前漏了这行导致"浏览"无反应)
1953
+ setDirPath(path);
1954
+ try {
1955
+ const r = await fetch(HOST_BASE + "/list-dirs?path=" + encodeURIComponent(path));
1956
+ const d = await r.json();
1957
+ setDirSubs(d.ok && d.subdirs ? d.subdirs : []);
1958
+ if (d.ok && d.home) setDirHome(d.home);
1959
+ if (d.ok && d.platform) setDirPlatform(d.platform);
1960
+ } catch { setDirSubs([]); }
1961
+ };
1962
+ // ①(新) 扫描自定义本地壁纸目录(任意文件夹,host 只读媒体文件)
1963
+ // ①(修正) 接受可选 dir 参数:目录选择器「选择此文件夹」时 state 尚未刷新,
1964
+ // 直接用传入值(否则 setCustomDirState 异步 → scanCustomDir 读到旧空值 → 误报"先输入路径")
1965
+ const scanCustomDir = async (dirArg) => {
1966
+ const dir = (dirArg !== void 0 ? dirArg : customDir).trim();
1967
+ if (!dir) { showError(t("lib.dirEmpty")); return; }
1968
+ setLibBusy(true);
1969
+ try {
1970
+ const r = await fetch(HOST_BASE + "/custom-dir", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ dir }) });
1971
+ const d = await r.json();
1972
+ if (!d.ok) { showError(t("lib.dirFail") + String(d.error || "")); }
1973
+ else {
1974
+ setCustomFiles(d.files || []);
1975
+ // ①(新) 合并进统一列表:自定义目录文件(图片+视频)
1976
+ setWallList((d.files || []).map((f) => ({
1977
+ key: "custom|" + f.name, title: f.name, name: f.name, type: f.type,
1978
+ image: "host:?custom=1&file=" + encodeURIComponent(f.name),
1979
+ converted: f.type === "video" ? "mp4" : (f.type === "mpkg" ? "mp4" : "gif")
1980
+ })));
1981
+ }
1982
+ } catch (err) { showError(t("lib.fail") + String(err && err.message || err)); }
1983
+ setLibBusy(false);
1984
+ };
1985
+ // ⑥(新) 应用自定义目录里的 .mpkg(host 解析选素材后流式播放)
1986
+ const applyCustomMpkg = async (w) => {
1987
+ setLibBusy(true);
1988
+ try {
1989
+ const r = await fetch(HOST_BASE + "/custom-mpkg?file=" + encodeURIComponent(w.name));
1990
+ const d = await r.json();
1991
+ if (!d.ok || !d.selected) { showError(t("mpkg.noAsset") + "(" + w.name + ")"); setLibBusy(false); return; }
1992
+ const sel = d.selected;
1993
+ commit({
1994
+ image: "host:?token=" + d.token + "&index=" + sel.index + (sel.offset ? "&offset=" + sel.offset : ""),
1995
+ source: sel.name, mpkgKey: "custommpkg|" + w.name, mpkgName: w.name,
1996
+ fromMpkg: true, converted: sel.isMp4 ? "mp4" : "gif", slot: null,
1997
+ info: undefined, timeVideos: undefined, timeConfig: undefined, activeSlot: null
1998
+ }, true);
1999
+ setMpkgMeta({ name: w.name, key: "custommpkg|" + w.name, info: null, entryName: sel.name, slot: null });
2000
+ setHint(t("lib.applied") + ":" + w.name);
2001
+ if (!sel.isMp4) { setPreviewModal(true); }
2002
+ } catch (err) { showError(t("lib.fail") + String(err && err.message || err)); }
2003
+ setLibBusy(false);
2004
+ };
2005
+ // ②(新) 应用列表中的某个壁纸(统一入口)
2006
+ const applyWallFromList = (w) => {
2007
+ if (w.type === "mpkg") { applyCustomMpkg(w); return; }
2008
+ // ②(新) Steam 库项 → 走 applyLibraryWallpaper(video 专用)
2009
+ if (w.src === "steam") {
2010
+ applyLibraryWallpaper({ type: "video", ltoken: w.ltoken, title: w.title, media: w.media });
2011
+ return;
2012
+ }
2013
+ commit({
2014
+ image: w.image, source: w.title, mpkgKey: w.key, mpkgName: w.title,
2015
+ fromMpkg: false, converted: w.converted, slot: null,
2016
+ info: undefined, timeVideos: undefined, timeConfig: undefined, activeSlot: null
2017
+ }, true);
2018
+ setMpkgMeta({ name: w.title, key: w.key, info: null, entryName: w.title, slot: null });
2019
+ setHint(t("lib.applied") + ":" + w.title);
2020
+ return w;
2021
+ };
2022
+ // ②(新) 切换到下一个壁纸(本地库 + 自定义目录合并列表)
2023
+ const nextWallpaper = () => {
2024
+ const list = wallList;
2025
+ if (!list.length) { showError(t("lib.empty")); return; }
2026
+ const idx = (wallIdx + 1) % list.length;
2027
+ setWallIdx(idx);
2028
+ applyWallFromList(list[idx]);
2029
+ };
2030
+ // 1(新) 切换到上一个壁纸
2031
+ const prevWallpaper = () => {
2032
+ const list = wallList;
2033
+ if (!list.length) { showError(t("lib.empty")); return; }
2034
+ const idx = (wallIdx - 1 + list.length) % list.length;
2035
+ setWallIdx(idx);
2036
+ applyWallFromList(list[idx]);
2037
+ };
2038
+ // ③(新) 壁纸轮换:定时自动切换
2039
+ const startRotation = () => {
2040
+ try { if (window.__mpwRotTimer) clearInterval(window.__mpwRotTimer); } catch {}
2041
+ const sec = section.rotateMin > 0 ? section.rotateMin : 5;
2042
+ window.__mpwRotTimer = setInterval(nextWallpaper, sec * 60 * 1000);
2043
+ };
2044
+ // ③(修正) 任一弹窗打开时给 body 打标记(背后设置面板禁虚化),关闭时移除
2045
+ react.useEffect(() => {
2046
+ const anyOpen = previewModal || errorModal || dirPick || updConfirm || conflictModal;
2047
+ try {
2048
+ if (anyOpen) document.body.setAttribute("data-mpw-modal", "");
2049
+ else document.body.removeAttribute("data-mpw-modal");
2050
+ } catch {}
2051
+ }, [previewModal, errorModal, dirPick, updConfirm, conflictModal]);
2052
+ // ③(新) 轮换开关变化时启停
2053
+ react.useEffect(() => {
2054
+ const on = section.rotate !== void 0 ? !!section.rotate : DEFAULT_ROTATE;
2055
+ if (on) startRotation();
2056
+ else { try { if (window.__mpwRotTimer) clearInterval(window.__mpwRotTimer); window.__mpwRotTimer = null; } catch {} }
2057
+ }, [section.rotate, section.rotateMin, wallList.length]);
2058
+ // ④(新) 选择本地壁纸库里的 video 壁纸播放
2059
+ const applyLibraryWallpaper = (wp) => {
2060
+ if (wp.type === "web") { showError(t("lib.webPending")); return; }
2061
+ if (wp.type !== "video" || !wp.media) { showError(t("lib.sceneOnly")); return; }
2062
+ const file = String(wp.media).split(/[\\/]/).pop();
2063
+ commit({
2064
+ image: "host:?ltoken=" + wp.ltoken + "&file=" + encodeURIComponent(file),
2065
+ source: wp.title, mpkgKey: "library|" + wp.ltoken, mpkgName: wp.title,
2066
+ fromMpkg: false, converted: "mp4", slot: null,
2067
+ info: undefined, timeVideos: undefined, timeConfig: undefined, activeSlot: null,
2068
+ propEdits: undefined
2069
+ }, true);
2070
+ setMpkgMeta({ name: wp.title, key: "library|" + wp.ltoken, info: null, entryName: file, slot: null });
2071
+ setHint(t("lib.applied") + ":" + wp.title);
2072
+ };
2073
+
2074
+ // ① 恢复默认:只重置外观数值,不清除已导入的壁纸
2075
+ const resetSettings = () => {
2076
+ const s = readSection();
2077
+ const keep = {
2078
+ image: s.image, source: s.source, mpkgKey: s.mpkgKey, mpkgName: s.mpkgName,
2079
+ info: s.info, slot: s.slot, fromMpkg: s.fromMpkg, converted: s.converted,
2080
+ propEdits: s.propEdits, forceEnabled: s.forceEnabled
2081
+ };
2082
+ writeSection(Object.assign({}, keep, {
2083
+ opacity: DEFAULT_OPACITY, blur: DEFAULT_BLUR, zoom: DEFAULT_ZOOM,
2084
+ sidebar: DEFAULT_SIDEBAR, sharp: DEFAULT_SHARP,
2085
+ headerBlur: DEFAULT_HEADER, headerBg: DEFAULT_HEADER_BG, headerBlurAmount: DEFAULT_HEADER_BLUR_AMOUNT,
2086
+ dialogBlur: DEFAULT_DIALOG_BLUR, dialogAmount: DEFAULT_DIALOG_AMOUNT,
2087
+ popoverBlur: DEFAULT_POPOVER_BLUR, popoverAmount: DEFAULT_POPOVER_AMOUNT,
2088
+ maskBlur: DEFAULT_MASK_BLUR, maskAmount: DEFAULT_MASK_AMOUNT,
2089
+ unifyTint: DEFAULT_UNIFY_TINT, unifyAmount: DEFAULT_UNIFY_AMOUNT, sidebarAlpha: DEFAULT_SIDEBAR_ALPHA, chatFollow: DEFAULT_CHAT_FOLLOW, sessionFollow: DEFAULT_SESSION_FOLLOW,
2090
+ hybrid: DEFAULT_HYBRID, roundCompat: DEFAULT_ROUND_COMPAT,
2091
+ rotate: DEFAULT_ROTATE, rotateMin: 5, customDirPath: "", brightness: DEFAULT_BRIGHTNESS, float: DEFAULT_FLOAT,
2092
+ thinkBg: DEFAULT_THINK_BG,
2093
+ enabled: DEFAULT_ENABLED
2094
+ }), true);
2095
+ setSection(readSection());
2096
+ applyFromStorage();
2097
+ };
2098
+ // ① 清除已导入的壁纸(保留外观数值)
2099
+ const clearBg = () => {
2100
+ idbDel("bg");
2101
+ const s = readSection();
2102
+ writeSection(Object.assign({}, s, { image: "", source: "", fromMpkg: false, converted: "", mpkgKey: "", mpkgName: "", info: undefined, slot: null }), true);
2103
+ setSection(readSection());
2104
+ setMpkgMeta(null);
2105
+ applyFromStorage();
2106
+ };
2107
+
2108
+ // ①(新) hybrid 模式导入:上传 mpkg 到宿主 → 用 HTTP Range URL 播放(大文件无限制)
2109
+ const importViaHost = async (file, t, showError, commit, setMpkgMeta, setHint, setBusy, setPreviewModal) => {
2110
+ const up = await fetch(HOST_BASE + "/upload", { method: "POST", body: file });
2111
+ const upj = await up.json();
2112
+ if (!upj.ok) { showError(t("mpkg.fail") + " upload: " + String(upj.error || "")); return; }
2113
+ const entries = upj.entries;
2114
+ const token = upj.token;
2115
+ const mediaUrl = (idx, offset) => HOST_BASE + "/media?token=" + token + "&index=" + idx + (offset ? "&offset=" + offset : "");
2116
+ const readHead = async (idx) => {
2117
+ const r = await fetch(mediaUrl(idx), { headers: { Range: "bytes=0-65535" } });
2118
+ return new Uint8Array(await r.arrayBuffer());
2119
+ };
2120
+ const readFull = async (idx) => new Uint8Array(await (await fetch(mediaUrl(idx))).arrayBuffer());
2121
+ // 视频纹理扫描(同纯 client 语义:抠像层/入场动画排除)
2122
+ const vtex = [];
2123
+ for (let i = 0; i < entries.length; i++) {
2124
+ const e = entries[i];
2125
+ if (e.name.endsWith(".tex") && e.size > 5 * 1024 * 1024
2126
+ && !/蓝幕|绿幕|bluescreen|greenscreen|chroma|keying|抠像/i.test(e.name)
2127
+ && !/入场|开场|intro|entry\s*animation|entryanimation/i.test(e.name)) {
2128
+ if (texHasVideo(await readHead(i))) vtex.push({ name: e.name, idx: i, size: e.size });
2129
+ }
2130
+ }
2131
+ let info = null;
2132
+ const projIdx = entries.findIndex((e) => e.name === "project.json");
2133
+ if (projIdx >= 0) { try { info = extractProjectInfo(new TextDecoder().decode(await readFull(projIdx))); } catch {} }
2134
+ const key = file.name + "|" + file.size;
2135
+ const meta = { name: file.name, key, info, entryName: "", slot: null };
2136
+ if (vtex.length) {
2137
+ let cfg = { enabled: true, morning: 4, day: 9, dusk: 17, night: 20 };
2138
+ if (projIdx >= 0) {
2139
+ try {
2140
+ const pj = JSON.parse(new TextDecoder().decode(await readFull(projIdx)));
2141
+ const pm = (pj.general && pj.general.properties) || null;
2142
+ if (pm) cfg = timeConfigFromProps(pm, {});
2143
+ } catch {}
2144
+ }
2145
+ const slot = slotForTime(cfg, new Date());
2146
+ const active = vtex.find((v) => slotFromName(v.name) === slot) || vtex[0];
2147
+ const head = await readHead(active.idx);
2148
+ const off = extractTexVideoOffset(head) || 0;
2149
+ meta.entryName = "视频纹理:" + active.name;
2150
+ commit({
2151
+ image: "host:" + mediaUrl(active.idx, off).replace(HOST_BASE + "/media", ""),
2152
+ source: "视频纹理:" + active.name, mpkgKey: key, mpkgName: file.name,
2153
+ info: { title: info ? info.title : "", properties: info ? info.properties : [] },
2154
+ slot: null, fromMpkg: true, converted: "mp4",
2155
+ timeVideos: undefined, timeConfig: undefined, activeSlot: null
2156
+ }, true);
2157
+ setHint(t("time.picked") + ":" + t("time." + (slot || "day")));
2158
+ } else {
2159
+ const { entry, slot } = pickBackgroundEntry(entries, new Date());
2160
+ if (!entry) { showError(t("mpkg.noAsset")); return; }
2161
+ const idx = entries.indexOf(entry);
2162
+ const isMp4 = /\.(mp4|webm|mov)$/i.test(entry.name);
2163
+ meta.entryName = entry.name;
2164
+ meta.slot = slot;
2165
+ commit({
2166
+ image: "host:" + mediaUrl(idx).replace(HOST_BASE + "/media", ""),
2167
+ source: entry.name, mpkgKey: key, mpkgName: file.name,
2168
+ info: { title: info ? info.title : "", properties: info ? info.properties : [] },
2169
+ slot: slot || null, fromMpkg: true, converted: isMp4 ? "mp4" : "gif",
2170
+ timeVideos: undefined, timeConfig: undefined, activeSlot: null
2171
+ }, true);
2172
+ if (isMp4) {
2173
+ setHint(slot ? t("time.picked") + ":" + t("time." + slot) : "");
2174
+ } else {
2175
+ setHint((slot ? t("time.picked") + ":" + t("time." + slot) + " · " : "") + t("mpkg.previewMode"));
2176
+ setPreviewModal(true);
2177
+ }
2178
+ }
2179
+ setMpkgMeta(meta);
2180
+ };
2181
+
2182
+ const onMpkg = async (e) => {
2183
+ const file = e.target.files && e.target.files[0];
2184
+ e.target.value = "";
2185
+ if (!file) return;
2186
+ // ③ 安全:先嗅探真实类型,拒绝 SVG/HTML/任意可执行内容
2187
+ const type = await sniffFileType(file);
2188
+ if (!type) { showError(t("file.unsafe")); return; }
2189
+ // ② 选择器静默兼容 mp4/视频:直接作为视频背景,界面不宣传
2190
+ if (type === "mp4" || type === "webm") {
2191
+ if (file.size > 100 * 1024 * 1024) { showError(t("file.tooLarge")); return; }
2192
+ setHint("");
2193
+ storeVideoBlob(file).then((marker) => commit({ image: marker, source: file.name }, true));
2194
+ return;
2195
+ }
2196
+ if (type !== "mpkg") { showError(t("file.unsafe")); return; }
2197
+ // ①(新) hybrid 模式:检测宿主端。可用 → 上传 + Range 流式播放(无 600MB 限制)
2198
+ const hybridOn = section.hybrid !== void 0 ? !!section.hybrid : DEFAULT_HYBRID;
2199
+ let hostOk = false;
2200
+ if (hybridOn) {
2201
+ try { const r = await fetch(HOST_BASE + "/ping", { method: "GET" }); hostOk = !!r.ok; } catch { hostOk = false; }
2202
+ }
2203
+ if (hostOk) {
2204
+ setBusy(true);
2205
+ setHint("");
2206
+ try {
2207
+ await importViaHost(file, t, showError, commit, setMpkgMeta, setHint, setBusy, setPreviewModal);
2208
+ } catch (err) {
2209
+ console.error("[dsh-mpkg-wallpaper] hybrid 导入失败:", err);
2210
+ showError(t("mpkg.fail") + String(err && err.message || err));
2211
+ } finally {
2212
+ setBusy(false);
2213
+ }
2214
+ return;
2215
+ }
2216
+ // ⑨(新) 整体大小上限(纯浏览器模式):超大 mpkg(>600MB)移动端浏览器几乎无法处理
2217
+ if (file.size > 600 * 1024 * 1024) { showError(t("mpkg.huge")); return; }
2218
+ setBusy(true);
2219
+ setHint("");
2220
+ try {
2221
+ // ③(重做) 按需读取:只读文件前部(头部+条目头,通常 <2MB)解析容器,
2222
+ // 条目数据(preview/视频纹理/project.json)用 file.slice 按需单独读。
2223
+ // 424MB 大包不再整体 arrayBuffer 进内存(移动端会 OOM 崩溃)。
2224
+ const HEAD_BYTES = 2 * 1024 * 1024;
2225
+ const headBuf = await file.slice(0, Math.min(file.size, HEAD_BYTES)).arrayBuffer();
2226
+ const mpkg = parseMpkg(headBuf);
2227
+ const readEntry = async (entry, limit, offset) => {
2228
+ const off = offset || 0;
2229
+ const len = limit ? Math.min(entry.size - off, limit) : entry.size - off;
2230
+ if (len <= 0) return new Uint8Array(0);
2231
+ return new Uint8Array(await file.slice(mpkg.dataStart + entry.index + off, mpkg.dataStart + entry.index + off + len).arrayBuffer());
2232
+ };
2233
+ const readEntryHead = (entry) => readEntry(entry, 65536);
2234
+ // 时间变化:扫描视频纹理(tex 内嵌 MP4),按壁纸时间设置选当前时段
2235
+ const vtex = [];
2236
+ for (const e of mpkg.entries) {
2237
+ // ①(修正) 视频纹理候选排除规则(基于语义,非特定壁纸):
2238
+ // - 抠像层:蓝幕/绿幕/抠像(只有人物,透明背景,不能当主背景)
2239
+ // - 入场动画:入场/开场/intro/entry animation(短开场,循环主背景才是要的)
2240
+ // ② 只把大尺寸视频纹理当主背景
2241
+ if (e.name.endsWith(".tex") && e.size > 5 * 1024 * 1024
2242
+ && !/蓝幕|绿幕|bluescreen|greenscreen|chroma|keying|抠像/i.test(e.name)
2243
+ && !/入场|开场|intro|entry\s*animation|entryanimation/i.test(e.name)) {
2244
+ const head = await readEntryHead(e);
2245
+ if (texHasVideo(head)) vtex.push(e);
2246
+ }
2247
+ }
2248
+ if (vtex.length) {
2249
+ const ok = await handleVideoTexes(mpkg, vtex, file, t, showError, readEntry);
2250
+ if (ok) {
2251
+ // ③(修正) 视频纹理导入成功后也要更新名称/素材显示(原来 setMpkgMeta(null)
2252
+ // 导致导入后元数据不刷新,需清壁纸或重复导入才更新)
2253
+ const s2 = readSection();
2254
+ setMpkgMeta(s2.fromMpkg && s2.mpkgKey && s2.info
2255
+ ? { name: s2.mpkgName, key: s2.mpkgKey, info: s2.info, entryName: s2.source || "preview.gif", slot: s2.slot }
2256
+ : null);
2257
+ setBusy(false);
2258
+ return;
2259
+ }
2260
+ // ⑨(新) 视频纹理无法提取 → 回退到 preview.gif(继续走下方图片路径)
2261
+ }
2262
+ let pick = pickBackgroundEntry(mpkg.entries, new Date());
2263
+ let entry = pick.entry, slot = pick.slot;
2264
+ if (!entry) { showError(t("mpkg.noAsset")); return; }
2265
+ let stored = null;
2266
+ let isVideo = false;
2267
+ const isMp4 = /\.(mp4|webm|mov)$/i.test(entry.name);
2268
+ // ② 修复:内嵌 mp4/mov(视频类壁纸)直接作为视频背景
2269
+ if (isMp4 && entry.size <= 600 * 1024 * 1024) {
2270
+ // ⑤(重做) 独立 mp4:file.slice 直接创建 Blob(懒引用,不读入 JS 内存)。
2271
+ // 洛茜系列是 260-445MB 的独立 mp4 壁纸,原 readEntry(arrayBuffer) 会 OOM;
2272
+ // Blob 引用文件区域,JS 内存峰值低,可尝试大视频(上限 600MB)
2273
+ const vblob = file.slice(mpkg.dataStart + entry.index, mpkg.dataStart + entry.index + entry.size, guessMime(entry.name));
2274
+ stored = await storeVideoBlob(vblob);
2275
+ isVideo = true;
2276
+ } else if (isMp4) {
2277
+ // ⑥(新) 超大独立视频(>600MB,如 zmd_01 的 747MB mp4):
2278
+ // 存储配额/播放内存都不可行 → 自动回退 preview 图片(至少能用上壁纸)
2279
+ const imgPick = pickBackgroundEntry(mpkg.entries.filter((e) => !/\.(mp4|webm|mov)$/i.test(e.name)), new Date());
2280
+ if (!imgPick || !imgPick.entry) { showError(t("mpkg.videoHuge")); return; }
2281
+ entry = imgPick.entry; slot = imgPick.slot;
2282
+ let bytes = await readEntry(entry);
2283
+ if (/\.gif$/i.test(entry.name)) bytes = ensureInfiniteGif(bytes.slice());
2284
+ const blob = new Blob([bytes], { type: guessMime(entry.name) });
2285
+ if (blob.size > 200 * 1024 * 1024) { showError(t("mpkg.tooLarge")); return; }
2286
+ stored = await storeImageBlob(blob);
2287
+ isVideo = false;
2288
+ setHint(t("mpkg.videoHuge"));
2289
+ } else {
2290
+ // 图片/GIF
2291
+ let bytes = await readEntry(entry);
2292
+ if (/\.gif$/i.test(entry.name)) bytes = ensureInfiniteGif(bytes.slice());
2293
+ const blob = new Blob([bytes], { type: guessMime(entry.name) });
2294
+ // ③(新) 大图片走 Blob 存储(idb:img),不走 dataURL(防膨胀爆内存);
2295
+ // 上限 50MB → 200MB(大壁纸包预览图经常很大)
2296
+ if (blob.size > 200 * 1024 * 1024) { setHint(t("mpkg.tooLarge")); return; }
2297
+ stored = await storeImageBlob(blob);
2298
+ }
2299
+ const info = await extractProjectInfoAsync(mpkg.entries, readEntry);
2300
+ const key = file.name + "|" + file.size;
2301
+ const meta = { name: file.name, key, info, entryName: entry.name, slot };
2302
+ commit({
2303
+ image: stored, source: entry.name, mpkgKey: key, mpkgName: file.name,
2304
+ info: { title: info ? info.title : "", properties: info ? info.properties : [] },
2305
+ slot: slot || null, fromMpkg: true, converted: isVideo ? "mp4" : "gif",
2306
+ // ⑦(修正) 非视频纹理壁纸不带时间变化:清掉上一个壁纸残留的时段配置
2307
+ timeVideos: undefined, timeConfig: undefined, activeSlot: null
2308
+ }, true);
2309
+ setMpkgMeta(meta);
2310
+ // ①(修正) 视频→图片切换:强制清掉背景内容缓存,确保新壁纸立即显示
2311
+ try { lastBgSig = null; } catch {}
2312
+ // ①(修正) 仅当最终显示的是图片/GIF(非 mp4)才提示预览模式;
2313
+ // 独立 mp4 视频壁纸(洛茜_01 等)不弹窗、不显示 GIF 提示
2314
+ if (isVideo) {
2315
+ setHint(slot ? t("time.picked") + ":" + t("time." + slot) : "");
2316
+ } else {
2317
+ setHint((slot ? t("time.picked") + ":" + t("time." + slot) + " · " : "") + t("mpkg.previewMode"));
2318
+ try { setPreviewModal(true); } catch {}
2319
+ }
2320
+ } catch (err) {
2321
+ console.error("[dsh-mpkg-wallpaper] onMpkg 失败:", err);
2322
+ // ⑨(新) 报错友好化:区分存储配额不足 / 内存不足 / 解析失败
2323
+ const en = err && err.name;
2324
+ const em = String(err && err.message || err);
2325
+ showError(en === "QuotaExceededError"
2326
+ ? t("mpkg.quota")
2327
+ : /memory|allocat|out of|ArrayBuffer|too large/i.test(em)
2328
+ ? t("mpkg.oom")
2329
+ : t("mpkg.fail") + em);
2330
+ } finally {
2331
+ setBusy(false);
2332
+ }
2333
+ };
2334
+
2335
+ const onMedia = async (e) => {
2336
+ const file = e.target.files && e.target.files[0];
2337
+ e.target.value = "";
2338
+ if (!file) return;
2339
+ // ③ 安全:先嗅探真实类型,拒绝 SVG/HTML 等
2340
+ const type = await sniffFileType(file);
2341
+ if (!type || (type !== "png" && type !== "gif" && type !== "jpeg" && type !== "webp" && type !== "mp4" && type !== "webm")) {
2342
+ setHint(t("file.unsafe"));
2343
+ return;
2344
+ }
2345
+ if (file.size > 100 * 1024 * 1024) { setHint(t("file.tooLarge")); return; }
2346
+ setMpkgMeta(null);
2347
+ if (type === "mp4" || type === "webm") {
2348
+ // ① 视频背景:存入 IndexedDB,<video> 循环播放
2349
+ setHint("");
2350
+ storeVideoBlob(file).then((marker) => commit({ image: marker, source: file.name }, true));
2351
+ return;
2352
+ }
2353
+ // ③(新) 图片:>2MB 直接存 Blob(idb:img,防 dataURL 膨胀);小图走 dataURL
2354
+ setHint("");
2355
+ storeImageBlob(file).then((stored) => commit({ image: stored, source: file.name }, true));
2356
+ };
2357
+
2358
+ const applyUrl = () => {
2359
+ const safe = sanitizeImageUrl(url);
2360
+ if (!safe) { setHint(t("url.unsafe")); return; }
2361
+ setMpkgMeta(null);
2362
+ commit({ image: safe, source: "url", fromMpkg: false }, true);
2363
+ };
2364
+
2365
+
2366
+ const propEdits = (section.propEdits && section.propEdits[section.mpkgKey]) || {};
2367
+ const setProp = (key, value) => {
2368
+ const key2 = section.mpkgKey;
2369
+ if (!key2 || !safePropKey(key)) return;
2370
+ const edits = Object.assign({}, propEdits, { [key]: value });
2371
+ const propEditsAll = Object.assign({}, section.propEdits || {}, { [key2]: edits });
2372
+ commit({ propEdits: propEditsAll }, true);
2373
+ };
2374
+ const allProps = mpkgMeta && mpkgMeta.info ? mpkgMeta.info.properties.slice() : [];
2375
+ // ④ 随现实时间变化关闭时,隐藏时间设置(清晨/白天/黄昏/夜晚开始时间 + 时间段选择)
2376
+ const tvProp = allProps.find((p) => /随现实时间|timevarying|real time/.test(p.label + " " + p.key));
2377
+ const tvEnabled = tvProp ? (propEdits[tvProp.key] !== void 0 ? !!propEdits[tvProp.key] : !!tvProp.value) : true;
2378
+ const TIME_KEYS = new Set(["morningtime", "daytime", "dusktime", "nighttime", "display"]);
2379
+ // ④(重做) 参数渲染暂不可用(浏览器显示的是预渲染素材,改了不生效):
2380
+ // 默认全部折叠,只显示「展开全部」按钮;点击展开后只读展示全部参数。
2381
+ const propsShown = tvEnabled ? allProps : allProps.filter((p) => !TIME_KEYS.has(p.key));
2382
+ const propsToShow = propsExpanded ? propsShown : [];
2383
+
2384
+ const h = react.createElement;
2385
+ const sliderRow = (label, field, min, max, suffix, step, def) => {
2386
+ const val = section[field] !== void 0 ? section[field] : def;
2387
+ const rangeRef = react.useRef(null);
2388
+ const numRef = react.useRef(null);
2389
+ const timer = react.useRef(null);
2390
+ const rafRef = react.useRef(null);
2391
+ const dragging = react.useRef(false);
2392
+ const fmt = (v) => (Number.isInteger(v) ? String(v) : String(Number(v.toFixed(2))));
2393
+ const apply = (v) => {
2394
+ let n = Number(v);
2395
+ if (isNaN(n)) return;
2396
+ n = Math.max(min, Math.min(max, n));
2397
+ if (rangeRef.current && String(rangeRef.current.value) !== String(n)) rangeRef.current.value = n;
2398
+ if (numRef.current && String(numRef.current.value) !== String(fmt(n))) numRef.current.value = fmt(n);
2399
+ previewCss(Object.assign({ [field]: n }, dragging.current ? { _noBlur: true } : {}));
2400
+ if (timer.current) clearTimeout(timer.current);
2401
+ timer.current = setTimeout(() => commit({ [field]: n }), 350);
2402
+ };
2403
+ // ⑤ 外部值变化(恢复默认等)时同步滑杆与数值框
2404
+ react.useEffect(() => {
2405
+ if (dragging.current) return;
2406
+ if (rangeRef.current && String(rangeRef.current.value) !== String(val)) rangeRef.current.value = val;
2407
+ if (numRef.current && String(numRef.current.value) !== String(fmt(val))) numRef.current.value = fmt(val);
2408
+ });
2409
+ const onRangeInput = (ev) => {
2410
+ dragging.current = true;
2411
+ const v = Number(ev.target.value);
2412
+ if (numRef.current) numRef.current.value = fmt(v);
2413
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
2414
+ rafRef.current = requestAnimationFrame(() => { previewCss(Object.assign({ [field]: v }, { _noBlur: true })); });
2415
+ if (timer.current) clearTimeout(timer.current);
2416
+ timer.current = setTimeout(() => { commit({ [field]: v }); }, 350);
2417
+ };
2418
+ const onNumInput = (ev) => {
2419
+ dragging.current = true;
2420
+ if (timer.current) clearTimeout(timer.current);
2421
+ // 清洗:只留数字和一个小数点;禁科学计数法(e/E/±)、限长 7 位、最多 2 位小数
2422
+ let raw = ev.target.value;
2423
+ raw = raw.replace(/[^0-9.]/g, "");
2424
+ const dot = raw.indexOf(".");
2425
+ if (dot >= 0) {
2426
+ raw = raw.slice(0, dot + 1) + raw.slice(dot + 1).replace(/\./g, "").slice(0, 2);
2427
+ }
2428
+ if (raw.length > 7) raw = raw.slice(0, 7);
2429
+ if (numRef.current && numRef.current.value !== raw) numRef.current.value = raw;
2430
+ // 空/非法输入:不提交,等失焦收敛
2431
+ if (raw === "" || raw === "." || isNaN(Number(raw))) return;
2432
+ const c = Math.max(min, Math.min(max, Number(raw)));
2433
+ if (rangeRef.current) rangeRef.current.value = c;
2434
+ previewCss(Object.assign({ [field]: c }, { _noBlur: true }));
2435
+ timer.current = setTimeout(() => commit({ [field]: c }), 500);
2436
+ };
2437
+ const onNumBlur = (ev) => {
2438
+ dragging.current = false;
2439
+ if (timer.current) { clearTimeout(timer.current); timer.current = null; }
2440
+ const raw = numRef.current ? numRef.current.value : "";
2441
+ let n = Number(raw);
2442
+ if (raw === "" || isNaN(n)) n = val;
2443
+ n = Math.max(min, Math.min(max, n));
2444
+ // 失焦时收敛并写回输入框
2445
+ if (numRef.current) numRef.current.value = fmt(n);
2446
+ apply(n);
2447
+ };
2448
+ const endDrag = () => {
2449
+ dragging.current = false;
2450
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
2451
+ previewCss({});
2452
+ };
2453
+ return h("div", { className: "mpw_field" }, [
2454
+ h("label", { className: "mpw_label" }, label),
2455
+ h("div", { className: "mpw_inline" }, [
2456
+ h("input", {
2457
+ ref: rangeRef,
2458
+ className: "mpw_slider", type: "range", min, max, step: step || 1,
2459
+ defaultValue: val, onInput: onRangeInput,
2460
+ onPointerDown: () => { dragging.current = true; },
2461
+ onPointerUp: endDrag,
2462
+ onPointerCancel: endDrag,
2463
+ onKeyUp: endDrag
2464
+ }),
2465
+ h("input", {
2466
+ ref: numRef,
2467
+ className: "mpw_numInput", type: "text", inputMode: "decimal", autoComplete: "off",
2468
+ defaultValue: fmt(val),
2469
+ onInput: onNumInput,
2470
+ onBlur: onNumBlur,
2471
+ onKeyDown: (ev) => {
2472
+ // 双保险:直接拦截 e/E/+/-(科学计数法键)
2473
+ if (ev.key === "e" || ev.key === "E" || ev.key === "+" || ev.key === "-") ev.preventDefault();
2474
+ }
2475
+ }),
2476
+ h("span", { className: "mpw_value" }, suffix),
2477
+ // ③(新) 「默认」按钮:一键恢复该滑条默认值
2478
+ def !== void 0
2479
+ ? h("button", { className: "mpw_reset mpw_miniBtn", type: "button", onClick: () => apply(def) }, t("default"))
2480
+ : null
2481
+ ])
2482
+ ]);
2483
+ };
2484
+ const toggleRow = (label, desc, field, def) => {
2485
+ const checked = section[field] !== void 0 ? !!section[field] : def;
2486
+ return h("div", { className: "mpw_field" }, [
2487
+ h("label", { className: "mpw_label" }, label),
2488
+ h("div", { className: "mpw_inline" }, [
2489
+ h(Toggle, { checked, onChange: (v) => commit({ [field]: v }) }),
2490
+ h("span", { className: "mpw_hint" }, desc)
2491
+ ])
2492
+ ]);
2493
+ };
2494
+
2495
+ return h("div", { className: "mpw_row" }, [
2496
+ h("div", { className: "mpw_title" }, t("title")),
2497
+ h("p", { className: "mpw_desc" }, t("desc")),
2498
+
2499
+ // ═══ 背景来源 ═══
2500
+ h("div", { className: "mpw_section" }, t("sec.source")),
2501
+
2502
+ // ④ 总开关:开启/关闭整个功能
2503
+ h("div", { className: "mpw_field" }, [
2504
+ h("label", { className: "mpw_label" }, t("master")),
2505
+ h("div", { className: "mpw_inline" }, [
2506
+ h(Toggle, {
2507
+ checked: section.enabled !== void 0 ? !!section.enabled : DEFAULT_ENABLED,
2508
+ onChange: onMaster
2509
+ }),
2510
+ h("span", { className: "mpw_hint" }, section.enabled !== void 0 && !section.enabled ? t("master.off") : t("master.desc"))
2511
+ ]),
2512
+ conflicts.length ? h("p", { className: "mpw_hint" }, `${t("conflict.detected")}:${conflicts.join(", ")}`) : null
2513
+ ]),
2514
+
2515
+ // ①(新) 大文件混合模式开关(背景来源组)
2516
+ toggleRow(t("hybrid"), t("hybrid.desc"), "hybrid", DEFAULT_HYBRID),
2517
+
2518
+ // ①(新) 自定义本地壁纸目录(任意文件夹,不限于 Wallpaper Engine)
2519
+ h("div", { className: "mpw_field" }, [
2520
+ h("label", { className: "mpw_label" }, t("lib.custom")),
2521
+ h("div", { className: "mpw_inline" }, [
2522
+ h("input", { className: "mpw_input", type: "text", style: { flex: 1, minWidth: 0 },
2523
+ value: customDir,
2524
+ placeholder: t("lib.dirPlaceholder"),
2525
+ onInput: (ev) => setCustomDirState(ev.target.value) }),
2526
+ h("button", { className: "mpw_fileBtn", type: "button", onClick: () => openDirPicker(customDir || (section.customDirPath || "")) }, t("lib.browse")),
2527
+ h("button", { className: "mpw_fileBtn", type: "button", onClick: () => {
2528
+ commit({ customDirPath: customDir.trim() }, true);
2529
+ scanCustomDir();
2530
+ } }, t("lib.scanDir"))
2531
+ ]),
2532
+ customFiles && customFiles.length ? h("p", { className: "mpw_hint" }, t("lib.dirFound") + customFiles.length + t("lib.dirFiles")) : null,
2533
+ wallList.length ? h("div", { className: "mpw_inline" }, [
2534
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => setLibOpen(!libOpen) }, libOpen ? t("lib.collapse") : t("lib.expandAll") + `(${wallList.length})`)
2535
+ ]) : null,
2536
+ libOpen && wallList.length ? h("div", { className: "mpw_props", style: { maxHeight: 260, overflowY: "auto" } }, [
2537
+ wallList.slice(0, libShow).map((w, wi) => h("div", { className: "mpw_prop", key: w.key }, [
2538
+ h("div", { className: "mpw_propLabel" }, h("b", null, w.title)),
2539
+ h("div", { className: "mpw_inline" }, [
2540
+ h("span", { className: "mpw_hint" }, w.type),
2541
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => { setWallIdx(wallList.indexOf(w)); applyWallFromList(w); } }, t("lib.use"))
2542
+ ])
2543
+ ])),
2544
+ wallList.length > libShow
2545
+ ? h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => setLibShow(libShow + 10) }, t("lib.more") + "(" + (wallList.length - libShow) + ")")
2546
+ : null
2547
+ ]) : null
2548
+ ]),
2549
+
2550
+ // ②③(新) 下一个壁纸 + 定时轮换
2551
+ h("div", { className: "mpw_field" }, [
2552
+ h("label", { className: "mpw_label" }, t("lib.rotate")),
2553
+ h("div", { className: "mpw_inline" }, [
2554
+ h("button", { className: "mpw_reset", type: "button", onClick: prevWallpaper }, t("lib.prev")),
2555
+ h("button", { className: "mpw_reset", type: "button", onClick: nextWallpaper }, t("lib.next")),
2556
+ h(Toggle, { checked: section.rotate !== void 0 ? !!section.rotate : DEFAULT_ROTATE, onChange: (v) => commit({ rotate: v }) }),
2557
+ h("span", { className: "mpw_hint" }, t("lib.rotateDesc"))
2558
+ ]),
2559
+ h("div", { style: { display: (section.rotate !== void 0 ? !!section.rotate : DEFAULT_ROTATE) ? "" : "none" } },
2560
+ sliderRow(t("lib.rotateMin"), "rotateMin", 1, 120, t("lib.minutes"), 1, 5))
2561
+ ]),
2562
+
2563
+ // ④(新) 本地壁纸库(Steam 自动发现)
2564
+ h("div", { className: "mpw_field" }, [
2565
+ h("label", { className: "mpw_label" }, t("lib.title")),
2566
+ h("div", { className: "mpw_inline" }, [
2567
+ h("button", { className: "mpw_fileBtn", type: "button", onClick: scanLibrary }, libBusy ? t("mpkg.busy") : t("lib.scan")),
2568
+ h("span", { className: "mpw_hint" }, t("lib.desc"))
2569
+ ]),
2570
+ libWalls && libWalls.length
2571
+ ? h("div", { className: "mpw_inline" }, [
2572
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => setLibOpen(!libOpen) }, libOpen ? t("lib.collapse") : t("lib.expandAll") + `(${libWalls.length})`)
2573
+ ])
2574
+ : null,
2575
+ libOpen && libWalls && libWalls.length
2576
+ ? h("div", { className: "mpw_props", style: { maxHeight: 220, overflowY: "auto" } }, [
2577
+ libWalls.slice(0, libShow).map((wp, wi) => h("div", { className: "mpw_prop", key: wp.ltoken }, [
2578
+ h("div", { className: "mpw_propLabel" }, h("b", null, wp.title || wp.ltoken)),
2579
+ h("div", { className: "mpw_inline" }, [
2580
+ h("span", { className: "mpw_hint" }, wp.type),
2581
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => {
2582
+ // ②(修正) 用 wallList 中的索引对齐轮播(wallIdx 是 wallList 下标)
2583
+ const mi = wallList.findIndex((w) => w.key === "steam|" + wp.ltoken);
2584
+ if (mi >= 0) setWallIdx(mi);
2585
+ applyLibraryWallpaper(wp);
2586
+ } }, t("lib.use"))
2587
+ ])
2588
+ ])),
2589
+ libWalls.length > libShow
2590
+ ? h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => setLibShow(libShow + 10) }, t("lib.more") + "(" + (libWalls.length - libShow) + ")")
2591
+ : null
2592
+ ])
2593
+ : libWalls && !libWalls.length ? h("p", { className: "mpw_hint" }, t("lib.empty")) : null
2594
+ ]),
2595
+ // ③(新) 宿主端状态(proot/本机测试方法:可用 = 大文件无限制生效)
2596
+ h("p", { className: "mpw_hint" },
2597
+ (section.hybrid !== void 0 ? !!section.hybrid : DEFAULT_HYBRID)
2598
+ ? (hostOk === true ? "混合模式:宿主端可用(大文件无 600MB 限制)"
2599
+ : hostOk === false ? "混合模式:宿主端不可用(已回退纯浏览器模式,600MB 上限)"
2600
+ : "混合模式:检测宿主端中…")
2601
+ : "纯浏览器模式(600MB 上限)"),
2602
+
2603
+ // mpkg 文件
2604
+ h("div", { className: "mpw_field" }, [
2605
+ h("div", { className: "mpw_inline" }, [
2606
+ h("button", { className: "mpw_fileBtn", type: "button", onClick: () => mpkgRef.current && mpkgRef.current.click() },
2607
+ busy ? t("mpkg.busy") : t("mpkg.pick")),
2608
+ h("span", { className: "mpw_hint" }, t("mpkg.hint"))
2609
+ ]),
2610
+ h("input", { ref: mpkgRef, type: "file", accept: ".mpkg,.mp4,.webm,.mkv,.mov", style: { display: "none" }, onChange: onMpkg }),
2611
+ // ① 单独清除背景的控件(不重置外观数值)
2612
+ section.image ? h("div", { className: "mpw_inline" }, [
2613
+ h("button", { className: "mpw_reset", type: "button", onClick: clearBg }, t("clear.bg"))
2614
+ ]) : null,
2615
+ mpkgMeta && mpkgMeta.info && section.fromMpkg ? h("div", { className: "mpw_props" }, [
2616
+ mpkgMeta.name ? h("p", { className: "mpw_info" }, h("b", null, mpkgMeta.name)) : null,
2617
+ mpkgMeta.info && mpkgMeta.info.title && mpkgMeta.info.title !== mpkgMeta.name ? h("p", { className: "mpw_hint" }, mpkgMeta.info.title) : null,
2618
+ h("p", { className: "mpw_hint" }, `${t("mpkg.using")}:${mpkgMeta.entryName}${section.converted === "mp4" ? "(视频)" : ""}${section.timeConfig && section.timeConfig.enabled && section.timeVideos && section.timeVideos.length ? " · " + t("time.now") + ":" + t("time." + slotForTime(section.timeConfig, new Date())) : ""}`)
2619
+ ]) : null
2620
+ ]),
2621
+
2622
+ // 可调参数展示(④(重做):默认折叠成「展开全部」按钮 + 只读 + "暂不可用"——
2623
+ // 浏览器显示的是预渲染素材,修改参数不会生效;值仅供对照壁纸引擎 App 使用)
2624
+ mpkgMeta && mpkgMeta.info && section.fromMpkg && allProps.length ? h("div", { className: "mpw_field" }, [
2625
+ h("label", { className: "mpw_label" }, `${t("props.title")}(${t("props.unavailable")})`),
2626
+ h("p", { className: "mpw_hint" }, t("props.desc")),
2627
+ propsExpanded ? h("div", { className: "mpw_props" }, [
2628
+ propsToShow.map((p) => {
2629
+ // 纯展示性条目(作者信息/说明):不渲染输入框
2630
+ if (p.displayOnly) {
2631
+ return h("div", { className: "mpw_prop mpw_static", key: p.key },
2632
+ h("span", { className: "mpw_propLabel" }, p.label));
2633
+ }
2634
+ const edited = propEdits[p.key] !== void 0 ? propEdits[p.key] : p.value;
2635
+ const isBool = typeof p.value === "boolean";
2636
+ const fmt = (v) => {
2637
+ if (typeof v === "boolean") return v ? t("props.on") : t("props.off");
2638
+ if (Array.isArray(p.options) && p.options.length) {
2639
+ const o = p.options.find((x) => String(x.value) === String(v));
2640
+ return o ? cleanLabel(o.label) : String(v);
2641
+ }
2642
+ return String(v);
2643
+ };
2644
+ return h("div", { className: "mpw_prop", key: p.key }, [
2645
+ h("div", { className: "mpw_propLabel" }, h("b", null, p.label)),
2646
+ h("span", { className: "mpw_propValue" }, fmt(edited))
2647
+ ]);
2648
+ }),
2649
+ !tvEnabled ? h("p", { className: "mpw_hint" }, t("props.tvOff")) : null,
2650
+ // ⑥(重做) 重置壁纸参数:放进折叠内容区(展开参数后显示在列表末尾)
2651
+ h("div", { className: "mpw_inline", style: { marginTop: 6 } }, [
2652
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => {
2653
+ const s = readSection();
2654
+ if (!s.mpkgKey) { setHint(t("props.none")); return; }
2655
+ const pe = Object.assign({}, s.propEdits || {});
2656
+ delete pe[s.mpkgKey];
2657
+ const next = Object.assign({}, s, { propEdits: pe });
2658
+ writeSection(next, true);
2659
+ setSection(next);
2660
+ setHint(t("props.resetDone"));
2661
+ } }, t("props.resetWallpaper"))
2662
+ ])
2663
+ ]) : null,
2664
+ h("div", { className: "mpw_inline" }, [
2665
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => setPropsExpanded(!propsExpanded) },
2666
+ propsExpanded ? t("props.collapse") : t("props.expand") + `(${propsShown.length})`)
2667
+ ])
2668
+ ]) : null,
2669
+
2670
+ // 图片链接
2671
+ h("div", { className: "mpw_field" }, [
2672
+ h("label", { className: "mpw_label" }, t("url.label")),
2673
+ h("div", { className: "mpw_inline" }, [
2674
+ h("input", {
2675
+ className: "mpw_input", type: "text", value: url,
2676
+ placeholder: t("url.placeholder"),
2677
+ onChange: (ev) => setUrl(ev.target.value),
2678
+ onKeyDown: (ev) => { if (ev.key === "Enter") applyUrl(); }
2679
+ }),
2680
+ h("button", { className: "mpw_button", type: "button", onClick: applyUrl }, t("url.apply"))
2681
+ ])
2682
+ ]),
2683
+
2684
+ // 本地图片/动图
2685
+ h("div", { className: "mpw_field" }, [
2686
+ h("div", { className: "mpw_inline" }, [
2687
+ h("button", { className: "mpw_fileBtn", type: "button", onClick: () => imgRef.current && imgRef.current.click() }, t("file.pick")),
2688
+ h("span", { className: "mpw_hint" }, t("file.hint"))
2689
+ ]),
2690
+ h("input", { ref: imgRef, type: "file", accept: "image/*,.gif,.mp4,.webm,.mkv,.mov", style: { display: "none" }, onChange: onMedia })
2691
+ ]),
2692
+
2693
+ // ═══ 外观 ═══
2694
+ h("div", { className: "mpw_section" }, t("sec.appearance")),
2695
+ // ②(新) 外观组说明:各项作用一目了然
2696
+ h("p", { className: "mpw_hint" }, t("sec.appearance.desc")),
2697
+
2698
+ toggleRow(t("float"), t("float.desc"), "float", DEFAULT_FLOAT),
2699
+ // ①(修正) 面板不透明度已删除:统一虚化开启时它被 sidebarAlpha 取代且无效果,
2700
+ // 非统一虚化下也无独立意义 → 移除滑条,保留内部默认值逻辑。
2701
+ // ①(修正) 磨砂模糊条:仅当「统一虚化开 + 聊天区跟随开」时被整屏虚化接管 → 禁用并提示;
2702
+ // 聊天区跟随关 → 磨砂条恢复可调(聊天区壁纸由它控制,统一虚化只管侧边栏/标题栏)
2703
+ h("div", { style: ((section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT) && (section.chatFollow !== void 0 ? !!section.chatFollow : DEFAULT_CHAT_FOLLOW)) ? { opacity: 0.45, pointerEvents: "none" } : {} },
2704
+ sliderRow(t("blur"), "blur", 0, 40, "px", 1, DEFAULT_BLUR)),
2705
+ // 仅当被接管时提示
2706
+ ((section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT) && (section.chatFollow !== void 0 ? !!section.chatFollow : DEFAULT_CHAT_FOLLOW))
2707
+ ? h("p", { className: "mpw_hint" }, t("blur.overridden"))
2708
+ : null,
2709
+ sliderRow(t("zoom"), "zoom", 10, 2000, "%", 5, DEFAULT_ZOOM),
2710
+ sliderRow(t("brightness"), "brightness", 50, 150, "%", 1, DEFAULT_BRIGHTNESS),
2711
+ // ⑥ 镜头位置(平移)
2712
+ h("div", { className: "mpw_field" }, [
2713
+ h("label", { className: "mpw_label" }, t("lens.pos")),
2714
+ h("div", { className: "mpw_inline" }, [
2715
+ h("input", {
2716
+ className: "mpw_numInput", type: "text", inputMode: "decimal", autoComplete: "off",
2717
+ defaultValue: section.lensX !== void 0 ? section.lensX : 0,
2718
+ onInput: (ev) => {
2719
+ // 清洗:只留数字和一个小数点;禁科学计数法、限长 7 位、最多 2 位小数
2720
+ let raw = ev.target.value;
2721
+ raw = raw.replace(/[^0-9.-]/g, "");
2722
+ const firstMinus = raw.indexOf("-");
2723
+ if (firstMinus > 0) raw = raw.slice(0, firstMinus) + raw.slice(firstMinus + 1);
2724
+ const minus = raw.startsWith("-") ? "-" : "";
2725
+ const body = minus ? raw.slice(1) : raw;
2726
+ const dot = body.indexOf(".");
2727
+ let clean = dot >= 0 ? body.slice(0, dot + 1) + body.slice(dot + 1).replace(/\./g, "").slice(0, 2) : body;
2728
+ if (clean.length > 7) clean = clean.slice(0, 7);
2729
+ raw = minus + clean;
2730
+ if (ev.target.value !== raw) ev.target.value = raw;
2731
+ if (raw === "" || raw === "-" || raw === "." || raw === "-." || isNaN(Number(raw))) return;
2732
+ const v = Number(raw);
2733
+ if (!isNaN(v)) commit({ lensX: Math.max(-2000, Math.min(2000, v)) });
2734
+ },
2735
+ onKeyDown: (ev) => {
2736
+ if (ev.key === "e" || ev.key === "E" || ev.key === "+") ev.preventDefault();
2737
+ }
2738
+ }),
2739
+ h("input", {
2740
+ className: "mpw_numInput", type: "text", inputMode: "decimal", autoComplete: "off",
2741
+ defaultValue: section.lensY !== void 0 ? section.lensY : 0,
2742
+ onInput: (ev) => {
2743
+ let raw = ev.target.value;
2744
+ raw = raw.replace(/[^0-9.-]/g, "");
2745
+ const firstMinus = raw.indexOf("-");
2746
+ if (firstMinus > 0) raw = raw.slice(0, firstMinus) + raw.slice(firstMinus + 1);
2747
+ const minus = raw.startsWith("-") ? "-" : "";
2748
+ const body = minus ? raw.slice(1) : raw;
2749
+ const dot = body.indexOf(".");
2750
+ let clean = dot >= 0 ? body.slice(0, dot + 1) + body.slice(dot + 1).replace(/\./g, "").slice(0, 2) : body;
2751
+ if (clean.length > 7) clean = clean.slice(0, 7);
2752
+ raw = minus + clean;
2753
+ if (ev.target.value !== raw) ev.target.value = raw;
2754
+ if (raw === "" || raw === "-" || raw === "." || raw === "-." || isNaN(Number(raw))) return;
2755
+ const v = Number(raw);
2756
+ if (!isNaN(v)) commit({ lensY: Math.max(-2000, Math.min(2000, v)) });
2757
+ },
2758
+ onKeyDown: (ev) => {
2759
+ if (ev.key === "e" || ev.key === "E" || ev.key === "+") ev.preventDefault();
2760
+ }
2761
+ }),
2762
+ h("span", { className: "mpw_hint" }, `${t("lens.x")} / ${t("lens.y")}`),
2763
+ // ③(新) 镜头位置「默认」按钮:恢复 0/0
2764
+ h("button", { className: "mpw_reset mpw_miniBtn", type: "button", onClick: () => commit({ lensX: 0, lensY: 0 }, true) }, t("default"))
2765
+ ])
2766
+ ]),
2767
+
2768
+ // ═══ 统一虚化(独立大标题分组)═══
2769
+ h("div", { className: "mpw_section" }, t("sec.unify")),
2770
+ h("p", { className: "mpw_hint" }, t("sec.unify.desc")),
2771
+
2772
+ // ①(重做) 统一虚化:开关 + 独立条(0 = 不虚化,40 = 强毛玻璃模糊)。
2773
+ // 开时其他虚化设置全部作废,只按这个条;设置界面/聊天框除外(按各自设置)。
2774
+ toggleRow(t("unifyTint"), t("unifyTint.desc"), "unifyTint", DEFAULT_UNIFY_TINT),
2775
+ // ⑥ 条件显示滑条:⚠️ sliderRow 内含 hooks,绝不能条件渲染(React 会因
2776
+ // hooks 数量变化崩溃导致整页空白)——始终渲染,用 wrapper display 隐藏。
2777
+ h("div", { style: { display: (section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT) ? "" : "none" } },
2778
+ sliderRow(t("unifyAmount"), "unifyAmount", 0, 40, "px", 1, DEFAULT_UNIFY_AMOUNT)),
2779
+ // ①(新) 侧边栏/标题栏透明度(统一虚化开启时显示):白雾厚度独立可调,
2780
+ // 与虚化程度解耦(低 = 透出更多模糊壁纸,高 = 更实心)
2781
+ h("div", { style: { display: (section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT) ? "" : "none" } },
2782
+ sliderRow(t("sidebarAlpha"), "sidebarAlpha", 0, 100, "%", 1, DEFAULT_SIDEBAR_ALPHA)),
2783
+ // ①(新) 统一虚化是否接管聊天区壁纸(统一虚化开启时显示):
2784
+ // 开 = 聊天区也跟随整屏虚化(磨砂条被接管禁用);
2785
+ // 关 = 磨砂条恢复可调(聊天区壁纸由磨砂条控制),统一虚化只管侧边栏/标题栏
2786
+ h("div", { style: { display: (section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT) ? "" : "none" } },
2787
+ toggleRow(t("chatFollow"), t("chatFollow.desc"), "chatFollow", DEFAULT_CHAT_FOLLOW)),
2788
+ // ④(新) 新会话按钮是否随面板不透明度(统一虚化开启时显示);关 = 保持原按钮色
2789
+ h("div", { style: { display: (section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT) ? "" : "none" } },
2790
+ toggleRow(t("sessionFollow"), t("sessionFollow.desc"), "sessionFollow", DEFAULT_SESSION_FOLLOW)),
2791
+
2792
+ // ═══ 界面虚化 ═══
2793
+ h("div", { className: "mpw_section" }, t("sec.blur")),
2794
+ h("p", { className: "mpw_hint" }, t("sec.blur.desc")),
2795
+
2796
+ // ── 虚化对话框(通用居中窗口 + 聊天输入框跟随)──
2797
+ toggleRow(t("dialogBlur"), t("dialogBlur.desc"), "dialogBlur", DEFAULT_DIALOG_BLUR),
2798
+ h("div", { style: { display: (section.dialogBlur !== void 0 ? !!section.dialogBlur : DEFAULT_DIALOG_BLUR) ? "" : "none" } },
2799
+ sliderRow(t("dialogBlurAmount"), "dialogAmount", 0, 40, "px", 1, DEFAULT_DIALOG_AMOUNT)),
2800
+
2801
+ // ── 虚化设置面板 ──
2802
+ toggleRow(t("settingsBlur"), t("settingsBlur.desc"), "settingsBlur", DEFAULT_SETTINGS_BLUR),
2803
+ h("div", { style: { display: (section.settingsBlur !== void 0 ? !!section.settingsBlur : DEFAULT_SETTINGS_BLUR) ? "" : "none" } },
2804
+ sliderRow(t("settingsBlurAmount"), "settingsAmount", 0, 40, "px", 1, DEFAULT_SETTINGS_AMOUNT)),
2805
+
2806
+ // ── 虚化下载/确认弹窗 ──
2807
+ toggleRow(t("confirmBlur"), t("confirmBlur.desc"), "confirmBlur", DEFAULT_CONFIRM_BLUR),
2808
+ h("div", { style: { display: (section.confirmBlur !== void 0 ? !!section.confirmBlur : DEFAULT_CONFIRM_BLUR) ? "" : "none" } },
2809
+ sliderRow(t("confirmBlurAmount"), "confirmAmount", 0, 40, "px", 1, DEFAULT_CONFIRM_AMOUNT)),
2810
+
2811
+ // ⑮(新) 浏览器虚化支持提示(Via/WebView 常声明支持但不渲染真模糊)
2812
+ window.__mpwBackdropRendered === false
2813
+ ? h("p", { className: "mpw_hint" }, t("blur.unsupported"))
2814
+ : null,
2815
+
2816
+ // ── 弹层虚化(菜单/提示/下拉,独立于对话框)──
2817
+ toggleRow(t("popoverBlur"), t("popoverBlur.desc"), "popoverBlur", DEFAULT_POPOVER_BLUR),
2818
+ h("div", { style: { display: (section.popoverBlur !== void 0 ? !!section.popoverBlur : DEFAULT_POPOVER_BLUR) ? "" : "none" } },
2819
+ sliderRow(t("popoverBlurAmount"), "popoverAmount", 0, 40, "px", 1, DEFAULT_POPOVER_AMOUNT)),
2820
+
2821
+ // ── 遮罩虚化(设置/弹层打开时的全屏背景遮罩)──
2822
+ toggleRow(t("maskBlur"), t("maskBlur.desc"), "maskBlur", DEFAULT_MASK_BLUR),
2823
+ h("div", { style: { display: (section.maskBlur !== void 0 ? !!section.maskBlur : DEFAULT_MASK_BLUR) ? "" : "none" } },
2824
+ sliderRow(t("maskBlurAmount"), "maskAmount", 0, 40, "px", 1, DEFAULT_MASK_AMOUNT)),
2825
+
2826
+ // ═══ 透出壁纸 ═══
2827
+ h("div", { className: "mpw_section" }, t("sec.show")),
2828
+ h("p", { className: "mpw_hint" }, t("sec.show.desc")),
2829
+
2830
+ // ⑥ 侧边栏透出开关
2831
+ toggleRow(t("sidebar"), t("sidebar.desc"), "sidebar", DEFAULT_SIDEBAR),
2832
+
2833
+ // ⑤ 标题栏透出壁纸(②:与侧边栏透出归一类)
2834
+ toggleRow(t("headerBg"), t("headerBg.desc"), "headerBg", DEFAULT_HEADER_BG),
2835
+
2836
+ // ①(重做) 标题栏磨砂:归到「标题栏透出壁纸」下面,透出关闭时隐藏。
2837
+ // ①(修正) 统一虚化开启时,「标题栏磨砂程度」条被整屏虚化接管(禁用+提示);
2838
+ // 开关(headerBg/headerBlur)仍独立:开关关 = 纯白背景。
2839
+ h("div", { style: { display: (section.headerBg !== void 0 ? !!section.headerBg : DEFAULT_HEADER_BG) ? "" : "none" } },
2840
+ toggleRow(t("headerBlur"), t("headerBlur.desc"), "headerBlur", DEFAULT_HEADER)),
2841
+ h("div", { style: {
2842
+ display: (section.headerBg !== void 0 ? !!section.headerBg : DEFAULT_HEADER_BG) && (section.headerBlur !== void 0 ? !!section.headerBlur : DEFAULT_HEADER) ? "" : "none",
2843
+ opacity: (section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT) ? 0.45 : 1,
2844
+ pointerEvents: (section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT) ? "none" : "auto"
2845
+ } },
2846
+ sliderRow(t("headerBlurAmount"), "headerBlurAmount", 0, 100, "%", 1, DEFAULT_HEADER_BLUR_AMOUNT)),
2847
+ (section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT)
2848
+ ? h("p", { className: "mpw_hint" }, t("headerBlur.overridden"))
2849
+ : null,
2850
+
2851
+ // ①(新) 侧边栏磨砂(Aqua 方案):与标题栏磨砂同类(区域自身玻璃化),
2852
+ // 弹窗打开时自动摘除(防弹窗被模糊层困住);统一虚化开启时被整屏虚化接管
2853
+ toggleRow(t("sidebarBlur"), t("sidebarBlur.desc"), "sidebarBlur", DEFAULT_SIDEBAR_BLUR),
2854
+ h("div", { style: {
2855
+ display: (section.sidebarBlur !== void 0 ? !!section.sidebarBlur : DEFAULT_SIDEBAR_BLUR) ? "" : "none",
2856
+ opacity: (section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT) ? 0.45 : 1,
2857
+ pointerEvents: (section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT) ? "none" : "auto"
2858
+ } },
2859
+ sliderRow(t("sidebarBlurAmount"), "sidebarBlurAmount", 0, 40, "px", 1, DEFAULT_SIDEBAR_BLUR_AMOUNT)),
2860
+ (section.unifyTint !== void 0 ? !!section.unifyTint : DEFAULT_UNIFY_TINT)
2861
+ ? h("p", { className: "mpw_hint" }, t("sidebarBlur.overridden"))
2862
+ : null,
2863
+
2864
+ // ═══ 其他 ═══
2865
+ h("div", { className: "mpw_section" }, t("sec.other")),
2866
+
2867
+ // ⑦ 轻度锐化(可能影响 GIF 流畅度)
2868
+ toggleRow(t("sharp"), t("sharp.desc"), "sharp", DEFAULT_SHARP),
2869
+
2870
+ // ②(新) Deep diving 背景方框开关(移到"其他"组)
2871
+ toggleRow(t("thinkBg"), t("thinkBg.desc"), "thinkBg", DEFAULT_THINK_BG),
2872
+
2873
+ // ①(新) 第三方 UI 圆角兼容开关(其他组)
2874
+ toggleRow(t("roundCompat"), t("roundCompat.desc"), "roundCompat", DEFAULT_ROUND_COMPAT),
2875
+
2876
+ // ①(新) 检测更新 / 一键热更新
2877
+ h("div", { className: "mpw_field" }, [
2878
+ h("label", { className: "mpw_label" }, t("update.title")),
2879
+ h("div", { className: "mpw_inline" }, [
2880
+ h("button", { className: "mpw_fileBtn", type: "button", onClick: checkUpdate },
2881
+ updState && updState.checking ? t("update.checking") : t("update.check")),
2882
+ updState && updState.hasUpdate
2883
+ ? h("button", { className: "mpw_button", type: "button", onClick: () => {
2884
+ if (updState.releaseAt && (Date.now() - new Date(updState.releaseAt).getTime()) < 24 * 3600 * 1000) {
2885
+ setUpdConfirm(updState); // ⑩ 新版本发布<24h → 先确认
2886
+ } else { applyUpdate(); }
2887
+ } }, updState && updState.applying ? t("update.applying") : t("update.apply"))
2888
+ : null
2889
+ ]),
2890
+ updState && updState.error ? h("p", { className: "mpw_hint mpw_err" }, t("update.fail") + updState.error) : null,
2891
+ updState && updState.hasUpdate ? h("p", { className: "mpw_hint" }, t("update.found") + updState.localVersion + " → " + updState.remoteVersion) : null,
2892
+ updState && updState.applied ? h("p", { className: "mpw_hint" }, t("update.applied")) : null,
2893
+ updState && updState.hasUpdate === false ? h("p", { className: "mpw_hint" }, t("update.latest")) : null
2894
+ ]),
2895
+
2896
+
2897
+ // ① 恢复默认(只重置外观数值,不清除已导入壁纸)
2898
+ h("div", { className: "mpw_field" }, [
2899
+ h("button", { className: "mpw_reset", type: "button", onClick: resetSettings }, t("reset"))
2900
+ ]),
2901
+ hint ? h("p", { className: "mpw_hint" + (/^(解析失败|文件过大|背景素材过大|存储空间|内存不足|此壁纸的视频纹理|不支持)/.test(hint) ? " mpw_err" : "") }, hint) : null,
2902
+
2903
+ // ⑩(新) 新版本确认下载弹窗(发布<24h 提示)
2904
+ updConfirm ? h("div", { className: "mpw_mask", onClick: () => setUpdConfirm(null) }, [
2905
+ h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
2906
+ h("div", { className: "mpw_title" }, t("update.warnTitle")),
2907
+ h("p", { className: "mpw_desc" }, t("update.warnBody") + updConfirm.localVersion + " → " + updConfirm.remoteVersion + ")"),
2908
+ h("div", { className: "mpw_inline" }, [
2909
+ h("button", { className: "mpw_button", type: "button", onClick: () => { const d = updConfirm; setUpdConfirm(null); applyUpdate(); } }, t("update.confirmDownload")),
2910
+ h("button", { className: "mpw_reset", type: "button", onClick: () => setUpdConfirm(null) }, t("conflict.cancel"))
2911
+ ])
2912
+ ])
2913
+ ]) : null,
2914
+
2915
+ // ①(新) 目录选择弹窗(文件夹选择器,跨平台)
2916
+ dirPick ? h("div", { className: "mpw_mask", onClick: () => setDirPick(false) }, [
2917
+ h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
2918
+ h("div", { className: "mpw_title" }, t("lib.pickDir")),
2919
+ h("p", { className: "mpw_hint", style: { wordBreak: "break-all" } }, t("lib.curDir") + ":" + (dirPath || "(默认)")),
2920
+ // ③(修正) 常用位置快捷入口(平台适配:Windows 盘符反斜杠,非 Windows 去 /sdcard)
2921
+ h("div", { className: "mpw_inline" }, [
2922
+ dirHome ? h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => openDirPicker(dirHome) }, "🏠 " + t("lib.home")) : null,
2923
+ dirPlatform === "win32"
2924
+ ? h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => openDirPicker("C:\\") }, "💿 C:\\")
2925
+ : h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => openDirPicker("/") }, "🗂 /"),
2926
+ ]),
2927
+ h("div", { className: "mpw_props", style: { maxHeight: 240, overflowY: "auto" } }, [
2928
+ dirPath ? h("div", { className: "mpw_prop", key: "__up" }, [
2929
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => {
2930
+ // 1(修正) 上级路径:保留根斜杠(之前丢了 / → 相对路径 → 显示空)
2931
+ if (dirPlatform === "win32" && /^[A-Za-z]:\\/.test(dirPath)) {
2932
+ const rest = dirPath.slice(3);
2933
+ const up = rest ? dirPath.slice(0, 3) + rest.split("\\").slice(0, -1).join("\\") : "C:\\";
2934
+ openDirPicker(up || "C:\\");
2935
+ } else {
2936
+ const parts = dirPath.split("/").filter(Boolean);
2937
+ const up = parts.length > 1 ? "/" + parts.slice(0, -1).join("/") : "/";
2938
+ openDirPicker(up);
2939
+ }
2940
+ } }, "⬆ " + t("lib.up"))
2941
+ ]) : null,
2942
+ dirSubs.length ? dirSubs.map((sd) => h("div", { className: "mpw_prop", key: sd }, [
2943
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => {
2944
+ const sep = dirPath.includes("\\") ? "\\" : "/";
2945
+ openDirPicker((dirPath ? dirPath + sep : "") + sd);
2946
+ } }, "📁 " + sd)
2947
+ ])) : h("p", { className: "mpw_hint" }, t("lib.noSub"))
2948
+ ]),
2949
+ h("div", { className: "mpw_inline" }, [
2950
+ h("button", { className: "mpw_button", type: "button", onClick: () => {
2951
+ const chosen = dirPath;
2952
+ setCustomDirState(chosen);
2953
+ setDirPick(false);
2954
+ commit({ customDirPath: chosen }, true);
2955
+ scanCustomDir(chosen);
2956
+ } }, t("lib.chooseHere")),
2957
+ h("button", { className: "mpw_reset", type: "button", onClick: () => setDirPick(false) }, t("conflict.cancel"))
2958
+ ])
2959
+ ])
2960
+ ]) : null,
2961
+
2962
+ // ①(新) 通用错误弹窗(文件过大 / 无法使用 / 解析失败等)
2963
+ errorModal ? h("div", { className: "mpw_mask", onClick: () => setErrorModal(false) }, [
2964
+ h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
2965
+ h("div", { className: "mpw_title" }, t("error.title")),
2966
+ h("p", { className: "mpw_desc" }, errorMsg),
2967
+ h("div", { className: "mpw_inline" }, [
2968
+ h("button", { className: "mpw_button", type: "button", onClick: () => setErrorModal(false) }, t("preview.ok"))
2969
+ ])
2970
+ ])
2971
+ ]) : null,
2972
+
2973
+ // ⑤(新) 预览模式弹窗(导入后最终用 GIF/图片时提示)
2974
+ previewModal ? h("div", { className: "mpw_mask", onClick: () => setPreviewModal(false) }, [
2975
+ h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
2976
+ h("div", { className: "mpw_title" }, t("preview.title")),
2977
+ h("p", { className: "mpw_desc" }, t("preview.desc")),
2978
+ h("div", { className: "mpw_inline" }, [
2979
+ h("button", { className: "mpw_button", type: "button", onClick: () => setPreviewModal(false) }, t("preview.ok"))
2980
+ ])
2981
+ ])
2982
+ ]) : null,
2983
+
2984
+ // ④ 冲突确认弹窗(自绘覆盖层)
2985
+ conflictModal ? h("div", { className: "mpw_mask", onClick: () => setConflictModal(false) }, [
2986
+ h("div", { className: "mpw_dialog", onClick: (ev) => ev.stopPropagation() }, [
2987
+ h("div", { className: "mpw_title" }, t("conflict.title")),
2988
+ h("p", { className: "mpw_desc" }, `${t("conflict.body")}:${conflicts.join(", ")}`),
2989
+ h("div", { className: "mpw_inline" }, [
2990
+ h("button", { className: "mpw_button", type: "button", onClick: confirmEnable }, t("conflict.confirm")),
2991
+ h("button", { className: "mpw_reset", type: "button", onClick: () => setConflictModal(false) }, t("conflict.cancel"))
2992
+ ])
2993
+ ])
2994
+ ]) : null
2995
+ ]);
2996
+ } catch (err) {
2997
+ // 渲染错误边界:任何渲染异常只显示错误信息,绝不整页空白
2998
+ console.error("[dsh-mpkg-wallpaper] 设置页渲染失败:", err);
2999
+ return h("div", { className: "mpw_field" }, [
3000
+ h("p", { className: "mpw_hint" }, "壁纸引擎设置渲染出错: " + String(err && err.message || err))
3001
+ ]);
3002
+ }
3003
+ }
3004
+
3005
+ // ═══════════════════════════════════════════════════════════════════
3006
+ // 多语言
3007
+ // ═══════════════════════════════════════════════════════════════════
3008
+ const zh = {
3009
+ "nav": "壁纸引擎背景",
3010
+ "master": "启用壁纸引擎背景功能",
3011
+ "clear.bg": "清除背景",
3012
+ "master.desc": "开启后应用所选背景",
3013
+ "master.off": "已关闭(检测到其他壁纸/主题插件或手动关闭)",
3014
+ "hybrid": "大文件混合模式",
3015
+ "lib.title": "本地壁纸库(Steam 自动发现)",
3016
+ "lib.scan": "扫描本地壁纸库",
3017
+ "lib.desc": "Windows + 壁纸引擎安装时自动发现视频/网页壁纸",
3018
+ "lib.use": "使用",
3019
+ "lib.empty": "未发现可用的壁纸(或不是 Windows 环境)",
3020
+ "lib.noHost": "宿主端不可用,无法扫描本地壁纸库",
3021
+ "lib.noInstall": "未检测到壁纸引擎安装(需 Windows + Steam 版 Wallpaper Engine)",
3022
+ "lib.fail": "扫描失败:",
3023
+ "lib.sceneOnly": "场景类壁纸无法在浏览器渲染(仅预览图)",
3024
+ "lib.webPending": "Web(HTML)壁纸支持开发中,请先使用 mpkg 导入",
3025
+ "lib.applied": "已应用壁纸",
3026
+ "lib.custom": "自定义本地壁纸目录",
3027
+ "lib.dirPlaceholder": "例如 C:\\壁纸 或 /home/壁纸",
3028
+ "lib.browse": "浏览…",
3029
+ "lib.pickDir": "选择文件夹",
3030
+ "lib.curDir": "当前路径",
3031
+ "lib.up": "上级",
3032
+ "lib.noSub": "(无子目录)",
3033
+ "lib.chooseHere": "选择此文件夹",
3034
+ "lib.home": "主目录",
3035
+ "lib.scanDir": "扫描该目录",
3036
+ "lib.dirEmpty": "请先输入壁纸文件夹路径",
3037
+ "lib.dirFail": "目录无效或无法读取:",
3038
+ "lib.dirFound": "发现 ",
3039
+ "lib.dirFiles": " 个媒体文件(图片/视频)",
3040
+ "lib.rotate": "壁纸切换与轮换",
3041
+ "lib.next": "下一个壁纸",
3042
+ "lib.prev": "上一个壁纸",
3043
+ "lib.more": "继续展开",
3044
+ "lib.collapse": "收起列表",
3045
+ "lib.expandAll": "展开壁纸",
3046
+ "lib.rotateDesc": "定时自动切换到下一个壁纸(范围:自定义目录 + 本地库 + Steam 自动发现的视频壁纸)",
3047
+ "lib.rotMerged": "已把 Steam 库中可播放的视频壁纸并入轮播:",
3048
+ "lib.rotateMin": "轮换间隔",
3049
+ "lib.minutes": "分钟",
3050
+ "hybrid.desc": "开启:mpkg 上传到 DSH 进程流式播放,支持 >600MB 大文件(需重启 dsh web 使宿主端生效);关闭:纯浏览器模式(600MB 上限)",
3051
+ "conflict.detected": "检测到可能冲突的插件",
3052
+ "conflict.title": "检测到插件冲突",
3053
+ "conflict.body": "以下插件可能与本功能冲突(都会改动界面背景/外观)。仍要开启吗?",
3054
+ "conflict.confirm": "仍然开启",
3055
+ "conflict.cancel": "取消",
3056
+ "title": "壁纸引擎 mpkg 背景",
3057
+ "desc": "直接加载 Wallpaper Engine 的 .mpkg 文件作为页面背景:视频类壁纸自动播放内嵌 mp4,场景类壁纸取 preview.gif。支持时间变化、时钟、镜头缩放与界面虚化。设置保存在当前浏览器。",
3058
+ "sec.source": "背景来源",
3059
+ "sec.appearance": "外观",
3060
+ "sec.appearance.desc": "面板不透明度=所有区域白雾厚度;磨砂模糊=整张壁纸的模糊程度(0=清晰);镜头缩放/位置=背景画面的放大与平移(缩小可看到画面边缘的组件)",
3061
+ "sec.blur": "界面虚化",
3062
+ "sec.blur.desc": "各类弹层/面板打开时的背景虚化:对话框(通用居中窗口+聊天输入框)、设置面板、下载确认弹窗各自独立;弹层=菜单/下拉/提示;遮罩=弹层背后的全屏背景;侧边栏磨砂=侧边栏自身玻璃化",
3063
+ "sec.unify": "统一虚化",
3064
+ "sec.unify.desc": "整屏所有控件的模糊感由一组设置统一管理(一个条全管侧边栏/标题栏/聊天区);开启后下方「界面虚化」里的侧边栏磨砂被接管。关闭则各区域单独调节",
3065
+ "sec.show": "透出壁纸",
3066
+ "sec.show.desc": "控制对应区域是否显示壁纸:关=纯色不透明",
3067
+ "sec.other": "其他",
3068
+ "mpkg.pick": "选择 .mpkg 文件",
3069
+ "mpkg.busy": "解析中…",
3070
+ "mpkg.hint": "支持 Wallpaper Engine 的 .mpkg 包;此位置也可直接选择 mp4/webm 视频文件;视频类壁纸自动播放内嵌 mp4",
3071
+ "mpkg.noAsset": "该 mpkg 内未找到图片/GIF 素材",
3072
+ "mpkg.previewMode": "预览模式:当前以预览图显示;该壁纸的完整动态内容(Live2D 场景/高清视频)需壁纸引擎 App 渲染",
3073
+ "mpkg.tooLarge": "背景素材过大(视频>600MB / 图片>200MB),浏览器无法处理,请换一个 mpkg",
3074
+ "mpkg.huge": "文件过大(>600MB),移动端浏览器无法处理,请换小一点的 mpkg",
3075
+ "mpkg.videoHuge": "视频文件过大(>600MB),浏览器无法播放,已自动回退使用预览图",
3076
+ "mpkg.quota": "浏览器存储空间不足,无法保存此壁纸素材(可清除其他壁纸后再试)",
3077
+ "mpkg.oom": "文件过大导致内存不足,解析失败。请换小于 300MB 的壁纸,或用壁纸引擎 App 查看",
3078
+ "mpkg.vtexBig": "此壁纸的视频纹理超过 250MB,浏览器无法播放(请用壁纸引擎 App 查看)",
3079
+ "mpkg.fail": "解析失败:",
3080
+ "mpkg.using": "当前背景素材",
3081
+ "props.title": "可调参数",
3082
+ "props.desc": "浏览器显示的是壁纸引擎预渲染的素材,修改参数不会改变画面。以下为壁纸自带的参数及当前值,供对照:如需修改,请在壁纸引擎 App 中调整",
3083
+ "props.unavailable": "暂不可用",
3084
+ "props.on": "开",
3085
+ "props.off": "关",
3086
+ "props.expand": "展开全部",
3087
+ "props.important": "★ 关键开关",
3088
+ "props.resetWallpaper": "重置壁纸参数",
3089
+ "props.resetDone": "壁纸参数已恢复默认",
3090
+ "props.tvOff": "随现实时间变化已关闭,时间设置已隐藏(打开开关后显示)",
3091
+ "roundCompat": "第三方 UI 圆角兼容",
3092
+ "update.title": "检查更新",
3093
+ "update.check": "检测更新",
3094
+ "update.checking": "检测中…",
3095
+ "update.apply": "一键更新",
3096
+ "update.applying": "更新中…",
3097
+ "update.found": "发现新版本:",
3098
+ "update.latest": "已是最新版本",
3099
+ "update.applied": "更新完成!请重启 dsh web 并 Ctrl+F5 生效",
3100
+ "update.fail": "检查失败:",
3101
+ "update.warnTitle": "新版本发布未满 24 小时",
3102
+ "update.warnBody": "新版本发布未满24h 可能存在未修复的bug,确认下载吗?(版本 ",
3103
+ "update.confirmDownload": "确认下载",
3104
+ "roundCompat.desc": "给其他插件注入界面的矩形容器补圆角(不覆盖插件自己的样式);如与某插件冲突可关闭",
3105
+ "error.title": "导入失败",
3106
+ "preview.title": "预览模式",
3107
+ "preview.desc": "该壁纸当前以预览图(GIF/图片)显示,浏览器无法播放其动态内容(Live2D 场景/高清视频需壁纸引擎 App 渲染)。",
3108
+ "preview.ok": "知道了",
3109
+ "props.none": "请先导入壁纸",
3110
+ "props.collapse": "收起",
3111
+ "url.label": "图片链接(支持 data:image 的 GIF)",
3112
+ "url.placeholder": "https://… 或 data:image/…",
3113
+ "url.apply": "应用",
3114
+ "url.unsafe": "仅支持 http/https 或 data:image 链接",
3115
+ "file.unsafe": "不支持的文件类型(已拒绝)",
3116
+ "file.pick": "选择本地图片/动图",
3117
+ "file.hint": "支持图片/动图(大文件自动存本地,刷新不丢)",
3118
+ "file.tooLarge": "文件超过 50MB,请换一张或使用链接。",
3119
+ "brightness": "画面亮度",
3120
+ "float": "悬浮效果",
3121
+ "float.desc": "侧边栏/标题栏变为悬浮卡片(圆角+阴影+透出模糊壁纸);默认关,开启后原有的透明/虚化功能不受影响",
3122
+ "blur": "磨砂模糊",
3123
+ "blur.overridden": "统一虚化 + 聊天区跟随均开启:壁纸模糊由「整屏虚化程度」接管,磨砂条暂不可调;关闭「聊天区跟随整屏虚化」后磨砂条恢复可调(此时统一虚化只管侧边栏/标题栏)",
3124
+ "zoom": "镜头缩放",
3125
+ "lens.pos": "镜头位置(平移)",
3126
+ "lens.x": "X",
3127
+ "lens.y": "Y",
3128
+ "sidebar": "侧边栏透出壁纸",
3129
+ "sidebar.desc": "关闭后侧边栏恢复不透明,避免左右透明度不一致",
3130
+ "headerBlur": "标题栏磨砂",
3131
+ "headerBg": "标题栏透出壁纸",
3132
+ "headerBg.desc": "关闭后标题栏为纯白(暗色主题为纯深色)不透明,不再显示壁纸",
3133
+ "headerBlur.desc": "标题栏半透明透出(已模糊的)壁纸,磨砂程度独立可调(下方滑条),即使整屏虚化调 0 也保持雾底保证文字可读。需先开启「标题栏透出壁纸」",
3134
+ "headerBlurAmount": "标题栏磨砂程度",
3135
+ "headerBlur.overridden": "统一虚化开启中:标题栏磨砂程度由「整屏虚化程度」接管(白雾跟随侧边栏/标题栏透明度条);关闭统一虚化后恢复",
3136
+ "unifyTint": "统一虚化",
3137
+ "unifyTint.desc": "开启后,侧边栏和标题栏获得类似聊天框的毛玻璃虚化(整块模糊、无缝隙,只模糊它们背后的内容,壁纸其他地方保持清晰),强度由「整屏虚化程度」条控制;表面白雾厚度由「侧边栏/标题栏透明度」条控制。关闭后各区域单独调节",
3138
+ "unifyAmount": "整屏虚化程度",
3139
+ "sidebarAlpha": "侧边栏/标题栏透明度",
3140
+ "sidebarAlpha.desc": "统一虚化开启时:侧边栏和标题栏表面的白雾厚度(0 = 全透明透出模糊壁纸,100 = 实心)。与「整屏虚化程度」解耦,可单独调",
3141
+ "chatFollow": "聊天区跟随整屏虚化",
3142
+ "chatFollow.desc": "统一虚化开启时:开 = 聊天区壁纸模糊也随「整屏虚化程度」(磨砂条被接管禁用);关 = 磨砂条恢复可调,聊天区壁纸由磨砂条控制(统一虚化只虚化侧边栏/标题栏)",
3143
+ "sessionFollow": "新会话按钮跟随面板不透明度",
3144
+ "sessionFollow.desc": "统一虚化开启时:「添加新会话」按钮是否随「不透明度」条。关 = 保持原按钮色",
3145
+ "dialogBlur": "虚化对话框",
3146
+ "dialogBlur.desc": "屏幕中央的通用居中窗口(非设置面板/非本插件弹窗)和聊天输入框的背景虚化;滚动经过输入框的文字会变朦胧。设置面板与下载确认弹窗各有独立开关",
3147
+ "dialogBlurAmount": "对话框虚化程度",
3148
+ "settingsBlur": "虚化设置面板",
3149
+ "settingsBlur.desc": "DSH 设置面板(本插件的设置界面所在的面板)的背景虚化,程度独立于通用对话框",
3150
+ "settingsBlurAmount": "设置面板虚化程度",
3151
+ "confirmBlur": "虚化下载/确认弹窗",
3152
+ "confirmBlur.desc": "本插件的下载确认、冲突检测、错误提示等弹窗的背景虚化;关闭则弹窗不透明",
3153
+ "confirmBlurAmount": "下载/确认弹窗虚化程度",
3154
+ "popoverBlur": "虚化弹层",
3155
+ "popoverBlur.desc": "从界面某处弹出的面板:右键菜单、下拉选择、提示气泡等",
3156
+ "popoverBlurAmount": "弹层虚化程度",
3157
+ "maskBlur": "虚化遮罩(全屏背景)",
3158
+ "maskBlur.desc": "打开设置面板或弹层时,窗口后面那层半透明背景的朦胧程度",
3159
+ "maskBlurAmount": "遮罩虚化程度",
3160
+ "sidebarBlur": "侧边栏磨砂",
3161
+ "sidebarBlur.desc": "侧边栏自身玻璃化(backdrop-filter 模糊其背后的壁纸),开启后侧边栏有类似 Aqua 的磨砂玻璃质感;打开弹窗时自动摘除以防弹窗被侧边栏模糊层困住",
3162
+ "sidebarBlurAmount": "侧边栏磨砂程度",
3163
+ "sidebarBlur.overridden": "统一虚化开启中:侧边栏磨砂由「整屏虚化程度」接管(壁纸层统一模糊);关闭统一虚化后此条恢复可调",
3164
+ "thinkBg": "Deep diving 背景方框",
3165
+ "thinkBg.desc": "开:显示思考状态(Deep diving)的模糊背景方框;关(默认):背景透明,文字直接显示在壁纸上",
3166
+ "blur.unsupported": "⚠️ 当前浏览器(Via/WebView)不真正渲染背景模糊,虚化仅显示半透明。建议用 Chrome/Firefox 浏览器获得完整磨砂效果",
3167
+ "sharp": "轻度锐化",
3168
+ "sharp.desc": "提升低清 GIF 观感;若动画卡顿请关闭",
3169
+ "time.picked": "已按当前时间选择素材",
3170
+ "time.now": "当前时段",
3171
+ "time.morning": "清晨",
3172
+ "time.day": "白天",
3173
+ "time.dusk": "黄昏",
3174
+ "time.night": "夜晚",
3175
+ "reset": "恢复默认",
3176
+ "default": "默认",
3177
+ };
3178
+ const en = {
3179
+ "nav": "MPKG Wallpaper",
3180
+ "master": "Enable mpkg background",
3181
+ "clear.bg": "Clear background",
3182
+ "master.desc": "On applies the chosen background",
3183
+ "master.off": "Disabled (conflicting wallpaper/theme plugins detected or manually off)",
3184
+ "hybrid": "Hybrid large-file mode",
3185
+ "lib.title": "Local wallpaper library (Steam discovery)",
3186
+ "lib.scan": "Scan local library",
3187
+ "lib.desc": "On Windows with Wallpaper Engine installed, discovers video/web wallpapers",
3188
+ "lib.use": "Use",
3189
+ "lib.empty": "No usable wallpapers found (or not a Windows environment)",
3190
+ "lib.noHost": "Host unavailable — cannot scan the local library",
3191
+ "lib.noInstall": "Wallpaper Engine install not found (requires Windows + Steam Wallpaper Engine)",
3192
+ "lib.fail": "Scan failed: ",
3193
+ "lib.sceneOnly": "Scene wallpapers cannot be rendered in the browser (preview only)",
3194
+ "lib.webPending": "Web (HTML) wallpaper support is in development; use mpkg import for now",
3195
+ "lib.applied": "Applied wallpaper",
3196
+ "lib.custom": "Custom local wallpaper folder",
3197
+ "lib.dirPlaceholder": "e.g. C:\\壁纸 or /home/user/壁纸",
3198
+ "lib.browse": "Browse…",
3199
+ "lib.pickDir": "Choose folder",
3200
+ "lib.curDir": "Current path",
3201
+ "lib.up": "Up",
3202
+ "lib.noSub": "(no subfolders)",
3203
+ "lib.chooseHere": "Choose this folder",
3204
+ "lib.home": "Home",
3205
+ "lib.scanDir": "Scan folder",
3206
+ "lib.dirEmpty": "Enter a wallpaper folder path first",
3207
+ "lib.dirFail": "Invalid or unreadable folder: ",
3208
+ "lib.dirFound": "Found ",
3209
+ "lib.dirFiles": " media files (images/videos)",
3210
+ "lib.rotate": "Wallpaper switching & rotation",
3211
+ "lib.next": "Next wallpaper",
3212
+ "lib.prev": "Previous wallpaper",
3213
+ "lib.more": "Expand more",
3214
+ "lib.collapse": "Collapse list",
3215
+ "lib.expandAll": "Expand wallpapers",
3216
+ "lib.rotateDesc": "Automatically switch to the next wallpaper on a timer (range: custom folder + local library + Steam-discovered video wallpapers)",
3217
+ "lib.rotMerged": "Playable Steam-library videos merged into the rotation: ",
3218
+ "lib.rotateMin": "Rotation interval",
3219
+ "lib.minutes": "min",
3220
+ "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)",
3221
+ "conflict.detected": "Potentially conflicting plugins detected",
3222
+ "conflict.title": "Plugin conflict detected",
3223
+ "conflict.body": "These plugins may conflict with this feature (both alter the UI background/appearance). Enable anyway?",
3224
+ "conflict.confirm": "Enable anyway",
3225
+ "conflict.cancel": "Cancel",
3226
+ "title": "Wallpaper Engine mpkg background",
3227
+ "desc": "Load Wallpaper Engine .mpkg files as the page background: video wallpapers play their embedded mp4, scene wallpapers use their preview.gif. Supports time-of-day switching, a clock, lens zoom, and UI frosted blur. Settings persist in this browser.",
3228
+ "sec.source": "Background source",
3229
+ "sec.appearance": "Appearance",
3230
+ "sec.appearance.desc": "Panel opacity = frosted tint thickness of all areas; Frosted blur = how blurred the wallpaper itself is (0 = sharp); Lens zoom/position = zoom and pan of the background image (zoom out to see components at the picture edges)",
3231
+ "sec.blur": "UI blur",
3232
+ "sec.blur.desc": "Backdrop blur when each popup layer opens: dialogs (generic center windows + chat input), the settings panel, and download/confirm popups each have their own switch; popovers = menus/dropdowns/tooltips; mask = the full-screen dim behind popups; sidebar frost = the sidebar itself becomes glass",
3233
+ "sec.unify": "Unified blur",
3234
+ "sec.unify.desc": "One set of controls rules the frosted feel of every surface (sidebar/titlebar/chat via one slider); when on, sidebar frost below is taken over. Off = each area adjusts separately",
3235
+ "sec.show": "Show wallpaper",
3236
+ "sec.show.desc": "Whether the corresponding area shows the wallpaper: off = solid color, opaque",
3237
+ "sec.other": "Other",
3238
+ "mpkg.pick": "Choose .mpkg file",
3239
+ "mpkg.busy": "Parsing…",
3240
+ "mpkg.hint": "Wallpaper Engine .mpkg packages; you can also pick an mp4/webm video file here; video wallpapers play their embedded mp4",
3241
+ "mpkg.noAsset": "No image/GIF asset found in this mpkg",
3242
+ "mpkg.previewMode": "Preview mode: showing the preview image; the wallpaper's full dynamic content (Live2D scene/HD video) requires the Wallpaper Engine app",
3243
+ "mpkg.tooLarge": "Background asset too large (video >600MB / image >200MB) — the browser cannot handle it",
3244
+ "mpkg.huge": "File too large (>600MB) — mobile browsers cannot handle it; pick a smaller mpkg",
3245
+ "mpkg.videoHuge": "Video too large (>600MB) — the browser cannot play it; automatically fell back to the preview image",
3246
+ "mpkg.quota": "Browser storage quota exceeded; free up space (clear other wallpapers) and retry",
3247
+ "mpkg.oom": "File too large — out of memory while parsing. Use a wallpaper under 300MB or view it in the Wallpaper Engine app",
3248
+ "mpkg.vtexBig": "This wallpaper's video texture exceeds 250MB — the browser cannot play it (view it in the Wallpaper Engine app)",
3249
+ "mpkg.fail": "Parse failed: ",
3250
+ "mpkg.using": "Current background asset",
3251
+ "props.title": "Adjustable options",
3252
+ "props.desc": "The browser shows pre-rendered wallpaper assets, so editing these options cannot change the picture. Listed below are the wallpaper's own parameters and their current values for reference; change them in the Wallpaper Engine app instead",
3253
+ "props.unavailable": "unavailable",
3254
+ "props.on": "On",
3255
+ "props.off": "Off",
3256
+ "props.expand": "Show all",
3257
+ "props.important": "★ Key switch",
3258
+ "props.resetWallpaper": "Reset wallpaper options",
3259
+ "props.resetDone": "Wallpaper options restored to defaults",
3260
+ "props.tvOff": "Real-time variation is off — time settings hidden (enable it to show them)",
3261
+ "roundCompat": "Third-party UI radius compat",
3262
+ "update.title": "Check for updates",
3263
+ "update.check": "Check updates",
3264
+ "update.checking": "Checking…",
3265
+ "update.apply": "Update now",
3266
+ "update.applying": "Updating…",
3267
+ "update.found": "New version found: ",
3268
+ "update.latest": "Already up to date",
3269
+ "update.applied": "Update done! Restart dsh web and Ctrl+F5",
3270
+ "update.fail": "Check failed: ",
3271
+ "update.warnTitle": "New version released <24h ago",
3272
+ "update.warnBody": "This version was published less than 24h ago and may contain unfixed bugs. Download anyway? (v",
3273
+ "update.confirmDownload": "Download",
3274
+ "roundCompat.desc": "Adds border-radius to rectangular containers injected by other plugins (does not override their own styles); turn off if it conflicts",
3275
+ "error.title": "Import failed",
3276
+ "preview.title": "Preview mode",
3277
+ "preview.desc": "This wallpaper is currently shown as a preview image (GIF/picture); the browser cannot play its dynamic content (Live2D scene / HD video requires the Wallpaper Engine app).",
3278
+ "preview.ok": "Got it",
3279
+ "props.none": "Import a wallpaper first",
3280
+ "props.collapse": "Collapse",
3281
+ "url.label": "Image URL (GIF data: URLs work too)",
3282
+ "url.placeholder": "https://… or data:image/…",
3283
+ "url.apply": "Apply",
3284
+ "url.unsafe": "Only http/https or data:image URLs are allowed",
3285
+ "file.unsafe": "Unsupported file type (rejected)",
3286
+ "file.pick": "Choose local image/GIF",
3287
+ "file.hint": "Images/GIF supported (large files auto-saved locally, persist on refresh)",
3288
+ "file.tooLarge": "File exceeds 50MB — pick another or use a URL.",
3289
+ "brightness": "Brightness",
3290
+ "float": "Floating cards",
3291
+ "float.desc": "Sidebar/title bar become floating cards (rounded + shadow + see-through frosted wallpaper); off by default, existing transparency/blur features stay intact",
3292
+ "blur": "Frosted blur",
3293
+ "blur.overridden": "Unified blur + Chat-follows are both on: wallpaper blur is taken over by the Full-screen blur degree slider, so this frosted slider is disabled; turn off \"Chat follows full-screen blur\" to restore it (unified blur then only controls sidebar/title-bar fog)",
3294
+ "zoom": "Lens zoom (wallpaper camera)",
3295
+ "lens.pos": "Lens position (pan)",
3296
+ "lens.x": "X",
3297
+ "lens.y": "Y",
3298
+ "sidebar": "Show wallpaper in sidebar",
3299
+ "sidebar.desc": "Off keeps the sidebar opaque so left/right translucency stays consistent",
3300
+ "headerBlur": "Frost the title bar",
3301
+ "headerBg": "Show wallpaper behind the title bar",
3302
+ "headerBg.desc": "Off makes the title bar solid white (solid dark in dark theme), no wallpaper behind it",
3303
+ "headerBlur.desc": "Title bar semi-transparent over the (blurred) wallpaper; frosted amount adjustable via the slider below and kept even when full-screen blur is 0, so text stays readable. Needs \"Show wallpaper behind the title bar\" on",
3304
+ "headerBlurAmount": "Title bar frost amount",
3305
+ "headerBlur.overridden": "Unified blur is on: the title-bar frost amount is taken over by the Full-screen blur degree (fog follows the sidebar/title-bar opacity slider); turn unified blur off to restore it",
3306
+ "unifyTint": "Unify blur",
3307
+ "unifyTint.desc": "On: the sidebar and title bar get a chat-box-like frosted blur (one seamless layer, no seams; only what's behind them is blurred, the wallpaper stays sharp elsewhere), strength controlled by the Full-screen blur degree slider; surface white-fog thickness by the Sidebar/title-bar opacity slider. Off = per-area control",
3308
+ "unifyAmount": "Full-screen blur degree",
3309
+ "sidebarAlpha": "Sidebar / title-bar opacity",
3310
+ "sidebarAlpha.desc": "When unified blur is on: white-fog thickness on the sidebar and title bar (0 = fully transparent showing blurred wallpaper, 100 = solid). Decoupled from the blur degree, adjustable independently",
3311
+ "chatFollow": "Chat follows full-screen blur",
3312
+ "chatFollow.desc": "When unified blur is on: On = the chat area wallpaper blur also follows the Full-screen blur degree (frosted slider taken over & disabled); Off = the frosted slider becomes adjustable again, the chat area wallpaper follows it (unified blur only frosts the sidebar/title bar)",
3313
+ "sessionFollow": "New-chat button follows panel opacity",
3314
+ "sessionFollow.desc": "When unified blur is on: whether the New chat button follows the Opacity slider. Off = keeps its original button color",
3315
+ "dialogBlur": "Blur dialogs",
3316
+ "dialogBlur.desc": "Generic center-screen windows (not the settings panel / not this plugin's popups) and the chat input box get a blurred backdrop; text scrolling under the input box turns hazy. The settings panel and download/confirm popups have their own switches",
3317
+ "dialogBlurAmount": "Dialog blur amount",
3318
+ "settingsBlur": "Blur settings panel",
3319
+ "settingsBlur.desc": "Backdrop blur of the DSH settings panel (the panel hosting this plugin's settings), with its own strength independent of generic dialogs",
3320
+ "settingsBlurAmount": "Settings panel blur amount",
3321
+ "confirmBlur": "Blur download/confirm popups",
3322
+ "confirmBlur.desc": "Backdrop blur of this plugin's download-confirm, conflict-detection and error popups; off = opaque popups",
3323
+ "confirmBlurAmount": "Download/confirm popup blur amount",
3324
+ "popoverBlur": "Blur popovers/mask",
3325
+ "popoverBlur.desc": "Panels popping out from somewhere: context menus, dropdowns, tooltip bubbles",
3326
+ "popoverBlurAmount": "Popover blur amount",
3327
+ "maskBlur": "Blur mask (full-screen backdrop)",
3328
+ "maskBlur.desc": "Haziness of the translucent backdrop layer behind a window when settings/popovers open",
3329
+ "maskBlurAmount": "Mask blur amount",
3330
+ "sidebarBlur": "Sidebar frost",
3331
+ "sidebarBlur.desc": "The sidebar itself becomes glass (backdrop-filter blurs the wallpaper behind it, Aqua-style); automatically lifted while a dialog is open so the blur layer cannot trap fixed popups",
3332
+ "sidebarBlurAmount": "Sidebar frost amount",
3333
+ "sidebarBlur.overridden": "Unified blur is on: sidebar frost is taken over by the Full-screen blur degree (wallpaper-layer blur); turn unified blur off to adjust this slider",
3334
+ "thinkBg": "Deep diving background box",
3335
+ "thinkBg.desc": "On: show a blurred background box behind the Deep diving thinking status; Off (default): transparent, text sits directly on the wallpaper",
3336
+ "blur.unsupported": "⚠️ This browser (Via/WebView) does not truly render backdrop blur; blur appears as translucency only. Use Chrome/Firefox for the full frosted effect",
3337
+ "sharp": "Light sharpen",
3338
+ "clock.title": "Show time (clock)",
3339
+ "clock.desc": "Overlay a live clock on the background, replacing the built-in time component",
3340
+ "clock.24h": "24-hour format",
3341
+ "clock.sec": "Show seconds",
3342
+ "clock.date": "Show date",
3343
+ "clock.pos": "Position",
3344
+ "clock.tl": "Top-left",
3345
+ "clock.tr": "Top-right",
3346
+ "clock.bl": "Bottom-left",
3347
+ "clock.br": "Bottom-right",
3348
+ "sharp.desc": "Improves low-res GIF look; disable if animation stutters",
3349
+ "time.picked": "Asset picked by current time",
3350
+ "time.now": "Current period",
3351
+ "time.morning": "Morning",
3352
+ "time.day": "Day",
3353
+ "time.dusk": "Dusk",
3354
+ "time.night": "Night",
3355
+ "reset": "Restore defaults",
3356
+ "default": "Default",
3357
+ };
3358
+
3359
+ // ═══════════════════════════════════════════════════════════════════
3360
+ // 插件主体
3361
+ // ═══════════════════════════════════════════════════════════════════
3362
+ const inject = ["slots", "locale", "theme"];
3363
+
3364
+ /** 当前 ctx(apply 时保存,供 applyFromStorage 里 overrideTokens 用)。 */
3365
+ let pluginCtx = null;
3366
+ /** 上一次 token override 的 disposer。 */
3367
+ let tokenDisposer = null;
3368
+
3369
+ let styleEl = null;
3370
+ function getStyleEl() {
3371
+ if (!styleEl) {
3372
+ styleEl = document.createElement("style");
3373
+ styleEl.setAttribute("data-plugin", "@local/dsh-mpkg-wallpaper");
3374
+ (document.head || document.documentElement).appendChild(styleEl);
3375
+ }
3376
+ return styleEl;
3377
+ }
3378
+
3379
+ function apply(ctx) {
3380
+ try {
3381
+ pluginCtx = ctx;
3382
+ applyInner(ctx);
3383
+ } catch (err) {
3384
+ // 防护:任何运行时错误只影响本插件,绝不拖垮 harness 启动
3385
+ console.error("[dsh-mpkg-wallpaper] apply failed:", err);
3386
+ }
3387
+ }
3388
+
3389
+ // ①(新) 侧边栏磨砂摘除:监听居中弹窗出现/消失,body 打 data-mpw-sblur-off。
3390
+ // backdrop-filter 会把 sidebarCol 变成 fixed 弹窗的 containing block(困住弹窗),
3391
+ // 弹窗打开时摘除侧边栏模糊、关闭即恢复。不用 :has()(Firefox 层叠 bug,见 E 块注释)。
3392
+ let sblurObserver = null;
3393
+ function setupSblurObserver() {
3394
+ if (sblurObserver !== null) return;
3395
+ const check = () => {
3396
+ try {
3397
+ const hasDlg = !!document.querySelector('[role="dialog"], [role="alertdialog"]');
3398
+ if (hasDlg) document.body.setAttribute("data-mpw-sblur-off", "");
3399
+ else document.body.removeAttribute("data-mpw-sblur-off");
3400
+ } catch {}
3401
+ };
3402
+ try {
3403
+ sblurObserver = new MutationObserver(check);
3404
+ sblurObserver.observe(document.body, { childList: true, subtree: true });
3405
+ } catch { sblurObserver = null; }
3406
+ check();
3407
+ }
3408
+
3409
+ function applyInner(ctx) {
3410
+ // 背景 DOM 常驻
3411
+ ensureBgDom();
3412
+ // ⑭ 内联虚化 watcher:持续应用 dialog/fade 内联样式
3413
+ applyDialogInline(readSection());
3414
+ if (window.__mpwInlineWatcher === void 0) {
3415
+ window.__mpwInlineWatcher = startInlineWatcher();
3416
+ }
3417
+ // ⑪(新) 与其他壁纸/主题插件冲突 → 自动关闭本功能(用户要求)。
3418
+ // 检测范围:已装插件 ID(data-plugin)+ 运行时背景检测(body/html 背景图、
3419
+ // 其他全屏背景层)。forceEnabled=true(用户手动强开)时豁免,避免反复关闭。
3420
+ try {
3421
+ const cf = detectConflicts();
3422
+ if (cf.length) {
3423
+ const s = readSection();
3424
+ if (s.enabled !== false && !s.forceEnabled) {
3425
+ console.warn("[dsh-mpkg-wallpaper] 检测到可能冲突的壁纸/主题插件,已自动关闭本功能:", cf.join(", "));
3426
+ writeSection(Object.assign({}, s, { enabled: false }), true);
3427
+ }
3428
+ }
3429
+ } catch {}
3430
+ applyFromStorage();
3431
+ window.addEventListener("storage", (e) => {
3432
+ if (e.key === STORE_KEY) {
3433
+ try { applyFromStorage(); } catch {}
3434
+ }
3435
+ });
3436
+ // 时间变化:每分钟检查时段,跨时段自动切换视频
3437
+ try {
3438
+ setInterval(() => {
3439
+ const s = readSection();
3440
+ if (!s.timeVideos || !s.timeConfig || !s.timeConfig.enabled) return;
3441
+ const slot = slotForTime(s.timeConfig, new Date());
3442
+ if (slot === s.activeSlot) return;
3443
+ const item = s.timeVideos.find((l) => l.slot === slot);
3444
+ if (!item) return;
3445
+ idbGet("bg-" + item.key).then((v) => {
3446
+ if (!v) return;
3447
+ idbPut("bg", v).then(() => {
3448
+ writeSection(Object.assign({}, readSection(), { activeSlot: item.slot || item.key }), true);
3449
+ applyFromStorage();
3450
+ });
3451
+ }).catch(() => {});
3452
+ }, 60000);
3453
+ } catch {}
3454
+ const sectionInjected = () => ({
3455
+ commit: () => { applyFromStorage(); }
3456
+ });
3457
+ // ⑤ 注册为设置左侧导航的独立页面
3458
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
3459
+ name: "settings.section",
3460
+ id: ROW_ID,
3461
+ // ①(修正) order:39 固定在「插件市场」(order:40) 前面 —— 两者原都是 40,
3462
+ // 同 order 时位置不稳定会互换(用户实测)。39 保证壁纸设置恒在插件市场前。
3463
+ order: 39,
3464
+ label: () => react.createElement(react.Fragment, null,
3465
+ react.createElement("img", { src: NAV_ICON, alt: "", className: "mpw_navIconImg" }),
3466
+ " " + ctx.locale.bind(NS)("nav")),
3467
+ locale: NS,
3468
+ inject: sectionInjected
3469
+ }, MpkgSection));
3470
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "ui-mpkg-wallpaper: settings section dictionaries");
3471
+ }
3472
+
3473
+ exports.inject = inject;
3474
+ exports.apply = apply;
3475
+ return module.exports;
3476
+ }
3477
+ });