dsh-folia 2.5.7 → 2.5.8

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 CHANGED
@@ -16991,6 +16991,9 @@ window.__ModuleLoader__.load({
16991
16991
  const [state, setState] = React.useState(null);
16992
16992
  const [busy, setBusy] = React.useState("");
16993
16993
  const [note, setNote] = React.useState(null);
16994
+ // 下载没有进度通道(宿主一次性 fetch 完才返回),但等待必须"看得见":
16995
+ // 每秒 +1 的秒表足以让用户知道它没卡死。
16996
+ const [tick, setTick] = React.useState(0);
16994
16997
 
16995
16998
  const load = React.useCallback(async (force) => {
16996
16999
  try {
@@ -17004,9 +17007,16 @@ window.__ModuleLoader__.load({
17004
17007
 
17005
17008
  React.useEffect(() => { load(false); }, [load]);
17006
17009
 
17010
+ React.useEffect(() => {
17011
+ if (!busy) return undefined;
17012
+ const t = setInterval(() => setTick((n) => n + 1), 1000);
17013
+ return () => clearInterval(t);
17014
+ }, [busy]);
17015
+
17007
17016
  async function act(kind, payload, label) {
17008
17017
  if (busy) return;
17009
17018
  setBusy(payload && payload.id ? payload.id : kind);
17019
+ setTick(0);
17010
17020
  setNote(null);
17011
17021
  try {
17012
17022
  const resp = await apiPostJson("/boot/" + kind, payload || {}, { parse: "always" });
@@ -17120,7 +17130,7 @@ window.__ModuleLoader__.load({
17120
17130
  }, label);
17121
17131
  const actions = [];
17122
17132
  if (!c.installed) {
17123
- actions.push(btn(isBusy ? weT("下载中…") : weT("下载"), () => act("download", { id: c.id }, c.title), false, true));
17133
+ actions.push(btn(isBusy ? (weT("下载中… ") + tick + "s") : weT("下载"), () => act("download", { id: c.id }, c.title), false, true));
17124
17134
  } else if (!isActive) {
17125
17135
  actions.push(btn(weT("使用"), () => act("select", { id: c.id }, c.title), false, true));
17126
17136
  actions.push(btn(weT("删除"), () => act("remove", { id: c.id }, c.title), isBusy, false));
@@ -17451,7 +17461,7 @@ window.__ModuleLoader__.load({
17451
17461
  React.createElement("span", { className: "we-qp__item-thumb" },
17452
17462
  w.preview
17453
17463
  ? React.createElement("img", {
17454
- src: w.preview, alt: "", loading: "lazy",
17464
+ src: w.preview, alt: "", loading: "lazy", decoding: "async",
17455
17465
  onError: (e) => { e.target.style.display = "none"; },
17456
17466
  onLoad: (e) => { e.target.style.opacity = "1"; },
17457
17467
  })
@@ -17479,7 +17489,7 @@ window.__ModuleLoader__.load({
17479
17489
  },
17480
17490
  w.preview
17481
17491
  ? React.createElement("img", {
17482
- src: w.preview, alt: "", loading: "lazy",
17492
+ src: w.preview, alt: "", loading: "lazy", decoding: "async",
17483
17493
  onError: (e) => { e.target.style.display = "none"; },
17484
17494
  onLoad: (e) => { e.target.style.opacity = "1"; },
17485
17495
  })
package/lib/index.js CHANGED
@@ -80,6 +80,7 @@ export { extOf };
80
80
  import { registerInventoryRoutes } from './routes/inventory.js';
81
81
  import { registerModulesRoutes } from './routes/modules.js';
82
82
  import { registerBootRoutes } from './routes/boot.js';
83
+ import { registerSiteThumbRoutes } from './routes/site-thumb.js';
83
84
  import { installBootInjection } from './boot/index.js';
84
85
  import { registerDiagRoutes } from './routes/diag.js';
85
86
  // 遮挡消息桥族(上游新增域模块):hub 处理 x11 遮挡探针,路由对外暴露状态/控制面。
@@ -303,6 +304,9 @@ export function apply(ctx) {
303
304
  // 开机动画 → lib/routes/boot.js(HTTP 面)+ lib/boot/index.js(注入与屏幕脚本)。
304
305
  // 注入是「关就一行都不注」:没有选片时不产生任何脚本、不产生任何请求。
305
306
  registerBootRoutes(webServer, { disposers, base: BASE, log });
307
+
308
+ // 站点图片缩略图代理 → lib/routes/site-thumb.js。卡片用 320px 小图,不再每张下 500 KB 原图。
309
+ registerSiteThumbRoutes(webServer, { disposers, base: BASE, ensureImagePreview, log });
306
310
  installBootInjection(ctx, (m) => { try { log(m); } catch { /* ignore */ } });
307
311
 
308
312
  // 3./3a. 派生媒体族路由(源元信息 / 抽帧转码 + 进度 / 视频缩略图)→ lib/routes/media-derived.js。
@@ -206,6 +206,9 @@ function foliaItemFromCatalog(it, base) {
206
206
  const media = foliaAbs(base, it && it.url);
207
207
  if (!media) return null;
208
208
  const slug = String((it && (it.slug || it.id)) || '');
209
+ // 站点图片的预览走宿主缩略图代理:卡片只下 ~10 KB,不再下 500 KB 原图。
210
+ // (视频没有封面,保持 null —— 卡片会显示「无预览」,不会去拉整段视频。)
211
+ const thumb = type === 'image' ? '/folia/site-thumb?u=' + encodeURIComponent(media) : null;
209
212
  const extHit = String(media).match(/\.([a-z0-9]{2,5})(?:[?#]|$)/i);
210
213
  const num = (v) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Number(v) : undefined);
211
214
  return {
@@ -217,7 +220,7 @@ function foliaItemFromCatalog(it, base) {
217
220
  media, // 绝对 URL
218
221
  mediaExt: '.' + (extHit ? extHit[1].toLowerCase() : (type === 'video' ? 'mp4' : 'jpg')),
219
222
  // 站点目录可能带 preview/poster;图片没封面时用自己当预览,视频没有就留空(卡片会是黑块)
220
- preview: foliaAbs(base, it && (it.preview || it.poster)) || (type === 'image' ? media : null),
223
+ preview: thumb || foliaAbs(base, it && (it.preview || it.poster)) || null,
221
224
  frameUrl: null,
222
225
  schemeColor: null,
223
226
  sceneLiveSrc: null,
@@ -0,0 +1,109 @@
1
+ /**
2
+ * routes/site-thumb.js — 站点图片的**缩略图代理**。
3
+ *
4
+ * 为什么需要它:站点上的壁纸是 1024×1024 起的原图(实测一张 536 KB)。卡片网格一页
5
+ * 9~12 张,一次要下 5 MB 左右,在慢网络上就是"站点壁纸加载很慢"。
6
+ *
7
+ * 做法:宿主把原图抓一次存到缓存,然后用**已有的** `ensureImagePreview`(ffmpeg 缩到
8
+ * 320px、q=5,约 10 KB)生成小图,之后每次都直接发小图。原图只下一次、只在第一次。
9
+ *
10
+ * 三条纪律:
11
+ * · **只代理白名单主机**(站点基址与素材桶),避免变成任意 URL 的开放代理;
12
+ * · 任何失败都 **302 回原图** —— 慢一点但一定能显示,绝不 404;
13
+ * · 缩略图与源图都按 URL 的 sha1 命名,天然幂等,不需要失效逻辑。
14
+ */
15
+ import { createHash } from 'node:crypto';
16
+ import { createReadStream, existsSync, mkdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
17
+ import { homedir } from 'node:os';
18
+ import { extname, join } from 'node:path';
19
+
20
+ const MAX_SOURCE_BYTES = 32 * 1024 * 1024;
21
+ const FETCH_TIMEOUT_MS = 30000;
22
+
23
+ const DEFAULT_SITE = 'https://folia-5ve.pages.dev';
24
+ const DEFAULT_ASSETS = 'https://pub-1275321f146e479eaac54948f4cdad6f.r2.dev';
25
+
26
+ function siteBase() {
27
+ const raw = process.env.DSH_FOLIA_SITE;
28
+ if (typeof raw === 'string' && raw.trim().toLowerCase() === 'off') return null;
29
+ const base = (typeof raw === 'string' && raw.trim()) ? raw.trim() : DEFAULT_SITE;
30
+ return base.replace(/\/+$/, '');
31
+ }
32
+ function assetsBase() {
33
+ const raw = process.env.DSH_FOLIA_ASSETS;
34
+ const base = (typeof raw === 'string' && raw.trim()) ? raw.trim() : DEFAULT_ASSETS;
35
+ return base.replace(/\/+$/, '');
36
+ }
37
+
38
+ /** 源图缓存目录(与 config.json 同一个数据根)。 */
39
+ function sourceDir() {
40
+ const base = process.env.DSH_FOLIA_HOME || join(homedir(), '.dsh-folia');
41
+ const d = join(base, 'site-src');
42
+ try { if (!existsSync(d)) mkdirSync(d, { recursive: true }); } catch { /* 只读盘:后面会失败并回退 */ }
43
+ return d;
44
+ }
45
+
46
+ /** 只允许抓我们自己的两个主机。 */
47
+ function allowed(url) {
48
+ try {
49
+ const u = new URL(url);
50
+ if (!/^https?:$/.test(u.protocol)) return false;
51
+ const hosts = [siteBase(), assetsBase()].filter(Boolean).map((b) => { try { return new URL(b).host; } catch { return ''; } });
52
+ return hosts.includes(u.host);
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ export function registerSiteThumbRoutes(webServer, c) {
59
+ const { disposers, base: BASE, ensureImagePreview, log } = c;
60
+
61
+ disposers.push(webServer.register({
62
+ kind: 'exact',
63
+ path: `${BASE}/site-thumb`,
64
+ handler: async (req, res) => {
65
+ let target = '';
66
+ try { target = new URL(req.url || '/', 'http://x').searchParams.get('u') || ''; } catch { target = ''; }
67
+
68
+ const redirect = () => {
69
+ res.statusCode = 302;
70
+ if (typeof res.setHeader === 'function') res.setHeader('location', target);
71
+ res.end();
72
+ };
73
+
74
+ if (!target || !allowed(target)) { redirect(); return; }
75
+
76
+ const key = createHash('sha1').update(target).digest('hex');
77
+ let ext = '.jpg';
78
+ try { ext = extname(new URL(target).pathname) || '.jpg'; } catch { /* 用默认 */ }
79
+ const src = join(sourceDir(), key + (ext.length <= 5 ? ext : '.jpg'));
80
+
81
+ try {
82
+ if (!existsSync(src) || statSync(src).size === 0) {
83
+ const r = await fetch(target, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
84
+ if (!r.ok) { redirect(); return; }
85
+ const ct = String(r.headers.get('content-type') || '');
86
+ if (!/^image\//i.test(ct)) { redirect(); return; }
87
+ const buf = Buffer.from(await r.arrayBuffer());
88
+ if (buf.length === 0 || buf.length > MAX_SOURCE_BYTES) { redirect(); return; }
89
+ writeFileSync(src, buf);
90
+ }
91
+ // 已有套件:缩到 320px(失败返回 null ⇒ 回落原图)
92
+ const thumb = await ensureImagePreview(src).catch(() => null);
93
+ const file = thumb && existsSync(thumb) ? thumb : src;
94
+ const size = statSync(file).size;
95
+ if (typeof res.setHeader === 'function') {
96
+ res.setHeader('content-type', 'image/jpeg');
97
+ res.setHeader('content-length', String(size));
98
+ res.setHeader('cache-control', 'public, max-age=31536000, immutable');
99
+ }
100
+ res.statusCode = 200;
101
+ createReadStream(file).pipe(res);
102
+ } catch (e) {
103
+ if (typeof log === 'function') log(`[site-thumb] ${String((e && e.message) || e)}`);
104
+ try { if (existsSync(src)) rmSync(src); } catch { /* ignore */ }
105
+ redirect();
106
+ }
107
+ },
108
+ }));
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-folia",
3
- "version": "2.5.7",
3
+ "version": "2.5.8",
4
4
  "description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
5
5
  "type": "module",
6
6
  "engines": {