dsh-mpkg-wallpaper 3.1.2 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lib/client.js +210 -88
  2. package/lib/index.js +104 -2
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -503,6 +503,18 @@ window.__ModuleLoader__.load({
503
503
  wrap: document.getElementById(BG_WRAP_ID)
504
504
  };
505
505
  }
506
+ /** ①(新) host: 标记 → 真实媒体 URL(缩略图/预览用;同 showImage 的转换逻辑) */
507
+ function resolveHostUrl(image) {
508
+ try {
509
+ if (typeof image === "string" && image.indexOf("host:") === 0) {
510
+ const q = image.slice(5);
511
+ if (q.indexOf("ltoken=") >= 0) return HOST_BASE + "/library-media" + q;
512
+ if (q.indexOf("custom=") >= 0) return HOST_BASE + "/custom-media" + q.replace("custom=1&", "");
513
+ return HOST_BASE + "/media" + q;
514
+ }
515
+ } catch {}
516
+ return image;
517
+ }
506
518
  let clockEl = null;
507
519
  let clockTimer = null;
508
520
  function ensureClockEl() {
@@ -703,6 +715,9 @@ window.__ModuleLoader__.load({
703
715
  if (image) {
704
716
  const zoom = section.zoom !== void 0 ? section.zoom : DEFAULT_ZOOM;
705
717
  wrap.style.setProperty("--mpw-zoom", String(zoom / 100));
718
+ // ①(新) 壁纸镜像翻转(flip,Wallpaper Engine 原生基础选项):scaleX/scaleY(-1)
719
+ wrap.style.setProperty("--mpw-flip-x", (section.flipX !== void 0 ? !!section.flipX : false) ? "-1" : "1");
720
+ wrap.style.setProperty("--mpw-flip-y", (section.flipY !== void 0 ? !!section.flipY : false) ? "-1" : "1");
706
721
  wrap.style.setProperty("--mpw-lensX", String(section.lensX !== void 0 ? section.lensX : 0) + "px");
707
722
  wrap.style.setProperty("--mpw-lensY", String(section.lensY !== void 0 ? section.lensY : 0) + "px");
708
723
  wrap.classList.toggle("mpw-sharp", section.sharp !== void 0 ? !!section.sharp : DEFAULT_SHARP);
@@ -942,36 +957,50 @@ window.__ModuleLoader__.load({
942
957
  const rgb = parts;
943
958
  const info = aquaInkForRgb(rgb);
944
959
  const tinted = (alpha) => `color-mix(in srgb, rgb(var(--mpw-aqua-rgb)) ${Math.round(alpha * 100)}%, transparent)`;
945
- const out = {
946
- "--dsw-alias-bg-base": { light: tinted(0.62), dark: tinted(0.62) },
947
- "--dsw-alias-bg-overlay": { light: tinted(0.72), dark: tinted(0.72) },
948
- "--dsw-alias-bg-layer-2": { light: tinted(0.72), dark: tinted(0.72) },
949
- "--dsw-alias-bg-layer-3": { light: tinted(0.72), dark: tinted(0.72) },
950
- "--dsw-specific-sidebar-fill": { light: tinted(0.85), dark: tinted(0.85) },
951
- "--dsw-specific-menu": { light: tinted(0.86), dark: tinted(0.86) },
952
- "--dsw-specific-bubble": { light: tinted(0.84), dark: tinted(0.84) },
953
- "--dsw-specific-bubble-highlight": { light: tinted(0.92), dark: tinted(0.92) },
954
- "--dsw-specific-selector": { light: tinted(0.78), dark: tinted(0.78) },
955
- "--dsw-specific-tip": { light: tinted(0.85), dark: tinted(0.85) },
956
- "--dsw-specific-input-major": { light: tinted(0.9), dark: tinted(0.9) },
957
- "--dsw-specific-sidebar-nav-item-active": { light: tinted(0.72), dark: tinted(0.72) },
958
- "--dsw-specific-sidebar-nav-item-hover": { light: tinted(0.6), dark: tinted(0.6) },
959
- "--dsw-specific-sidebar-nav-item-active-accent": { light: tinted(0.6), dark: tinted(0.6) },
960
- "--dsw-alias-bg-module-platform": { light: tinted(0.86), dark: tinted(0.86) },
961
- "--dsw-alias-bg-multi-select": { light: tinted(0.82), dark: tinted(0.82) },
962
- "--dsw-alias-button-elevated-fill": { light: tinted(0.78), dark: tinted(0.78) },
963
- "--dsw-alias-button-floating-fill": { light: tinted(0.85), dark: tinted(0.85) },
964
- "--dsw-alias-button-ghost-active-fill": { light: tinted(0.7), dark: tinted(0.7) },
965
- "--dsw-alias-button-ghost-active-border": { light: tinted(0.6), dark: tinted(0.6) },
966
- "--dsw-alias-button-contrast-fill": { light: tinted(0.9), dark: tinted(0.9) },
967
- "--dsw-alias-interactive-bg-selected": { light: tinted(0.68), dark: tinted(0.68) },
968
- "--dsw-alias-interactive-bg-hover": { light: tinted(0.55), dark: tinted(0.55) },
969
- "--dsw-alias-interactive-bg-hover-solid": { light: tinted(0.72), dark: tinted(0.72) },
970
- "--dsw-alias-tooltip-bg": { light: `color-mix(in srgb, rgb(var(--mpw-aqua-rgb)) 96%, var(--dsw-static-neutral-bluish-850))`, dark: `color-mix(in srgb, rgb(var(--mpw-aqua-rgb)) 96%, var(--dsw-static-neutral-bluish-850))` },
971
- "--dsw-alias-toast-bg": { light: tinted(0.9), dark: tinted(0.9) },
972
- "--dsw-alias-markdown-inline-code": { light: tinted(0.24), dark: tinted(0.24) },
973
- "--dsw-alias-markdown-code-block": { light: tinted(0.32), dark: tinted(0.32) }
974
- };
960
+ // ①(修正) 职责分离:aquaTint(面板取色)只影响**面板/表面**颜色;
961
+ // **弹层**(菜单/选择器/下拉/提示等)透明只归 aquaMask(统一雾)管——
962
+ // 用户实测 aquaTint 开时 full access/加号/模型/推理/上下文选择器被变透明(不该)
963
+ const tintOn = section.aquaTint !== void 0 ? !!section.aquaTint : DEFAULT_AQUA_TINT;
964
+ const maskOn = section.aquaMask !== void 0 ? !!section.aquaMask : DEFAULT_AQUA_MASK;
965
+ const out = {};
966
+ // 主画布 + 面板/表面类(取色或统一雾开启时生效)
967
+ if (tintOn || maskOn) {
968
+ Object.assign(out, {
969
+ "--dsw-alias-bg-base": { light: tinted(0.62), dark: tinted(0.62) },
970
+ "--dsw-specific-sidebar-fill": { light: tinted(0.85), dark: tinted(0.85) },
971
+ "--dsw-specific-sidebar-nav-item-active": { light: tinted(0.72), dark: tinted(0.72) },
972
+ "--dsw-specific-sidebar-nav-item-hover": { light: tinted(0.6), dark: tinted(0.6) },
973
+ "--dsw-specific-sidebar-nav-item-active-accent": { light: tinted(0.6), dark: tinted(0.6) },
974
+ "--dsw-specific-bubble": { light: tinted(0.84), dark: tinted(0.84) },
975
+ "--dsw-specific-bubble-highlight": { light: tinted(0.92), dark: tinted(0.92) },
976
+ "--dsw-alias-button-elevated-fill": { light: tinted(0.78), dark: tinted(0.78) },
977
+ "--dsw-alias-button-floating-fill": { light: tinted(0.85), dark: tinted(0.85) },
978
+ "--dsw-alias-button-ghost-active-fill": { light: tinted(0.7), dark: tinted(0.7) },
979
+ "--dsw-alias-button-ghost-active-border": { light: tinted(0.6), dark: tinted(0.6) },
980
+ "--dsw-alias-button-contrast-fill": { light: tinted(0.9), dark: tinted(0.9) },
981
+ "--dsw-alias-interactive-bg-selected": { light: tinted(0.68), dark: tinted(0.68) },
982
+ "--dsw-alias-interactive-bg-hover": { light: tinted(0.55), dark: tinted(0.55) },
983
+ "--dsw-alias-interactive-bg-hover-solid": { light: tinted(0.72), dark: tinted(0.72) },
984
+ "--dsw-alias-markdown-inline-code": { light: tinted(0.24), dark: tinted(0.24) },
985
+ "--dsw-alias-markdown-code-block": { light: tinted(0.32), dark: tinted(0.32) }
986
+ });
987
+ }
988
+ // 弹层类(仅统一雾开启时透明——取色不该管弹层)
989
+ if (maskOn) {
990
+ Object.assign(out, {
991
+ "--dsw-alias-bg-overlay": { light: tinted(0.72), dark: tinted(0.72) },
992
+ "--dsw-alias-bg-layer-2": { light: tinted(0.72), dark: tinted(0.72) },
993
+ "--dsw-alias-bg-layer-3": { light: tinted(0.72), dark: tinted(0.72) },
994
+ "--dsw-specific-menu": { light: tinted(0.86), dark: tinted(0.86) },
995
+ "--dsw-specific-selector": { light: tinted(0.78), dark: tinted(0.78) },
996
+ "--dsw-specific-tip": { light: tinted(0.85), dark: tinted(0.85) },
997
+ "--dsw-specific-input-major": { light: tinted(0.9), dark: tinted(0.9) },
998
+ "--dsw-alias-bg-module-platform": { light: tinted(0.86), dark: tinted(0.86) },
999
+ "--dsw-alias-bg-multi-select": { light: tinted(0.82), dark: tinted(0.82) },
1000
+ "--dsw-alias-tooltip-bg": { light: `color-mix(in srgb, rgb(var(--mpw-aqua-rgb)) 96%, var(--dsw-static-neutral-bluish-850))`, dark: `color-mix(in srgb, rgb(var(--mpw-aqua-rgb)) 96%, var(--dsw-static-neutral-bluish-850))` },
1001
+ "--dsw-alias-toast-bg": { light: tinted(0.9), dark: tinted(0.9) }
1002
+ });
1003
+ }
975
1004
  const inkOn = section.aquaInk !== void 0 ? !!section.aquaInk : DEFAULT_AQUA_INK;
976
1005
  if (inkOn) {
977
1006
  // ⑲(修正) 品牌色支持自定义(aquaInkColor 取色器):默认用 ink,
@@ -1265,7 +1294,7 @@ html, body {
1265
1294
  (原 scale 在前导致 zoom>100 时平移被放大、zoom<100 时平移不够,
1266
1295
  用户反馈"只能渲染整个屏幕的位置")。transform 列表从左到右复合,
1267
1296
  translate() scale() = 先缩放后平移(平移是原始像素)。 */
1268
- transform: translate(var(--mpw-lensX, 0), var(--mpw-lensY, 0)) scale(var(--mpw-zoom, 1));
1297
+ transform: translate(var(--mpw-lensX, 0), var(--mpw-lensY, 0)) scale(var(--mpw-zoom, 1)) scaleX(var(--mpw-flip-x, 1)) scaleY(var(--mpw-flip-y, 1));
1269
1298
  transform-origin: center center;
1270
1299
  /* ⑯ 磨砂模糊条:壁纸层自身 blur,拖到 0 = 完全清晰 */
1271
1300
  filter: var(--mpw-bg-blur, none) brightness(var(--mpw-brightness, 1)) ${section.sharp !== void 0 && !section.sharp ? "" : "contrast(1.06) saturate(1.12)"};
@@ -1442,11 +1471,12 @@ body[data-ds-dark-theme] .wSkVaW_header {
1442
1471
  const settingsFrosted = settingsBlur && settingsAmount > 0 && bdSupported;
1443
1472
  const confirmFrosted = confirmBlur && confirmAmount > 0 && bdSupported;
1444
1473
  const sidebarFrosted = sidebarBlur && sidebarBlurAmount > 0 && bdSupported;
1445
- // ── A. 不透明兜底(对应开关关时防 token 半透明透出聊天内容)──
1446
- // 设置面板/通用对话框是 role=dialog:token override bg-base 变半透明后
1447
- // 面板会透出背后聊天标题/按钮(用户实测"聊天内容显示在设置界面")。
1448
- if (!dlgFrosted) css += `
1449
- /* 对话框虚化关 → 通用居中窗口不透明兜底(防透出) */
1474
+ // ── A. 不透明兜底(防 token 半透明透出聊天内容)──
1475
+ // ①(修正) **始终输出**:所有 [role=dialog] 实体背景——特别是**非 overlay
1476
+ // 小弹窗**(上下文占用 264px 面板等)必须实体(用户实测 aqua 全关时它也被
1477
+ // dialogBlur 磨砂成透明);磨砂只在 B 块给 overlay 内的真正对话框覆盖。
1478
+ css += `
1479
+ /* 通用居中窗口不透明兜底(防透出;overlay 内的对话框由 B 块磨砂覆盖) */
1450
1480
  [role="dialog"],
1451
1481
  [role="alertdialog"] {
1452
1482
  background-color: var(--dsw-static-neutral-bluish-00) !important;
@@ -1478,9 +1508,11 @@ body:not([data-ds-dark-theme]) .mpw_dialog {
1478
1508
  if (dlgFrosted) {
1479
1509
  const dlgFilter = `blur(${dialogAmount}px)`;
1480
1510
  css += `
1481
- /* ── 对话框虚化(通用居中窗口 + 聊天输入框;设置面板由 settingsBlur 单独覆盖) ── */
1482
- [role="dialog"],
1483
- [role="alertdialog"],
1511
+ /* ── 对话框虚化(overlay 内的居中窗口 + 聊天输入框;设置面板由 settingsBlur 单独覆盖) ──
1512
+ ①(修正) 只作用于 overlay(遮罩层)内的对话框——真正的居中弹窗;
1513
+ 非 overlay 的小弹窗(上下文占用面板等)保持 A 块实体背景(用户实测小弹窗被磨砂成透明) */
1514
+ [class*="overlay"] [role="dialog"],
1515
+ [class*="overlay"] [role="alertdialog"],
1484
1516
  [data-composer-card] {
1485
1517
  backdrop-filter: ${dlgFilter} !important;
1486
1518
  -webkit-backdrop-filter: ${dlgFilter} !important;
@@ -1490,12 +1522,12 @@ body:not([data-ds-dark-theme]) .mpw_dialog {
1490
1522
  半透明但无 blur 会透出清晰聊天文字 → 设置界面污染(旧 bug)。
1491
1523
  80% 半透明 + blur:背后文字变糊影不可读,模糊的壁纸色调明显透出
1492
1524
  (90% 时磨砂感几乎不可见,用户实测仍像纯白)。 */
1493
- [role="dialog"],
1494
- [role="alertdialog"] {
1525
+ [class*="overlay"] [role="dialog"],
1526
+ [class*="overlay"] [role="alertdialog"] {
1495
1527
  background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-00) 80%, transparent) !important;
1496
1528
  }
1497
- body[data-ds-dark-theme] [role="dialog"],
1498
- body[data-ds-dark-theme] [role="alertdialog"] {
1529
+ body[data-ds-dark-theme] [class*="overlay"] [role="dialog"],
1530
+ body[data-ds-dark-theme] [class*="overlay"] [role="alertdialog"] {
1499
1531
  background-color: color-mix(in srgb, var(--dsw-static-neutral-bluish-950) 80%, transparent) !important;
1500
1532
  }
1501
1533
  [role="dialog"] [class*="card"] {
@@ -1906,6 +1938,30 @@ body[data-ds-dark-theme] .mpw_tabBar {
1906
1938
  }
1907
1939
  .mpw_miniBtn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
1908
1940
  .mpw_hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); margin: 0; }
1941
+ /* ①(新) 标题行:仓库链接(灰色、无下划线、hover 变深)+ 版本号 */
1942
+ .mpw_titleRow { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
1943
+ .mpw_repoLink {
1944
+ font-size: 13px; font-weight: 500; color: var(--dsw-alias-label-secondary);
1945
+ text-decoration: none; cursor: pointer; border: none; background: none;
1946
+ }
1947
+ .mpw_repoLink:hover { color: var(--dsw-alias-brand-primary, #4f6ef7); }
1948
+ .mpw_repoLink:visited { color: var(--dsw-alias-label-secondary); }
1949
+ .mpw_version { font-size: 12px; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
1950
+ /* ①(新) 取色盘预置色:elysia395 风格圆(加大、白边、阴影、hover 放大) */
1951
+ .mpw_presetSwatch {
1952
+ flex: none; width: 30px; height: 30px; box-sizing: border-box;
1953
+ border-radius: 50%; padding: 0;
1954
+ border: 2px solid rgba(255, 255, 255, 0.75);
1955
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
1956
+ cursor: pointer; transition: transform 0.12s ease, box-shadow 0.12s ease;
1957
+ }
1958
+ .mpw_presetSwatch:hover { transform: scale(1.12); }
1959
+ /* ①(新) 壁纸扫描结果预览缩略图 */
1960
+ .mpw_thumb { flex: none; width: 72px; height: 40px; border-radius: 6px; overflow: hidden; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2); }
1961
+ .mpw_thumbImg { width: 100%; height: 100%; object-fit: cover; display: block; }
1962
+ /* ①(新) 壁纸列表两列网格(一排两个) */
1963
+ .mpw_props.mpw_wallGrid { display: grid !important; grid-template-columns: 1fr 1fr; gap: 8px; align-items: start; }
1964
+ .mpw_wallGrid .mpw_wallProp { min-width: 0; }
1909
1965
  /* ④(新) 导入失败/错误提示:红色醒目,不再一闪而过看不清 */
1910
1966
  .mpw_hint.mpw_err { color: #ff6b6b; font-weight: 500; }
1911
1967
  .mpw_info { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); margin: 0; }
@@ -2219,6 +2275,8 @@ body[data-mpw-modal] [role="dialog"] {
2219
2275
 
2220
2276
  // ⑳(新) Tab 顺序(内容区 translateX 滑动用)
2221
2277
  const TAB_ORDER = ["source", "appearance", "unify", "blur", "show", "aqua", "other"];
2278
+ // ①(新) 取色盘预置色(借鉴 elysia395/dsh-wallpaper-engine):点击即用
2279
+ const AQUA_PRESETS = ["#4f8cff", "#67DCE7", "#DD8FAC", "#F3B75F", "#F1717F", "#CBE77D"];
2222
2280
 
2223
2281
  function MpkgSection(props) {
2224
2282
  try {
@@ -2248,6 +2306,7 @@ body[data-mpw-modal] [role="dialog"] {
2248
2306
  const [errorMsg, setErrorMsg] = react.useState("");
2249
2307
  const [conflicts, setConflicts] = react.useState([]);
2250
2308
  const [hostOk, setHostOk] = react.useState(null); // null=未检测 true=可用 false=不可用
2309
+ const [hostVersion, setHostVersion] = react.useState(""); // host 返回的插件版本(标题显示)
2251
2310
  const [libWalls, setLibWalls] = react.useState(null); // null=未扫描 [] = 空列表
2252
2311
  const [libBusy, setLibBusy] = react.useState(false);
2253
2312
  const [customDir, setCustomDirState] = react.useState(readSection().customDirPath || "");
@@ -2272,7 +2331,10 @@ body[data-mpw-modal] [role="dialog"] {
2272
2331
  react.useEffect(() => {
2273
2332
  setConflicts(detectConflicts());
2274
2333
  // ③(新) 检测宿主端可用性(大文件混合模式是否生效)
2275
- fetch(HOST_BASE + "/ping", { method: "GET" }).then((r) => setHostOk(!!r.ok)).catch(() => setHostOk(false));
2334
+ fetch(HOST_BASE + "/ping", { method: "GET" }).then(async (r) => {
2335
+ setHostOk(!!r.ok);
2336
+ if (r.ok) { try { const d = await r.json(); if (d && d.version) setHostVersion(d.version); } catch {} }
2337
+ }).catch(() => setHostOk(false));
2276
2338
  }, []);
2277
2339
 
2278
2340
  // ⑳(修正) Tab 下划线平滑移动:测量激活 tab 位置/宽度 → indicator 平移过去
@@ -3014,7 +3076,11 @@ body[data-mpw-modal] [role="dialog"] {
3014
3076
 
3015
3077
  return h("div", { className: "mpw_row" }, [
3016
3078
  // ①(修正) 标题/描述在 Tab 条上方
3017
- h("div", { className: "mpw_title" }, t("title")),
3079
+ h("div", { className: "mpw_titleRow" }, [
3080
+ h("span", { className: "mpw_title" }, t("title")),
3081
+ h("a", { className: "mpw_repoLink", href: "https://github.com/XHR666/dsh-mpkg-wallpaper", target: "_blank", rel: "noopener" }, "dsh-mpkg-wallpaper"),
3082
+ hostVersion ? h("span", { className: "mpw_version" }, "v" + hostVersion) : null
3083
+ ]),
3018
3084
  h("p", { className: "mpw_desc" }, t("desc")),
3019
3085
  // ②(修正) 圆角导航栏 + 下划线平滑移动(indicator 平移)+ 内容真滑动(translateX)
3020
3086
  h("div", { className: "mpw_tabBar", "data-mpw-tabbar": "" }, [
@@ -3052,6 +3118,11 @@ body[data-mpw-modal] [role="dialog"] {
3052
3118
  // ①(新) 大文件混合模式开关(背景来源组)
3053
3119
  toggleRow(t("hybrid"), t("hybrid.desc"), "hybrid", DEFAULT_HYBRID),
3054
3120
 
3121
+ // ①(修正) 清除背景按钮:挪到自定义本地壁纸目录上方(用户要求;不重置外观数值)
3122
+ section.image ? h("div", { className: "mpw_field" }, [
3123
+ h("button", { className: "mpw_reset", type: "button", onClick: clearBg }, t("clear.bg"))
3124
+ ]) : null,
3125
+
3055
3126
  // ①(新) 自定义本地壁纸目录(任意文件夹,不限于 Wallpaper Engine)
3056
3127
  h("div", { className: "mpw_field" }, [
3057
3128
  h("label", { className: "mpw_label" }, t("lib.custom")),
@@ -3070,12 +3141,32 @@ body[data-mpw-modal] [role="dialog"] {
3070
3141
  wallList.length ? h("div", { className: "mpw_inline" }, [
3071
3142
  h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => setLibOpen(!libOpen) }, libOpen ? t("lib.collapse") : t("lib.expandAll") + `(${wallList.length})`)
3072
3143
  ]) : null,
3073
- libOpen && wallList.length ? h("div", { className: "mpw_props", style: { maxHeight: 260, overflowY: "auto" } }, [
3074
- wallList.slice(0, libShow).map((w, wi) => h("div", { className: "mpw_prop", key: w.key }, [
3075
- h("div", { className: "mpw_propLabel" }, h("b", null, w.title)),
3076
- h("div", { className: "mpw_inline" }, [
3077
- h("span", { className: "mpw_hint" }, w.type),
3078
- h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => { setWallIdx(wallList.indexOf(w)); applyWallFromList(w); } }, t("lib.use"))
3144
+ libOpen && wallList.length ? h("div", { className: "mpw_props mpw_wallGrid", style: { maxHeight: 260, overflowY: "auto" } }, [
3145
+ wallList.slice(0, libShow).map((w, wi) => h("div", { className: "mpw_prop mpw_wallProp", key: w.key }, [
3146
+ h("div", { className: "mpw_inline", style: { alignItems: "flex-start", flexWrap: "nowrap" } }, [
3147
+ // ①(新) 扫描结果预览缩略图(借鉴 elysia395/dsh-wallpaper-engine preview 样式)
3148
+ h("div", { className: "mpw_thumb" }, [
3149
+ // (修正) 视频/动图项用 <video> 首帧预览(muted + preload=metadata 显示第一帧);
3150
+ // 图片项用 <img>(参照 elysia395 的 preview 显示)
3151
+ // ①(修正) mpkg 项用容器内 preview 图(custom-mpkg-preview 路由);视频用首帧;图片直接用
3152
+ w.type === "mpkg"
3153
+ ? h("img", { className: "mpw_thumbImg", src: HOST_BASE + "/custom-mpkg-preview?file=" + encodeURIComponent(w.name), alt: "", loading: "lazy",
3154
+ onError: (ev) => { ev.target.style.display = "none"; } })
3155
+ : ((w.type === "video" || w.converted === "mp4")
3156
+ ? h("video", { className: "mpw_thumbImg", src: resolveHostUrl(w.image), muted: true, playsInline: true, preload: "metadata",
3157
+ // ①(修正) 视频封面:loadedmetadata 后 seek 到 0.05s 触发首帧绘制(部分浏览器默认不显示首帧)
3158
+ onLoadedMetadata: (ev) => { try { const v = ev.target; if (v.duration && v.duration > 0.1) v.currentTime = 0.05; } catch {} },
3159
+ onError: (ev) => { ev.target.style.display = "none"; } })
3160
+ : h("img", { className: "mpw_thumbImg", src: resolveHostUrl(w.image), alt: "", loading: "lazy",
3161
+ onError: (ev) => { ev.target.style.display = "none"; } }))
3162
+ ]),
3163
+ h("div", { style: { flex: 1, minWidth: 0 } }, [
3164
+ h("div", { className: "mpw_propLabel" }, h("b", null, w.title)),
3165
+ h("div", { className: "mpw_inline" }, [
3166
+ h("span", { className: "mpw_hint" }, w.type),
3167
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => { setWallIdx(wallList.indexOf(w)); applyWallFromList(w); } }, t("lib.use"))
3168
+ ])
3169
+ ])
3079
3170
  ])
3080
3171
  ])),
3081
3172
  wallList.length > libShow
@@ -3084,19 +3175,6 @@ body[data-mpw-modal] [role="dialog"] {
3084
3175
  ]) : null
3085
3176
  ]),
3086
3177
 
3087
- // ②③(新) 下一个壁纸 + 定时轮换
3088
- h("div", { className: "mpw_field" }, [
3089
- h("label", { className: "mpw_label" }, t("lib.rotate")),
3090
- h("div", { className: "mpw_inline" }, [
3091
- h("button", { className: "mpw_reset", type: "button", onClick: prevWallpaper }, t("lib.prev")),
3092
- h("button", { className: "mpw_reset", type: "button", onClick: nextWallpaper }, t("lib.next")),
3093
- h(Toggle, { checked: section.rotate !== void 0 ? !!section.rotate : DEFAULT_ROTATE, onChange: (v) => commit({ rotate: v }) }),
3094
- h("span", { className: "mpw_hint" }, t("lib.rotateDesc"))
3095
- ]),
3096
- h("div", { style: { display: (section.rotate !== void 0 ? !!section.rotate : DEFAULT_ROTATE) ? "" : "none" } },
3097
- sliderRow(t("lib.rotateMin"), "rotateMin", 1, 120, t("lib.minutes"), 1, 5))
3098
- ]),
3099
-
3100
3178
  // ④(新) 本地壁纸库(Steam 自动发现)
3101
3179
  h("div", { className: "mpw_field" }, [
3102
3180
  h("label", { className: "mpw_label" }, t("lib.title")),
@@ -3110,17 +3188,30 @@ body[data-mpw-modal] [role="dialog"] {
3110
3188
  ])
3111
3189
  : null,
3112
3190
  libOpen && libWalls && libWalls.length
3113
- ? h("div", { className: "mpw_props", style: { maxHeight: 220, overflowY: "auto" } }, [
3114
- libWalls.slice(0, libShow).map((wp, wi) => h("div", { className: "mpw_prop", key: wp.ltoken }, [
3115
- h("div", { className: "mpw_propLabel" }, h("b", null, wp.title || wp.ltoken)),
3116
- h("div", { className: "mpw_inline" }, [
3117
- h("span", { className: "mpw_hint" }, wp.type),
3118
- h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => {
3119
- // (修正) wallList 中的索引对齐轮播(wallIdx wallList 下标)
3120
- const mi = wallList.findIndex((w) => w.key === "steam|" + wp.ltoken);
3121
- if (mi >= 0) setWallIdx(mi);
3122
- applyLibraryWallpaper(wp);
3123
- } }, t("lib.use"))
3191
+ ? h("div", { className: "mpw_props mpw_wallGrid", style: { maxHeight: 220, overflowY: "auto" } }, [
3192
+ libWalls.slice(0, libShow).map((wp, wi) => h("div", { className: "mpw_prop mpw_wallProp", key: wp.ltoken }, [
3193
+ h("div", { className: "mpw_inline", style: { alignItems: "flex-start", flexWrap: "nowrap" } }, [
3194
+ // ①(新) Steam 库预览缩略图(preview.jpg /library-media;视频用 media 首帧)
3195
+ h("div", { className: "mpw_thumb" }, [
3196
+ wp.type === "video" && wp.media
3197
+ ? h("video", { className: "mpw_thumbImg", src: HOST_BASE + "/library-media?ltoken=" + encodeURIComponent(wp.ltoken) + "&file=" + encodeURIComponent(String(wp.media).split(/[\\/]/).pop()), muted: true, playsInline: true, preload: "metadata",
3198
+ onLoadedMetadata: (ev) => { try { const v = ev.target; if (v.duration && v.duration > 0.1) v.currentTime = 0.05; } catch {} },
3199
+ onError: (ev) => { ev.target.style.display = "none"; } })
3200
+ : h("img", { className: "mpw_thumbImg", src: HOST_BASE + "/library-media?ltoken=" + encodeURIComponent(wp.ltoken) + "&file=" + encodeURIComponent(String(wp.preview || "").split(/[\\/]/).pop() || "preview.jpg"), alt: "", loading: "lazy",
3201
+ onError: (ev) => { ev.target.style.display = "none"; } })
3202
+ ]),
3203
+ h("div", { style: { flex: 1, minWidth: 0 } }, [
3204
+ h("div", { className: "mpw_propLabel" }, h("b", null, wp.title || wp.ltoken)),
3205
+ h("div", { className: "mpw_inline" }, [
3206
+ h("span", { className: "mpw_hint" }, wp.type),
3207
+ h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => {
3208
+ // ②(修正) 用 wallList 中的索引对齐轮播(wallIdx 是 wallList 下标)
3209
+ const mi = wallList.findIndex((w) => w.key === "steam|" + wp.ltoken);
3210
+ if (mi >= 0) setWallIdx(mi);
3211
+ applyLibraryWallpaper(wp);
3212
+ } }, t("lib.use"))
3213
+ ])
3214
+ ])
3124
3215
  ])
3125
3216
  ])),
3126
3217
  libWalls.length > libShow
@@ -3129,6 +3220,19 @@ body[data-mpw-modal] [role="dialog"] {
3129
3220
  ])
3130
3221
  : libWalls && !libWalls.length ? h("p", { className: "mpw_hint" }, t("lib.empty")) : null
3131
3222
  ]),
3223
+ // ②③(新) 下一个壁纸 + 定时轮换
3224
+ h("div", { className: "mpw_field" }, [
3225
+ h("label", { className: "mpw_label" }, t("lib.rotate")),
3226
+ h("div", { className: "mpw_inline" }, [
3227
+ h("button", { className: "mpw_reset", type: "button", onClick: prevWallpaper }, t("lib.prev")),
3228
+ h("button", { className: "mpw_reset", type: "button", onClick: nextWallpaper }, t("lib.next")),
3229
+ h(Toggle, { checked: section.rotate !== void 0 ? !!section.rotate : DEFAULT_ROTATE, onChange: (v) => commit({ rotate: v }) }),
3230
+ h("span", { className: "mpw_hint" }, t("lib.rotateDesc"))
3231
+ ]),
3232
+ h("div", { style: { display: (section.rotate !== void 0 ? !!section.rotate : DEFAULT_ROTATE) ? "" : "none" } },
3233
+ sliderRow(t("lib.rotateMin"), "rotateMin", 1, 120, t("lib.minutes"), 1, 5))
3234
+ ]),
3235
+
3132
3236
  // ③(新) 宿主端状态(proot/本机测试方法:可用 = 大文件无限制生效)
3133
3237
  h("p", { className: "mpw_hint" },
3134
3238
  (section.hybrid !== void 0 ? !!section.hybrid : DEFAULT_HYBRID)
@@ -3145,10 +3249,7 @@ body[data-mpw-modal] [role="dialog"] {
3145
3249
  h("span", { className: "mpw_hint" }, t("mpkg.hint"))
3146
3250
  ]),
3147
3251
  h("input", { ref: mpkgRef, type: "file", accept: ".mpkg,.mp4,.webm,.mkv,.mov", style: { display: "none" }, onChange: onMpkg }),
3148
- // ① 单独清除背景的控件(不重置外观数值)
3149
- section.image ? h("div", { className: "mpw_inline" }, [
3150
- h("button", { className: "mpw_reset", type: "button", onClick: clearBg }, t("clear.bg"))
3151
- ]) : null,
3252
+
3152
3253
  mpkgMeta && mpkgMeta.info && section.fromMpkg ? h("div", { className: "mpw_props" }, [
3153
3254
  mpkgMeta.name ? h("p", { className: "mpw_info" }, h("b", null, mpkgMeta.name)) : null,
3154
3255
  mpkgMeta.info && mpkgMeta.info.title && mpkgMeta.info.title !== mpkgMeta.name ? h("p", { className: "mpw_hint" }, mpkgMeta.info.title) : null,
@@ -3235,6 +3336,11 @@ body[data-mpw-modal] [role="dialog"] {
3235
3336
  h("p", { className: "mpw_hint" }, t("sec.appearance.desc")),
3236
3337
 
3237
3338
  toggleRow(t("float"), t("float.desc"), "float", DEFAULT_FLOAT),
3339
+ // ①(新) 壁纸镜像翻转(Wallpaper Engine 原生基础选项)
3340
+ h("div", { className: "mpw_inline" }, [
3341
+ toggleRow(t("flipX"), t("flipX.desc"), "flipX", false),
3342
+ toggleRow(t("flipY"), t("flipY.desc"), "flipY", false)
3343
+ ]),
3238
3344
  // ①(修正) 面板不透明度已删除:统一虚化开启时它被 sidebarAlpha 取代且无效果,
3239
3345
  // 非统一虚化下也无独立意义 → 移除滑条,保留内部默认值逻辑。
3240
3346
  // ①(修正) 磨砂模糊条:仅当「统一虚化开 + 聊天区跟随开」时被整屏虚化接管 → 禁用并提示;
@@ -3425,9 +3531,15 @@ body[data-mpw-modal] [role="dialog"] {
3425
3531
  style: { background: (section.aquaColor && /^#[0-9a-fA-F]{6}$/.test(section.aquaColor) ? section.aquaColor : "#808080") },
3426
3532
  onClick: () => openPicker("aquaColor", section.aquaColor)
3427
3533
  }),
3428
- h("button", { className: "mpw_miniBtn", type: "button", onClick: () => commit({ aquaColor: "" }, true) }, t("aquaColorReset")),
3429
- h("span", { className: "mpw_hint" }, t("aquaColor.hint"))
3430
- ])
3534
+ // () 预置色 swatch(点击即用,借鉴 elysia395 项目)
3535
+ ...(AQUA_PRESETS.map((hex) => h("button", {
3536
+ className: "mpw_presetSwatch", type: "button", title: hex,
3537
+ style: { background: hex }, onClick: () => commit({ aquaColor: hex }, true)
3538
+ }))),
3539
+ h("button", { className: "mpw_miniBtn", type: "button", onClick: () => commit({ aquaColor: "" }, true) }, t("aquaColorReset"))
3540
+ ]),
3541
+ // ①(修正) 说明换行到下方(旁边放预置色)
3542
+ h("p", { className: "mpw_hint" }, t("aquaColor.hint"))
3431
3543
  ]),
3432
3544
  h("div", { className: "mpw_field" }, [
3433
3545
  h("label", { className: "mpw_label" }, t("aquaInkColor")),
@@ -3437,9 +3549,13 @@ body[data-mpw-modal] [role="dialog"] {
3437
3549
  style: { background: (section.aquaInkColor && /^#[0-9a-fA-F]{6}$/.test(section.aquaInkColor) ? section.aquaInkColor : "#808080") },
3438
3550
  onClick: () => openPicker("aquaInkColor", section.aquaInkColor)
3439
3551
  }),
3440
- h("button", { className: "mpw_miniBtn", type: "button", onClick: () => commit({ aquaInkColor: "" }, true) }, t("aquaColorReset")),
3441
- h("span", { className: "mpw_hint" }, t("aquaInkColor.hint"))
3442
- ])
3552
+ ...(AQUA_PRESETS.map((hex) => h("button", {
3553
+ className: "mpw_presetSwatch", type: "button", title: hex,
3554
+ style: { background: hex }, onClick: () => commit({ aquaInkColor: hex }, true)
3555
+ }))),
3556
+ h("button", { className: "mpw_miniBtn", type: "button", onClick: () => commit({ aquaInkColor: "" }, true) }, t("aquaColorReset"))
3557
+ ]),
3558
+ h("p", { className: "mpw_hint" }, t("aquaInkColor.hint"))
3443
3559
  ]),
3444
3560
  // ⑲(新) 深底文字可读增强(近似方案:全局双色描边)
3445
3561
  toggleRow(t("aquaTextEnhance"), t("aquaTextEnhance.desc"), "aquaTextEnhance", DEFAULT_AQUA_TEXT_ENHANCE),
@@ -3475,9 +3591,11 @@ body[data-mpw-modal] [role="dialog"] {
3475
3591
  : null
3476
3592
  ]),
3477
3593
  updState && updState.error ? h("p", { className: "mpw_hint mpw_err" }, t("update.fail") + updState.error) : null,
3478
- updState && updState.hasUpdate && updState.sameVersionDiff
3479
- ? h("p", { className: "mpw_hint mpw_err" }, t("update.diff"))
3480
- : (updState && updState.hasUpdate ? h("p", { className: "mpw_hint" }, t("update.found") + updState.localVersion + " → " + updState.remoteVersion) : null),
3594
+ updState && updState.hasUpdate
3595
+ ? h("p", { className: "mpw_hint" }, t("update.found") + updState.localVersion + " → " + updState.remoteVersion)
3596
+ : (updState && updState.hasUpdate === false && updState.contentDiff
3597
+ ? h("p", { className: "mpw_hint mpw_err" }, t("update.diff"))
3598
+ : null),
3481
3599
  updState && updState.applied ? h("p", { className: "mpw_hint" }, t("update.applied")) : null,
3482
3600
  updState && updState.hasUpdate === false ? h("p", { className: "mpw_hint" }, t("update.latest")) : null
3483
3601
  ]),
@@ -3732,6 +3850,10 @@ body[data-mpw-modal] [role="dialog"] {
3732
3850
  "brightness": "画面亮度",
3733
3851
  "float": "悬浮效果",
3734
3852
  "float.desc": "侧边栏/标题栏变为悬浮卡片(圆角+阴影+透出模糊壁纸);默认关,开启后原有的透明/虚化功能不受影响",
3853
+ "flipX": "水平翻转(镜像)",
3854
+ "flipX.desc": "壁纸左右镜像(scaleX -1,Wallpaper Engine 原生基础选项)",
3855
+ "flipY": "垂直翻转(镜像)",
3856
+ "flipY.desc": "壁纸上下镜像(scaleY -1)",
3735
3857
  "blur": "磨砂模糊",
3736
3858
  "blur.overridden": "统一虚化 + 聊天区跟随均开启:壁纸模糊由「整屏虚化程度」接管,磨砂条暂不可调;关闭「聊天区跟随整屏虚化」后磨砂条恢复可调(此时统一虚化只管侧边栏/标题栏)",
3737
3859
  "zoom": "镜头缩放",
package/lib/index.js CHANGED
@@ -11,6 +11,20 @@ import crypto from 'node:crypto';
11
11
  import { execFileSync } from 'node:child_process';
12
12
 
13
13
  const BASE = '/api/mpkg-wallpaper';
14
+ /** ①(修正) 更新检测:版本号主导(semver),哈希仅作内容差异提示。
15
+ * 之前纯哈希对比——本地有未推送改动就误报"新版本 3.1.2 → 3.1.2"(用户实测)。 */
16
+ function semverGt(a, b) {
17
+ try {
18
+ const pa = String(a || '').replace(/[^\d.]/g, '').split('.').map(Number);
19
+ const pb = String(b || '').replace(/[^\d.]/g, '').split('.').map(Number);
20
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
21
+ const x = pa[i] || 0, y = pb[i] || 0;
22
+ if (x > y) return true;
23
+ if (x < y) return false;
24
+ }
25
+ return false;
26
+ } catch { return false; }
27
+ }
14
28
  const HEAD_BYTES = 2 * 1024 * 1024; // 与客户端一致的容器头读取量
15
29
  const WE_APPID = '431960';
16
30
  const STEAM_PROBE_DIRS = [
@@ -31,6 +45,34 @@ const files = new Map();
31
45
  const library = new Map();
32
46
  /** ①(新) 自定义本地壁纸目录:用户指定的文件夹路径(只读媒体文件,安全校验) */
33
47
  let customDir = null;
48
+ /** ①(新) mpkg preview 缓存:key=文件名 → { mtimeMs, mime, bytes }。
49
+ * 扫描列表缩略图会请求每个 mpkg 的 preview——不缓存则每次都重新解析头部(2MB 读)+ 打开大文件。
50
+ * ①(修正) **LRU + 容量上限**:几万张壁纸时缓存 bytes 会撑爆内存(用户实测担忧)——
51
+ * 总字节上限 64MB、条目上限 128,超限淘汰最久未用;get 时 touch(重插到末尾保持 LRU 顺序)。 */
52
+ const mpkgPreviewCache = new Map();
53
+ const MPKG_PREVIEW_MAX_BYTES = 64 * 1024 * 1024; // 总缓存上限 64MB
54
+ const MPKG_PREVIEW_MAX_ITEMS = 128; // 条目上限 128
55
+ let mpkgPreviewBytes = 0;
56
+ function mpkgPreviewGet(file) {
57
+ const c = mpkgPreviewCache.get(file);
58
+ if (c) { mpkgPreviewCache.delete(file); mpkgPreviewCache.set(file, c); }
59
+ return c;
60
+ }
61
+ function mpkgPreviewSet(file, c) {
62
+ const old = mpkgPreviewCache.get(file);
63
+ if (old) mpkgPreviewBytes -= old.bytes.length;
64
+ mpkgPreviewCache.delete(file);
65
+ mpkgPreviewCache.set(file, c);
66
+ mpkgPreviewBytes += c.bytes.length;
67
+ // 超限 → 淘汰最久未用(Map 头部即最早插入)
68
+ while ((mpkgPreviewBytes > MPKG_PREVIEW_MAX_BYTES || mpkgPreviewCache.size > MPKG_PREVIEW_MAX_ITEMS) && mpkgPreviewCache.size > 1) {
69
+ const firstKey = mpkgPreviewCache.keys().next().value;
70
+ if (firstKey === undefined) break;
71
+ const evicted = mpkgPreviewCache.get(firstKey);
72
+ mpkgPreviewBytes -= evicted.bytes.length;
73
+ mpkgPreviewCache.delete(firstKey);
74
+ }
75
+ }
34
76
  /** ①(修正) customDir 持久化:dsh 重启后恢复(否则 /custom-media 404 → 自定义目录壁纸消失)。
35
77
  * 存入 tmpdir 下的 JSON,随 tmpdir 清理策略(上传的 mpkg 同目录)。 */
36
78
  function persistCustomDir() {
@@ -165,7 +207,14 @@ function apply(ctx) {
165
207
  // 探测 host 可用性
166
208
  webServer.register({
167
209
  kind: 'exact', path: BASE + '/ping',
168
- handler: (req, res) => json(res, 200, { ok: true }),
210
+ handler: (req, res) => {
211
+ let version = null;
212
+ try {
213
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
214
+ version = pkg.version || null;
215
+ } catch { /* 忽略 */ }
216
+ json(res, 200, { ok: true, version });
217
+ },
169
218
  });
170
219
 
171
220
  // 流式接收 mpkg → 磁盘 → 返回条目索引(hybrid 大文件模式)
@@ -266,7 +315,7 @@ function apply(ctx) {
266
315
  } catch { /* 忽略 */ }
267
316
  } catch { /* 网络失败 → remote 保持 null */ }
268
317
  const pkgLocal = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
269
- json(res, 200, { ok: true, local, localVersion: pkgLocal.version, remote, remoteVersion, releaseAt, hasUpdate: !!remote && remote !== local });
318
+ json(res, 200, { ok: true, local, localVersion: pkgLocal.version, remote, remoteVersion, releaseAt, hasUpdate: !!remoteVersion && semverGt(remoteVersion, pkgLocal.version), contentDiff: !!remote && remote !== local });
270
319
  } catch (err) { json(res, 500, { ok: false, error: String(err && err.message || err) }); }
271
320
  },
272
321
  });
@@ -378,6 +427,59 @@ function apply(ctx) {
378
427
  },
379
428
  });
380
429
 
430
+ // ①(新) 自定义目录 mpkg 的预览图(容器内 preview.jpg/gif/png):扫描列表缩略图用
431
+ webServer.register({
432
+ kind: 'exact', path: BASE + '/custom-mpkg-preview',
433
+ handler: (req, res) => {
434
+ try {
435
+ if (!customDir) restoreCustomDir();
436
+ if (!customDir) { json(res, 404, { ok: false, error: 'no dir' }); return; }
437
+ const url = new URL(req.url || '', 'http://localhost');
438
+ const file = url.searchParams.get('file') || '';
439
+ if (!file || file.includes('..') || file.includes('/') || file.includes('\\')) { json(res, 403, { ok: false, error: 'forbidden' }); return; }
440
+ const filePath = join(customDir, file);
441
+ if (!existsSync(filePath) || !file.toLowerCase().endsWith('.mpkg')) { json(res, 404, { ok: false, error: 'not found' }); return; }
442
+ // ①(新) 缓存:mtime 未变 → 直接返回缓存 bytes(不再解析头部/打开大文件)
443
+ const st = statSync(filePath);
444
+ const cached = mpkgPreviewGet(file);
445
+ if (cached && cached.mtimeMs === st.mtimeMs && cached.bytes) {
446
+ res.writeHead(200, { 'content-type': cached.mime, 'content-length': cached.bytes.length, 'cache-control': 'no-cache' });
447
+ res.end(cached.bytes);
448
+ return;
449
+ }
450
+ const head = readFileSync(filePath).subarray(0, HEAD_BYTES);
451
+ const { dataStart, entries } = parseMpkgHead(head);
452
+ // 优先 preview.* 图片条目,其次任意图片条目(jpg/gif/png/webp)
453
+ let img = entries.findIndex((e) => /preview.*\.(gif|png|jpe?g|webp)$/i.test(e.name));
454
+ if (img < 0) img = entries.findIndex((e) => /\.(gif|png|jpe?g|webp)$/i.test(e.name));
455
+ if (img < 0) { json(res, 404, { ok: false, error: 'no preview' }); return; }
456
+ const e = entries[img];
457
+ const mime = e.name.toLowerCase().endsWith('.gif') ? 'image/gif'
458
+ : e.name.toLowerCase().endsWith('.png') ? 'image/png'
459
+ : e.name.toLowerCase().endsWith('.webp') ? 'image/webp' : 'image/jpeg';
460
+ // preview 一般较小(< 12MB),缓存 bytes 到内存,后续请求零磁盘 IO
461
+ if (e.size <= 12 * 1024 * 1024) {
462
+ const buf = Buffer.alloc(e.size);
463
+ const fd = require('node:fs').openSync(filePath, 'r');
464
+ let off = dataStart + e.index, got = 0;
465
+ try {
466
+ while (got < e.size) {
467
+ const n = require('node:fs').readSync(fd, buf, got, e.size - got, off + got);
468
+ if (n <= 0) break; got += n;
469
+ }
470
+ } finally { require('node:fs').closeSync(fd); }
471
+ mpkgPreviewSet(file, { mtimeMs: st.mtimeMs, mime, bytes: buf });
472
+ res.writeHead(200, { 'content-type': mime, 'content-length': buf.length, 'cache-control': 'no-cache' });
473
+ res.end(buf);
474
+ return;
475
+ }
476
+ // 超大 preview:流式读(不缓存)
477
+ res.writeHead(200, { 'content-type': mime, 'content-length': e.size, 'cache-control': 'no-cache' });
478
+ createReadStream(filePath, { start: dataStart + e.index, end: dataStart + e.index + e.size - 1 }).pipe(res);
479
+ } catch (err) { json(res, 500, { ok: false, error: String(err && err.message || err) }); }
480
+ },
481
+ });
482
+
381
483
  // ①(新) 自定义目录媒体:按文件名读取(Range),校验文件在自定义目录内且无路径穿越
382
484
  webServer.register({
383
485
  kind: 'exact', path: BASE + '/custom-media',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mpkg-wallpaper",
3
- "version": "3.1.2",
3
+ "version": "3.1.3",
4
4
  "description": "DSH Web 壁纸引擎 Wallpaper Engine mpkg 背景插件:浏览器内直接解析 .mpkg(preview.gif 动态背景/内嵌 mp4 视频/多时段切换),整屏统一虚化/对话框/弹层/遮罩独立虚化、镜头缩放平移、时钟、冲突检测。",
5
5
  "private": false,
6
6
  "type": "module",