dsh-folia 4.0.5 → 4.0.7

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/boot/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * 3. **任何异常都必须放行**。注入的脚本顶层 try/catch,超时自毁,绝不把用户挡在门外。
10
10
  */
11
11
  import { createHash } from 'node:crypto';
12
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
12
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
13
13
  import { homedir } from 'node:os';
14
14
  import { extname, join } from 'node:path';
15
15
 
@@ -96,8 +96,42 @@ export function clipMetaFile(id) {
96
96
  if (!String(id || '')) return null;
97
97
  return join(bootDir(), clipStem(id) + '.json');
98
98
  }
99
+ /** 旧命名规则下的路径:`<id>.mp4`(id 当年被限制成 ASCII 安全字符,所以能直接用)。 */
100
+ function legacyClipFile(id) {
101
+ const raw = String(id || '');
102
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(raw)) return null;
103
+ return join(bootDir(), raw + '.mp4');
104
+ }
105
+
106
+ /**
107
+ * 找出这个 id 实际对应的片段文件。
108
+ *
109
+ * 为什么需要"找"而不是直接算:4.0.2 把磁盘命名从 `<id>.mp4` 改成了
110
+ * `<id>-<hash>.mp4`(为了支持中文 slug),于是**升级前下载的片段**还叫旧名字。
111
+ * 列表是按文件名枚举的,所以旧文件看得见;可点「使用」时按新名字找不到 ⇒ `not-installed`。
112
+ * 这里按"新名字 → 旧名字"的顺序找,找到旧名字就**顺手迁移**成新名字(幂等)。
113
+ */
114
+ export function resolveClipFile(id) {
115
+ const next = clipFile(id);
116
+ if (next && existsSync(next) && statSync(next).size > 0) return next;
117
+ const legacy = legacyClipFile(id);
118
+ if (legacy && existsSync(legacy) && statSync(legacy).size > 0) {
119
+ try {
120
+ if (next && !existsSync(next)) writeFileSync(next, readFileSync(legacy));
121
+ try { renameSync(legacy, next); } catch { /* 跨盘/占用:复制已经够用,旧文件留着无害 */ }
122
+ try { rmSync(legacy); } catch { /* ignore */ }
123
+ const metaPath = clipMetaFile(id);
124
+ if (metaPath && !existsSync(metaPath)) {
125
+ writeFileSync(metaPath, JSON.stringify({ id, migratedFrom: 'legacy' }, null, 2) + '\n');
126
+ }
127
+ } catch { /* 迁移失败也不影响这次使用 */ }
128
+ return next || legacy;
129
+ }
130
+ return next;
131
+ }
132
+
99
133
  export function clipExists(id) {
100
- const p = clipFile(id);
134
+ const p = resolveClipFile(id);
101
135
  return Boolean(p && existsSync(p) && statSync(p).size > 0);
102
136
  }
103
137
 
@@ -128,11 +162,13 @@ export function listLocalClips() {
128
162
  }
129
163
 
130
164
  export function removeClip(id) {
131
- const p = clipFile(id);
132
- if (!p) return false;
133
- try { if (existsSync(p)) rmSync(p); } catch { return false; }
165
+ const paths = [clipFile(id), legacyClipFile(id)].filter(Boolean);
166
+ let removed = false;
167
+ for (const p of paths) {
168
+ try { if (existsSync(p)) { rmSync(p); removed = true; } } catch { /* 继续尝试下一个 */ }
169
+ }
134
170
  try { const m = clipMetaFile(id); if (m && existsSync(m)) rmSync(m); } catch { /* ignore */ }
135
- return true;
171
+ return removed || true;
136
172
  }
137
173
 
