dsh-background-by-model 0.9.2 → 0.10.0
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/CHANGELOG.md +17 -1
- package/CHANGELOG.zh.md +17 -1
- package/README.md +22 -19
- package/README.zh.md +23 -20
- package/lib/client.js +405 -131
- package/lib/index.js +271 -92
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -108,6 +108,28 @@ const DEFAULT_TRANSITION = {
|
|
|
108
108
|
easing: "ease",
|
|
109
109
|
durationMs: 320
|
|
110
110
|
};
|
|
111
|
+
/**
|
|
112
|
+
* One image entry for a place that CREATES a picture rather than reads one back
|
|
113
|
+
* (a fresh holiday, and every upload in the panel).
|
|
114
|
+
*
|
|
115
|
+
* The two effects whose neutral value is also the shipped default are written
|
|
116
|
+
* explicitly (`opacity`, `blur`), even though `normalizeImage` would default them:
|
|
117
|
+
* the read-time lifts key off the ABSENCE of a key, so an entry created without one
|
|
118
|
+
* would come back as "written before this field existed" and inherit whatever the
|
|
119
|
+
* rule happened to carry. `brightness` is the one field whose absence is meaningful
|
|
120
|
+
* (see `BgImage.brightness`), so it is left out — "untouched" is exactly what a new
|
|
121
|
+
* picture is.
|
|
122
|
+
*/
|
|
123
|
+
function defaultImage(slot, color = null, bgMode = "fit") {
|
|
124
|
+
return {
|
|
125
|
+
slot,
|
|
126
|
+
bgMode,
|
|
127
|
+
bgState: { ...DEFAULT_BG_STATE },
|
|
128
|
+
color,
|
|
129
|
+
opacity: 1,
|
|
130
|
+
blur: 0
|
|
131
|
+
};
|
|
132
|
+
}
|
|
111
133
|
const DEFAULT_CHAT_TEXT_OPACITY = .35;
|
|
112
134
|
const DEFAULT_TRAJECTORY_OPACITY = .73;
|
|
113
135
|
const DEFAULT_RIGHTBAR_OPACITY = .45;
|
|
@@ -156,18 +178,10 @@ function defaultHolidayRule(def) {
|
|
|
156
178
|
const color = hexToHsl(def.color);
|
|
157
179
|
return {
|
|
158
180
|
id: def.id,
|
|
159
|
-
images: [
|
|
160
|
-
slot: def.slot,
|
|
161
|
-
bgMode: "fill",
|
|
162
|
-
bgState: { ...DEFAULT_BG_STATE },
|
|
163
|
-
color
|
|
164
|
-
}],
|
|
181
|
+
images: [defaultImage(def.slot, color, "fill")],
|
|
165
182
|
match: "",
|
|
166
183
|
enabled: true,
|
|
167
184
|
color,
|
|
168
|
-
wallpaperOpacity: 1,
|
|
169
|
-
brightness: 100,
|
|
170
|
-
blur: 0,
|
|
171
185
|
rotate: defaultRotation()
|
|
172
186
|
};
|
|
173
187
|
}
|
|
@@ -187,9 +201,6 @@ function blankRule(id) {
|
|
|
187
201
|
match: "",
|
|
188
202
|
enabled: true,
|
|
189
203
|
color: null,
|
|
190
|
-
wallpaperOpacity: 1,
|
|
191
|
-
brightness: 100,
|
|
192
|
-
blur: 0,
|
|
193
204
|
rotate: defaultRotation()
|
|
194
205
|
};
|
|
195
206
|
}
|
|
@@ -276,28 +287,41 @@ function normalizeTransition(raw, legacyMs) {
|
|
|
276
287
|
durationMs: clamp(t.durationMs, 0, TRANSITION_MAX_MS, fallback)
|
|
277
288
|
};
|
|
278
289
|
}
|
|
290
|
+
/** The inherit block for a caller that has no rule-level values at all. */
|
|
291
|
+
const DEFAULT_IMAGE_INHERIT = {
|
|
292
|
+
color: null,
|
|
293
|
+
opacity: 1,
|
|
294
|
+
brightness: void 0,
|
|
295
|
+
blur: 0
|
|
296
|
+
};
|
|
279
297
|
/**
|
|
280
298
|
* Coerce one persisted image entry, or null when it lacks a usable slot.
|
|
281
299
|
*
|
|
282
|
-
* `inherit`
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
300
|
+
* `inherit` is the rule-level block this entry may take its values from, and each
|
|
301
|
+
* field is lifted ONLY when the entry has no key of its own — that absence is the
|
|
302
|
+
* whole signal. `color: null` is a deliberate "follow the system theme" and must
|
|
303
|
+
* not be re-inherited on every load; `brightness` keeps its key absent when neither
|
|
304
|
+
* side ever named one, so an upgrade never writes a default nobody chose.
|
|
286
305
|
*
|
|
287
306
|
* `inheritMode` is the same one-shot lift for the layout mode. It is the caller's to
|
|
288
307
|
* resolve because the callers answer differently: a user rule passes the mode its
|
|
289
308
|
* config carried at the RULE level (schema 5), a holiday passes its own `fill`. An
|
|
290
309
|
* entry that HAS a `bgMode` key keeps it, even when it equals the default.
|
|
291
310
|
*/
|
|
292
|
-
function normalizeImage(raw, inherit =
|
|
311
|
+
function normalizeImage(raw, inherit = DEFAULT_IMAGE_INHERIT, inheritMode = "fit") {
|
|
293
312
|
const i = raw ?? {};
|
|
294
313
|
if (typeof i.slot !== "string" || !SLOT_RE.test(i.slot)) return null;
|
|
295
|
-
|
|
314
|
+
const image = {
|
|
296
315
|
slot: i.slot,
|
|
297
316
|
bgMode: BG_MODES.includes(i.bgMode) ? i.bgMode : inheritMode,
|
|
298
317
|
bgState: normalizeBgState(i.bgState),
|
|
299
|
-
color: hasOwn(i, "color") ? normalizeHsl(i.color) : inherit
|
|
318
|
+
color: hasOwn(i, "color") ? normalizeHsl(i.color) : inherit.color,
|
|
319
|
+
opacity: clamp01(i.opacity, inherit.opacity),
|
|
320
|
+
blur: clamp(i.blur, 0, 60, inherit.blur)
|
|
300
321
|
};
|
|
322
|
+
const brightness = i.brightness === void 0 ? inherit.brightness : clamp(i.brightness, 0, 200, 100);
|
|
323
|
+
if (brightness !== void 0) image.brightness = brightness;
|
|
324
|
+
return image;
|
|
301
325
|
}
|
|
302
326
|
/**
|
|
303
327
|
* Coerce one persisted rule, or null when it names no image ANYWHERE.
|
|
@@ -322,38 +346,45 @@ function normalizeRule(raw) {
|
|
|
322
346
|
if (id === null) return null;
|
|
323
347
|
const color = normalizeHsl(r.color);
|
|
324
348
|
const mode = BG_MODES.includes(r.bgMode) ? r.bgMode : "fit";
|
|
349
|
+
const inherit = {
|
|
350
|
+
color,
|
|
351
|
+
opacity: clamp01(r.wallpaperOpacity, 1),
|
|
352
|
+
brightness: r.brightness === void 0 ? void 0 : clamp(r.brightness, 0, 200, 100),
|
|
353
|
+
blur: clamp(r.blur, 0, 60, 0)
|
|
354
|
+
};
|
|
325
355
|
const images = [];
|
|
326
356
|
const seen = /* @__PURE__ */ new Set();
|
|
327
357
|
const hadList = Array.isArray(r.images);
|
|
328
358
|
if (hadList) for (const entry of r.images) {
|
|
329
|
-
const image = normalizeImage(entry,
|
|
359
|
+
const image = normalizeImage(entry, inherit, mode);
|
|
330
360
|
if (image === null || seen.has(image.slot)) continue;
|
|
331
361
|
seen.add(image.slot);
|
|
332
362
|
images.push(image);
|
|
333
363
|
}
|
|
334
364
|
const legacySlot = typeof r.slot === "string" && SLOT_RE.test(r.slot) ? r.slot : null;
|
|
335
|
-
if (images.length === 0 && legacySlot !== null)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
365
|
+
if (images.length === 0 && legacySlot !== null) {
|
|
366
|
+
const image = {
|
|
367
|
+
slot: legacySlot,
|
|
368
|
+
bgMode: mode,
|
|
369
|
+
bgState: normalizeBgState(r.bgState),
|
|
370
|
+
color,
|
|
371
|
+
opacity: inherit.opacity,
|
|
372
|
+
blur: inherit.blur
|
|
373
|
+
};
|
|
374
|
+
if (inherit.brightness !== void 0) image.brightness = inherit.brightness;
|
|
375
|
+
images.push(image);
|
|
376
|
+
}
|
|
341
377
|
if (images.length === 0 && !hadList) return null;
|
|
342
378
|
const rotate = normalizeRotation(r.rotate);
|
|
343
379
|
if (images.length < 2) rotate.enabled = false;
|
|
344
|
-
|
|
345
|
-
const rule = {
|
|
380
|
+
return {
|
|
346
381
|
id,
|
|
347
382
|
images,
|
|
348
383
|
match: typeof r.match === "string" ? r.match : "",
|
|
349
384
|
enabled: r.enabled !== false,
|
|
350
385
|
color,
|
|
351
|
-
wallpaperOpacity: clamp01(r.wallpaperOpacity, 1),
|
|
352
|
-
blur: clamp(r.blur, 0, 60, 0),
|
|
353
386
|
rotate
|
|
354
387
|
};
|
|
355
|
-
if (brightness !== void 0) rule.brightness = brightness;
|
|
356
|
-
return rule;
|
|
357
388
|
}
|
|
358
389
|
/**
|
|
359
390
|
* Coerce one persisted holiday entry.
|
|
@@ -370,17 +401,29 @@ function normalizeHolidayRule(def, raw) {
|
|
|
370
401
|
return {
|
|
371
402
|
...base,
|
|
372
403
|
enabled: r.enabled !== false,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
404
|
+
images: base.images.map((image) => {
|
|
405
|
+
const stored = r.images?.[0];
|
|
406
|
+
const merged = {
|
|
407
|
+
slot: image.slot,
|
|
408
|
+
bgMode: image.bgMode,
|
|
409
|
+
bgState: normalizeBgState(stored?.bgState ?? r.bgState),
|
|
410
|
+
color: image.color,
|
|
411
|
+
opacity: clamp01(stored?.opacity ?? legacyWallpaperOpacity(r.wallpaperOpacity), image.opacity),
|
|
412
|
+
blur: clamp(stored?.blur ?? r.blur, 0, 60, image.blur)
|
|
413
|
+
};
|
|
414
|
+
const brightness = stored?.brightness === void 0 ? r.brightness === void 0 ? void 0 : clamp(r.brightness, 0, 200, 100) : clamp(stored.brightness, 0, 200, 100);
|
|
415
|
+
if (brightness !== void 0) merged.brightness = brightness;
|
|
416
|
+
return merged;
|
|
417
|
+
})
|
|
382
418
|
};
|
|
383
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* The rule-level opacity a schema-9 holiday entry carried, or `undefined` so the
|
|
422
|
+
* caller can tell "absent" from "set to 1" and leave `clamp01` its default.
|
|
423
|
+
*/
|
|
424
|
+
function legacyWallpaperOpacity(raw) {
|
|
425
|
+
return typeof raw === "number" && isFinite(raw) ? raw : void 0;
|
|
426
|
+
}
|
|
384
427
|
/** Coerce the whole holiday block; unknown ids are dropped, missing ones defaulted. */
|
|
385
428
|
function normalizeHolidays(raw) {
|
|
386
429
|
const r = raw ?? {};
|
|
@@ -508,8 +551,9 @@ function orphanSlots(stored, referenced, kept = []) {
|
|
|
508
551
|
* `/api`, which slash commands own).
|
|
509
552
|
*
|
|
510
553
|
* theme-config.json the ordered rule list + the global interface settings
|
|
511
|
-
*
|
|
512
|
-
*
|
|
554
|
+
* theme-config.json.bak the last config that parsed, rotated in before every save
|
|
555
|
+
* modelbg-<slot> one background image (raw bytes, no extension; the MIME is
|
|
556
|
+
* sniffed from the magic bytes when served)
|
|
513
557
|
* holiday-cache/<tag>/ festival art downloaded from the CDN, one directory per
|
|
514
558
|
* pinned release, deliberately NOT in the slot namespace
|
|
515
559
|
*
|
|
@@ -521,6 +565,20 @@ const name = "dsh-background-by-model";
|
|
|
521
565
|
const inject = ["connection", "webServer"];
|
|
522
566
|
const DATA_DIR = ".dsh-background-by-model-data";
|
|
523
567
|
const CONFIG_FILE = "theme-config.json";
|
|
568
|
+
/**
|
|
569
|
+
* The last config that was known to parse, kept beside the live one.
|
|
570
|
+
*
|
|
571
|
+
* A config file is the whole rule list, and it is rewritten on every edit. Writing
|
|
572
|
+
* it in place meant a crash, a full disk or a pulled power cable could leave half a
|
|
573
|
+
* JSON document behind — which `readConfig` greets by silently falling back to the
|
|
574
|
+
* defaults, so the next slider drag would persist those defaults over the ruined
|
|
575
|
+
* file and the original rules were gone for good. Two pieces fix that, and they are
|
|
576
|
+
* one feature: the write is atomic (temp + rename), and the previous good file is
|
|
577
|
+
* rotated to `.bak` before it is replaced, so even a lost file has a copy.
|
|
578
|
+
*/
|
|
579
|
+
const CONFIG_BACKUP = "theme-config.json.bak";
|
|
580
|
+
/** Suffix of the in-flight write. Deliberately outside `listSlots`' `modelbg-` prefix. */
|
|
581
|
+
const CONFIG_TEMP = ".tmp";
|
|
524
582
|
const IMAGE_PREFIX = "modelbg-";
|
|
525
583
|
const LEGACY_WALLPAPER = "wallpaper.jpg";
|
|
526
584
|
const LEGACY_FILES = [
|
|
@@ -622,6 +680,8 @@ const PRESET_IMAGE_COUNT_MAX = 64;
|
|
|
622
680
|
const PRESET_FETCH_TIMEOUT = 6e4;
|
|
623
681
|
const dataDir = () => dshHomePath(DATA_DIR);
|
|
624
682
|
const configPath = () => dshHomePath(DATA_DIR, CONFIG_FILE);
|
|
683
|
+
const configBackupPath = () => dshHomePath(DATA_DIR, CONFIG_BACKUP);
|
|
684
|
+
const configTempPath = () => dshHomePath(DATA_DIR, `${CONFIG_FILE}${CONFIG_TEMP}`);
|
|
625
685
|
const legacyWallpaperPath = () => dshHomePath(DATA_DIR, LEGACY_WALLPAPER);
|
|
626
686
|
const imagePath = (slot) => dshHomePath(DATA_DIR, `${IMAGE_PREFIX}${slot}`);
|
|
627
687
|
const holidayCachePath = (asset) => dshHomePath(DATA_DIR, HOLIDAY_CACHE_DIR, HOLIDAY_ASSETS_REF, asset);
|
|
@@ -670,14 +730,15 @@ async function migrateLegacy(raw) {
|
|
|
670
730
|
images: [{
|
|
671
731
|
slot: "m1",
|
|
672
732
|
bgMode: typeof r.bgMode === "string" ? r.bgMode : "fit",
|
|
673
|
-
bgState: r.bgState ?? {}
|
|
733
|
+
bgState: r.bgState ?? {},
|
|
734
|
+
color: Array.isArray(r.color) ? r.color : null,
|
|
735
|
+
opacity: typeof r.wallpaperOpacity === "number" ? r.wallpaperOpacity : 1,
|
|
736
|
+
...typeof r.brightness === "number" ? { brightness: r.brightness } : {},
|
|
737
|
+
blur: typeof r.blur === "number" ? r.blur : 0
|
|
674
738
|
}],
|
|
675
739
|
match: "",
|
|
676
740
|
enabled: true,
|
|
677
|
-
color: Array.isArray(r.color) ? r.color : null
|
|
678
|
-
wallpaperOpacity: typeof r.wallpaperOpacity === "number" ? r.wallpaperOpacity : 1,
|
|
679
|
-
brightness: typeof r.brightness === "number" ? r.brightness : 100,
|
|
680
|
-
blur: typeof r.blur === "number" ? r.blur : 0
|
|
741
|
+
color: Array.isArray(r.color) ? r.color : null
|
|
681
742
|
}],
|
|
682
743
|
opacities: r.opacities,
|
|
683
744
|
settingsOpacity: r.settingsOpacity,
|
|
@@ -686,7 +747,7 @@ async function migrateLegacy(raw) {
|
|
|
686
747
|
rightbarOpacity: r.rightbarOpacity
|
|
687
748
|
};
|
|
688
749
|
try {
|
|
689
|
-
await
|
|
750
|
+
await writeConfigFile(serializeConfig(next));
|
|
690
751
|
} catch (e) {
|
|
691
752
|
console.warn("dsh-background-by-model: could not rewrite the migrated config", e);
|
|
692
753
|
}
|
|
@@ -699,11 +760,121 @@ async function ensureDir() {
|
|
|
699
760
|
console.warn(`dsh-background-by-model: cannot create data dir "${dataDir()}"`, e);
|
|
700
761
|
}
|
|
701
762
|
}
|
|
763
|
+
/** The exact bytes a config file should hold. */
|
|
764
|
+
function serializeConfig(config) {
|
|
765
|
+
return JSON.stringify(config, null, 2);
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Whether a string is a config this build would accept: JSON, and an object.
|
|
769
|
+
*
|
|
770
|
+
* Deliberately looser than `normalizeConfig` (which accepts anything and defaults
|
|
771
|
+
* every field): this is the test for "is there something worth keeping", so a
|
|
772
|
+
* schema-3 file and a hand-edited one both count. What it rejects is the two ways a
|
|
773
|
+
* write can be interrupted — half a document, and an empty one.
|
|
774
|
+
*/
|
|
775
|
+
function parsesAsConfig(text) {
|
|
776
|
+
try {
|
|
777
|
+
const value = JSON.parse(text);
|
|
778
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
779
|
+
} catch {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Write one config file so that no reader can ever see a partial document, keeping
|
|
785
|
+
* the previous good one as `.bak`.
|
|
786
|
+
*
|
|
787
|
+
* Two steps, in this order, because the order is what makes the copy worth having:
|
|
788
|
+
*
|
|
789
|
+
* 1. the file on disk NOW is copied to `theme-config.json.bak` — but only when it
|
|
790
|
+
* parses. A corrupted live file must never overwrite the last good backup, or
|
|
791
|
+
* the first write after the corruption would also destroy the copy that could
|
|
792
|
+
* have replaced it;
|
|
793
|
+
* 2. the new bytes go to a temp name and are RENAMED over the live path. A rename
|
|
794
|
+
* is atomic within one directory, so a crash either leaves the old file or the
|
|
795
|
+
* new one — never half of either.
|
|
796
|
+
*
|
|
797
|
+
* The rename is not `writeFile` for the same reason: writing in place truncates
|
|
798
|
+
* first, and a crash between the truncation and the last byte is exactly the state
|
|
799
|
+
* that used to eat a rule list.
|
|
800
|
+
*/
|
|
801
|
+
async function writeConfigFile(text) {
|
|
802
|
+
await ensureDir();
|
|
803
|
+
const target = configPath();
|
|
804
|
+
try {
|
|
805
|
+
const current = await readFile(target, "utf8");
|
|
806
|
+
if (parsesAsConfig(current)) {
|
|
807
|
+
const backupTemp = `${configBackupPath()}${CONFIG_TEMP}`;
|
|
808
|
+
await writeFile(backupTemp, current, "utf8");
|
|
809
|
+
await renameOver(backupTemp, configBackupPath());
|
|
810
|
+
}
|
|
811
|
+
} catch {}
|
|
812
|
+
const temp = configTempPath();
|
|
813
|
+
await writeFile(temp, text, "utf8");
|
|
814
|
+
await renameOver(temp, target);
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* `rename` over an existing file, which replaces it atomically on POSIX.
|
|
818
|
+
*
|
|
819
|
+
* Windows refuses to rename onto a name that exists (EEXIST / EPERM), so the one
|
|
820
|
+
* platform that cannot do this in one step gets a two-step fallback — and the
|
|
821
|
+
* difference matters: unlinking first leaves a window with NO file, which is the
|
|
822
|
+
* state this whole function exists to avoid, so on POSIX the unlink must never be
|
|
823
|
+
* attempted. The fallback is only reached when the rename itself said the target is
|
|
824
|
+
* in the way, which is exactly the Windows case.
|
|
825
|
+
*/
|
|
826
|
+
async function renameOver(from, to) {
|
|
827
|
+
try {
|
|
828
|
+
await rename(from, to);
|
|
829
|
+
} catch (e) {
|
|
830
|
+
const code = e?.code;
|
|
831
|
+
if (code !== "EEXIST" && code !== "EPERM" && code !== "ENOTEMPTY") throw e;
|
|
832
|
+
await rm(to, { force: true });
|
|
833
|
+
await rename(from, to);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* The last good config that can be restored, as raw text — or null when there is
|
|
838
|
+
* none.
|
|
839
|
+
*
|
|
840
|
+
* Two candidates, in order of authority: the live file (when it parses, which is
|
|
841
|
+
* the ordinary case and costs one read) and the `.bak`. The backup is only
|
|
842
|
+
* consulted for a live file that is missing or corrupt, so a half-written document
|
|
843
|
+
* from an interrupted save is answered with the copy rather than with the shipped
|
|
844
|
+
* defaults — which is the difference between "your rules are back" and "your rules
|
|
845
|
+
* were never there".
|
|
846
|
+
*/
|
|
847
|
+
async function readConfigFile() {
|
|
848
|
+
try {
|
|
849
|
+
const text = await readFile(configPath(), "utf8");
|
|
850
|
+
if (parsesAsConfig(text)) return {
|
|
851
|
+
text,
|
|
852
|
+
source: "live"
|
|
853
|
+
};
|
|
854
|
+
} catch {}
|
|
855
|
+
try {
|
|
856
|
+
const text = await readFile(configBackupPath(), "utf8");
|
|
857
|
+
if (!parsesAsConfig(text)) return null;
|
|
858
|
+
try {
|
|
859
|
+
await writeConfigFile(text);
|
|
860
|
+
} catch (e) {
|
|
861
|
+
console.warn("dsh-background-by-model: could not restore the config from its backup", e);
|
|
862
|
+
}
|
|
863
|
+
console.warn("dsh-background-by-model: \"theme-config.json\" was missing or unreadable; restored the last saved copy from \"theme-config.json.bak\"");
|
|
864
|
+
return {
|
|
865
|
+
text,
|
|
866
|
+
source: "backup"
|
|
867
|
+
};
|
|
868
|
+
} catch {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
702
872
|
async function readConfig() {
|
|
703
873
|
await ensureDir();
|
|
874
|
+
const file = await readConfigFile();
|
|
704
875
|
let raw = {};
|
|
705
|
-
try {
|
|
706
|
-
raw = JSON.parse(
|
|
876
|
+
if (file !== null) try {
|
|
877
|
+
raw = JSON.parse(file.text);
|
|
707
878
|
} catch {
|
|
708
879
|
raw = {};
|
|
709
880
|
}
|
|
@@ -712,21 +883,21 @@ async function readConfig() {
|
|
|
712
883
|
/**
|
|
713
884
|
* The config, plus whether it is safe to decide what the store still owns from it.
|
|
714
885
|
*
|
|
715
|
-
* `readConfig` deliberately swallows a corrupt file and answers with
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
* rather than guesses:
|
|
721
|
-
*
|
|
722
|
-
* -
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* - a file that is there but does not parse, or is not an object →
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
886
|
+
* `readConfig` deliberately swallows a corrupt file and answers with the last good
|
|
887
|
+
* copy (or, when there is none, with defaults) — right for painting and WRONG as a
|
|
888
|
+
* basis for deleting images, because a store whose config could not be read at all
|
|
889
|
+
* would then have zero referenced slots and every file on disk would look like an
|
|
890
|
+
* orphan. That is the one way this feature could destroy a wallpaper collection, so
|
|
891
|
+
* the two cases are told apart here and the sweep refuses rather than guesses:
|
|
892
|
+
*
|
|
893
|
+
* - a live file that parses to an object → trusted (its rules are what they say);
|
|
894
|
+
* - no live file, and no backup either → trusted. A fresh store has images only
|
|
895
|
+
* if something wrote them, and nothing did;
|
|
896
|
+
* - a live file that is there but does not parse, or is not an object → the
|
|
897
|
+
* BACKUP is the answer, and only because it is exactly the config `readConfig`
|
|
898
|
+
* would restore: pruning against it is pruning against the rules the user is
|
|
899
|
+
* about to get back. When there is no parseable backup, this is `untrusted` and
|
|
900
|
+
* the sweep refuses.
|
|
730
901
|
*
|
|
731
902
|
* The untrusted branch deliberately does NOT go through `migrateLegacy`: that one
|
|
732
903
|
* WRITES a repaired config back to disk for the shapes earlier releases left
|
|
@@ -737,31 +908,33 @@ async function readConfig() {
|
|
|
737
908
|
*/
|
|
738
909
|
async function readConfigForSweep() {
|
|
739
910
|
await ensureDir();
|
|
740
|
-
let
|
|
911
|
+
let live = null;
|
|
741
912
|
try {
|
|
742
|
-
|
|
913
|
+
live = await readFile(configPath(), "utf8");
|
|
743
914
|
} catch {
|
|
744
|
-
|
|
745
|
-
config: await readConfig(),
|
|
746
|
-
trusted: true
|
|
747
|
-
};
|
|
915
|
+
live = null;
|
|
748
916
|
}
|
|
749
|
-
|
|
917
|
+
if (live !== null && parsesAsConfig(live)) return {
|
|
918
|
+
config: normalizeConfig(await migrateLegacy(JSON.parse(live))),
|
|
919
|
+
trusted: true
|
|
920
|
+
};
|
|
921
|
+
let backup = null;
|
|
750
922
|
try {
|
|
751
|
-
|
|
923
|
+
backup = await readFile(configBackupPath(), "utf8");
|
|
752
924
|
} catch {
|
|
753
|
-
|
|
754
|
-
config: normalizeConfig({}),
|
|
755
|
-
trusted: false
|
|
756
|
-
};
|
|
925
|
+
backup = null;
|
|
757
926
|
}
|
|
758
|
-
if (
|
|
927
|
+
if (backup !== null && parsesAsConfig(backup)) return {
|
|
928
|
+
config: normalizeConfig(JSON.parse(backup)),
|
|
929
|
+
trusted: true
|
|
930
|
+
};
|
|
931
|
+
if (live === null && backup === null) return {
|
|
759
932
|
config: normalizeConfig({}),
|
|
760
|
-
trusted:
|
|
933
|
+
trusted: true
|
|
761
934
|
};
|
|
762
935
|
return {
|
|
763
|
-
config: normalizeConfig(
|
|
764
|
-
trusted:
|
|
936
|
+
config: normalizeConfig({}),
|
|
937
|
+
trusted: false
|
|
765
938
|
};
|
|
766
939
|
}
|
|
767
940
|
const LEGACY_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -775,7 +948,10 @@ const LEGACY_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
775
948
|
const LEGACY_RULE_KEYS = /* @__PURE__ */ new Set([
|
|
776
949
|
"slot",
|
|
777
950
|
"bgState",
|
|
778
|
-
"bgMode"
|
|
951
|
+
"bgMode",
|
|
952
|
+
"wallpaperOpacity",
|
|
953
|
+
"brightness",
|
|
954
|
+
"blur"
|
|
779
955
|
]);
|
|
780
956
|
const LEGACY_NESTED_KEYS = /* @__PURE__ */ new Set(["rules[].rotate.fadeMs"]);
|
|
781
957
|
const warnedConfigKeys = /* @__PURE__ */ new Set();
|
|
@@ -829,11 +1005,10 @@ function warnUnknownConfigKeys(raw, normalized) {
|
|
|
829
1005
|
}
|
|
830
1006
|
}
|
|
831
1007
|
async function writeConfig(config) {
|
|
832
|
-
await ensureDir();
|
|
833
1008
|
try {
|
|
834
1009
|
const normalized = normalizeConfig(config);
|
|
835
1010
|
warnUnknownConfigKeys(config, normalized);
|
|
836
|
-
await
|
|
1011
|
+
await writeConfigFile(serializeConfig(normalized));
|
|
837
1012
|
return true;
|
|
838
1013
|
} catch (e) {
|
|
839
1014
|
console.error(`dsh-background-by-model: failed to write "${CONFIG_FILE}"`, e);
|
|
@@ -1062,14 +1237,14 @@ async function fetchPresetConfig() {
|
|
|
1062
1237
|
ok: false,
|
|
1063
1238
|
error: "not a profile"
|
|
1064
1239
|
};
|
|
1065
|
-
if (declared >
|
|
1066
|
-
console.warn(`dsh-background-by-model: the recommended profile declares schema ${declared}, this build writes
|
|
1240
|
+
if (declared > 10) {
|
|
1241
|
+
console.warn(`dsh-background-by-model: the recommended profile declares schema ${declared}, this build writes 10`);
|
|
1067
1242
|
return {
|
|
1068
1243
|
ok: false,
|
|
1069
1244
|
error: "newer version"
|
|
1070
1245
|
};
|
|
1071
1246
|
}
|
|
1072
|
-
if (declared <
|
|
1247
|
+
if (declared < 10) console.warn(`dsh-background-by-model: the recommended profile is written for schema ${declared}; importing it with defaults for the fields it predates`);
|
|
1073
1248
|
const configRaw = await fetchPresetFile(PRESET_CONFIG, PRESET_CONFIG_MAX);
|
|
1074
1249
|
if (configRaw === null) return {
|
|
1075
1250
|
ok: false,
|
|
@@ -1216,7 +1391,7 @@ async function listSlots() {
|
|
|
1216
1391
|
}
|
|
1217
1392
|
}
|
|
1218
1393
|
/**
|
|
1219
|
-
* Empty the store: the config and every rule image, and nothing else.
|
|
1394
|
+
* Empty the store: the config, its backup and every rule image, and nothing else.
|
|
1220
1395
|
*
|
|
1221
1396
|
* The hard reset behind "use the recommended profile", and it has to be a real one
|
|
1222
1397
|
* rather than a config overwrite: `nextSlot` hands out `m1`, `m2`, … as a running
|
|
@@ -1224,6 +1399,10 @@ async function listSlots() {
|
|
|
1224
1399
|
* as a matter of course. Emptying first is what makes the profile's own slots free
|
|
1225
1400
|
* to write, which is why no slot remapping is needed.
|
|
1226
1401
|
*
|
|
1402
|
+
* The backup goes WITH the live file: a `.bak` left behind would be restored by the
|
|
1403
|
+
* next read, which would quietly undo the very replacement this call exists to
|
|
1404
|
+
* perform (`applyThemeFile`'s `wipeFirst`).
|
|
1405
|
+
*
|
|
1227
1406
|
* `holiday-cache/` is deliberately untouched: downloaded art that belongs to no
|
|
1228
1407
|
* slot and costs a round trip to get back. Pruning generations is
|
|
1229
1408
|
* `pruneHolidayCache`'s job.
|
|
@@ -1234,8 +1413,8 @@ async function listSlots() {
|
|
|
1234
1413
|
*/
|
|
1235
1414
|
async function resetStore() {
|
|
1236
1415
|
let ok = true;
|
|
1237
|
-
try {
|
|
1238
|
-
await rm(
|
|
1416
|
+
for (const path of [configPath(), configBackupPath()]) try {
|
|
1417
|
+
await rm(path, { force: true });
|
|
1239
1418
|
} catch (e) {
|
|
1240
1419
|
ok = false;
|
|
1241
1420
|
console.warn(`dsh-background-by-model: failed to remove "${CONFIG_FILE}"`, e);
|
|
@@ -1476,7 +1655,7 @@ async function handleRpcMethod(ctx, endpoint, payload) {
|
|
|
1476
1655
|
value: {
|
|
1477
1656
|
config: await readConfig(),
|
|
1478
1657
|
slots: await listSlots(),
|
|
1479
|
-
schema:
|
|
1658
|
+
schema: 10
|
|
1480
1659
|
}
|
|
1481
1660
|
};
|
|
1482
1661
|
case "writeConfig": return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-background-by-model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Appearance plugin for DeepSeek Harness: an ordered list of model rules, each carrying its own wallpapers — every image with its own framing and theme color — plus layout mode, opacity, brightness and blur; the background follows the model you switch to, and a rule can rotate through several images on its own rhythm.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|