dsh-any-background 0.2.7 → 0.2.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/README.md +20 -20
- package/README.zh.md +19 -20
- package/lib/client.js +947 -37
- package/lib/client.js.map +1 -1
- package/lib/index.js +389 -24
- package/package.json +2 -1
package/lib/index.js
CHANGED
|
@@ -27,10 +27,14 @@ const VIDEO_ROUTE = "/dsh-any-background/video";
|
|
|
27
27
|
const UPLOAD_ROUTE = "/dsh-any-background/video/upload";
|
|
28
28
|
const WALLPAPER_ROUTE = "/dsh-any-background/wallpaper";
|
|
29
29
|
const WALLPAPER_UPLOAD_ROUTE = "/dsh-any-background/wallpaper/upload";
|
|
30
|
+
const FONT_ROUTE = "/dsh-any-background/font";
|
|
31
|
+
const FONT_UPLOAD_ROUTE = "/dsh-any-background/font/upload";
|
|
30
32
|
const UPLOAD_TMP = "wallpaper.upload.tmp";
|
|
31
33
|
const VIDEO_UPLOAD_TMP = "video.upload.tmp";
|
|
34
|
+
const FONT_UPLOAD_TMP = "font.upload.tmp";
|
|
32
35
|
const WALLPAPER_UPLOAD_MAX = 104857600;
|
|
33
36
|
const VIDEO_UPLOAD_MAX = 2147483648;
|
|
37
|
+
const FONT_UPLOAD_MAX = 104857600;
|
|
34
38
|
const WALLPAPER_FETCH_MAX = 26214400;
|
|
35
39
|
const WALLPAPER_FETCH_TIMEOUT = 2e4;
|
|
36
40
|
const VIDEO_FETCH_MAX = 2147483648;
|
|
@@ -54,27 +58,100 @@ const VIDEO_CANDIDATES = [
|
|
|
54
58
|
"wallpaper.mkv",
|
|
55
59
|
"wallpaper.video"
|
|
56
60
|
];
|
|
61
|
+
/** Font slot: one font owns the slot, named by format like the video slot. */
|
|
62
|
+
function fontFileName(mime) {
|
|
63
|
+
switch (mime) {
|
|
64
|
+
case "font/woff2": return "font.woff2";
|
|
65
|
+
case "font/woff": return "font.woff";
|
|
66
|
+
case "font/otf": return "font.otf";
|
|
67
|
+
case "font/ttf": return "font.ttf";
|
|
68
|
+
default: return "font.ttf";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const FONT_CANDIDATES = [
|
|
72
|
+
"font.woff2",
|
|
73
|
+
"font.woff",
|
|
74
|
+
"font.otf",
|
|
75
|
+
"font.ttf"
|
|
76
|
+
];
|
|
77
|
+
/** Sniff a font's container format from its leading magic bytes (null when the
|
|
78
|
+
* bytes are not a recognized font — uploads are rejected rather than stored). */
|
|
79
|
+
function sniffFontMime(buf) {
|
|
80
|
+
if (buf.length >= 4 && buf[0] === 119 && buf[1] === 79 && buf[2] === 70 && buf[3] === 50) return "font/woff2";
|
|
81
|
+
if (buf.length >= 4 && buf[0] === 119 && buf[1] === 79 && buf[2] === 70 && buf[3] === 70) return "font/woff";
|
|
82
|
+
if (buf.length >= 4 && buf[0] === 79 && buf[1] === 84 && buf[2] === 84 && buf[3] === 79) return "font/otf";
|
|
83
|
+
if (buf.length >= 4 && buf[0] === 0 && buf[1] === 1 && buf[2] === 0 && buf[3] === 0) return "font/ttf";
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
57
86
|
const DEFAULT_CONFIG = {
|
|
58
87
|
color: null,
|
|
59
88
|
opacities: {
|
|
60
|
-
bg: .
|
|
61
|
-
sidebar: .
|
|
62
|
-
card:
|
|
63
|
-
input:
|
|
89
|
+
bg: .5,
|
|
90
|
+
sidebar: .5,
|
|
91
|
+
card: .5,
|
|
92
|
+
input: .5
|
|
64
93
|
},
|
|
65
94
|
blurs: {
|
|
66
|
-
bg:
|
|
67
|
-
sidebar:
|
|
68
|
-
card:
|
|
69
|
-
settings:
|
|
70
|
-
chat:
|
|
71
|
-
trajectory:
|
|
72
|
-
input:
|
|
73
|
-
panel:
|
|
95
|
+
bg: 30,
|
|
96
|
+
sidebar: 30,
|
|
97
|
+
card: 30,
|
|
98
|
+
settings: 30,
|
|
99
|
+
chat: 30,
|
|
100
|
+
trajectory: 30,
|
|
101
|
+
input: 30,
|
|
102
|
+
panel: 30,
|
|
103
|
+
produced: 30
|
|
74
104
|
},
|
|
75
|
-
|
|
105
|
+
strokes: {
|
|
106
|
+
bg: {
|
|
107
|
+
width: 0,
|
|
108
|
+
color: "auto",
|
|
109
|
+
customColor: "#808080"
|
|
110
|
+
},
|
|
111
|
+
sidebar: {
|
|
112
|
+
width: 0,
|
|
113
|
+
color: "auto",
|
|
114
|
+
customColor: "#808080"
|
|
115
|
+
},
|
|
116
|
+
card: {
|
|
117
|
+
width: 0,
|
|
118
|
+
color: "auto",
|
|
119
|
+
customColor: "#808080"
|
|
120
|
+
},
|
|
121
|
+
settings: {
|
|
122
|
+
width: 0,
|
|
123
|
+
color: "auto",
|
|
124
|
+
customColor: "#808080"
|
|
125
|
+
},
|
|
126
|
+
chat: {
|
|
127
|
+
width: 0,
|
|
128
|
+
color: "auto",
|
|
129
|
+
customColor: "#808080"
|
|
130
|
+
},
|
|
131
|
+
trajectory: {
|
|
132
|
+
width: 0,
|
|
133
|
+
color: "auto",
|
|
134
|
+
customColor: "#808080"
|
|
135
|
+
},
|
|
136
|
+
input: {
|
|
137
|
+
width: 0,
|
|
138
|
+
color: "auto",
|
|
139
|
+
customColor: "#808080"
|
|
140
|
+
},
|
|
141
|
+
panel: {
|
|
142
|
+
width: 0,
|
|
143
|
+
color: "auto",
|
|
144
|
+
customColor: "#808080"
|
|
145
|
+
},
|
|
146
|
+
produced: {
|
|
147
|
+
width: 0,
|
|
148
|
+
color: "auto",
|
|
149
|
+
customColor: "#808080"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
settingsOpacity: .5,
|
|
76
153
|
wallpaperOpacity: 1,
|
|
77
|
-
blur:
|
|
154
|
+
blur: 30,
|
|
78
155
|
bgState: {
|
|
79
156
|
zoom: 1,
|
|
80
157
|
x: 0,
|
|
@@ -92,11 +169,14 @@ const DEFAULT_CONFIG = {
|
|
|
92
169
|
backgroundType: "image",
|
|
93
170
|
bgMode: "fit",
|
|
94
171
|
videoMime: null,
|
|
172
|
+
fontMime: null,
|
|
173
|
+
fontEnabled: true,
|
|
95
174
|
generatedBg: null,
|
|
96
175
|
regenerateOnReload: false,
|
|
97
|
-
chatTextOpacity:
|
|
98
|
-
trajectoryOpacity:
|
|
99
|
-
panelOpacity:
|
|
176
|
+
chatTextOpacity: .5,
|
|
177
|
+
trajectoryOpacity: .5,
|
|
178
|
+
panelOpacity: .5,
|
|
179
|
+
producedOpacity: .5,
|
|
100
180
|
profiles: [],
|
|
101
181
|
rotation: {
|
|
102
182
|
enabled: false,
|
|
@@ -121,6 +201,7 @@ const dataDir = () => dshHomePath(DATA_DIR);
|
|
|
121
201
|
const configPath = () => dshHomePath(DATA_DIR, CONFIG_FILE);
|
|
122
202
|
const wallpaperPath = () => dshHomePath(DATA_DIR, WALLPAPER_FILE);
|
|
123
203
|
const videoPathFor = (mime) => dshHomePath(DATA_DIR, videoFileName(mime));
|
|
204
|
+
const fontPathFor = (mime) => dshHomePath(DATA_DIR, fontFileName(mime));
|
|
124
205
|
const exists = async (p) => {
|
|
125
206
|
try {
|
|
126
207
|
await access(p);
|
|
@@ -160,6 +241,43 @@ async function findVideoFile() {
|
|
|
160
241
|
function clamp(n, lo, hi, def) {
|
|
161
242
|
return typeof n === "number" && isFinite(n) ? Math.min(hi, Math.max(lo, n)) : def;
|
|
162
243
|
}
|
|
244
|
+
/** Locate the stored font: the recorded MIME decides the expected name; stray
|
|
245
|
+
* files from a lost config write are adopted via rename (mirrors the video
|
|
246
|
+
* slot's recovery). */
|
|
247
|
+
async function findFontFile() {
|
|
248
|
+
const mime = (await readConfig()).fontMime ?? "font/ttf";
|
|
249
|
+
const expected = fontPathFor(mime);
|
|
250
|
+
if (await exists(expected)) return {
|
|
251
|
+
path: expected,
|
|
252
|
+
mime
|
|
253
|
+
};
|
|
254
|
+
for (const name of FONT_CANDIDATES) {
|
|
255
|
+
const p = dshHomePath(DATA_DIR, name);
|
|
256
|
+
if (!await exists(p)) continue;
|
|
257
|
+
const foundMime = mimeForFontFile(name);
|
|
258
|
+
try {
|
|
259
|
+
await rename(p, expected);
|
|
260
|
+
return {
|
|
261
|
+
path: expected,
|
|
262
|
+
mime: foundMime
|
|
263
|
+
};
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
function mimeForFontFile(name) {
|
|
271
|
+
switch (name) {
|
|
272
|
+
case "font.woff2": return "font/woff2";
|
|
273
|
+
case "font.woff": return "font/woff";
|
|
274
|
+
case "font.otf": return "font/otf";
|
|
275
|
+
default: return "font/ttf";
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function fontUrl() {
|
|
279
|
+
return await findFontFile() ? FONT_ROUTE : null;
|
|
280
|
+
}
|
|
163
281
|
function normalizeBgState(s) {
|
|
164
282
|
return {
|
|
165
283
|
zoom: clamp(s.zoom, .1, 10, 1),
|
|
@@ -170,6 +288,40 @@ function normalizeBgState(s) {
|
|
|
170
288
|
};
|
|
171
289
|
}
|
|
172
290
|
/** Coerce an unknown persisted value into a valid ThemeConfig, falling back per-field. */
|
|
291
|
+
const STROKE_GROUPS = [
|
|
292
|
+
"bg",
|
|
293
|
+
"sidebar",
|
|
294
|
+
"card",
|
|
295
|
+
"settings",
|
|
296
|
+
"chat",
|
|
297
|
+
"trajectory",
|
|
298
|
+
"input",
|
|
299
|
+
"panel",
|
|
300
|
+
"produced"
|
|
301
|
+
];
|
|
302
|
+
const STROKE_COLOR_KEYS = [
|
|
303
|
+
"auto",
|
|
304
|
+
"gray",
|
|
305
|
+
"black",
|
|
306
|
+
"white",
|
|
307
|
+
"theme",
|
|
308
|
+
"custom"
|
|
309
|
+
];
|
|
310
|
+
const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
311
|
+
function normalizeStroke(raw) {
|
|
312
|
+
const s = raw ?? {};
|
|
313
|
+
return {
|
|
314
|
+
width: clamp(s.width, 0, 4, 0),
|
|
315
|
+
color: STROKE_COLOR_KEYS.includes(s.color) ? s.color : "auto",
|
|
316
|
+
customColor: typeof s.customColor === "string" && HEX_RE.test(s.customColor) ? s.customColor : "#808080"
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function normalizeStrokes(raw) {
|
|
320
|
+
const s = raw ?? {};
|
|
321
|
+
const out = {};
|
|
322
|
+
for (const k of STROKE_GROUPS) out[k] = normalizeStroke(s[k]);
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
173
325
|
function normalizeConfig(raw) {
|
|
174
326
|
const r = raw ?? {};
|
|
175
327
|
const c = r.color;
|
|
@@ -206,7 +358,8 @@ function normalizeConfig(raw) {
|
|
|
206
358
|
"chat",
|
|
207
359
|
"trajectory",
|
|
208
360
|
"input",
|
|
209
|
-
"panel"
|
|
361
|
+
"panel",
|
|
362
|
+
"produced"
|
|
210
363
|
]) blurs[k] = clamp(bl[k], 0, 60, DEFAULT_CONFIG.blurs[k]);
|
|
211
364
|
return {
|
|
212
365
|
color,
|
|
@@ -217,6 +370,7 @@ function normalizeConfig(raw) {
|
|
|
217
370
|
input: clamp(ops.input, 0, 1, DEFAULT_CONFIG.opacities.input)
|
|
218
371
|
},
|
|
219
372
|
blurs,
|
|
373
|
+
strokes: normalizeStrokes(r.strokes),
|
|
220
374
|
settingsOpacity: clamp(r.settingsOpacity, 0, 1, DEFAULT_CONFIG.settingsOpacity),
|
|
221
375
|
wallpaperOpacity: clamp(r.wallpaperOpacity, 0, 1, DEFAULT_CONFIG.wallpaperOpacity),
|
|
222
376
|
blur: clamp(r.blur, 0, 60, DEFAULT_CONFIG.blur),
|
|
@@ -225,11 +379,14 @@ function normalizeConfig(raw) {
|
|
|
225
379
|
backgroundType: bgType,
|
|
226
380
|
bgMode,
|
|
227
381
|
videoMime: typeof r.videoMime === "string" ? r.videoMime : null,
|
|
382
|
+
fontMime: typeof r.fontMime === "string" ? r.fontMime : null,
|
|
383
|
+
fontEnabled: typeof r.fontEnabled === "boolean" ? r.fontEnabled : DEFAULT_CONFIG.fontEnabled,
|
|
228
384
|
generatedBg,
|
|
229
385
|
regenerateOnReload: typeof r.regenerateOnReload === "boolean" ? r.regenerateOnReload : DEFAULT_CONFIG.regenerateOnReload,
|
|
230
386
|
chatTextOpacity: clamp(r.chatTextOpacity, 0, 1, DEFAULT_CONFIG.chatTextOpacity),
|
|
231
387
|
trajectoryOpacity: clamp(r.trajectoryOpacity, 0, 1, DEFAULT_CONFIG.trajectoryOpacity),
|
|
232
388
|
panelOpacity: clamp(r.panelOpacity, 0, 1, DEFAULT_CONFIG.panelOpacity),
|
|
389
|
+
producedOpacity: clamp(r.producedOpacity, 0, 1, DEFAULT_CONFIG.producedOpacity),
|
|
233
390
|
profiles: normalizeProfiles(r.profiles),
|
|
234
391
|
rotation: normalizeRotation(r.rotation),
|
|
235
392
|
schedule: normalizeSchedule(r.schedule),
|
|
@@ -291,7 +448,8 @@ function normalizeProfileAppearance(raw) {
|
|
|
291
448
|
"chat",
|
|
292
449
|
"trajectory",
|
|
293
450
|
"input",
|
|
294
|
-
"panel"
|
|
451
|
+
"panel",
|
|
452
|
+
"produced"
|
|
295
453
|
]) blurs[k] = clamp(bl[k], 0, 60, DEFAULT_CONFIG.blurs[k]);
|
|
296
454
|
return {
|
|
297
455
|
color: Array.isArray(c) && c.length === 3 && c.every((x) => typeof x === "number" && isFinite(x)) ? [
|
|
@@ -306,12 +464,14 @@ function normalizeProfileAppearance(raw) {
|
|
|
306
464
|
input: clamp(ops.input, 0, 1, DEFAULT_CONFIG.opacities.input)
|
|
307
465
|
},
|
|
308
466
|
blurs,
|
|
467
|
+
strokes: normalizeStrokes(a.strokes),
|
|
309
468
|
settingsOpacity: clamp(a.settingsOpacity, 0, 1, DEFAULT_CONFIG.settingsOpacity),
|
|
310
469
|
wallpaperOpacity: clamp(a.wallpaperOpacity, 0, 1, DEFAULT_CONFIG.wallpaperOpacity),
|
|
311
470
|
blur: clamp(a.blur, 0, 60, DEFAULT_CONFIG.blur),
|
|
312
471
|
chatTextOpacity: clamp(a.chatTextOpacity, 0, 1, DEFAULT_CONFIG.chatTextOpacity),
|
|
313
472
|
trajectoryOpacity: clamp(a.trajectoryOpacity, 0, 1, DEFAULT_CONFIG.trajectoryOpacity),
|
|
314
|
-
panelOpacity: clamp(a.panelOpacity, 0, 1, DEFAULT_CONFIG.panelOpacity)
|
|
473
|
+
panelOpacity: clamp(a.panelOpacity, 0, 1, DEFAULT_CONFIG.panelOpacity),
|
|
474
|
+
producedOpacity: clamp(a.producedOpacity, 0, 1, DEFAULT_CONFIG.producedOpacity)
|
|
315
475
|
};
|
|
316
476
|
}
|
|
317
477
|
function normalizeProfiles(raw) {
|
|
@@ -385,10 +545,38 @@ async function readConfig() {
|
|
|
385
545
|
return { ...DEFAULT_CONFIG };
|
|
386
546
|
}
|
|
387
547
|
}
|
|
548
|
+
const LEGACY_CONFIG_KEYS = /* @__PURE__ */ new Set(["opacity"]);
|
|
549
|
+
const warnedConfigKeys = /* @__PURE__ */ new Set();
|
|
550
|
+
function warnUnknownConfigKeys(raw, normalized) {
|
|
551
|
+
if (raw === null || typeof raw !== "object") return;
|
|
552
|
+
const r = raw;
|
|
553
|
+
const warn = (id) => {
|
|
554
|
+
if (warnedConfigKeys.has(id)) return;
|
|
555
|
+
warnedConfigKeys.add(id);
|
|
556
|
+
console.warn(`dsh-any-background: ignoring unknown config field "${id}" (declared in one half only?)`);
|
|
557
|
+
};
|
|
558
|
+
const known = new Set(Object.keys(normalized));
|
|
559
|
+
for (const key of Object.keys(r)) {
|
|
560
|
+
if (known.has(key) || LEGACY_CONFIG_KEYS.has(key)) continue;
|
|
561
|
+
warn(key);
|
|
562
|
+
}
|
|
563
|
+
for (const group of [
|
|
564
|
+
"blurs",
|
|
565
|
+
"opacities",
|
|
566
|
+
"strokes"
|
|
567
|
+
]) {
|
|
568
|
+
const got = r[group];
|
|
569
|
+
if (got === null || typeof got !== "object") continue;
|
|
570
|
+
const have = new Set(Object.keys(normalized[group]));
|
|
571
|
+
for (const key of Object.keys(got)) if (!have.has(key)) warn(`${group}.${key}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
388
574
|
async function writeConfig(config) {
|
|
389
575
|
await ensureDir();
|
|
390
576
|
try {
|
|
391
|
-
|
|
577
|
+
const normalized = normalizeConfig(config);
|
|
578
|
+
warnUnknownConfigKeys(config, normalized);
|
|
579
|
+
await writeFile(configPath(), JSON.stringify(normalized, null, 2), "utf8");
|
|
392
580
|
return true;
|
|
393
581
|
} catch (e) {
|
|
394
582
|
console.error(`dsh-any-background: failed to write "${CONFIG_FILE}"`, e);
|
|
@@ -1093,6 +1281,164 @@ async function handleWallpaperUpload(req, res) {
|
|
|
1093
1281
|
} catch {}
|
|
1094
1282
|
}
|
|
1095
1283
|
}
|
|
1284
|
+
/** Stream the stored custom font: sniffed MIME, no caching (uploads replace
|
|
1285
|
+
* the file in place). Fonts need no Range support — the browser fetches once. */
|
|
1286
|
+
async function serveFont(req, res) {
|
|
1287
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1288
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
1289
|
+
res.end(JSON.stringify({
|
|
1290
|
+
ok: false,
|
|
1291
|
+
error: "font route only serves GET/HEAD"
|
|
1292
|
+
}));
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
try {
|
|
1296
|
+
const found = await findFontFile();
|
|
1297
|
+
if (found === null) {
|
|
1298
|
+
res.writeHead(404);
|
|
1299
|
+
res.end("no custom font stored");
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
const st = await stat(found.path);
|
|
1303
|
+
res.writeHead(200, {
|
|
1304
|
+
"Content-Type": found.mime,
|
|
1305
|
+
"Content-Length": st.size,
|
|
1306
|
+
"Cache-Control": "no-store"
|
|
1307
|
+
});
|
|
1308
|
+
if (req.method === "HEAD") {
|
|
1309
|
+
res.end();
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
createReadStream(found.path).pipe(res);
|
|
1313
|
+
} catch (e) {
|
|
1314
|
+
console.error("dsh-any-background: failed to serve the custom font", e);
|
|
1315
|
+
try {
|
|
1316
|
+
res.writeHead(500);
|
|
1317
|
+
res.end();
|
|
1318
|
+
} catch {}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/** Accept a raw font upload (POST): pipe the body into a temp file, sniff the
|
|
1322
|
+
* container format from its magic bytes (rejecting anything that is not a
|
|
1323
|
+
* recognizable font), then rename it into the format-derived slot and record
|
|
1324
|
+
* the MIME in the config so serve/find resolve immediately. */
|
|
1325
|
+
async function handleFontUpload(req, res) {
|
|
1326
|
+
if (req.method !== "POST") {
|
|
1327
|
+
res.writeHead(405);
|
|
1328
|
+
res.end();
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
try {
|
|
1332
|
+
await ensureDir();
|
|
1333
|
+
const tmp = dshHomePath(DATA_DIR, FONT_UPLOAD_TMP);
|
|
1334
|
+
const out = createWriteStream(tmp);
|
|
1335
|
+
let received = 0;
|
|
1336
|
+
let failed = false;
|
|
1337
|
+
const fail = () => {
|
|
1338
|
+
if (failed) return;
|
|
1339
|
+
failed = true;
|
|
1340
|
+
out.destroy();
|
|
1341
|
+
rm(tmp, { force: true });
|
|
1342
|
+
};
|
|
1343
|
+
req.on("aborted", fail);
|
|
1344
|
+
req.on("error", fail);
|
|
1345
|
+
out.on("error", () => {
|
|
1346
|
+
fail();
|
|
1347
|
+
try {
|
|
1348
|
+
res.writeHead(500);
|
|
1349
|
+
res.end();
|
|
1350
|
+
} catch {}
|
|
1351
|
+
});
|
|
1352
|
+
req.on("data", (chunk) => {
|
|
1353
|
+
received += chunk.byteLength;
|
|
1354
|
+
if (received > FONT_UPLOAD_MAX) {
|
|
1355
|
+
req.destroy();
|
|
1356
|
+
fail();
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
req.pipe(out);
|
|
1360
|
+
out.on("finish", async () => {
|
|
1361
|
+
if (failed) return;
|
|
1362
|
+
try {
|
|
1363
|
+
if (received === 0) {
|
|
1364
|
+
fail();
|
|
1365
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1366
|
+
res.end(JSON.stringify({
|
|
1367
|
+
ok: false,
|
|
1368
|
+
error: "empty upload"
|
|
1369
|
+
}));
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const mime = sniffFontMime(await new Promise((resolve, reject) => {
|
|
1373
|
+
const chunks = [];
|
|
1374
|
+
const s = createReadStream(tmp, {
|
|
1375
|
+
start: 0,
|
|
1376
|
+
end: 15
|
|
1377
|
+
});
|
|
1378
|
+
s.on("data", (c) => chunks.push(c));
|
|
1379
|
+
s.on("end", () => resolve(Buffer.concat(chunks)));
|
|
1380
|
+
s.on("error", reject);
|
|
1381
|
+
}));
|
|
1382
|
+
if (mime === null) {
|
|
1383
|
+
fail();
|
|
1384
|
+
res.writeHead(415, { "Content-Type": "application/json" });
|
|
1385
|
+
res.end(JSON.stringify({
|
|
1386
|
+
ok: false,
|
|
1387
|
+
error: "not a font (expected ttf/otf/woff/woff2)"
|
|
1388
|
+
}));
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
const target = fontPathFor(mime);
|
|
1392
|
+
for (const name of FONT_CANDIDATES) {
|
|
1393
|
+
const p = dshHomePath(DATA_DIR, name);
|
|
1394
|
+
if (p !== target) await rm(p, { force: true });
|
|
1395
|
+
}
|
|
1396
|
+
await rm(target, { force: true });
|
|
1397
|
+
await rename(tmp, target);
|
|
1398
|
+
const cfg = await readConfig();
|
|
1399
|
+
if (cfg.fontMime !== mime) {
|
|
1400
|
+
cfg.fontMime = mime;
|
|
1401
|
+
await writeConfig(cfg);
|
|
1402
|
+
}
|
|
1403
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1404
|
+
res.end(JSON.stringify({
|
|
1405
|
+
ok: true,
|
|
1406
|
+
fontUrl: FONT_ROUTE,
|
|
1407
|
+
mime
|
|
1408
|
+
}));
|
|
1409
|
+
} catch (e) {
|
|
1410
|
+
console.error("dsh-any-background: failed to finalize the font upload", e);
|
|
1411
|
+
rm(tmp, { force: true });
|
|
1412
|
+
try {
|
|
1413
|
+
res.writeHead(500);
|
|
1414
|
+
res.end();
|
|
1415
|
+
} catch {}
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
} catch (e) {
|
|
1419
|
+
console.error("dsh-any-background: failed to accept the font upload", e);
|
|
1420
|
+
try {
|
|
1421
|
+
res.writeHead(500);
|
|
1422
|
+
res.end();
|
|
1423
|
+
} catch {}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
/** Remove every stored font variant and clear the recorded MIME. */
|
|
1427
|
+
async function removeFontFile() {
|
|
1428
|
+
try {
|
|
1429
|
+
for (const name of FONT_CANDIDATES) await rm(dshHomePath(DATA_DIR, name), { force: true });
|
|
1430
|
+
await rm(dshHomePath(DATA_DIR, FONT_UPLOAD_TMP), { force: true });
|
|
1431
|
+
const cfg = await readConfig();
|
|
1432
|
+
if (cfg.fontMime !== null) {
|
|
1433
|
+
cfg.fontMime = null;
|
|
1434
|
+
await writeConfig(cfg);
|
|
1435
|
+
}
|
|
1436
|
+
return true;
|
|
1437
|
+
} catch (e) {
|
|
1438
|
+
console.error("dsh-any-background: failed to remove the custom font", e);
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1096
1442
|
const NS = "dshAnyBackground";
|
|
1097
1443
|
const RPC_CHANNEL = "/dsh-any-background";
|
|
1098
1444
|
const RPC_BODY_MAX = 314572800;
|
|
@@ -1147,13 +1493,18 @@ async function handleRpcMethod(endpoint, payload) {
|
|
|
1147
1493
|
switch (method) {
|
|
1148
1494
|
case "read": {
|
|
1149
1495
|
const rotated = await advanceRotationIfDue();
|
|
1496
|
+
const config = await readConfig();
|
|
1497
|
+
const firstRun = !await exists(configPath());
|
|
1498
|
+
if (firstRun) await writeConfig(config);
|
|
1150
1499
|
return {
|
|
1151
1500
|
ok: true,
|
|
1152
1501
|
value: {
|
|
1153
|
-
config
|
|
1502
|
+
config,
|
|
1154
1503
|
wallpaperUrl: await wallpaperServeUrl(),
|
|
1155
1504
|
videoUrl: await videoUrl(),
|
|
1156
|
-
|
|
1505
|
+
fontUrl: await fontUrl(),
|
|
1506
|
+
rotated,
|
|
1507
|
+
firstRun
|
|
1157
1508
|
}
|
|
1158
1509
|
};
|
|
1159
1510
|
}
|
|
@@ -1189,6 +1540,10 @@ async function handleRpcMethod(endpoint, payload) {
|
|
|
1189
1540
|
ok: true,
|
|
1190
1541
|
value: await handleRotationSet(payload)
|
|
1191
1542
|
};
|
|
1543
|
+
case "removeFont": return {
|
|
1544
|
+
ok: true,
|
|
1545
|
+
value: await removeFontFile()
|
|
1546
|
+
};
|
|
1192
1547
|
default: return {
|
|
1193
1548
|
ok: false,
|
|
1194
1549
|
error: {
|
|
@@ -1334,7 +1689,17 @@ function apply(ctx) {
|
|
|
1334
1689
|
path: WALLPAPER_UPLOAD_ROUTE,
|
|
1335
1690
|
handler: fenceUpload(handleWallpaperUpload)
|
|
1336
1691
|
}), "dsh-any-background: wallpaper upload route");
|
|
1692
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
1693
|
+
kind: "prefix",
|
|
1694
|
+
path: FONT_ROUTE,
|
|
1695
|
+
handler: serveFont
|
|
1696
|
+
}), "dsh-any-background: font route");
|
|
1697
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
1698
|
+
kind: "exact",
|
|
1699
|
+
path: FONT_UPLOAD_ROUTE,
|
|
1700
|
+
handler: fenceUpload(handleFontUpload)
|
|
1701
|
+
}), "dsh-any-background: font upload route");
|
|
1337
1702
|
});
|
|
1338
1703
|
}
|
|
1339
1704
|
//#endregion
|
|
1340
|
-
export { apply, inject, name };
|
|
1705
|
+
export { DEFAULT_CONFIG, apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-any-background",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Appearance plugin for DeepSeek Harness: custom theme color (PS-style color wheel), background wallpaper with opacity/blur controls, and separate main/settings interface opacity.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
},
|
|
104
104
|
"dsh": {
|
|
105
105
|
"compatibility": {
|
|
106
|
+
"dsh": "0.1.5-rc.2",
|
|
106
107
|
"dshReleases": {
|
|
107
108
|
"0.1.5-rc.2": "compatible"
|
|
108
109
|
}
|