138
174
  /* ── 注入的启动屏幕脚本 ─────────────────────────────────────────────────────
package/lib/client.js CHANGED
@@ -17149,6 +17149,14 @@ window.__ModuleLoader__.load({
17149
17149
  actions.push(btn(weT("使用中"), () => {}, true, false));
17150
17150
  }
17151
17151
  return React.createElement("div", { key: c.id, style: BOOT_STYLE.card },
17152
+ // 封面:站点给了真正的缩略图才显示;老条目没有就留空(不显示破图)。
17153
+ c.cover
17154
+ ? React.createElement("img", {
17155
+ src: c.cover, alt: "", loading: "lazy", decoding: "async",
17156
+ style: { width: "100%", aspectRatio: "16 / 9", objectFit: "cover", borderRadius: "6px", background: "rgba(0,0,0,0.3)" },
17157
+ onError: (e) => { e.target.style.display = "none"; },
17158
+ })
17159
+ : null,
17152
17160
  React.createElement("div", { style: BOOT_STYLE.title }, c.title || c.id),
17153
17161
  React.createElement("div", { style: BOOT_STYLE.meta },
17154
17162
  [c.durationSec ? Number(c.durationSec).toFixed(1) + "s" : null,
@@ -14,7 +14,7 @@ import { join } from 'node:path';
14
14
  import { sendJson } from '../json-response.js';
15
15
  import { bodyReader } from '../http-body.js';
16
16
  import { resolveModules } from './modules.js';
17
- import { clipExists, clipFile, clipMetaFile, listLocalClips, readBoot, removeClip, writeBoot } from '../boot/index.js';
17
+ import { clipExists, clipFile, clipMetaFile, listLocalClips, readBoot, removeClip, resolveClipFile, writeBoot } from '../boot/index.js';
18
18
 
19
19
  /**
20
20
  * 站点基址。开机动画的候选**从站点分类取**(`?category=boot-animation`),
@@ -190,6 +190,14 @@ async function fetchCandidatesUncached() {
190
190
  bytes: Number(it && it.bytes) || null,
191
191
  sha256: it && it.sha256 ? String(it.sha256) : null,
192
192
  durationSec: it && it.durationMs ? Number(it.durationMs) / 1000 : null,
193
+ // 封面只认图片扩展名:站点上有几条老条目的 thumbUrl 直接指向 mp4
194
+ // (那时还没有缩略图功能)。把视频当 <img> 用只会得到一张破图。
195
+ cover: (() => {
196
+ const u = String((it && it.thumbUrl) || '');
197
+ if (!u) return null;
198
+ if (!/\.(jpg|jpeg|png|webp|gif|avif)(\?|#|$)/i.test(u)) return null;
199
+ return /^https?:/i.test(u) ? u : site + u;
200
+ })(),
193
201
  faststart: true,
194
202
  })).filter((c) => c.id && c.url);
195
203
  if (clips.length) return { clips, error: null };
@@ -253,7 +261,7 @@ export function registerBootRoutes(webServer, c) {
253
261
  if (known.has(it.id)) continue;
254
262
  clips.push({
255
263
  id: it.id, title: it.title, url: null, bytes: null, sha256: null,
256
- durationSec: it.durationSec, faststart: true,
264
+ durationSec: it.durationSec, faststart: true, cover: null,
257
265
  installed: true, localBytes: it.bytes, orphan: true,
258
266
  });
259
267
  }
@@ -389,7 +397,8 @@ export function registerBootRoutes(webServer, c) {
389
397
  handler: async (req, res) => {
390
398
  const boot = readBoot();
391
399
  const id = boot.clipId;
392
- const file = clipFile(id);
400
+ // 旧命名的片段(升级前下载的)也要能播 —— 解析器会顺手迁移成新名字。
401
+ const file = resolveClipFile(id);
393
402
  if (!file || !existsSync(file)) { res.statusCode = 404; res.end('no clip'); return; }
394
403
  const size = statSync(file).size;
395
404
  const range = String((req.headers && req.headers.range) || '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-folia",
3
- "version": "4.0.5",
3
+ "version": "4.0.7",
4
4
  "description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
5
5
  "type": "module",
6
6
  "engines": {