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.
- package/lib/client.js +210 -88
- package/lib/index.js +104 -2
- 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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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.
|
|
1446
|
-
//
|
|
1447
|
-
//
|
|
1448
|
-
|
|
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
|
-
/* ──
|
|
1482
|
-
|
|
1483
|
-
|
|
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(
|
|
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: "
|
|
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: "
|
|
3076
|
-
|
|
3077
|
-
h("
|
|
3078
|
-
|
|
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: "
|
|
3116
|
-
|
|
3117
|
-
h("
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
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
|
-
|
|
3429
|
-
h("
|
|
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
|
-
|
|
3441
|
-
|
|
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
|
|
3479
|
-
? h("p", { className: "mpw_hint
|
|
3480
|
-
: (updState && updState.hasUpdate
|
|
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) =>
|
|
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