dsh-folia 2.5.7 → 2.5.9
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 +13 -3
- package/lib/index.js +4 -0
- package/lib/routes/inventory.js +13 -2
- package/lib/routes/site-thumb.js +109 -0
- package/package.json +1 -1
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。
|
package/lib/routes/inventory.js
CHANGED
|
@@ -126,6 +126,10 @@ const FOLIA_API_CATEGORY = 'wallpaper';
|
|
|
126
126
|
/** 明确**不是**壁纸的分类。没列进来的(含未来新增的)一律放行 —— 宁可多显示,不静默丢。 */
|
|
127
127
|
const FOLIA_NON_WALLPAPER_CATEGORIES = ['boot-animation', 'skin-pack', 'skin', 'pet', 'pets', 'assistant-skin'];
|
|
128
128
|
/** 分页上限:防止站点游标异常时无限翻页(每页 12~24,20 页足够几百条)。 */
|
|
129
|
+
/** 每次请求要多少条:接口支持 `?limit=`(实测 limit=5 生效),一次要 100 条能把
|
|
130
|
+
* 几百张图的清单压到个位数请求。 */
|
|
131
|
+
const FOLIA_API_LIMIT = 100;
|
|
132
|
+
/** 条目总数上限(防止站点数据异常时无限翻页)。100 × 20 = 2000 条。 */
|
|
129
133
|
const FOLIA_API_MAX_PAGES = 20;
|
|
130
134
|
const FOLIA_CATALOG_PATH = '/data/catalog.json';
|
|
131
135
|
const FOLIA_MANIFEST_PATH = '/manifest.json';
|
|
@@ -206,6 +210,13 @@ function foliaItemFromCatalog(it, base) {
|
|
|
206
210
|
const media = foliaAbs(base, it && it.url);
|
|
207
211
|
if (!media) return null;
|
|
208
212
|
const slug = String((it && (it.slug || it.id)) || '');
|
|
213
|
+
// 站点图片的预览走宿主缩略图代理:卡片只下 ~10 KB,不再下 500 KB 原图。
|
|
214
|
+
// (视频没有封面,保持 null —— 卡片会显示「无预览」,不会去拉整段视频。)
|
|
215
|
+
// 预览优先级:**站点自己给的 preview 最高**(省掉宿主这一跳,也最省流量);
|
|
216
|
+
// 站点没给才退回宿主缩略图代理。只有预览走小图,`media` 永远是原图 ——
|
|
217
|
+
// 应用壁纸用的是 `media`,所以画质不受影响。
|
|
218
|
+
const sitePreview = foliaAbs(base, it && (it.preview || it.poster)) || null;
|
|
219
|
+
const thumb = sitePreview ? null : (type === 'image' ? '/folia/site-thumb?u=' + encodeURIComponent(media) : null);
|
|
209
220
|
const extHit = String(media).match(/\.([a-z0-9]{2,5})(?:[?#]|$)/i);
|
|
210
221
|
const num = (v) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Number(v) : undefined);
|
|
211
222
|
return {
|
|
@@ -217,7 +228,7 @@ function foliaItemFromCatalog(it, base) {
|
|
|
217
228
|
media, // 绝对 URL
|
|
218
229
|
mediaExt: '.' + (extHit ? extHit[1].toLowerCase() : (type === 'video' ? 'mp4' : 'jpg')),
|
|
219
230
|
// 站点目录可能带 preview/poster;图片没封面时用自己当预览,视频没有就留空(卡片会是黑块)
|
|
220
|
-
preview:
|
|
231
|
+
preview: sitePreview || thumb || null,
|
|
221
232
|
frameUrl: null,
|
|
222
233
|
schemeColor: null,
|
|
223
234
|
sceneLiveSrc: null,
|
|
@@ -323,7 +334,7 @@ async function foliaRemote(force = false) {
|
|
|
323
334
|
const out = [];
|
|
324
335
|
let cursor = null;
|
|
325
336
|
for (let page = 0; page < FOLIA_API_MAX_PAGES; page++) {
|
|
326
|
-
const scope = '?category=' + encodeURIComponent(FOLIA_API_CATEGORY);
|
|
337
|
+
const scope = '?category=' + encodeURIComponent(FOLIA_API_CATEGORY) + '&limit=' + FOLIA_API_LIMIT;
|
|
327
338
|
const qs = cursor ? FOLIA_API_PATH + scope + '&cursor=' + encodeURIComponent(cursor) : FOLIA_API_PATH + scope;
|
|
328
339
|
const { doc, error } = await fetchJson(qs);
|
|
329
340
|
if (!doc || !doc.ok || !doc.data) return { items: out, error: error || 'api-shape' };
|
|
@@ -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
|
+
}
|