dsh-background-by-model 0.9.1 → 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 +27 -1
- package/CHANGELOG.zh.md +27 -1
- package/README.md +41 -19
- package/README.zh.md +42 -20
- package/lib/client.js +634 -133
- package/lib/index.js +441 -62
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -330,6 +330,28 @@ window.__ModuleLoader__.load({
|
|
|
330
330
|
function defaultTransition() {
|
|
331
331
|
return { ...DEFAULT_TRANSITION };
|
|
332
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* One image entry for a place that CREATES a picture rather than reads one back
|
|
335
|
+
* (a fresh holiday, and every upload in the panel).
|
|
336
|
+
*
|
|
337
|
+
* The two effects whose neutral value is also the shipped default are written
|
|
338
|
+
* explicitly (`opacity`, `blur`), even though `normalizeImage` would default them:
|
|
339
|
+
* the read-time lifts key off the ABSENCE of a key, so an entry created without one
|
|
340
|
+
* would come back as "written before this field existed" and inherit whatever the
|
|
341
|
+
* rule happened to carry. `brightness` is the one field whose absence is meaningful
|
|
342
|
+
* (see `BgImage.brightness`), so it is left out — "untouched" is exactly what a new
|
|
343
|
+
* picture is.
|
|
344
|
+
*/
|
|
345
|
+
function defaultImage(slot, color = null, bgMode = "fit") {
|
|
346
|
+
return {
|
|
347
|
+
slot,
|
|
348
|
+
bgMode,
|
|
349
|
+
bgState: { ...DEFAULT_BG_STATE },
|
|
350
|
+
color,
|
|
351
|
+
opacity: 1,
|
|
352
|
+
blur: 0
|
|
353
|
+
};
|
|
354
|
+
}
|
|
333
355
|
/** Every slot a rule owns, in order. The one thing the render and store layers
|
|
334
356
|
* ask of the image list, so a rule's slots are never enumerated ad hoc. */
|
|
335
357
|
function ruleSlots(rule) {
|
|
@@ -399,18 +421,10 @@ window.__ModuleLoader__.load({
|
|
|
399
421
|
const color = hexToHsl(def.color);
|
|
400
422
|
return {
|
|
401
423
|
id: def.id,
|
|
402
|
-
images: [
|
|
403
|
-
slot: def.slot,
|
|
404
|
-
bgMode: "fill",
|
|
405
|
-
bgState: { ...DEFAULT_BG_STATE },
|
|
406
|
-
color
|
|
407
|
-
}],
|
|
424
|
+
images: [defaultImage(def.slot, color, "fill")],
|
|
408
425
|
match: "",
|
|
409
426
|
enabled: true,
|
|
410
427
|
color,
|
|
411
|
-
wallpaperOpacity: 1,
|
|
412
|
-
brightness: 100,
|
|
413
|
-
blur: 0,
|
|
414
428
|
rotate: defaultRotation()
|
|
415
429
|
};
|
|
416
430
|
}
|
|
@@ -434,9 +448,6 @@ window.__ModuleLoader__.load({
|
|
|
434
448
|
match: "",
|
|
435
449
|
enabled: true,
|
|
436
450
|
color: null,
|
|
437
|
-
wallpaperOpacity: 1,
|
|
438
|
-
brightness: 100,
|
|
439
|
-
blur: 0,
|
|
440
451
|
rotate: defaultRotation()
|
|
441
452
|
};
|
|
442
453
|
}
|
|
@@ -547,28 +558,41 @@ window.__ModuleLoader__.load({
|
|
|
547
558
|
durationMs: clamp$1(t.durationMs, 0, TRANSITION_MAX_MS, fallback)
|
|
548
559
|
};
|
|
549
560
|
}
|
|
561
|
+
/** The inherit block for a caller that has no rule-level values at all. */
|
|
562
|
+
const DEFAULT_IMAGE_INHERIT = {
|
|
563
|
+
color: null,
|
|
564
|
+
opacity: 1,
|
|
565
|
+
brightness: void 0,
|
|
566
|
+
blur: 0
|
|
567
|
+
};
|
|
550
568
|
/**
|
|
551
569
|
* Coerce one persisted image entry, or null when it lacks a usable slot.
|
|
552
570
|
*
|
|
553
|
-
* `inherit`
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
571
|
+
* `inherit` is the rule-level block this entry may take its values from, and each
|
|
572
|
+
* field is lifted ONLY when the entry has no key of its own — that absence is the
|
|
573
|
+
* whole signal. `color: null` is a deliberate "follow the system theme" and must
|
|
574
|
+
* not be re-inherited on every load; `brightness` keeps its key absent when neither
|
|
575
|
+
* side ever named one, so an upgrade never writes a default nobody chose.
|
|
557
576
|
*
|
|
558
577
|
* `inheritMode` is the same one-shot lift for the layout mode. It is the caller's to
|
|
559
578
|
* resolve because the callers answer differently: a user rule passes the mode its
|
|
560
579
|
* config carried at the RULE level (schema 5), a holiday passes its own `fill`. An
|
|
561
580
|
* entry that HAS a `bgMode` key keeps it, even when it equals the default.
|
|
562
581
|
*/
|
|
563
|
-
function normalizeImage(raw, inherit =
|
|
582
|
+
function normalizeImage(raw, inherit = DEFAULT_IMAGE_INHERIT, inheritMode = "fit") {
|
|
564
583
|
const i = raw ?? {};
|
|
565
584
|
if (typeof i.slot !== "string" || !SLOT_RE$1.test(i.slot)) return null;
|
|
566
|
-
|
|
585
|
+
const image = {
|
|
567
586
|
slot: i.slot,
|
|
568
587
|
bgMode: BG_MODES$1.includes(i.bgMode) ? i.bgMode : inheritMode,
|
|
569
588
|
bgState: normalizeBgState(i.bgState),
|
|
570
|
-
color: hasOwn(i, "color") ? normalizeHsl(i.color) : inherit
|
|
589
|
+
color: hasOwn(i, "color") ? normalizeHsl(i.color) : inherit.color,
|
|
590
|
+
opacity: clamp01(i.opacity, inherit.opacity),
|
|
591
|
+
blur: clamp$1(i.blur, 0, 60, inherit.blur)
|
|
571
592
|
};
|
|
593
|
+
const brightness = i.brightness === void 0 ? inherit.brightness : clamp$1(i.brightness, 0, 200, 100);
|
|
594
|
+
if (brightness !== void 0) image.brightness = brightness;
|
|
595
|
+
return image;
|
|
572
596
|
}
|
|
573
597
|
/**
|
|
574
598
|
* Coerce one persisted rule, or null when it names no image ANYWHERE.
|
|
@@ -593,38 +617,45 @@ window.__ModuleLoader__.load({
|
|
|
593
617
|
if (id === null) return null;
|
|
594
618
|
const color = normalizeHsl(r.color);
|
|
595
619
|
const mode = BG_MODES$1.includes(r.bgMode) ? r.bgMode : "fit";
|
|
620
|
+
const inherit = {
|
|
621
|
+
color,
|
|
622
|
+
opacity: clamp01(r.wallpaperOpacity, 1),
|
|
623
|
+
brightness: r.brightness === void 0 ? void 0 : clamp$1(r.brightness, 0, 200, 100),
|
|
624
|
+
blur: clamp$1(r.blur, 0, 60, 0)
|
|
625
|
+
};
|
|
596
626
|
const images = [];
|
|
597
627
|
const seen = /* @__PURE__ */ new Set();
|
|
598
628
|
const hadList = Array.isArray(r.images);
|
|
599
629
|
if (hadList) for (const entry of r.images) {
|
|
600
|
-
const image = normalizeImage(entry,
|
|
630
|
+
const image = normalizeImage(entry, inherit, mode);
|
|
601
631
|
if (image === null || seen.has(image.slot)) continue;
|
|
602
632
|
seen.add(image.slot);
|
|
603
633
|
images.push(image);
|
|
604
634
|
}
|
|
605
635
|
const legacySlot = typeof r.slot === "string" && SLOT_RE$1.test(r.slot) ? r.slot : null;
|
|
606
|
-
if (images.length === 0 && legacySlot !== null)
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
636
|
+
if (images.length === 0 && legacySlot !== null) {
|
|
637
|
+
const image = {
|
|
638
|
+
slot: legacySlot,
|
|
639
|
+
bgMode: mode,
|
|
640
|
+
bgState: normalizeBgState(r.bgState),
|
|
641
|
+
color,
|
|
642
|
+
opacity: inherit.opacity,
|
|
643
|
+
blur: inherit.blur
|
|
644
|
+
};
|
|
645
|
+
if (inherit.brightness !== void 0) image.brightness = inherit.brightness;
|
|
646
|
+
images.push(image);
|
|
647
|
+
}
|
|
612
648
|
if (images.length === 0 && !hadList) return null;
|
|
613
649
|
const rotate = normalizeRotation(r.rotate);
|
|
614
650
|
if (images.length < 2) rotate.enabled = false;
|
|
615
|
-
|
|
616
|
-
const rule = {
|
|
651
|
+
return {
|
|
617
652
|
id,
|
|
618
653
|
images,
|
|
619
654
|
match: typeof r.match === "string" ? r.match : "",
|
|
620
655
|
enabled: r.enabled !== false,
|
|
621
656
|
color,
|
|
622
|
-
wallpaperOpacity: clamp01(r.wallpaperOpacity, 1),
|
|
623
|
-
blur: clamp$1(r.blur, 0, 60, 0),
|
|
624
657
|
rotate
|
|
625
658
|
};
|
|
626
|
-
if (brightness !== void 0) rule.brightness = brightness;
|
|
627
|
-
return rule;
|
|
628
659
|
}
|
|
629
660
|
/**
|
|
630
661
|
* Coerce one persisted holiday entry.
|
|
@@ -641,17 +672,29 @@ window.__ModuleLoader__.load({
|
|
|
641
672
|
return {
|
|
642
673
|
...base,
|
|
643
674
|
enabled: r.enabled !== false,
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
675
|
+
images: base.images.map((image) => {
|
|
676
|
+
const stored = r.images?.[0];
|
|
677
|
+
const merged = {
|
|
678
|
+
slot: image.slot,
|
|
679
|
+
bgMode: image.bgMode,
|
|
680
|
+
bgState: normalizeBgState(stored?.bgState ?? r.bgState),
|
|
681
|
+
color: image.color,
|
|
682
|
+
opacity: clamp01(stored?.opacity ?? legacyWallpaperOpacity(r.wallpaperOpacity), image.opacity),
|
|
683
|
+
blur: clamp$1(stored?.blur ?? r.blur, 0, 60, image.blur)
|
|
684
|
+
};
|
|
685
|
+
const brightness = stored?.brightness === void 0 ? r.brightness === void 0 ? void 0 : clamp$1(r.brightness, 0, 200, 100) : clamp$1(stored.brightness, 0, 200, 100);
|
|
686
|
+
if (brightness !== void 0) merged.brightness = brightness;
|
|
687
|
+
return merged;
|
|
688
|
+
})
|
|
653
689
|
};
|
|
654
690
|
}
|
|
691
|
+
/**
|
|
692
|
+
* The rule-level opacity a schema-9 holiday entry carried, or `undefined` so the
|
|
693
|
+
* caller can tell "absent" from "set to 1" and leave `clamp01` its default.
|
|
694
|
+
*/
|
|
695
|
+
function legacyWallpaperOpacity(raw) {
|
|
696
|
+
return typeof raw === "number" && isFinite(raw) ? raw : void 0;
|
|
697
|
+
}
|
|
655
698
|
/** Coerce the whole holiday block; unknown ids are dropped, missing ones defaulted. */
|
|
656
699
|
function normalizeHolidays(raw) {
|
|
657
700
|
const r = raw ?? {};
|
|
@@ -892,24 +935,34 @@ window.__ModuleLoader__.load({
|
|
|
892
935
|
function rBgMode() {
|
|
893
936
|
return activeImage()?.bgMode ?? "fit";
|
|
894
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* Opacity of the wallpaper layer, 0..1 (per image, see BgImage).
|
|
940
|
+
*
|
|
941
|
+
* Read from the image being painted, not from the rule: one rule's rotation can go
|
|
942
|
+
* through a full-bleed photo and a faint decorative plate, and a rule-level answer
|
|
943
|
+
* would be wrong for one of them. A rule with no image paints nothing at all, so
|
|
944
|
+
* there is no layer for the value to describe.
|
|
945
|
+
*/
|
|
895
946
|
function rWop() {
|
|
896
|
-
return clamp01(
|
|
947
|
+
return clamp01(activeImage()?.opacity, 1);
|
|
897
948
|
}
|
|
898
949
|
/**
|
|
899
950
|
* Brightness of the wallpaper layer, in percent (100 = the picture exactly as the
|
|
900
951
|
* file has it).
|
|
901
952
|
*
|
|
902
|
-
* Read from the
|
|
903
|
-
* same way:
|
|
904
|
-
* the setting existed
|
|
905
|
-
*
|
|
906
|
-
* numbers of its own (see
|
|
953
|
+
* Read from the CURRENT image, like the opacity and blur beside it, and defaulted
|
|
954
|
+
* the same way: an image whose `brightness` key is absent — every config written
|
|
955
|
+
* before the setting existed, and every picture whose rule never named one — is
|
|
956
|
+
* left alone, so an upgrade changes nothing on screen. The default and the ceiling
|
|
957
|
+
* live in ../schema, so the render layer holds no numbers of its own (see
|
|
958
|
+
* ./wallpaper's `wpFilter`).
|
|
907
959
|
*/
|
|
908
960
|
function rBrightness() {
|
|
909
|
-
return clamp$1(
|
|
961
|
+
return clamp$1(activeImage()?.brightness, 0, 200, 100);
|
|
910
962
|
}
|
|
963
|
+
/** Blur of the CURRENT image, in px (0 = the picture as its file has it). */
|
|
911
964
|
function rBl() {
|
|
912
|
-
return clamp$1(
|
|
965
|
+
return clamp$1(activeImage()?.blur, 0, 60, 0);
|
|
913
966
|
}
|
|
914
967
|
/** Framing of the CURRENT image (per image, see BgImage). */
|
|
915
968
|
function rBgState() {
|
|
@@ -1064,7 +1117,7 @@ window.__ModuleLoader__.load({
|
|
|
1064
1117
|
const d = data;
|
|
1065
1118
|
const slots = Array.isArray(d.slots) ? d.slots.filter((s) => typeof s === "string") : [];
|
|
1066
1119
|
const schema = typeof d.schema === "number" && isFinite(d.schema) ? d.schema : null;
|
|
1067
|
-
hostShape = typeof schema === "number" && schema >=
|
|
1120
|
+
hostShape = typeof schema === "number" && schema >= 10 ? "current" : "legacy";
|
|
1068
1121
|
if (hostShape === "legacy") holdWrite();
|
|
1069
1122
|
return {
|
|
1070
1123
|
config: d.config,
|
|
@@ -1167,6 +1220,50 @@ window.__ModuleLoader__.load({
|
|
|
1167
1220
|
const parts = [d.provider, d.model].filter((s) => typeof s === "string" && s !== "");
|
|
1168
1221
|
return parts.length > 0 ? parts.join(" ") : null;
|
|
1169
1222
|
}
|
|
1223
|
+
/**
|
|
1224
|
+
* What the store occupies and what nothing points at — read-only.
|
|
1225
|
+
*
|
|
1226
|
+
* `keep` is this bundle's live slot list, which the node half unions with the config
|
|
1227
|
+
* on disk (see `storeUsage`): the two are written at different moments, and the
|
|
1228
|
+
* report must never call a file an orphan in the gap between an upload's bytes
|
|
1229
|
+
* landing and its rule entry arriving. Sending the list can therefore only SPARE a
|
|
1230
|
+
* slot, never condemn one.
|
|
1231
|
+
*/
|
|
1232
|
+
async function readStoreUsage(keep) {
|
|
1233
|
+
const data = await rpcCall("storeUsage", { keep });
|
|
1234
|
+
if (data === null || typeof data !== "object") return null;
|
|
1235
|
+
const d = data;
|
|
1236
|
+
return {
|
|
1237
|
+
totalBytes: numberOr(d.totalBytes, 0),
|
|
1238
|
+
usedBytes: numberOr(d.usedBytes, 0),
|
|
1239
|
+
orphanBytes: numberOr(d.orphanBytes, 0),
|
|
1240
|
+
files: numberOr(d.files, 0),
|
|
1241
|
+
slots: Array.isArray(d.slots) ? d.slots.filter((s) => typeof s === "string") : [],
|
|
1242
|
+
orphans: Array.isArray(d.orphans) ? d.orphans.filter((o) => o !== null && typeof o === "object" && typeof o.slot === "string").map((o) => ({
|
|
1243
|
+
slot: o.slot,
|
|
1244
|
+
bytes: numberOr(o.bytes, 0)
|
|
1245
|
+
})) : [],
|
|
1246
|
+
trusted: d.trusted === true
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Delete every stored image nothing points at. The one destructive half of the
|
|
1251
|
+
* pair, and it takes the same `keep` for the same reason.
|
|
1252
|
+
*/
|
|
1253
|
+
async function pruneOrphanImages(keep) {
|
|
1254
|
+
const data = await rpcCall("pruneOrphans", { keep });
|
|
1255
|
+
if (data === null || typeof data !== "object") return null;
|
|
1256
|
+
const d = data;
|
|
1257
|
+
return {
|
|
1258
|
+
removed: Array.isArray(d.removed) ? d.removed.filter((s) => typeof s === "string") : [],
|
|
1259
|
+
failed: Array.isArray(d.failed) ? d.failed.filter((s) => typeof s === "string") : [],
|
|
1260
|
+
freedBytes: numberOr(d.freedBytes, 0),
|
|
1261
|
+
refused: d.refused === true
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function numberOr(value, fallback) {
|
|
1265
|
+
return typeof value === "number" && isFinite(value) && value >= 0 ? value : fallback;
|
|
1266
|
+
}
|
|
1170
1267
|
//#endregion
|
|
1171
1268
|
//#region src/client/utils/color.ts
|
|
1172
1269
|
function hsvToHsl(h, s, v) {
|
|
@@ -1927,6 +2024,13 @@ window.__ModuleLoader__.load({
|
|
|
1927
2024
|
.dab-profile-ico{width:38px;height:38px;border-radius:11px;display:grid;place-items:center;margin-bottom:13px;color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
|
|
1928
2025
|
.dab-profile-title{font-size:14px;font-weight:650}
|
|
1929
2026
|
.dab-profile-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;margin:5px 0 15px}
|
|
2027
|
+
/* The image store's three numbers. A row of VALUES with a caption under each,
|
|
2028
|
+
not a sentence: they are read as a measurement ("how much is orphaned"), and a
|
|
2029
|
+
sentence makes the reader hunt for the one number that matters. */
|
|
2030
|
+
.dab-store-stats{display:flex;gap:16px;margin:2px 0 14px}
|
|
2031
|
+
.dab-store-stat{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
2032
|
+
.dab-store-stat-v{font-size:14px;font-weight:650;font-family:var(--dab-mono);letter-spacing:.01em;color:var(--dsw-alias-label-primary)}
|
|
2033
|
+
.dab-store-stat-k{font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}
|
|
1930
2034
|
.dab-footer{margin-top:6px;padding:14px 4px 0;border-top:1px solid var(--dsw-alias-border-l2);display:flex;align-items:center;justify-content:space-between;font-size:11px;color:var(--dsw-alias-label-tertiary)}
|
|
1931
2035
|
.dab-footer-mono{font-family:var(--dab-mono);letter-spacing:.02em}
|
|
1932
2036
|
/* Same reset as the brand: the package name links to the repository, and the
|
|
@@ -3146,13 +3250,25 @@ window.__ModuleLoader__.load({
|
|
|
3146
3250
|
/** Index of the layer fading in right now, if any. */
|
|
3147
3251
|
let pending = null;
|
|
3148
3252
|
let fadeTimer = null;
|
|
3253
|
+
/** The wallpaper's steady opacity — the ACTIVE picture's, as last committed.
|
|
3254
|
+
*
|
|
3255
|
+
* Mirrored rather than read back off the style string, because a repaint that
|
|
3256
|
+
* runs in the middle of a fade (a theme re-registration, a panel re-render) has
|
|
3257
|
+
* to re-write the value the fade is HEADING for; reading `rWop()` there would
|
|
3258
|
+
* snap the whole layer to the incoming picture's alpha and cut the fade short.
|
|
3259
|
+
*/
|
|
3260
|
+
let wpAlpha = 1;
|
|
3149
3261
|
function createLayer() {
|
|
3150
3262
|
const el = document.createElement("div");
|
|
3151
3263
|
el.dataset.dabWpLayer = "";
|
|
3152
3264
|
el.style.cssText = "position:absolute;inset:0;opacity:0;background-repeat:no-repeat;background-position:center;";
|
|
3153
3265
|
return {
|
|
3154
3266
|
el,
|
|
3155
|
-
url: null
|
|
3267
|
+
url: null,
|
|
3268
|
+
posX: null,
|
|
3269
|
+
posY: null,
|
|
3270
|
+
brightness: 100,
|
|
3271
|
+
blur: 0
|
|
3156
3272
|
};
|
|
3157
3273
|
}
|
|
3158
3274
|
function ensureWpContainer() {
|
|
@@ -3170,6 +3286,7 @@ window.__ModuleLoader__.load({
|
|
|
3170
3286
|
layers = [a, b];
|
|
3171
3287
|
front = 0;
|
|
3172
3288
|
pending = null;
|
|
3289
|
+
wpAlpha = 1;
|
|
3173
3290
|
document.body.prepend(wpEl);
|
|
3174
3291
|
}
|
|
3175
3292
|
/** Drop the whole wallpaper layer (no rule carries an image). */
|
|
@@ -3181,6 +3298,7 @@ window.__ModuleLoader__.load({
|
|
|
3181
3298
|
pending = null;
|
|
3182
3299
|
front = 0;
|
|
3183
3300
|
layers = null;
|
|
3301
|
+
wpAlpha = 1;
|
|
3184
3302
|
wpEl?.remove();
|
|
3185
3303
|
wpEl = null;
|
|
3186
3304
|
}
|
|
@@ -3188,9 +3306,14 @@ window.__ModuleLoader__.load({
|
|
|
3188
3306
|
function clearLayer(l) {
|
|
3189
3307
|
if (l.url === null) return;
|
|
3190
3308
|
l.url = null;
|
|
3309
|
+
l.posX = null;
|
|
3310
|
+
l.posY = null;
|
|
3311
|
+
l.brightness = 100;
|
|
3312
|
+
l.blur = 0;
|
|
3191
3313
|
l.el.style.backgroundImage = "";
|
|
3192
3314
|
l.el.style.backgroundSize = "";
|
|
3193
3315
|
l.el.style.backgroundPosition = "";
|
|
3316
|
+
l.el.style.inset = "0px";
|
|
3194
3317
|
l.el.style.transform = "none";
|
|
3195
3318
|
}
|
|
3196
3319
|
/** Write the two properties a switch animates. Both are always written, so a
|
|
@@ -3218,6 +3341,34 @@ window.__ModuleLoader__.load({
|
|
|
3218
3341
|
img.onerror = () => cb(0, 0);
|
|
3219
3342
|
img.src = url;
|
|
3220
3343
|
}
|
|
3344
|
+
/** How far a layer's box is grown on every side to hide a blurred edge.
|
|
3345
|
+
*
|
|
3346
|
+
* `filter: blur(σ)` convolves the layer's own rendering with a Gaussian of
|
|
3347
|
+
* standard deviation σ and treats everything outside the box as transparent, so
|
|
3348
|
+
* the outermost pixels come out half opaque and the image fades inward over
|
|
3349
|
+
* ~2σ — composited over the page behind it, that half-transparent rim is the
|
|
3350
|
+
* black border the blur slider paints, and it grows thicker with the slider.
|
|
3351
|
+
* Growing the box by 2σ puts every visible pixel at least 2σ inside the edge,
|
|
3352
|
+
* where ~2% of the page still shows through the outermost row: gone to the eye.
|
|
3353
|
+
* Two sigma rather than three is the trade for the `cover` modes, which pay for
|
|
3354
|
+
* the growth by framing slightly tighter — a fifth of the image at most, at the
|
|
3355
|
+
* slider's maximum, where the detail a wider frame would reveal is blurred out.
|
|
3356
|
+
*/
|
|
3357
|
+
function wpBlurPad(blur) {
|
|
3358
|
+
return blur > 0 ? Math.ceil(blur * 2) + 2 : 0;
|
|
3359
|
+
}
|
|
3360
|
+
/** Grow a layer's box by `pad` on every side while keeping its image on the same
|
|
3361
|
+
* pixels. The background positioning area starts at the border box, so a box
|
|
3362
|
+
* whose origin moved by -pad needs a numeric placement moved by +pad; `center`
|
|
3363
|
+
* (and the box-relative `cover` / `100% 100%` sizes) sit in the middle of a
|
|
3364
|
+
* symmetrically grown box and need no compensation. The container clips the
|
|
3365
|
+
* growth, so nothing about the visible layout changes: it exists only to give
|
|
3366
|
+
* the blur pixels to sample beyond the viewport. */
|
|
3367
|
+
function writeLayerBox(l, pad) {
|
|
3368
|
+
const { posX, posY } = l;
|
|
3369
|
+
l.el.style.inset = `${-pad}px`;
|
|
3370
|
+
l.el.style.backgroundPosition = posX === null || posY === null ? "center" : `${posX + pad}px ${posY + pad}px`;
|
|
3371
|
+
}
|
|
3221
3372
|
/** Paint one layer's image and its placement. The placement always follows the
|
|
3222
3373
|
* ACTIVE rule: the incoming layer is painted right after a switch, and a
|
|
3223
3374
|
* repaint of the same URL (framing edit, viewport resize) must use the rule's
|
|
@@ -3229,6 +3380,8 @@ window.__ModuleLoader__.load({
|
|
|
3229
3380
|
const mode = rBgMode();
|
|
3230
3381
|
const next = `url("${url}")`;
|
|
3231
3382
|
if (el.style.backgroundImage !== next) el.style.backgroundImage = next;
|
|
3383
|
+
l.posX = null;
|
|
3384
|
+
l.posY = null;
|
|
3232
3385
|
if (mode === "fit") {
|
|
3233
3386
|
el.style.backgroundRepeat = "no-repeat";
|
|
3234
3387
|
if (bg.iw > 0) {
|
|
@@ -3236,35 +3389,31 @@ window.__ModuleLoader__.load({
|
|
|
3236
3389
|
const w = bg.iw * fit * bg.zoom;
|
|
3237
3390
|
const h = bg.ih * fit * bg.zoom;
|
|
3238
3391
|
el.style.backgroundSize = `${w}px ${h}px`;
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
el.style.backgroundPosition = "center";
|
|
3243
|
-
}
|
|
3392
|
+
l.posX = bg.x * window.innerWidth - w / 2;
|
|
3393
|
+
l.posY = bg.y * window.innerHeight - h / 2;
|
|
3394
|
+
} else el.style.backgroundSize = "contain";
|
|
3244
3395
|
} else if (mode === "fill") {
|
|
3245
3396
|
el.style.backgroundRepeat = "no-repeat";
|
|
3246
3397
|
el.style.backgroundSize = "cover";
|
|
3247
|
-
el.style.backgroundPosition = "center";
|
|
3248
3398
|
} else if (mode === "stretch") {
|
|
3249
3399
|
el.style.backgroundRepeat = "no-repeat";
|
|
3250
3400
|
el.style.backgroundSize = "100% 100%";
|
|
3251
|
-
el.style.backgroundPosition = "center";
|
|
3252
3401
|
} else if (mode === "tile") {
|
|
3253
3402
|
el.style.backgroundRepeat = "repeat";
|
|
3254
3403
|
el.style.backgroundSize = "auto";
|
|
3255
|
-
|
|
3404
|
+
l.posX = 0;
|
|
3405
|
+
l.posY = 0;
|
|
3256
3406
|
} else {
|
|
3257
3407
|
el.style.backgroundRepeat = "no-repeat";
|
|
3258
3408
|
el.style.backgroundSize = "contain";
|
|
3259
|
-
el.style.backgroundPosition = "center";
|
|
3260
3409
|
imageNatSize(url, (w, h) => {
|
|
3261
3410
|
if (l.url !== url || el.style.backgroundImage !== next || rBgMode() !== "center") return;
|
|
3262
|
-
if (w > 0 && h > 0) {
|
|
3263
|
-
el.style.backgroundSize = `${w}px ${h}px`;
|
|
3264
|
-
el.style.backgroundPosition = "center";
|
|
3265
|
-
}
|
|
3411
|
+
if (w > 0 && h > 0) el.style.backgroundSize = `${w}px ${h}px`;
|
|
3266
3412
|
});
|
|
3267
3413
|
}
|
|
3414
|
+
l.brightness = rBrightness();
|
|
3415
|
+
l.blur = rBl();
|
|
3416
|
+
applyLayerEffects(l);
|
|
3268
3417
|
}
|
|
3269
3418
|
/** Whether there is anything to animate FROM: a wallpaper on screen that is
|
|
3270
3419
|
* actually visible. The duration question belongs to `rFadeMs`, and the
|
|
@@ -3288,6 +3437,10 @@ window.__ModuleLoader__.load({
|
|
|
3288
3437
|
}
|
|
3289
3438
|
pending = null;
|
|
3290
3439
|
front = idx;
|
|
3440
|
+
if (wpEl !== null) {
|
|
3441
|
+
wpEl.style.transition = "none";
|
|
3442
|
+
wpEl.style.opacity = String(wpAlpha);
|
|
3443
|
+
}
|
|
3291
3444
|
const el = layers[idx].el;
|
|
3292
3445
|
el.style.opacity = "1";
|
|
3293
3446
|
el.style.transform = "none";
|
|
@@ -3334,12 +3487,14 @@ window.__ModuleLoader__.load({
|
|
|
3334
3487
|
if (pending !== null) {
|
|
3335
3488
|
if (ls[pending].url === url) {
|
|
3336
3489
|
paintLayer(ls[pending], url);
|
|
3490
|
+
wpAlpha = rWop();
|
|
3337
3491
|
applyWpEffects();
|
|
3338
3492
|
return;
|
|
3339
3493
|
}
|
|
3340
3494
|
if (ls[front].url === url) {
|
|
3341
3495
|
cancelFade();
|
|
3342
3496
|
paintLayer(ls[front], url);
|
|
3497
|
+
wpAlpha = rWop();
|
|
3343
3498
|
applyWpEffects();
|
|
3344
3499
|
return;
|
|
3345
3500
|
}
|
|
@@ -3347,6 +3502,7 @@ window.__ModuleLoader__.load({
|
|
|
3347
3502
|
}
|
|
3348
3503
|
if (ls[front].url === url) {
|
|
3349
3504
|
paintLayer(ls[front], url);
|
|
3505
|
+
wpAlpha = rWop();
|
|
3350
3506
|
applyWpEffects();
|
|
3351
3507
|
return;
|
|
3352
3508
|
}
|
|
@@ -3367,7 +3523,11 @@ window.__ModuleLoader__.load({
|
|
|
3367
3523
|
applyLayerStyle(el, plan.from);
|
|
3368
3524
|
el.style.zIndex = "1";
|
|
3369
3525
|
from.el.style.zIndex = "0";
|
|
3526
|
+
const toAlpha = rWop();
|
|
3527
|
+
const fadeAlpha = plan.animate && Math.abs(toAlpha - wpAlpha) > .001;
|
|
3528
|
+
if (fadeAlpha) wpEl.style.transition = "none";
|
|
3370
3529
|
paintLayer(target, url);
|
|
3530
|
+
if (!fadeAlpha) wpAlpha = toAlpha;
|
|
3371
3531
|
applyWpEffects();
|
|
3372
3532
|
if (!plan.animate) {
|
|
3373
3533
|
applyLayerStyle(el, plan.to);
|
|
@@ -3380,15 +3540,24 @@ window.__ModuleLoader__.load({
|
|
|
3380
3540
|
if (pending !== idx || layers === null) return;
|
|
3381
3541
|
el.style.transition = plan.transition;
|
|
3382
3542
|
applyLayerStyle(el, plan.to);
|
|
3543
|
+
if (fadeAlpha) {
|
|
3544
|
+
wpEl.style.transition = plan.transition;
|
|
3545
|
+
wpEl.style.opacity = String(toAlpha);
|
|
3546
|
+
wpAlpha = toAlpha;
|
|
3547
|
+
}
|
|
3383
3548
|
});
|
|
3384
3549
|
fadeTimer = window.setTimeout(() => finishFade(idx), plan.durationMs + 160);
|
|
3385
3550
|
}
|
|
3386
3551
|
/**
|
|
3387
|
-
* The `filter`
|
|
3388
|
-
*
|
|
3552
|
+
* The `filter` ONE LAYER carries, from the brightness and blur of the picture
|
|
3553
|
+
* painted on IT.
|
|
3554
|
+
*
|
|
3555
|
+
* Both are per image (see BgImage): one rule's rotation can run a daylight photo
|
|
3556
|
+
* and a night shot, and a rule-level answer forced the second one to live at the
|
|
3557
|
+
* first one's exposure.
|
|
3389
3558
|
*
|
|
3390
3559
|
* ONE declaration, because the two are one CSS property: assigning `filter` twice
|
|
3391
|
-
* would leave whichever ran last, so
|
|
3560
|
+
* would leave whichever ran last, so an image that is both brightened and blurred
|
|
3392
3561
|
* would silently lose one of them. Brightness is written first and blur second for
|
|
3393
3562
|
* a reason that is visible: blurring a dimmed picture softens the highlights the
|
|
3394
3563
|
* dimming just brought down, while dimming an already-blurred one simply darkens
|
|
@@ -3398,21 +3567,34 @@ window.__ModuleLoader__.load({
|
|
|
3398
3567
|
* The neutral brightness is LEFT OUT rather than written as `brightness(100%)`.
|
|
3399
3568
|
* A layer with neither effect gets `''`, which is what an untouched wallpaper has
|
|
3400
3569
|
* always carried, and keeps the layer off the compositor's filter path entirely.
|
|
3570
|
+
*
|
|
3571
|
+
* The numbers are the LAYER's, not live reads: during a switch the two layers
|
|
3572
|
+
* hold two different photos, and a live read made the outgoing one jump to the
|
|
3573
|
+
* incoming one's exposure at the first frame of every fade.
|
|
3401
3574
|
*/
|
|
3402
|
-
function wpFilter() {
|
|
3575
|
+
function wpFilter(brightness, blur) {
|
|
3403
3576
|
const parts = [];
|
|
3404
|
-
const brightness = rBrightness();
|
|
3405
3577
|
if (brightness !== 100) parts.push(`brightness(${brightness}%)`);
|
|
3406
|
-
const blur = rBl();
|
|
3407
3578
|
if (blur > 0) parts.push(`blur(${blur}px)`);
|
|
3408
3579
|
return parts.join(" ");
|
|
3409
3580
|
}
|
|
3410
3581
|
function applyWpEffects() {
|
|
3411
3582
|
if (wpEl === null || layers === null) return;
|
|
3412
|
-
wpEl.style.opacity = String(
|
|
3413
|
-
|
|
3414
|
-
layers[
|
|
3415
|
-
|
|
3583
|
+
wpEl.style.opacity = String(wpAlpha);
|
|
3584
|
+
applyLayerEffects(layers[0]);
|
|
3585
|
+
applyLayerEffects(layers[1]);
|
|
3586
|
+
}
|
|
3587
|
+
/** Write what a layer takes from ITS picture: the filter, and the box growth that
|
|
3588
|
+
* keeps a blurred edge from drawing a dark rim.
|
|
3589
|
+
*
|
|
3590
|
+
* ONE function for both, because the two are one visible decision — the same
|
|
3591
|
+
* blur that makes `filter` non-empty is what makes the pad non-zero — and
|
|
3592
|
+
* because BOTH update paths (a full repaint and a drag frame) write it, so a
|
|
3593
|
+
* blur dragged to its top cannot leave the rim behind until the slider is
|
|
3594
|
+
* released. */
|
|
3595
|
+
function applyLayerEffects(l) {
|
|
3596
|
+
l.el.style.filter = wpFilter(l.brightness, l.blur);
|
|
3597
|
+
writeLayerBox(l, wpBlurPad(l.blur));
|
|
3416
3598
|
}
|
|
3417
3599
|
/** Repaint the wallpaper layer, the token palette and every interface part from
|
|
3418
3600
|
* the ACTIVE rule. This is the single entry point a rule switch goes through. */
|
|
@@ -3461,6 +3643,38 @@ window.__ModuleLoader__.load({
|
|
|
3461
3643
|
tableFixStyleEl = null;
|
|
3462
3644
|
stopWatchingParts();
|
|
3463
3645
|
}
|
|
3646
|
+
/** Live wallpaper-opacity updates during slider drag (no full re-apply).
|
|
3647
|
+
*
|
|
3648
|
+
* The container is the one holder of this value (see `applyWpEffects`), so the
|
|
3649
|
+
* mirror and the style are written together — a later repaint then re-asserts
|
|
3650
|
+
* what the drag ended on rather than the value it started from. */
|
|
3651
|
+
function setWpOpacity(v) {
|
|
3652
|
+
wpAlpha = v;
|
|
3653
|
+
if (wpEl) wpEl.style.opacity = String(v);
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Live wallpaper-filter updates during a slide, with no full re-apply.
|
|
3657
|
+
*
|
|
3658
|
+
* Deliberately parameterless: the filter composes TWO settings (brightness and
|
|
3659
|
+
* blur), so a function trusting one caller's value would write half of it and
|
|
3660
|
+
* drop the other. Both are read back from the store instead (./state), which is
|
|
3661
|
+
* the same store the drag has already written to before calling this.
|
|
3662
|
+
*
|
|
3663
|
+
* Written onto the layer PAINTING that picture rather than onto both: during a
|
|
3664
|
+
* switch the other layer is still showing the previous photo, and giving it this
|
|
3665
|
+
* one's brightness and blur is exactly the snap this snapshot exists to prevent.
|
|
3666
|
+
*/
|
|
3667
|
+
function setWpFilter() {
|
|
3668
|
+
if (layers === null) return;
|
|
3669
|
+
const url = rWp();
|
|
3670
|
+
if (url === null) return;
|
|
3671
|
+
for (const l of layers) {
|
|
3672
|
+
if (l.url !== url) continue;
|
|
3673
|
+
l.brightness = rBrightness();
|
|
3674
|
+
l.blur = rBl();
|
|
3675
|
+
applyLayerEffects(l);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3464
3678
|
//#endregion
|
|
3465
3679
|
//#region src/client/cadence.ts
|
|
3466
3680
|
/**
|
|
@@ -4248,6 +4462,11 @@ window.__ModuleLoader__.load({
|
|
|
4248
4462
|
className,
|
|
4249
4463
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M7.2 1.8l1.2 3.1 3.1 1.2-3.1 1.2-1.2 3.1-1.2-3.1L2.9 6.1 6 4.9l1.2-3.1z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 10.5l.5 1.2 1.2.5-1.2.5-.5 1.2-.5-1.2-1.2-.5 1.2-.5.5-1.2z" })]
|
|
4250
4464
|
});
|
|
4465
|
+
const RefreshIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
|
|
4466
|
+
size,
|
|
4467
|
+
className,
|
|
4468
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.2 8A5.2 5.2 0 1 1 11.5 4.2" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.4 1.8v2.9h-2.9" })]
|
|
4469
|
+
});
|
|
4251
4470
|
const DownloadIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
|
|
4252
4471
|
size,
|
|
4253
4472
|
className,
|
|
@@ -4470,7 +4689,7 @@ window.__ModuleLoader__.load({
|
|
|
4470
4689
|
if (url !== null) images[slot] = url;
|
|
4471
4690
|
}
|
|
4472
4691
|
return {
|
|
4473
|
-
version:
|
|
4692
|
+
version: 10,
|
|
4474
4693
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4475
4694
|
config: cfg,
|
|
4476
4695
|
images
|
|
@@ -4509,7 +4728,7 @@ window.__ModuleLoader__.load({
|
|
|
4509
4728
|
* "replace everything with this" literally true.
|
|
4510
4729
|
*/
|
|
4511
4730
|
async function applyThemeFile(host, data, wipeFirst) {
|
|
4512
|
-
if (typeof data.version === "number" && data.version >
|
|
4731
|
+
if (typeof data.version === "number" && data.version > 10) return false;
|
|
4513
4732
|
if (typeof data.config !== "object" || data.config === null) return false;
|
|
4514
4733
|
if (wipeFirst) {
|
|
4515
4734
|
await resetStore();
|
|
@@ -4697,6 +4916,7 @@ window.__ModuleLoader__.load({
|
|
|
4697
4916
|
ruleBrightness: "图片亮度",
|
|
4698
4917
|
ruleBlur: "图片模糊",
|
|
4699
4918
|
ruleEffectPick: "调节项",
|
|
4919
|
+
ruleEffectNoImage: "添加图片后可分别设置每张图的透明度、亮度与模糊",
|
|
4700
4920
|
rotTitle: "多图轮换",
|
|
4701
4921
|
rotEnable: "轮换这组图",
|
|
4702
4922
|
rotNeedTwo: "至少两张图才能轮换",
|
|
@@ -4767,6 +4987,18 @@ window.__ModuleLoader__.load({
|
|
|
4767
4987
|
recFailTooLarge: "这份推荐配置体积过大,未做任何改动",
|
|
4768
4988
|
recFailImage: "这张壁纸没拿到(素材库里缺这个文件):",
|
|
4769
4989
|
recFailApply: "未能写入推荐配置,你原来的配置没有被改动",
|
|
4990
|
+
storeCardTitle: "图片存储",
|
|
4991
|
+
storeCardDesc: "因意外导致的孤儿图片这里可以清掉",
|
|
4992
|
+
storeOrphans: "可清理",
|
|
4993
|
+
storeFiles: "个可删文件",
|
|
4994
|
+
storeReading: "正在读取…",
|
|
4995
|
+
storeUnknown: "读不到存储信息(宿主进程可能是旧版插件,重启 DSH 后可用)",
|
|
4996
|
+
storeCleanNone: "没有多余的文件",
|
|
4997
|
+
storeClean: "清理",
|
|
4998
|
+
storeDoneFreed: "已清理,释放",
|
|
4999
|
+
storePartial: "有些文件没能删除:",
|
|
5000
|
+
storeRefused: "配置文件读不出来(theme-config.json 解析失败),现在分不清哪些图片还在用,所以没有删除任何东西。修好或删掉那个文件后重启 DSH 再试",
|
|
5001
|
+
storeRefresh: "重新读取",
|
|
4770
5002
|
footerTag: "按模型换背景插件",
|
|
4771
5003
|
repoLink: "在 GitHub 上打开仓库",
|
|
4772
5004
|
holidayTitle: "节日特殊背景",
|
|
@@ -4868,6 +5100,7 @@ window.__ModuleLoader__.load({
|
|
|
4868
5100
|
ruleBrightness: "Image brightness",
|
|
4869
5101
|
ruleBlur: "Image blur",
|
|
4870
5102
|
ruleEffectPick: "Effect",
|
|
5103
|
+
ruleEffectNoImage: "Add an image to give each one its own opacity, brightness and blur",
|
|
4871
5104
|
rotTitle: "Image rotation",
|
|
4872
5105
|
rotEnable: "Rotate these images",
|
|
4873
5106
|
rotNeedTwo: "At least two images are needed to rotate",
|
|
@@ -4938,6 +5171,18 @@ window.__ModuleLoader__.load({
|
|
|
4938
5171
|
recFailTooLarge: "The recommended profile is too large. Nothing was changed.",
|
|
4939
5172
|
recFailImage: "Could not fetch this wallpaper — the asset host has no such file:",
|
|
4940
5173
|
recFailApply: "Could not write the recommended profile — your own is untouched",
|
|
5174
|
+
storeCardTitle: "Image storage",
|
|
5175
|
+
storeCardDesc: "Orphaned image files left behind by an accident can be cleared here",
|
|
5176
|
+
storeOrphans: "Cleanable",
|
|
5177
|
+
storeFiles: "deletable files",
|
|
5178
|
+
storeReading: "Reading…",
|
|
5179
|
+
storeUnknown: "Could not read the store — the host process may be an older build; restart DSH",
|
|
5180
|
+
storeCleanNone: "Nothing to clean up",
|
|
5181
|
+
storeClean: "Clean up",
|
|
5182
|
+
storeDoneFreed: "Cleaned up, freed",
|
|
5183
|
+
storePartial: "Some files could not be deleted: ",
|
|
5184
|
+
storeRefused: "The config file could not be read (theme-config.json does not parse), so nothing on disk can be told apart from an orphan. Nothing was deleted — fix or remove that file and restart DSH",
|
|
5185
|
+
storeRefresh: "Refresh",
|
|
4941
5186
|
footerTag: "Per-model wallpaper plugin",
|
|
4942
5187
|
repoLink: "Open the repository on GitHub",
|
|
4943
5188
|
holidayTitle: "Holiday backgrounds",
|
|
@@ -5142,37 +5387,80 @@ window.__ModuleLoader__.load({
|
|
|
5142
5387
|
//#region src/client/components/LiveSlider.tsx
|
|
5143
5388
|
/**
|
|
5144
5389
|
* Zero-lag slider: the thumb and value label update through DOM refs while
|
|
5145
|
-
* dragging
|
|
5146
|
-
*
|
|
5147
|
-
*
|
|
5148
|
-
*
|
|
5390
|
+
* dragging, so the caller can preview the change without a React re-render; the
|
|
5391
|
+
* settled value is committed ONCE, when the gesture ends.
|
|
5392
|
+
*
|
|
5393
|
+
* The split is the whole point and it is not an optimisation detail. A range
|
|
5394
|
+
* input fires `input` on every frame of a drag, and React's `change` handler for
|
|
5395
|
+
* one is wired to that same event — so a slider that commits from `onChange`
|
|
5396
|
+
* commits dozens of times a second, and every commit is a full repaint (skin,
|
|
5397
|
+
* palette, wallpaper layer). That is why dragging a wallpaper effect cost a whole
|
|
5398
|
+
* `applyActive()` per frame while the cheap live writers beside it were never
|
|
5399
|
+
* reached.
|
|
5400
|
+
*
|
|
5401
|
+
* The gesture's end is therefore read from the events that actually end one: the
|
|
5402
|
+
* native `change` event, a pointer released over the track, and a blur. No timer
|
|
5403
|
+
* is armed — nothing here may compete with the plugin's one scheduler.
|
|
5149
5404
|
*/
|
|
5150
5405
|
function LiveSlider({ min, max, step, def, fmt, label, ariaLabel, onInput, onChange }) {
|
|
5151
5406
|
const inputRef = (0, react.useRef)(null);
|
|
5152
5407
|
const valRef = (0, react.useRef)(null);
|
|
5408
|
+
/**
|
|
5409
|
+
* The value the current gesture has reached but has not committed yet.
|
|
5410
|
+
*
|
|
5411
|
+
* Non-null is also the answer to "is a gesture in flight?", which the `def` sync
|
|
5412
|
+
* below has to ask: the model watch re-emits on its own beat, and the render that
|
|
5413
|
+
* causes would otherwise snap the knob back to the committed value mid-drag.
|
|
5414
|
+
*/
|
|
5415
|
+
const pending = (0, react.useRef)(null);
|
|
5416
|
+
/** `onChange` is read through a ref so the native listener cannot go stale. */
|
|
5417
|
+
const commitRef = (0, react.useRef)(onChange);
|
|
5418
|
+
commitRef.current = onChange;
|
|
5153
5419
|
const paint = (el, v) => {
|
|
5154
5420
|
el.style.setProperty("--pct", String((v - min) / (max - min) * 100));
|
|
5155
5421
|
};
|
|
5156
|
-
|
|
5422
|
+
/** Move the knob and the label without calling anybody back. */
|
|
5423
|
+
const show = (v) => {
|
|
5157
5424
|
if (inputRef.current) {
|
|
5158
|
-
inputRef.current.value = String(
|
|
5159
|
-
paint(inputRef.current,
|
|
5425
|
+
inputRef.current.value = String(v);
|
|
5426
|
+
paint(inputRef.current, v);
|
|
5160
5427
|
}
|
|
5161
|
-
if (valRef.current) valRef.current.textContent = fmt(
|
|
5428
|
+
if (valRef.current) valRef.current.textContent = fmt(v);
|
|
5429
|
+
};
|
|
5430
|
+
const commit = () => {
|
|
5431
|
+
const v = pending.current;
|
|
5432
|
+
if (v === null) return;
|
|
5433
|
+
pending.current = null;
|
|
5434
|
+
commitRef.current(v);
|
|
5435
|
+
};
|
|
5436
|
+
/** One frame of a gesture: the caller's cheap path, then remember the value. */
|
|
5437
|
+
const live = (v) => {
|
|
5438
|
+
show(v);
|
|
5439
|
+
pending.current = v;
|
|
5440
|
+
onInput?.(v);
|
|
5441
|
+
};
|
|
5442
|
+
(0, react.useEffect)(() => {
|
|
5443
|
+
if (pending.current !== null) return;
|
|
5444
|
+
show(def);
|
|
5162
5445
|
}, [
|
|
5163
5446
|
def,
|
|
5164
5447
|
fmt,
|
|
5165
5448
|
min,
|
|
5166
5449
|
max
|
|
5167
5450
|
]);
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5451
|
+
(0, react.useEffect)(() => {
|
|
5452
|
+
const el = inputRef.current;
|
|
5453
|
+
if (el === null) return;
|
|
5454
|
+
const onEnd = () => commit();
|
|
5455
|
+
el.addEventListener("change", onEnd);
|
|
5456
|
+
return () => {
|
|
5457
|
+
el.removeEventListener("change", onEnd);
|
|
5458
|
+
commit();
|
|
5459
|
+
};
|
|
5460
|
+
}, []);
|
|
5461
|
+
const reset = () => {
|
|
5462
|
+
live(def);
|
|
5463
|
+
commit();
|
|
5176
5464
|
};
|
|
5177
5465
|
const name = label ?? ariaLabel;
|
|
5178
5466
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5197,15 +5485,10 @@ window.__ModuleLoader__.load({
|
|
|
5197
5485
|
defaultValue: def,
|
|
5198
5486
|
"aria-label": name,
|
|
5199
5487
|
title: name !== void 0 ? `${name} · ${fmt(def)}` : fmt(def),
|
|
5200
|
-
onDoubleClick:
|
|
5201
|
-
onInput: (e) =>
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
paint(el, v);
|
|
5205
|
-
onInput?.(v);
|
|
5206
|
-
if (valRef.current) valRef.current.textContent = fmt(v);
|
|
5207
|
-
},
|
|
5208
|
-
onChange: (e) => onChange(Number(e.target.value))
|
|
5488
|
+
onDoubleClick: reset,
|
|
5489
|
+
onInput: (e) => live(Number(e.target.value)),
|
|
5490
|
+
onPointerUp: commit,
|
|
5491
|
+
onBlur: commit
|
|
5209
5492
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5210
5493
|
ref: valRef,
|
|
5211
5494
|
className: "dab-slider-val",
|
|
@@ -6467,49 +6750,92 @@ window.__ModuleLoader__.load({
|
|
|
6467
6750
|
});
|
|
6468
6751
|
}
|
|
6469
6752
|
/**
|
|
6470
|
-
* The
|
|
6471
|
-
* single track edits it.
|
|
6753
|
+
* The selected image's three paint effects on ONE row: the menu names the target
|
|
6754
|
+
* and a single track edits it.
|
|
6755
|
+
*
|
|
6756
|
+
* Per IMAGE, like the layout mode, the framing and the colour above it. The move
|
|
6757
|
+
* is the same argument: a rotation through a daylight photo and a night shot does
|
|
6758
|
+
* not want one exposure, and a rule-level value forced one of them to live at the
|
|
6759
|
+
* other's compromise. A rule with no picture has nothing to expose, so the row is
|
|
6760
|
+
* DISABLED (not hidden) there — the same way the layout chips behave.
|
|
6472
6761
|
*
|
|
6473
6762
|
* The three ranges are not interchangeable — opacity and brightness are
|
|
6474
6763
|
* percentages (brightness ceilings at the shared `WALLPAPER_BRIGHTNESS_MAX`),
|
|
6475
6764
|
* blur is pixels — so changing the menu swaps the whole track: limits, formatter
|
|
6476
6765
|
* and writer together. It never reinterprets one track under another name. The
|
|
6477
|
-
* `key` remounts the input so `defaultValue` starts at the new
|
|
6478
|
-
*
|
|
6479
|
-
*
|
|
6766
|
+
* `key` remounts the input so `defaultValue` starts at the new target's value.
|
|
6767
|
+
*
|
|
6768
|
+
* A drag has TWO writers, and which one runs is the whole reason this row is not
|
|
6769
|
+
* a one-liner. `onInput` runs on every frame and must cost a wallpaper-layer
|
|
6770
|
+
* property write (./wallpaper's `setWpOpacity` / `setWpFilter`) — the same cheap
|
|
6771
|
+
* path the Interface page uses. `onChange` runs once, when the gesture ends, and
|
|
6772
|
+
* is the one that goes through `setImageEffects`: that is a full repaint (skin,
|
|
6773
|
+
* palette, wallpaper), and paying it per frame made a drag stall on a big image.
|
|
6480
6774
|
*/
|
|
6481
|
-
function WallpaperEffectRow({ p, rule, field, onField }) {
|
|
6775
|
+
function WallpaperEffectRow({ p, rule, image, painted, field, onField, active }) {
|
|
6482
6776
|
const { t } = p;
|
|
6777
|
+
/**
|
|
6778
|
+
* One frame of a drag: write the image in place, then touch only the layer
|
|
6779
|
+
* property that field owns.
|
|
6780
|
+
*
|
|
6781
|
+
* Gated on `active && painted`: the wallpaper layer paints the ACTIVE rule's
|
|
6782
|
+
* CURRENT image, so previewing any other one would write this card's numbers onto
|
|
6783
|
+
* a different picture's pixels — the visible lie the per-image split makes
|
|
6784
|
+
* possible and this gate prevents. The commit still lands, addressed by slot.
|
|
6785
|
+
*
|
|
6786
|
+
* Writing in place rather than through `setImageEffects` is deliberate and narrow:
|
|
6787
|
+
* that one bumps `rulesRev` and closes with a repaint, which is exactly the
|
|
6788
|
+
* per-frame cost being removed. Nothing is lost — the gesture's `onChange` hands
|
|
6789
|
+
* the settled value over, and that path clamps and saves.
|
|
6790
|
+
*/
|
|
6791
|
+
const preview = (patch, paint) => {
|
|
6792
|
+
if (!active || !painted || image === void 0) return;
|
|
6793
|
+
Object.assign(image, patch);
|
|
6794
|
+
paint();
|
|
6795
|
+
};
|
|
6796
|
+
const slot = image?.slot;
|
|
6483
6797
|
const spec = field === "opacity" ? {
|
|
6484
6798
|
label: t("ruleOpacity"),
|
|
6485
6799
|
min: 0,
|
|
6486
6800
|
max: 100,
|
|
6487
6801
|
step: 1,
|
|
6488
|
-
def: Math.round(
|
|
6802
|
+
def: Math.round((image?.opacity ?? 1) * 100),
|
|
6489
6803
|
fmt: (v) => `${v}%`,
|
|
6490
|
-
|
|
6804
|
+
preview: (v) => preview({ opacity: v / 100 }, () => setWpOpacity(v / 100)),
|
|
6805
|
+
onChange: (v) => {
|
|
6806
|
+
if (slot !== void 0) p.setImageEffects(rule.id, slot, { opacity: v / 100 });
|
|
6807
|
+
}
|
|
6491
6808
|
} : field === "brightness" ? {
|
|
6492
6809
|
label: t("ruleBrightness"),
|
|
6493
6810
|
min: 0,
|
|
6494
6811
|
max: 200,
|
|
6495
6812
|
step: 1,
|
|
6496
|
-
def:
|
|
6813
|
+
def: image?.brightness ?? 100,
|
|
6497
6814
|
fmt: (v) => `${v}%`,
|
|
6498
|
-
|
|
6815
|
+
preview: (v) => preview({ brightness: v }, setWpFilter),
|
|
6816
|
+
onChange: (v) => {
|
|
6817
|
+
if (slot !== void 0) p.setImageEffects(rule.id, slot, { brightness: v });
|
|
6818
|
+
}
|
|
6499
6819
|
} : {
|
|
6500
6820
|
label: t("ruleBlur"),
|
|
6501
6821
|
min: 0,
|
|
6502
6822
|
max: 60,
|
|
6503
6823
|
step: 1,
|
|
6504
|
-
def:
|
|
6824
|
+
def: image?.blur ?? 0,
|
|
6505
6825
|
fmt: (v) => `${v}px`,
|
|
6506
|
-
|
|
6826
|
+
preview: (v) => preview({ blur: v }, setWpFilter),
|
|
6827
|
+
onChange: (v) => {
|
|
6828
|
+
if (slot !== void 0) p.setImageEffects(rule.id, slot, { blur: v });
|
|
6829
|
+
}
|
|
6507
6830
|
};
|
|
6831
|
+
const disabled = image === void 0;
|
|
6508
6832
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6509
6833
|
className: "dab-wp-row",
|
|
6510
6834
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
6511
6835
|
className: "dab-select",
|
|
6512
6836
|
value: field,
|
|
6837
|
+
disabled,
|
|
6838
|
+
title: disabled ? t("ruleEffectNoImage") : void 0,
|
|
6513
6839
|
"aria-label": t("ruleEffectPick"),
|
|
6514
6840
|
onChange: (e) => onField(e.target.value),
|
|
6515
6841
|
children: [
|
|
@@ -6533,8 +6859,9 @@ window.__ModuleLoader__.load({
|
|
|
6533
6859
|
step: spec.step,
|
|
6534
6860
|
def: spec.def,
|
|
6535
6861
|
fmt: spec.fmt,
|
|
6862
|
+
onInput: spec.preview,
|
|
6536
6863
|
onChange: spec.onChange
|
|
6537
|
-
}, field)]
|
|
6864
|
+
}, `${field}:${slot ?? ""}`)]
|
|
6538
6865
|
});
|
|
6539
6866
|
}
|
|
6540
6867
|
function RuleCard({ p, rule, index, total, active, liveIndex, notify, sel, onSel, field, onField, onRemove }) {
|
|
@@ -6562,6 +6889,15 @@ window.__ModuleLoader__.load({
|
|
|
6562
6889
|
const selIdx = rule.images.length === 0 ? 0 : Math.min(sel, rule.images.length - 1);
|
|
6563
6890
|
const image = rule.images[selIdx];
|
|
6564
6891
|
const url = image === void 0 ? null : p.imageOf(image.slot);
|
|
6892
|
+
/**
|
|
6893
|
+
* Whether the image being EDITED is also the one on screen.
|
|
6894
|
+
*
|
|
6895
|
+
* The wallpaper layer paints the active rule's CURRENT image, so a drag preview
|
|
6896
|
+
* may only write a layer property when the two coincide — otherwise the card's
|
|
6897
|
+
* numbers would land on another picture's pixels. This is the per-image split's
|
|
6898
|
+
* own version of the `active` gate beside it.
|
|
6899
|
+
*/
|
|
6900
|
+
const painted = active && selIdx === liveIndex;
|
|
6565
6901
|
const ownColor = image === void 0 ? rule.color : image.color;
|
|
6566
6902
|
const setColor = (color) => {
|
|
6567
6903
|
if (image === void 0) p.setRule(rule.id, { color });
|
|
@@ -7135,14 +7471,21 @@ window.__ModuleLoader__.load({
|
|
|
7135
7471
|
})
|
|
7136
7472
|
]
|
|
7137
7473
|
})] }),
|
|
7138
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
7474
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7139
7475
|
style: { marginTop: 16 },
|
|
7140
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WallpaperEffectRow, {
|
|
7476
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WallpaperEffectRow, {
|
|
7141
7477
|
p,
|
|
7142
7478
|
rule,
|
|
7479
|
+
image,
|
|
7480
|
+
painted,
|
|
7143
7481
|
field,
|
|
7144
|
-
onField
|
|
7145
|
-
|
|
7482
|
+
onField,
|
|
7483
|
+
active
|
|
7484
|
+
}), image === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7485
|
+
className: "dab-hint",
|
|
7486
|
+
style: { marginTop: 8 },
|
|
7487
|
+
children: t("ruleEffectNoImage")
|
|
7488
|
+
}) : null]
|
|
7146
7489
|
}),
|
|
7147
7490
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7148
7491
|
style: { marginTop: 14 },
|
|
@@ -7316,6 +7659,41 @@ window.__ModuleLoader__.load({
|
|
|
7316
7659
|
});
|
|
7317
7660
|
}
|
|
7318
7661
|
//#endregion
|
|
7662
|
+
//#region src/format.ts
|
|
7663
|
+
/**
|
|
7664
|
+
* Byte counts as a person reads them — the only two sentences this plugin says
|
|
7665
|
+
* about size, in one place so their difference cannot be an accident.
|
|
7666
|
+
*
|
|
7667
|
+
* They are NOT the same question, which is why there are two:
|
|
7668
|
+
*
|
|
7669
|
+
* - a download's size describes ONE file that just arrived;
|
|
7670
|
+
* - a storage figure describes a total that is routinely zero.
|
|
7671
|
+
*
|
|
7672
|
+
* The floor below is right for the first and wrong for the second, and the wrong
|
|
7673
|
+
* answer is not a crash: it is a card that offers to clean up 1 KB and a button
|
|
7674
|
+
* that refuses to, on a store where there is nothing to clean. Neither number is
|
|
7675
|
+
* ever deleted or written — these are display strings — but a maintenance panel
|
|
7676
|
+
* whose numbers disagree with its own button is worse than no panel.
|
|
7677
|
+
*/
|
|
7678
|
+
/**
|
|
7679
|
+
* Size of one download, at the precision a person reads it at.
|
|
7680
|
+
*
|
|
7681
|
+
* Floors at 1 KB on purpose: it is shown beside a file that just arrived, and
|
|
7682
|
+
* "0 KB" beside a picture that downloaded fine reads as a failure.
|
|
7683
|
+
*/
|
|
7684
|
+
const fmtBytes = (n) => n >= 1048576 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`;
|
|
7685
|
+
/**
|
|
7686
|
+
* A figure on the storage card, where zero is a real and — on a tidy store — the
|
|
7687
|
+
* usual answer.
|
|
7688
|
+
*
|
|
7689
|
+
* `fmtBytes` would print "1 KB" for it, so the card claimed 1 KB of waste while
|
|
7690
|
+
* the clean button sat disabled (its condition is `orphanBytes === 0`, which was
|
|
7691
|
+
* true all along). Rounding remains conservative in the other direction: a real
|
|
7692
|
+
* orphan is never allowed to read as zero, because a number that cannot go down
|
|
7693
|
+
* is exactly the complaint this feature exists to answer.
|
|
7694
|
+
*/
|
|
7695
|
+
const fmtStoreBytes = (n) => n > 0 ? fmtBytes(n) : "0 KB";
|
|
7696
|
+
//#endregion
|
|
7319
7697
|
//#region src/client/components/pages/ProfilePage.tsx
|
|
7320
7698
|
/** Chip labels, keyed by the value they set — so no chip can be built from a
|
|
7321
7699
|
* value the schema does not accept, and `check:ui` still sees every key used. */
|
|
@@ -7359,8 +7737,6 @@ window.__ModuleLoader__.load({
|
|
|
7359
7737
|
"too many images": "recFailTooMany",
|
|
7360
7738
|
"too large": "recFailTooLarge"
|
|
7361
7739
|
};
|
|
7362
|
-
/** Size of a download, at the precision a person reads it at. */
|
|
7363
|
-
const fmtBytes = (n) => n >= 1048576 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`;
|
|
7364
7740
|
function ProfilePage({ p, notify }) {
|
|
7365
7741
|
const { t, exportTheme, importTheme, setTransition } = p;
|
|
7366
7742
|
const importRef = (0, react.useRef)(null);
|
|
@@ -7463,6 +7839,46 @@ window.__ModuleLoader__.load({
|
|
|
7463
7839
|
setPending(null);
|
|
7464
7840
|
notify(ok ? t("recDone") : t("recFailApply"), ok);
|
|
7465
7841
|
};
|
|
7842
|
+
const [usage, setUsage] = (0, react.useState)(null);
|
|
7843
|
+
/** Distinguishes "not asked yet" from "asked, and the host cannot answer". */
|
|
7844
|
+
const [usageRead, setUsageRead] = (0, react.useState)(false);
|
|
7845
|
+
const [storeBusy, setStoreBusy] = (0, react.useState)(false);
|
|
7846
|
+
const refreshStore = async () => {
|
|
7847
|
+
const next = await p.readStoreUsage();
|
|
7848
|
+
setUsage(next);
|
|
7849
|
+
setUsageRead(true);
|
|
7850
|
+
};
|
|
7851
|
+
(0, react.useEffect)(() => {
|
|
7852
|
+
let live = true;
|
|
7853
|
+
(async () => {
|
|
7854
|
+
const next = await p.readStoreUsage();
|
|
7855
|
+
if (!live) return;
|
|
7856
|
+
setUsage(next);
|
|
7857
|
+
setUsageRead(true);
|
|
7858
|
+
})();
|
|
7859
|
+
return () => {
|
|
7860
|
+
live = false;
|
|
7861
|
+
};
|
|
7862
|
+
}, [store.rulesRev]);
|
|
7863
|
+
const onClean = async () => {
|
|
7864
|
+
setStoreBusy(true);
|
|
7865
|
+
const res = await p.pruneOrphanImages();
|
|
7866
|
+
await refreshStore();
|
|
7867
|
+
setStoreBusy(false);
|
|
7868
|
+
if (res === null) {
|
|
7869
|
+
notify(t("storeUnknown"), false);
|
|
7870
|
+
return;
|
|
7871
|
+
}
|
|
7872
|
+
if (res.refused) {
|
|
7873
|
+
notify(t("storeRefused"), false);
|
|
7874
|
+
return;
|
|
7875
|
+
}
|
|
7876
|
+
if (res.failed.length > 0) {
|
|
7877
|
+
notify(`${t("storePartial")}${res.failed.join(", ")}`, false);
|
|
7878
|
+
return;
|
|
7879
|
+
}
|
|
7880
|
+
notify(res.removed.length === 0 ? t("storeCleanNone") : `${t("storeDoneFreed")} ${fmtStoreBytes(res.freedBytes)}`, true);
|
|
7881
|
+
};
|
|
7466
7882
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
7467
7883
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
7468
7884
|
className: "dab-head dab-rise",
|
|
@@ -7745,6 +8161,74 @@ window.__ModuleLoader__.load({
|
|
|
7745
8161
|
}
|
|
7746
8162
|
})
|
|
7747
8163
|
]
|
|
8164
|
+
}),
|
|
8165
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
8166
|
+
className: "dab-card dab-card-hover",
|
|
8167
|
+
children: [
|
|
8168
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8169
|
+
className: "dab-profile-ico",
|
|
8170
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 17 })
|
|
8171
|
+
}),
|
|
8172
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8173
|
+
className: "dab-profile-title",
|
|
8174
|
+
children: t("storeCardTitle")
|
|
8175
|
+
}),
|
|
8176
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8177
|
+
className: "dab-profile-desc",
|
|
8178
|
+
children: t("storeCardDesc")
|
|
8179
|
+
}),
|
|
8180
|
+
usage === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8181
|
+
className: "dab-hint",
|
|
8182
|
+
style: { marginBottom: 12 },
|
|
8183
|
+
children: usageRead ? t("storeUnknown") : t("storeReading")
|
|
8184
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8185
|
+
className: "dab-store-stats",
|
|
8186
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8187
|
+
className: "dab-store-stat",
|
|
8188
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8189
|
+
className: "dab-store-stat-v",
|
|
8190
|
+
children: fmtStoreBytes(usage.orphanBytes)
|
|
8191
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8192
|
+
className: "dab-store-stat-k",
|
|
8193
|
+
children: t("storeOrphans")
|
|
8194
|
+
})]
|
|
8195
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8196
|
+
className: "dab-store-stat",
|
|
8197
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8198
|
+
className: "dab-store-stat-v",
|
|
8199
|
+
children: usage.orphans.length
|
|
8200
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8201
|
+
className: "dab-store-stat-k",
|
|
8202
|
+
children: t("storeFiles")
|
|
8203
|
+
})]
|
|
8204
|
+
})]
|
|
8205
|
+
}), usage.trusted ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8206
|
+
className: "dab-hint dab-stale",
|
|
8207
|
+
style: { marginBottom: 12 },
|
|
8208
|
+
children: t("storeRefused")
|
|
8209
|
+
})] }),
|
|
8210
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8211
|
+
style: {
|
|
8212
|
+
display: "flex",
|
|
8213
|
+
gap: 8,
|
|
8214
|
+
flexWrap: "wrap"
|
|
8215
|
+
},
|
|
8216
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
8217
|
+
type: "button",
|
|
8218
|
+
className: "dab-btn",
|
|
8219
|
+
disabled: storeBusy || usage === null || usage.orphanBytes === 0 || !usage.trusted,
|
|
8220
|
+
title: usage !== null && !usage.trusted ? t("storeRefused") : usage !== null && usage.orphanBytes === 0 ? t("storeCleanNone") : t("storeClean"),
|
|
8221
|
+
onClick: () => void onClean(),
|
|
8222
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 14 }), t("storeClean")]
|
|
8223
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
8224
|
+
type: "button",
|
|
8225
|
+
className: "dab-btn dab-btn-ghost",
|
|
8226
|
+
disabled: storeBusy,
|
|
8227
|
+
onClick: () => void refreshStore(),
|
|
8228
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RefreshIcon, { size: 14 }), t("storeRefresh")]
|
|
8229
|
+
})]
|
|
8230
|
+
})
|
|
8231
|
+
]
|
|
7748
8232
|
})
|
|
7749
8233
|
]
|
|
7750
8234
|
}),
|
|
@@ -8329,13 +8813,7 @@ window.__ModuleLoader__.load({
|
|
|
8329
8813
|
const added = [];
|
|
8330
8814
|
for (const dataUrl of dataUrls) {
|
|
8331
8815
|
const slot = nextSlot();
|
|
8332
|
-
|
|
8333
|
-
slot,
|
|
8334
|
-
bgMode: "fit",
|
|
8335
|
-
bgState: { ...DEFAULT_BG_STATE },
|
|
8336
|
-
color: null
|
|
8337
|
-
};
|
|
8338
|
-
rule.images.push(image);
|
|
8816
|
+
rule.images.push(defaultImage(slot));
|
|
8339
8817
|
added.push(slot);
|
|
8340
8818
|
setImage(slot, dataUrl);
|
|
8341
8819
|
writeImage(slot, dataUrl);
|
|
@@ -8411,6 +8889,28 @@ window.__ModuleLoader__.load({
|
|
|
8411
8889
|
saveConfig();
|
|
8412
8890
|
repaintIfMoved(id, winner);
|
|
8413
8891
|
},
|
|
8892
|
+
/**
|
|
8893
|
+
* Store ONE image's three paint effects. Addressed by slot like the mode and
|
|
8894
|
+
* the framing: how a picture is exposed belongs to that picture, and a rule's
|
|
8895
|
+
* rotation can run a daylight photo next to a night shot.
|
|
8896
|
+
*
|
|
8897
|
+
* `normalizeRuleInPlace` is the clamp, so a drag can never write a value the
|
|
8898
|
+
* next load would trim; the layer's own preview path (`setWpOpacity` /
|
|
8899
|
+
* `setWpFilter`) has already painted the value for the frames of the gesture.
|
|
8900
|
+
*/
|
|
8901
|
+
setImageEffects: (id, slot, patch) => {
|
|
8902
|
+
const rule = ruleById(id);
|
|
8903
|
+
const image = rule?.images.find((i) => i.slot === slot);
|
|
8904
|
+
if (rule === null || image === void 0) return;
|
|
8905
|
+
const winner = winnerId();
|
|
8906
|
+
if (patch.opacity !== void 0) image.opacity = patch.opacity;
|
|
8907
|
+
if (patch.brightness !== void 0) image.brightness = patch.brightness;
|
|
8908
|
+
if (patch.blur !== void 0) image.blur = patch.blur;
|
|
8909
|
+
normalizeRuleInPlace(rule);
|
|
8910
|
+
rulesRev++;
|
|
8911
|
+
saveConfig();
|
|
8912
|
+
repaintIfMoved(id, winner);
|
|
8913
|
+
},
|
|
8414
8914
|
setRuleRotation: (id, patch) => {
|
|
8415
8915
|
const rule = ruleById(id);
|
|
8416
8916
|
if (rule === null) return;
|
|
@@ -8463,12 +8963,7 @@ window.__ModuleLoader__.load({
|
|
|
8463
8963
|
const res = await fetchImageUrl(slot, url);
|
|
8464
8964
|
if (!res.ok) return res;
|
|
8465
8965
|
const winner = winnerId();
|
|
8466
|
-
rule.images.push(
|
|
8467
|
-
slot,
|
|
8468
|
-
bgMode: "fit",
|
|
8469
|
-
bgState: { ...DEFAULT_BG_STATE },
|
|
8470
|
-
color: null
|
|
8471
|
-
});
|
|
8966
|
+
rule.images.push(defaultImage(slot));
|
|
8472
8967
|
setImage(slot, res.dataUrl ?? null);
|
|
8473
8968
|
rulesRev++;
|
|
8474
8969
|
persistConfig();
|
|
@@ -8648,6 +9143,12 @@ window.__ModuleLoader__.load({
|
|
|
8648
9143
|
applyRecommended: async (preset) => {
|
|
8649
9144
|
if (!canWriteConfig()) return false;
|
|
8650
9145
|
return await applyThemeFile({ applyActive }, preset, true);
|
|
9146
|
+
},
|
|
9147
|
+
readStoreUsage: () => readStoreUsage(cfg.rules.flatMap(ruleSlots)),
|
|
9148
|
+
pruneOrphanImages: async () => {
|
|
9149
|
+
const res = await pruneOrphanImages(cfg.rules.flatMap(ruleSlots));
|
|
9150
|
+
for (const slot of res?.removed ?? []) setImage(slot, null);
|
|
9151
|
+
return res;
|
|
8651
9152
|
}
|
|
8652
9153
|
};
|
|
8653
9154
|
};
|