dsh-background-by-model 0.9.2 → 0.11.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/lib/client.js CHANGED
@@ -103,12 +103,14 @@ window.__ModuleLoader__.load({
103
103
  id: "mid-autumn",
104
104
  slot: "h-midautumn",
105
105
  asset: "mid-autumn.webp",
106
+ source: "/bfs/new_dyn/42e22801827aff71267b03c95fcfd7834168597.png@1672w.webp",
106
107
  color: "#384A77",
107
108
  kind: "lunar"
108
109
  }, {
109
110
  id: "national-day",
110
111
  slot: "h-nationalday",
111
112
  asset: "national-day.webp",
113
+ source: "/bfs/new_dyn/30e97211189f7869dae83b909d2a3cc84168597.png@3456w.webp",
112
114
  color: "#FFF6EB",
113
115
  kind: "gregorian"
114
116
  }];
@@ -330,6 +332,28 @@ window.__ModuleLoader__.load({
330
332
  function defaultTransition() {
331
333
  return { ...DEFAULT_TRANSITION };
332
334
  }
335
+ /**
336
+ * One image entry for a place that CREATES a picture rather than reads one back
337
+ * (a fresh holiday, and every upload in the panel).
338
+ *
339
+ * The two effects whose neutral value is also the shipped default are written
340
+ * explicitly (`opacity`, `blur`), even though `normalizeImage` would default them:
341
+ * the read-time lifts key off the ABSENCE of a key, so an entry created without one
342
+ * would come back as "written before this field existed" and inherit whatever the
343
+ * rule happened to carry. `brightness` is the one field whose absence is meaningful
344
+ * (see `BgImage.brightness`), so it is left out — "untouched" is exactly what a new
345
+ * picture is.
346
+ */
347
+ function defaultImage(slot, color = null, bgMode = "fit") {
348
+ return {
349
+ slot,
350
+ bgMode,
351
+ bgState: { ...DEFAULT_BG_STATE },
352
+ color,
353
+ opacity: 1,
354
+ blur: 0
355
+ };
356
+ }
333
357
  /** Every slot a rule owns, in order. The one thing the render and store layers
334
358
  * ask of the image list, so a rule's slots are never enumerated ad hoc. */
335
359
  function ruleSlots(rule) {
@@ -399,18 +423,10 @@ window.__ModuleLoader__.load({
399
423
  const color = hexToHsl(def.color);
400
424
  return {
401
425
  id: def.id,
402
- images: [{
403
- slot: def.slot,
404
- bgMode: "fill",
405
- bgState: { ...DEFAULT_BG_STATE },
406
- color
407
- }],
426
+ images: [defaultImage(def.slot, color, "fill")],
408
427
  match: "",
409
428
  enabled: true,
410
429
  color,
411
- wallpaperOpacity: 1,
412
- brightness: 100,
413
- blur: 0,
414
430
  rotate: defaultRotation()
415
431
  };
416
432
  }
@@ -434,9 +450,6 @@ window.__ModuleLoader__.load({
434
450
  match: "",
435
451
  enabled: true,
436
452
  color: null,
437
- wallpaperOpacity: 1,
438
- brightness: 100,
439
- blur: 0,
440
453
  rotate: defaultRotation()
441
454
  };
442
455
  }
@@ -547,28 +560,41 @@ window.__ModuleLoader__.load({
547
560
  durationMs: clamp$1(t.durationMs, 0, TRANSITION_MAX_MS, fallback)
548
561
  };
549
562
  }
563
+ /** The inherit block for a caller that has no rule-level values at all. */
564
+ const DEFAULT_IMAGE_INHERIT = {
565
+ color: null,
566
+ opacity: 1,
567
+ brightness: void 0,
568
+ blur: 0
569
+ };
550
570
  /**
551
571
  * Coerce one persisted image entry, or null when it lacks a usable slot.
552
572
  *
553
- * `inherit` (the RULE's colour) is applied only to an entry with no `color` KEY at
554
- * all — the shape written before per-image colours, where the rule's colour was the
555
- * only one there was. The key's PRESENCE is the whole distinction: `color: null` is
556
- * a deliberate "follow the system theme" and must not be re-inherited on every load.
573
+ * `inherit` is the rule-level block this entry may take its values from, and each
574
+ * field is lifted ONLY when the entry has no key of its own — that absence is the
575
+ * whole signal. `color: null` is a deliberate "follow the system theme" and must
576
+ * not be re-inherited on every load; `brightness` keeps its key absent when neither
577
+ * side ever named one, so an upgrade never writes a default nobody chose.
557
578
  *
558
579
  * `inheritMode` is the same one-shot lift for the layout mode. It is the caller's to
559
580
  * resolve because the callers answer differently: a user rule passes the mode its
560
581
  * config carried at the RULE level (schema 5), a holiday passes its own `fill`. An
561
582
  * entry that HAS a `bgMode` key keeps it, even when it equals the default.
562
583
  */
563
- function normalizeImage(raw, inherit = null, inheritMode = "fit") {
584
+ function normalizeImage(raw, inherit = DEFAULT_IMAGE_INHERIT, inheritMode = "fit") {
564
585
  const i = raw ?? {};
565
586
  if (typeof i.slot !== "string" || !SLOT_RE$1.test(i.slot)) return null;
566
- return {
587
+ const image = {
567
588
  slot: i.slot,
568
589
  bgMode: BG_MODES$1.includes(i.bgMode) ? i.bgMode : inheritMode,
569
590
  bgState: normalizeBgState(i.bgState),
570
- color: hasOwn(i, "color") ? normalizeHsl(i.color) : inherit
591
+ color: hasOwn(i, "color") ? normalizeHsl(i.color) : inherit.color,
592
+ opacity: clamp01(i.opacity, inherit.opacity),
593
+ blur: clamp$1(i.blur, 0, 60, inherit.blur)
571
594
  };
595
+ const brightness = i.brightness === void 0 ? inherit.brightness : clamp$1(i.brightness, 0, 200, 100);
596
+ if (brightness !== void 0) image.brightness = brightness;
597
+ return image;
572
598
  }
573
599
  /**
574
600
  * Coerce one persisted rule, or null when it names no image ANYWHERE.
@@ -593,38 +619,45 @@ window.__ModuleLoader__.load({
593
619
  if (id === null) return null;
594
620
  const color = normalizeHsl(r.color);
595
621
  const mode = BG_MODES$1.includes(r.bgMode) ? r.bgMode : "fit";
622
+ const inherit = {
623
+ color,
624
+ opacity: clamp01(r.wallpaperOpacity, 1),
625
+ brightness: r.brightness === void 0 ? void 0 : clamp$1(r.brightness, 0, 200, 100),
626
+ blur: clamp$1(r.blur, 0, 60, 0)
627
+ };
596
628
  const images = [];
597
629
  const seen = /* @__PURE__ */ new Set();
598
630
  const hadList = Array.isArray(r.images);
599
631
  if (hadList) for (const entry of r.images) {
600
- const image = normalizeImage(entry, color, mode);
632
+ const image = normalizeImage(entry, inherit, mode);
601
633
  if (image === null || seen.has(image.slot)) continue;
602
634
  seen.add(image.slot);
603
635
  images.push(image);
604
636
  }
605
637
  const legacySlot = typeof r.slot === "string" && SLOT_RE$1.test(r.slot) ? r.slot : null;
606
- if (images.length === 0 && legacySlot !== null) images.push({
607
- slot: legacySlot,
608
- bgMode: mode,
609
- bgState: normalizeBgState(r.bgState),
610
- color
611
- });
638
+ if (images.length === 0 && legacySlot !== null) {
639
+ const image = {
640
+ slot: legacySlot,
641
+ bgMode: mode,
642
+ bgState: normalizeBgState(r.bgState),
643
+ color,
644
+ opacity: inherit.opacity,
645
+ blur: inherit.blur
646
+ };
647
+ if (inherit.brightness !== void 0) image.brightness = inherit.brightness;
648
+ images.push(image);
649
+ }
612
650
  if (images.length === 0 && !hadList) return null;
613
651
  const rotate = normalizeRotation(r.rotate);
614
652
  if (images.length < 2) rotate.enabled = false;
615
- const brightness = r.brightness === void 0 ? void 0 : clamp$1(r.brightness, 0, 200, 100);
616
- const rule = {
653
+ return {
617
654
  id,
618
655
  images,
619
656
  match: typeof r.match === "string" ? r.match : "",
620
657
  enabled: r.enabled !== false,
621
658
  color,
622
- wallpaperOpacity: clamp01(r.wallpaperOpacity, 1),
623
- blur: clamp$1(r.blur, 0, 60, 0),
624
659
  rotate
625
660
  };
626
- if (brightness !== void 0) rule.brightness = brightness;
627
- return rule;
628
661
  }
629
662
  /**
630
663
  * Coerce one persisted holiday entry.
@@ -641,17 +674,29 @@ window.__ModuleLoader__.load({
641
674
  return {
642
675
  ...base,
643
676
  enabled: r.enabled !== false,
644
- wallpaperOpacity: clamp01(r.wallpaperOpacity, base.wallpaperOpacity),
645
- brightness: clamp$1(r.brightness, 0, 200, base.brightness ?? 100),
646
- blur: clamp$1(r.blur, 0, 60, base.blur),
647
- images: base.images.map((image) => ({
648
- slot: image.slot,
649
- bgMode: image.bgMode,
650
- bgState: normalizeBgState(r.images?.[0]?.bgState ?? r.bgState),
651
- color: image.color
652
- }))
677
+ images: base.images.map((image) => {
678
+ const stored = r.images?.[0];
679
+ const merged = {
680
+ slot: image.slot,
681
+ bgMode: image.bgMode,
682
+ bgState: normalizeBgState(stored?.bgState ?? r.bgState),
683
+ color: image.color,
684
+ opacity: clamp01(stored?.opacity ?? legacyWallpaperOpacity(r.wallpaperOpacity), image.opacity),
685
+ blur: clamp$1(stored?.blur ?? r.blur, 0, 60, image.blur)
686
+ };
687
+ 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);
688
+ if (brightness !== void 0) merged.brightness = brightness;
689
+ return merged;
690
+ })
653
691
  };
654
692
  }
693
+ /**
694
+ * The rule-level opacity a schema-9 holiday entry carried, or `undefined` so the
695
+ * caller can tell "absent" from "set to 1" and leave `clamp01` its default.
696
+ */
697
+ function legacyWallpaperOpacity(raw) {
698
+ return typeof raw === "number" && isFinite(raw) ? raw : void 0;
699
+ }
655
700
  /** Coerce the whole holiday block; unknown ids are dropped, missing ones defaulted. */
656
701
  function normalizeHolidays(raw) {
657
702
  const r = raw ?? {};
@@ -892,24 +937,34 @@ window.__ModuleLoader__.load({
892
937
  function rBgMode() {
893
938
  return activeImage()?.bgMode ?? "fit";
894
939
  }
940
+ /**
941
+ * Opacity of the wallpaper layer, 0..1 (per image, see BgImage).
942
+ *
943
+ * Read from the image being painted, not from the rule: one rule's rotation can go
944
+ * through a full-bleed photo and a faint decorative plate, and a rule-level answer
945
+ * would be wrong for one of them. A rule with no image paints nothing at all, so
946
+ * there is no layer for the value to describe.
947
+ */
895
948
  function rWop() {
896
- return clamp01(activeRule()?.wallpaperOpacity, 1);
949
+ return clamp01(activeImage()?.opacity, 1);
897
950
  }
898
951
  /**
899
952
  * Brightness of the wallpaper layer, in percent (100 = the picture exactly as the
900
953
  * file has it).
901
954
  *
902
- * Read from the ACTIVE rule like the opacity and blur beside it, and defaulted the
903
- * same way: a rule whose `brightness` key is absent — every config written before
904
- * the setting existed — is left alone, so an upgrade changes nothing on screen.
905
- * The default and the ceiling live in ../schema, so the render layer holds no
906
- * numbers of its own (see ./wallpaper's `wpFilter`).
955
+ * Read from the CURRENT image, like the opacity and blur beside it, and defaulted
956
+ * the same way: an image whose `brightness` key is absent — every config written
957
+ * before the setting existed, and every picture whose rule never named one — is
958
+ * left alone, so an upgrade changes nothing on screen. The default and the ceiling
959
+ * live in ../schema, so the render layer holds no numbers of its own (see
960
+ * ./wallpaper's `wpFilter`).
907
961
  */
908
962
  function rBrightness() {
909
- return clamp$1(activeRule()?.brightness, 0, 200, 100);
963
+ return clamp$1(activeImage()?.brightness, 0, 200, 100);
910
964
  }
965
+ /** Blur of the CURRENT image, in px (0 = the picture as its file has it). */
911
966
  function rBl() {
912
- return clamp$1(activeRule()?.blur, 0, 60, 0);
967
+ return clamp$1(activeImage()?.blur, 0, 60, 0);
913
968
  }
914
969
  /** Framing of the CURRENT image (per image, see BgImage). */
915
970
  function rBgState() {
@@ -1064,7 +1119,7 @@ window.__ModuleLoader__.load({
1064
1119
  const d = data;
1065
1120
  const slots = Array.isArray(d.slots) ? d.slots.filter((s) => typeof s === "string") : [];
1066
1121
  const schema = typeof d.schema === "number" && isFinite(d.schema) ? d.schema : null;
1067
- hostShape = typeof schema === "number" && schema >= 9 ? "current" : "legacy";
1122
+ hostShape = typeof schema === "number" && schema >= 10 ? "current" : "legacy";
1068
1123
  if (hostShape === "legacy") holdWrite();
1069
1124
  return {
1070
1125
  config: d.config,
@@ -1114,17 +1169,13 @@ window.__ModuleLoader__.load({
1114
1169
  };
1115
1170
  }
1116
1171
  /**
1117
- * The profile's manifest and config, in one answer. Reads nothing, writes nothing.
1172
+ * One of the profile's wallpapers, addressed by the slot the built-in config names.
1118
1173
  *
1119
1174
  * Raw rather than `rpcCall` for the same reason `fetchImageUrl` is: the node half
1120
- * answers this one with a value that CARRIES its own failure, and the `error`
1121
- * inside it is the only thing worth showing the user — the generic "rpc failed"
1122
- * log would throw that distinction away.
1175
+ * answers this one with a value that CARRIES its own failure, and the `error` inside
1176
+ * it is the only thing worth showing the user — the generic "rpc failed" log would
1177
+ * throw that distinction away.
1123
1178
  */
1124
- async function fetchPresetConfig() {
1125
- return unwrapPreset(await rpcRaw("fetchPresetConfig", {}));
1126
- }
1127
- /** One of the profile's wallpapers, addressed by the slot the config names. */
1128
1179
  async function fetchPresetImage(slot) {
1129
1180
  return unwrapPreset(await rpcRaw("fetchPresetImage", { slot }));
1130
1181
  }
@@ -3197,13 +3248,25 @@ window.__ModuleLoader__.load({
3197
3248
  /** Index of the layer fading in right now, if any. */
3198
3249
  let pending = null;
3199
3250
  let fadeTimer = null;
3251
+ /** The wallpaper's steady opacity — the ACTIVE picture's, as last committed.
3252
+ *
3253
+ * Mirrored rather than read back off the style string, because a repaint that
3254
+ * runs in the middle of a fade (a theme re-registration, a panel re-render) has
3255
+ * to re-write the value the fade is HEADING for; reading `rWop()` there would
3256
+ * snap the whole layer to the incoming picture's alpha and cut the fade short.
3257
+ */
3258
+ let wpAlpha = 1;
3200
3259
  function createLayer() {
3201
3260
  const el = document.createElement("div");
3202
3261
  el.dataset.dabWpLayer = "";
3203
3262
  el.style.cssText = "position:absolute;inset:0;opacity:0;background-repeat:no-repeat;background-position:center;";
3204
3263
  return {
3205
3264
  el,
3206
- url: null
3265
+ url: null,
3266
+ posX: null,
3267
+ posY: null,
3268
+ brightness: 100,
3269
+ blur: 0
3207
3270
  };
3208
3271
  }
3209
3272
  function ensureWpContainer() {
@@ -3221,6 +3284,7 @@ window.__ModuleLoader__.load({
3221
3284
  layers = [a, b];
3222
3285
  front = 0;
3223
3286
  pending = null;
3287
+ wpAlpha = 1;
3224
3288
  document.body.prepend(wpEl);
3225
3289
  }
3226
3290
  /** Drop the whole wallpaper layer (no rule carries an image). */
@@ -3232,6 +3296,7 @@ window.__ModuleLoader__.load({
3232
3296
  pending = null;
3233
3297
  front = 0;
3234
3298
  layers = null;
3299
+ wpAlpha = 1;
3235
3300
  wpEl?.remove();
3236
3301
  wpEl = null;
3237
3302
  }
@@ -3239,9 +3304,14 @@ window.__ModuleLoader__.load({
3239
3304
  function clearLayer(l) {
3240
3305
  if (l.url === null) return;
3241
3306
  l.url = null;
3307
+ l.posX = null;
3308
+ l.posY = null;
3309
+ l.brightness = 100;
3310
+ l.blur = 0;
3242
3311
  l.el.style.backgroundImage = "";
3243
3312
  l.el.style.backgroundSize = "";
3244
3313
  l.el.style.backgroundPosition = "";
3314
+ l.el.style.inset = "0px";
3245
3315
  l.el.style.transform = "none";
3246
3316
  }
3247
3317
  /** Write the two properties a switch animates. Both are always written, so a
@@ -3269,6 +3339,34 @@ window.__ModuleLoader__.load({
3269
3339
  img.onerror = () => cb(0, 0);
3270
3340
  img.src = url;
3271
3341
  }
3342
+ /** How far a layer's box is grown on every side to hide a blurred edge.
3343
+ *
3344
+ * `filter: blur(σ)` convolves the layer's own rendering with a Gaussian of
3345
+ * standard deviation σ and treats everything outside the box as transparent, so
3346
+ * the outermost pixels come out half opaque and the image fades inward over
3347
+ * ~2σ — composited over the page behind it, that half-transparent rim is the
3348
+ * black border the blur slider paints, and it grows thicker with the slider.
3349
+ * Growing the box by 2σ puts every visible pixel at least 2σ inside the edge,
3350
+ * where ~2% of the page still shows through the outermost row: gone to the eye.
3351
+ * Two sigma rather than three is the trade for the `cover` modes, which pay for
3352
+ * the growth by framing slightly tighter — a fifth of the image at most, at the
3353
+ * slider's maximum, where the detail a wider frame would reveal is blurred out.
3354
+ */
3355
+ function wpBlurPad(blur) {
3356
+ return blur > 0 ? Math.ceil(blur * 2) + 2 : 0;
3357
+ }
3358
+ /** Grow a layer's box by `pad` on every side while keeping its image on the same
3359
+ * pixels. The background positioning area starts at the border box, so a box
3360
+ * whose origin moved by -pad needs a numeric placement moved by +pad; `center`
3361
+ * (and the box-relative `cover` / `100% 100%` sizes) sit in the middle of a
3362
+ * symmetrically grown box and need no compensation. The container clips the
3363
+ * growth, so nothing about the visible layout changes: it exists only to give
3364
+ * the blur pixels to sample beyond the viewport. */
3365
+ function writeLayerBox(l, pad) {
3366
+ const { posX, posY } = l;
3367
+ l.el.style.inset = `${-pad}px`;
3368
+ l.el.style.backgroundPosition = posX === null || posY === null ? "center" : `${posX + pad}px ${posY + pad}px`;
3369
+ }
3272
3370
  /** Paint one layer's image and its placement. The placement always follows the
3273
3371
  * ACTIVE rule: the incoming layer is painted right after a switch, and a
3274
3372
  * repaint of the same URL (framing edit, viewport resize) must use the rule's
@@ -3280,6 +3378,8 @@ window.__ModuleLoader__.load({
3280
3378
  const mode = rBgMode();
3281
3379
  const next = `url("${url}")`;
3282
3380
  if (el.style.backgroundImage !== next) el.style.backgroundImage = next;
3381
+ l.posX = null;
3382
+ l.posY = null;
3283
3383
  if (mode === "fit") {
3284
3384
  el.style.backgroundRepeat = "no-repeat";
3285
3385
  if (bg.iw > 0) {
@@ -3287,35 +3387,31 @@ window.__ModuleLoader__.load({
3287
3387
  const w = bg.iw * fit * bg.zoom;
3288
3388
  const h = bg.ih * fit * bg.zoom;
3289
3389
  el.style.backgroundSize = `${w}px ${h}px`;
3290
- el.style.backgroundPosition = `${bg.x * window.innerWidth - w / 2}px ${bg.y * window.innerHeight - h / 2}px`;
3291
- } else {
3292
- el.style.backgroundSize = "contain";
3293
- el.style.backgroundPosition = "center";
3294
- }
3390
+ l.posX = bg.x * window.innerWidth - w / 2;
3391
+ l.posY = bg.y * window.innerHeight - h / 2;
3392
+ } else el.style.backgroundSize = "contain";
3295
3393
  } else if (mode === "fill") {
3296
3394
  el.style.backgroundRepeat = "no-repeat";
3297
3395
  el.style.backgroundSize = "cover";
3298
- el.style.backgroundPosition = "center";
3299
3396
  } else if (mode === "stretch") {
3300
3397
  el.style.backgroundRepeat = "no-repeat";
3301
3398
  el.style.backgroundSize = "100% 100%";
3302
- el.style.backgroundPosition = "center";
3303
3399
  } else if (mode === "tile") {
3304
3400
  el.style.backgroundRepeat = "repeat";
3305
3401
  el.style.backgroundSize = "auto";
3306
- el.style.backgroundPosition = "0px 0px";
3402
+ l.posX = 0;
3403
+ l.posY = 0;
3307
3404
  } else {
3308
3405
  el.style.backgroundRepeat = "no-repeat";
3309
3406
  el.style.backgroundSize = "contain";
3310
- el.style.backgroundPosition = "center";
3311
3407
  imageNatSize(url, (w, h) => {
3312
3408
  if (l.url !== url || el.style.backgroundImage !== next || rBgMode() !== "center") return;
3313
- if (w > 0 && h > 0) {
3314
- el.style.backgroundSize = `${w}px ${h}px`;
3315
- el.style.backgroundPosition = "center";
3316
- }
3409
+ if (w > 0 && h > 0) el.style.backgroundSize = `${w}px ${h}px`;
3317
3410
  });
3318
3411
  }
3412
+ l.brightness = rBrightness();
3413
+ l.blur = rBl();
3414
+ applyLayerEffects(l);
3319
3415
  }
3320
3416
  /** Whether there is anything to animate FROM: a wallpaper on screen that is
3321
3417
  * actually visible. The duration question belongs to `rFadeMs`, and the
@@ -3339,6 +3435,10 @@ window.__ModuleLoader__.load({
3339
3435
  }
3340
3436
  pending = null;
3341
3437
  front = idx;
3438
+ if (wpEl !== null) {
3439
+ wpEl.style.transition = "none";
3440
+ wpEl.style.opacity = String(wpAlpha);
3441
+ }
3342
3442
  const el = layers[idx].el;
3343
3443
  el.style.opacity = "1";
3344
3444
  el.style.transform = "none";
@@ -3385,12 +3485,14 @@ window.__ModuleLoader__.load({
3385
3485
  if (pending !== null) {
3386
3486
  if (ls[pending].url === url) {
3387
3487
  paintLayer(ls[pending], url);
3488
+ wpAlpha = rWop();
3388
3489
  applyWpEffects();
3389
3490
  return;
3390
3491
  }
3391
3492
  if (ls[front].url === url) {
3392
3493
  cancelFade();
3393
3494
  paintLayer(ls[front], url);
3495
+ wpAlpha = rWop();
3394
3496
  applyWpEffects();
3395
3497
  return;
3396
3498
  }
@@ -3398,6 +3500,7 @@ window.__ModuleLoader__.load({
3398
3500
  }
3399
3501
  if (ls[front].url === url) {
3400
3502
  paintLayer(ls[front], url);
3503
+ wpAlpha = rWop();
3401
3504
  applyWpEffects();
3402
3505
  return;
3403
3506
  }
@@ -3418,7 +3521,11 @@ window.__ModuleLoader__.load({
3418
3521
  applyLayerStyle(el, plan.from);
3419
3522
  el.style.zIndex = "1";
3420
3523
  from.el.style.zIndex = "0";
3524
+ const toAlpha = rWop();
3525
+ const fadeAlpha = plan.animate && Math.abs(toAlpha - wpAlpha) > .001;
3526
+ if (fadeAlpha) wpEl.style.transition = "none";
3421
3527
  paintLayer(target, url);
3528
+ if (!fadeAlpha) wpAlpha = toAlpha;
3422
3529
  applyWpEffects();
3423
3530
  if (!plan.animate) {
3424
3531
  applyLayerStyle(el, plan.to);
@@ -3431,15 +3538,24 @@ window.__ModuleLoader__.load({
3431
3538
  if (pending !== idx || layers === null) return;
3432
3539
  el.style.transition = plan.transition;
3433
3540
  applyLayerStyle(el, plan.to);
3541
+ if (fadeAlpha) {
3542
+ wpEl.style.transition = plan.transition;
3543
+ wpEl.style.opacity = String(toAlpha);
3544
+ wpAlpha = toAlpha;
3545
+ }
3434
3546
  });
3435
3547
  fadeTimer = window.setTimeout(() => finishFade(idx), plan.durationMs + 160);
3436
3548
  }
3437
3549
  /**
3438
- * The `filter` a wallpaper layer carries, from the active rule's brightness and
3439
- * blur.
3550
+ * The `filter` ONE LAYER carries, from the brightness and blur of the picture
3551
+ * painted on IT.
3552
+ *
3553
+ * Both are per image (see BgImage): one rule's rotation can run a daylight photo
3554
+ * and a night shot, and a rule-level answer forced the second one to live at the
3555
+ * first one's exposure.
3440
3556
  *
3441
3557
  * ONE declaration, because the two are one CSS property: assigning `filter` twice
3442
- * would leave whichever ran last, so a rule that is both brightened and blurred
3558
+ * would leave whichever ran last, so an image that is both brightened and blurred
3443
3559
  * would silently lose one of them. Brightness is written first and blur second for
3444
3560
  * a reason that is visible: blurring a dimmed picture softens the highlights the
3445
3561
  * dimming just brought down, while dimming an already-blurred one simply darkens
@@ -3449,21 +3565,34 @@ window.__ModuleLoader__.load({
3449
3565
  * The neutral brightness is LEFT OUT rather than written as `brightness(100%)`.
3450
3566
  * A layer with neither effect gets `''`, which is what an untouched wallpaper has
3451
3567
  * always carried, and keeps the layer off the compositor's filter path entirely.
3568
+ *
3569
+ * The numbers are the LAYER's, not live reads: during a switch the two layers
3570
+ * hold two different photos, and a live read made the outgoing one jump to the
3571
+ * incoming one's exposure at the first frame of every fade.
3452
3572
  */
3453
- function wpFilter() {
3573
+ function wpFilter(brightness, blur) {
3454
3574
  const parts = [];
3455
- const brightness = rBrightness();
3456
3575
  if (brightness !== 100) parts.push(`brightness(${brightness}%)`);
3457
- const blur = rBl();
3458
3576
  if (blur > 0) parts.push(`blur(${blur}px)`);
3459
3577
  return parts.join(" ");
3460
3578
  }
3461
3579
  function applyWpEffects() {
3462
3580
  if (wpEl === null || layers === null) return;
3463
- wpEl.style.opacity = String(rWop());
3464
- const filter = wpFilter();
3465
- layers[0].el.style.filter = filter;
3466
- layers[1].el.style.filter = filter;
3581
+ wpEl.style.opacity = String(wpAlpha);
3582
+ applyLayerEffects(layers[0]);
3583
+ applyLayerEffects(layers[1]);
3584
+ }
3585
+ /** Write what a layer takes from ITS picture: the filter, and the box growth that
3586
+ * keeps a blurred edge from drawing a dark rim.
3587
+ *
3588
+ * ONE function for both, because the two are one visible decision — the same
3589
+ * blur that makes `filter` non-empty is what makes the pad non-zero — and
3590
+ * because BOTH update paths (a full repaint and a drag frame) write it, so a
3591
+ * blur dragged to its top cannot leave the rim behind until the slider is
3592
+ * released. */
3593
+ function applyLayerEffects(l) {
3594
+ l.el.style.filter = wpFilter(l.brightness, l.blur);
3595
+ writeLayerBox(l, wpBlurPad(l.blur));
3467
3596
  }
3468
3597
  /** Repaint the wallpaper layer, the token palette and every interface part from
3469
3598
  * the ACTIVE rule. This is the single entry point a rule switch goes through. */
@@ -3512,6 +3641,38 @@ window.__ModuleLoader__.load({
3512
3641
  tableFixStyleEl = null;
3513
3642
  stopWatchingParts();
3514
3643
  }
3644
+ /** Live wallpaper-opacity updates during slider drag (no full re-apply).
3645
+ *
3646
+ * The container is the one holder of this value (see `applyWpEffects`), so the
3647
+ * mirror and the style are written together — a later repaint then re-asserts
3648
+ * what the drag ended on rather than the value it started from. */
3649
+ function setWpOpacity(v) {
3650
+ wpAlpha = v;
3651
+ if (wpEl) wpEl.style.opacity = String(v);
3652
+ }
3653
+ /**
3654
+ * Live wallpaper-filter updates during a slide, with no full re-apply.
3655
+ *
3656
+ * Deliberately parameterless: the filter composes TWO settings (brightness and
3657
+ * blur), so a function trusting one caller's value would write half of it and
3658
+ * drop the other. Both are read back from the store instead (./state), which is
3659
+ * the same store the drag has already written to before calling this.
3660
+ *
3661
+ * Written onto the layer PAINTING that picture rather than onto both: during a
3662
+ * switch the other layer is still showing the previous photo, and giving it this
3663
+ * one's brightness and blur is exactly the snap this snapshot exists to prevent.
3664
+ */
3665
+ function setWpFilter() {
3666
+ if (layers === null) return;
3667
+ const url = rWp();
3668
+ if (url === null) return;
3669
+ for (const l of layers) {
3670
+ if (l.url !== url) continue;
3671
+ l.brightness = rBrightness();
3672
+ l.blur = rBl();
3673
+ applyLayerEffects(l);
3674
+ }
3675
+ }
3515
3676
  //#endregion
3516
3677
  //#region src/client/cadence.ts
3517
3678
  /**
@@ -4526,7 +4687,7 @@ window.__ModuleLoader__.load({
4526
4687
  if (url !== null) images[slot] = url;
4527
4688
  }
4528
4689
  return {
4529
- version: 9,
4690
+ version: 10,
4530
4691
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
4531
4692
  config: cfg,
4532
4693
  images
@@ -4565,7 +4726,7 @@ window.__ModuleLoader__.load({
4565
4726
  * "replace everything with this" literally true.
4566
4727
  */
4567
4728
  async function applyThemeFile(host, data, wipeFirst) {
4568
- if (typeof data.version === "number" && data.version > 9) return false;
4729
+ if (typeof data.version === "number" && data.version > 10) return false;
4569
4730
  if (typeof data.config !== "object" || data.config === null) return false;
4570
4731
  if (wipeFirst) {
4571
4732
  await resetStore();
@@ -4753,6 +4914,7 @@ window.__ModuleLoader__.load({
4753
4914
  ruleBrightness: "图片亮度",
4754
4915
  ruleBlur: "图片模糊",
4755
4916
  ruleEffectPick: "调节项",
4917
+ ruleEffectNoImage: "添加图片后可分别设置每张图的透明度、亮度与模糊",
4756
4918
  rotTitle: "多图轮换",
4757
4919
  rotEnable: "轮换这组图",
4758
4920
  rotNeedTwo: "至少两张图才能轮换",
@@ -4806,7 +4968,7 @@ window.__ModuleLoader__.load({
4806
4968
  importDone: "已导入配置",
4807
4969
  importFail: "导入失败,文件格式不正确",
4808
4970
  recCardTitle: "推荐配置",
4809
- recCardDesc: "从项目素材库下载作者维护的一整套配置(规则、壁纸与接口设置),一键替换当前的全部内容",
4971
+ recCardDesc: "换上作者维护的一整套配置(规则与界面数值随插件内置,壁纸从图床下载),一键替换当前的全部内容",
4810
4972
  recAdopt: "采用推荐配置",
4811
4973
  recBusy: "正在下载",
4812
4974
  recConfirmTitle: "替换当前的全部配置?",
@@ -4816,12 +4978,9 @@ window.__ModuleLoader__.load({
4816
4978
  recConfirmOk: "删除并替换",
4817
4979
  recConfirmCancel: "取消",
4818
4980
  recDone: "已采用推荐配置",
4819
- recFailNetwork: "推荐配置下载失败,请检查网络后重试",
4820
- recFailNotProfile: "素材库返回的内容不是配置,请稍后重试",
4821
- recFailNewer: "这份推荐配置需要更新的插件版本,未做任何改动",
4822
- recFailTooMany: "这份推荐配置列出的图片数量超出限制,未做任何改动",
4981
+ recFailNetwork: "壁纸下载失败,请检查网络后重试(已下到的会留到下一次)",
4823
4982
  recFailTooLarge: "这份推荐配置体积过大,未做任何改动",
4824
- recFailImage: "这张壁纸没拿到(素材库里缺这个文件):",
4983
+ recFailImage: "这张壁纸没拿到(图床上这个地址没有回应):",
4825
4984
  recFailApply: "未能写入推荐配置,你原来的配置没有被改动",
4826
4985
  storeCardTitle: "图片存储",
4827
4986
  storeCardDesc: "因意外导致的孤儿图片这里可以清掉",
@@ -4936,6 +5095,7 @@ window.__ModuleLoader__.load({
4936
5095
  ruleBrightness: "Image brightness",
4937
5096
  ruleBlur: "Image blur",
4938
5097
  ruleEffectPick: "Effect",
5098
+ ruleEffectNoImage: "Add an image to give each one its own opacity, brightness and blur",
4939
5099
  rotTitle: "Image rotation",
4940
5100
  rotEnable: "Rotate these images",
4941
5101
  rotNeedTwo: "At least two images are needed to rotate",
@@ -4989,7 +5149,7 @@ window.__ModuleLoader__.load({
4989
5149
  importDone: "Profile imported",
4990
5150
  importFail: "Import failed — invalid file",
4991
5151
  recCardTitle: "Recommended profile",
4992
- recCardDesc: "Download the author's own full setup — rules, wallpapers and interface settings — and replace everything with it",
5152
+ recCardDesc: "Install the author's own full setup — rules and interface numbers ship with the plugin, wallpapers are downloaded from the image host — replacing everything you have",
4993
5153
  recAdopt: "Use recommended profile",
4994
5154
  recBusy: "Downloading",
4995
5155
  recConfirmTitle: "Replace your entire profile?",
@@ -4999,12 +5159,9 @@ window.__ModuleLoader__.load({
4999
5159
  recConfirmOk: "Delete and replace",
5000
5160
  recConfirmCancel: "Cancel",
5001
5161
  recDone: "Recommended profile applied",
5002
- recFailNetwork: "Could not download the recommended profile — check your connection and retry",
5003
- recFailNotProfile: "The asset host answered with something that is not a profile — try again later",
5004
- recFailNewer: "This profile needs a newer plugin version. Nothing was changed.",
5005
- recFailTooMany: "The recommended profile names more images than the plugin will fetch. Nothing was changed.",
5162
+ recFailNetwork: "Could not download the wallpapers — check your connection and retry (what arrived is kept for the next try)",
5006
5163
  recFailTooLarge: "The recommended profile is too large. Nothing was changed.",
5007
- recFailImage: "Could not fetch this wallpaper — the asset host has no such file:",
5164
+ recFailImage: "Could not fetch this wallpaper — the image host does not answer for that address:",
5008
5165
  recFailApply: "Could not write the recommended profile — your own is untouched",
5009
5166
  storeCardTitle: "Image storage",
5010
5167
  storeCardDesc: "Orphaned image files left behind by an accident can be cleared here",
@@ -5041,41 +5198,6 @@ window.__ModuleLoader__.load({
5041
5198
  //#endregion
5042
5199
  //#region src/preset.ts
5043
5200
  /**
5044
- * JSON with object keys in a fixed (sorted) order — the same string for the same
5045
- * value, whatever order the object that carried it happened to build its keys in.
5046
- *
5047
- * `JSON.stringify` alone is not an identity for a value that came off the wire and
5048
- * through a sanitizer: key order is an accident of how the object was assembled,
5049
- * and a resume that missed on it would silently re-download everything (the safe
5050
- * direction) while a resume that MATCHED on a value it should not have would be
5051
- * the unsafe one — so this is deliberately total as well: anything JSON cannot
5052
- * carry (a function, a `Symbol`, `undefined`) serializes as `null` and an
5053
- * `undefined` property is skipped, exactly as `JSON.stringify` treats it.
5054
- */
5055
- function canonical(value) {
5056
- if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
5057
- if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
5058
- return JSON.stringify(value) ?? "null";
5059
- }
5060
- /**
5061
- * The identity of one revision of the profile: the manifest's shape version plus
5062
- * the config, in canonical form.
5063
- *
5064
- * The profile is served at ONE version of the assets repository, but a version
5065
- * reference is only as fixed as the tag behind it: re-point the tag and every
5066
- * edge that has not been purged keeps answering with the previous bytes of the
5067
- * same URL. So "the profile" is still not guaranteed to be one fixed thing, and
5068
- * the bytes a previous attempt is holding are only known to belong to what is
5069
- * being installed when that config comes back the same. Order-insensitivity is
5070
- * what lets this be a comparison of VALUES rather than of spellings —
5071
- * `normalizeConfig` (./schema) rebuilds the config it reads, so pinning the key
5072
- * to the exact key order the wire happened to use would make the hold useless
5073
- * the moment anything upstream normalized differently.
5074
- */
5075
- function presetKey(version, config) {
5076
- return `${version}\n${canonical(config)}`;
5077
- }
5078
- /**
5079
5201
  * The bytes of a failed attempt that may be reused for the revision `key` names.
5080
5202
  *
5081
5203
  * A fresh object every time, never the held record itself: the caller fills it in
@@ -5099,6 +5221,145 @@ window.__ModuleLoader__.load({
5099
5221
  function presetMissing(slots, images) {
5100
5222
  return slots.filter((slot) => images[slot] === void 0);
5101
5223
  }
5224
+ /** Every slot the profile installs, in list order — the download set. */
5225
+ const PROFILE_SLOTS = [
5226
+ {
5227
+ slot: "m1",
5228
+ path: "/bfs/new_dyn/e4565384e7e5b2d75756e577f505534b4168597.png"
5229
+ },
5230
+ {
5231
+ slot: "m2",
5232
+ path: "/bfs/new_dyn/aa6e8649701c91e5ac8f43ff28b97a924168597.png"
5233
+ },
5234
+ {
5235
+ slot: "m3",
5236
+ path: "/bfs/new_dyn/5da92b31df5ee2a20b17ec6be353cf5c4168597.png"
5237
+ },
5238
+ {
5239
+ slot: "m4",
5240
+ path: "/bfs/new_dyn/14bb1e53cb2c13ca80bd23e3c1d2a04e4168597.png"
5241
+ },
5242
+ {
5243
+ slot: "m5",
5244
+ path: "/bfs/new_dyn/f51fff40de2d6eaa5a73d177f7bbfa794168597.png"
5245
+ },
5246
+ {
5247
+ slot: "m6",
5248
+ path: "/bfs/new_dyn/cfa71d4e60a2f12aaeec3efe1aa680b34168597.png"
5249
+ },
5250
+ {
5251
+ slot: "m7",
5252
+ path: "/bfs/new_dyn/c09d34b79d9b447d7db98da68b97afc54168597.png"
5253
+ }
5254
+ ].map((i) => i.slot);
5255
+ /**
5256
+ * Identity of this generation of the built-in profile.
5257
+ *
5258
+ * Only one thing reads it: what a failed download KEEPS for the next press
5259
+ * (`PresetHeld.key` in ./preset). Editing the rules or the pictures below must bump
5260
+ * it, or a retry could staple a wallpaper from the old set to a config from the new
5261
+ * one — the same reason the remote profile was compared by value rather than
5262
+ * assumed. Nothing else in the plugin persists it.
5263
+ */
5264
+ const RECOMMENDED_REVISION = "builtin-1";
5265
+ /**
5266
+ * One rule of the profile: the match string, its picture and that picture's color.
5267
+ *
5268
+ * The color is a per-image value (schema 10), and the triple here is the one the
5269
+ * profile has always carried — an HSL accent per model, used to tint the interface
5270
+ * while its wallpaper is on screen.
5271
+ */
5272
+ function profileRule(id, match, slot, color) {
5273
+ return {
5274
+ id,
5275
+ images: [defaultImage(slot, color, "fill")],
5276
+ match,
5277
+ enabled: true,
5278
+ color: null,
5279
+ rotate: defaultRotation()
5280
+ };
5281
+ }
5282
+ /**
5283
+ * The profile, as a fresh config every call.
5284
+ *
5285
+ * ORDER IS THE FALLBACK: matching runs top→bottom and the first usable rule is what
5286
+ * an unmatched model gets, so `deepseek` leads — the model this plugin was written
5287
+ * for. Everything below the rules is the interface numbers a fresh install already
5288
+ * starts on (`DEFAULT_PART_OPACITIES` and friends), which is what makes installing
5289
+ * the profile "add the rules and the pictures" on a clean store.
5290
+ *
5291
+ * Deliberately NOT taken from `freshThemeConfig()`: that one seeds a single BLANK
5292
+ * rule, and this one must not inherit it.
5293
+ */
5294
+ function recommendedConfig() {
5295
+ return {
5296
+ rules: [
5297
+ profileRule("r1", "deepseek", "m3", [
5298
+ 191.88931210132625,
5299
+ .5046198735613555,
5300
+ .82
5301
+ ]),
5302
+ profileRule("r2", "gpt", "m1", [
5303
+ 263.5122459614382,
5304
+ .31076923076923096,
5305
+ .82
5306
+ ]),
5307
+ profileRule("r3", "claude", "m2", [
5308
+ 18.923814597762384,
5309
+ .5486699795381468,
5310
+ .6
5311
+ ]),
5312
+ profileRule("r4", "qwen", "m4", [
5313
+ 26.25977410947,
5314
+ .6228354978354975,
5315
+ .82
5316
+ ]),
5317
+ profileRule("r5", "glm", "m5", [
5318
+ 30.15559737458117,
5319
+ .6552284141830319,
5320
+ .7646280172718907
5321
+ ]),
5322
+ profileRule("r6", "kimi", "m6", [
5323
+ 229.39645097591443,
5324
+ .5752269829617896,
5325
+ .2
5326
+ ]),
5327
+ profileRule("r7", "grok", "m7", [
5328
+ 217.3791348600509,
5329
+ .7702645947081058,
5330
+ .2
5331
+ ])
5332
+ ],
5333
+ opacities: { ...DEFAULT_PART_OPACITIES },
5334
+ transition: defaultTransition(),
5335
+ settingsOpacity: 1,
5336
+ chatTextOpacity: DEFAULT_CHAT_TEXT_OPACITY,
5337
+ trajectoryOpacity: DEFAULT_TRAJECTORY_OPACITY,
5338
+ rightbarOpacity: DEFAULT_RIGHTBAR_OPACITY,
5339
+ pageOpacity: 0,
5340
+ autoExtract: true,
5341
+ holidays: {
5342
+ enabled: true,
5343
+ items: defaultHolidayRules()
5344
+ }
5345
+ };
5346
+ }
5347
+ /**
5348
+ * The profile as the document both imports consume — the same shape `exportTheme`
5349
+ * writes, so everything downstream (the wipe, the slot loop) is the hand-picked
5350
+ * file's path.
5351
+ *
5352
+ * `images` is filled by the caller as the downloads land, which is what makes a
5353
+ * partially downloaded profile a resumable one rather than an all-or-nothing press.
5354
+ */
5355
+ function recommendedThemeFile(images = {}) {
5356
+ return {
5357
+ version: 10,
5358
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
5359
+ config: recommendedConfig(),
5360
+ images
5361
+ };
5362
+ }
5102
5363
  //#endregion
5103
5364
  //#region src/client/repaint.ts
5104
5365
  /**
@@ -5222,37 +5483,80 @@ window.__ModuleLoader__.load({
5222
5483
  //#region src/client/components/LiveSlider.tsx
5223
5484
  /**
5224
5485
  * Zero-lag slider: the thumb and value label update through DOM refs while
5225
- * dragging (onInput) so the caller can mutate the live UI directly without a
5226
- * React re-render; onChange commits the settled value. Double-click resets to
5227
- * the canonical default. The track fill is a gradient driven by the --pct
5228
- * custom property, updated imperatively alongside the thumb.
5486
+ * dragging, so the caller can preview the change without a React re-render; the
5487
+ * settled value is committed ONCE, when the gesture ends.
5488
+ *
5489
+ * The split is the whole point and it is not an optimisation detail. A range
5490
+ * input fires `input` on every frame of a drag, and React's `change` handler for
5491
+ * one is wired to that same event — so a slider that commits from `onChange`
5492
+ * commits dozens of times a second, and every commit is a full repaint (skin,
5493
+ * palette, wallpaper layer). That is why dragging a wallpaper effect cost a whole
5494
+ * `applyActive()` per frame while the cheap live writers beside it were never
5495
+ * reached.
5496
+ *
5497
+ * The gesture's end is therefore read from the events that actually end one: the
5498
+ * native `change` event, a pointer released over the track, and a blur. No timer
5499
+ * is armed — nothing here may compete with the plugin's one scheduler.
5229
5500
  */
5230
5501
  function LiveSlider({ min, max, step, def, fmt, label, ariaLabel, onInput, onChange }) {
5231
5502
  const inputRef = (0, react.useRef)(null);
5232
5503
  const valRef = (0, react.useRef)(null);
5504
+ /**
5505
+ * The value the current gesture has reached but has not committed yet.
5506
+ *
5507
+ * Non-null is also the answer to "is a gesture in flight?", which the `def` sync
5508
+ * below has to ask: the model watch re-emits on its own beat, and the render that
5509
+ * causes would otherwise snap the knob back to the committed value mid-drag.
5510
+ */
5511
+ const pending = (0, react.useRef)(null);
5512
+ /** `onChange` is read through a ref so the native listener cannot go stale. */
5513
+ const commitRef = (0, react.useRef)(onChange);
5514
+ commitRef.current = onChange;
5233
5515
  const paint = (el, v) => {
5234
5516
  el.style.setProperty("--pct", String((v - min) / (max - min) * 100));
5235
5517
  };
5236
- (0, react.useEffect)(() => {
5518
+ /** Move the knob and the label without calling anybody back. */
5519
+ const show = (v) => {
5237
5520
  if (inputRef.current) {
5238
- inputRef.current.value = String(def);
5239
- paint(inputRef.current, def);
5521
+ inputRef.current.value = String(v);
5522
+ paint(inputRef.current, v);
5240
5523
  }
5241
- if (valRef.current) valRef.current.textContent = fmt(def);
5524
+ if (valRef.current) valRef.current.textContent = fmt(v);
5525
+ };
5526
+ const commit = () => {
5527
+ const v = pending.current;
5528
+ if (v === null) return;
5529
+ pending.current = null;
5530
+ commitRef.current(v);
5531
+ };
5532
+ /** One frame of a gesture: the caller's cheap path, then remember the value. */
5533
+ const live = (v) => {
5534
+ show(v);
5535
+ pending.current = v;
5536
+ onInput?.(v);
5537
+ };
5538
+ (0, react.useEffect)(() => {
5539
+ if (pending.current !== null) return;
5540
+ show(def);
5242
5541
  }, [
5243
5542
  def,
5244
5543
  fmt,
5245
5544
  min,
5246
5545
  max
5247
5546
  ]);
5248
- const apply = (v) => {
5249
- if (inputRef.current) {
5250
- inputRef.current.value = String(v);
5251
- paint(inputRef.current, v);
5252
- }
5253
- if (valRef.current) valRef.current.textContent = fmt(v);
5254
- onInput?.(v);
5255
- onChange(v);
5547
+ (0, react.useEffect)(() => {
5548
+ const el = inputRef.current;
5549
+ if (el === null) return;
5550
+ const onEnd = () => commit();
5551
+ el.addEventListener("change", onEnd);
5552
+ return () => {
5553
+ el.removeEventListener("change", onEnd);
5554
+ commit();
5555
+ };
5556
+ }, []);
5557
+ const reset = () => {
5558
+ live(def);
5559
+ commit();
5256
5560
  };
5257
5561
  const name = label ?? ariaLabel;
5258
5562
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -5277,15 +5581,10 @@ window.__ModuleLoader__.load({
5277
5581
  defaultValue: def,
5278
5582
  "aria-label": name,
5279
5583
  title: name !== void 0 ? `${name} · ${fmt(def)}` : fmt(def),
5280
- onDoubleClick: () => apply(def),
5281
- onInput: (e) => {
5282
- const el = e.target;
5283
- const v = Number(el.value);
5284
- paint(el, v);
5285
- onInput?.(v);
5286
- if (valRef.current) valRef.current.textContent = fmt(v);
5287
- },
5288
- onChange: (e) => onChange(Number(e.target.value))
5584
+ onDoubleClick: reset,
5585
+ onInput: (e) => live(Number(e.target.value)),
5586
+ onPointerUp: commit,
5587
+ onBlur: commit
5289
5588
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5290
5589
  ref: valRef,
5291
5590
  className: "dab-slider-val",
@@ -6547,49 +6846,92 @@ window.__ModuleLoader__.load({
6547
6846
  });
6548
6847
  }
6549
6848
  /**
6550
- * The rule's three wallpaper effects on ONE row: the menu names the target and a
6551
- * single track edits it.
6849
+ * The selected image's three paint effects on ONE row: the menu names the target
6850
+ * and a single track edits it.
6851
+ *
6852
+ * Per IMAGE, like the layout mode, the framing and the colour above it. The move
6853
+ * is the same argument: a rotation through a daylight photo and a night shot does
6854
+ * not want one exposure, and a rule-level value forced one of them to live at the
6855
+ * other's compromise. A rule with no picture has nothing to expose, so the row is
6856
+ * DISABLED (not hidden) there — the same way the layout chips behave.
6552
6857
  *
6553
6858
  * The three ranges are not interchangeable — opacity and brightness are
6554
6859
  * percentages (brightness ceilings at the shared `WALLPAPER_BRIGHTNESS_MAX`),
6555
6860
  * blur is pixels — so changing the menu swaps the whole track: limits, formatter
6556
6861
  * and writer together. It never reinterprets one track under another name. The
6557
- * `key` remounts the input so `defaultValue` starts at the new field's current
6558
- * value; LiveSlider's drag-live path then works exactly as it did when the three
6559
- * tracks were stacked.
6862
+ * `key` remounts the input so `defaultValue` starts at the new target's value.
6863
+ *
6864
+ * A drag has TWO writers, and which one runs is the whole reason this row is not
6865
+ * a one-liner. `onInput` runs on every frame and must cost a wallpaper-layer
6866
+ * property write (./wallpaper's `setWpOpacity` / `setWpFilter`) — the same cheap
6867
+ * path the Interface page uses. `onChange` runs once, when the gesture ends, and
6868
+ * is the one that goes through `setImageEffects`: that is a full repaint (skin,
6869
+ * palette, wallpaper), and paying it per frame made a drag stall on a big image.
6560
6870
  */
6561
- function WallpaperEffectRow({ p, rule, field, onField }) {
6871
+ function WallpaperEffectRow({ p, rule, image, painted, field, onField, active }) {
6562
6872
  const { t } = p;
6873
+ /**
6874
+ * One frame of a drag: write the image in place, then touch only the layer
6875
+ * property that field owns.
6876
+ *
6877
+ * Gated on `active && painted`: the wallpaper layer paints the ACTIVE rule's
6878
+ * CURRENT image, so previewing any other one would write this card's numbers onto
6879
+ * a different picture's pixels — the visible lie the per-image split makes
6880
+ * possible and this gate prevents. The commit still lands, addressed by slot.
6881
+ *
6882
+ * Writing in place rather than through `setImageEffects` is deliberate and narrow:
6883
+ * that one bumps `rulesRev` and closes with a repaint, which is exactly the
6884
+ * per-frame cost being removed. Nothing is lost — the gesture's `onChange` hands
6885
+ * the settled value over, and that path clamps and saves.
6886
+ */
6887
+ const preview = (patch, paint) => {
6888
+ if (!active || !painted || image === void 0) return;
6889
+ Object.assign(image, patch);
6890
+ paint();
6891
+ };
6892
+ const slot = image?.slot;
6563
6893
  const spec = field === "opacity" ? {
6564
6894
  label: t("ruleOpacity"),
6565
6895
  min: 0,
6566
6896
  max: 100,
6567
6897
  step: 1,
6568
- def: Math.round(rule.wallpaperOpacity * 100),
6898
+ def: Math.round((image?.opacity ?? 1) * 100),
6569
6899
  fmt: (v) => `${v}%`,
6570
- onChange: (v) => p.setRule(rule.id, { wallpaperOpacity: v / 100 })
6900
+ preview: (v) => preview({ opacity: v / 100 }, () => setWpOpacity(v / 100)),
6901
+ onChange: (v) => {
6902
+ if (slot !== void 0) p.setImageEffects(rule.id, slot, { opacity: v / 100 });
6903
+ }
6571
6904
  } : field === "brightness" ? {
6572
6905
  label: t("ruleBrightness"),
6573
6906
  min: 0,
6574
6907
  max: 200,
6575
6908
  step: 1,
6576
- def: rule.brightness ?? 100,
6909
+ def: image?.brightness ?? 100,
6577
6910
  fmt: (v) => `${v}%`,
6578
- onChange: (v) => p.setRule(rule.id, { brightness: v })
6911
+ preview: (v) => preview({ brightness: v }, setWpFilter),
6912
+ onChange: (v) => {
6913
+ if (slot !== void 0) p.setImageEffects(rule.id, slot, { brightness: v });
6914
+ }
6579
6915
  } : {
6580
6916
  label: t("ruleBlur"),
6581
6917
  min: 0,
6582
6918
  max: 60,
6583
6919
  step: 1,
6584
- def: rule.blur,
6920
+ def: image?.blur ?? 0,
6585
6921
  fmt: (v) => `${v}px`,
6586
- onChange: (v) => p.setRule(rule.id, { blur: v })
6922
+ preview: (v) => preview({ blur: v }, setWpFilter),
6923
+ onChange: (v) => {
6924
+ if (slot !== void 0) p.setImageEffects(rule.id, slot, { blur: v });
6925
+ }
6587
6926
  };
6927
+ const disabled = image === void 0;
6588
6928
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6589
6929
  className: "dab-wp-row",
6590
6930
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
6591
6931
  className: "dab-select",
6592
6932
  value: field,
6933
+ disabled,
6934
+ title: disabled ? t("ruleEffectNoImage") : void 0,
6593
6935
  "aria-label": t("ruleEffectPick"),
6594
6936
  onChange: (e) => onField(e.target.value),
6595
6937
  children: [
@@ -6613,8 +6955,9 @@ window.__ModuleLoader__.load({
6613
6955
  step: spec.step,
6614
6956
  def: spec.def,
6615
6957
  fmt: spec.fmt,
6958
+ onInput: spec.preview,
6616
6959
  onChange: spec.onChange
6617
- }, field)]
6960
+ }, `${field}:${slot ?? ""}`)]
6618
6961
  });
6619
6962
  }
6620
6963
  function RuleCard({ p, rule, index, total, active, liveIndex, notify, sel, onSel, field, onField, onRemove }) {
@@ -6642,6 +6985,15 @@ window.__ModuleLoader__.load({
6642
6985
  const selIdx = rule.images.length === 0 ? 0 : Math.min(sel, rule.images.length - 1);
6643
6986
  const image = rule.images[selIdx];
6644
6987
  const url = image === void 0 ? null : p.imageOf(image.slot);
6988
+ /**
6989
+ * Whether the image being EDITED is also the one on screen.
6990
+ *
6991
+ * The wallpaper layer paints the active rule's CURRENT image, so a drag preview
6992
+ * may only write a layer property when the two coincide — otherwise the card's
6993
+ * numbers would land on another picture's pixels. This is the per-image split's
6994
+ * own version of the `active` gate beside it.
6995
+ */
6996
+ const painted = active && selIdx === liveIndex;
6645
6997
  const ownColor = image === void 0 ? rule.color : image.color;
6646
6998
  const setColor = (color) => {
6647
6999
  if (image === void 0) p.setRule(rule.id, { color });
@@ -7215,14 +7567,21 @@ window.__ModuleLoader__.load({
7215
7567
  })
7216
7568
  ]
7217
7569
  })] }),
7218
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7219
7571
  style: { marginTop: 16 },
7220
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WallpaperEffectRow, {
7572
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WallpaperEffectRow, {
7221
7573
  p,
7222
7574
  rule,
7575
+ image,
7576
+ painted,
7223
7577
  field,
7224
- onField
7225
- })
7578
+ onField,
7579
+ active
7580
+ }), image === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7581
+ className: "dab-hint",
7582
+ style: { marginTop: 8 },
7583
+ children: t("ruleEffectNoImage")
7584
+ }) : null]
7226
7585
  }),
7227
7586
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7228
7587
  style: { marginTop: 14 },
@@ -7459,19 +7818,15 @@ window.__ModuleLoader__.load({
7459
7818
  /**
7460
7819
  * The ways the recommended-profile download can fail, mapped to their copy.
7461
7820
  *
7462
- * They are worth telling apart because they ask different things of the user and
7463
- * of whoever maintains the assets repository: nothing answering is a network to
7464
- * retry, a mirror that answered with something that is not a profile is not
7465
- * going to start working on a retry, a profile written for a newer plugin is a
7466
- * "update this build", and too many images is a profile nobody should install.
7467
- * A failure that names a SLOT is a fifth case and is rendered separately — that
7468
- * one is the author's file naming, and only the slot tells them which.
7821
+ * Two cases, because the profile is now only PICTURES: the rules and their numbers
7822
+ * are compiled in, so there is no manifest to be malformed, no shape to be too new
7823
+ * and no config to name too many slots. What is left is the network ("retry") and
7824
+ * the cap on the total ("nobody should install this"). A failure that names a SLOT
7825
+ * is rendered separately — that one is about one BFS address, and the slot is what
7826
+ * says which.
7469
7827
  */
7470
7828
  const REC_ERROR_KEYS = {
7471
7829
  "download failed": "recFailNetwork",
7472
- "not a profile": "recFailNotProfile",
7473
- "newer version": "recFailNewer",
7474
- "too many images": "recFailTooMany",
7475
7830
  "too large": "recFailTooLarge"
7476
7831
  };
7477
7832
  function ProfilePage({ p, notify }) {
@@ -8155,12 +8510,12 @@ window.__ModuleLoader__.load({
8155
8510
  /**
8156
8511
  * Ceiling on what one recommended profile may add up to, across every wallpaper.
8157
8512
  *
8158
- * The node half caps each file and the number of files, but it answers one
8159
- * independent call at a time and never sees a running total — the client is the
8160
- * only side that does, so the total is bounded here. The real profile is 2.2 MB
8161
- * of images across seven of them, so this is generous by design: it exists to
8162
- * stop a config that names sixty-four eight-megabyte slots from turning one
8163
- * button press into half a gigabyte, not to second-guess the author.
8513
+ * The node half caps ONE file, but it answers one independent call at a time and
8514
+ * never sees a running total — the client is the only side that does, so the total
8515
+ * is bounded here. The real profile is 2.2 MB of images across seven of them, so
8516
+ * this is generous by design: it exists so that a build which grew a wrong URL
8517
+ * (or an edge that served an upload instead of the transform) cannot turn one
8518
+ * button press into half a gigabyte.
8164
8519
  */
8165
8520
  const PRESET_TOTAL_MAX = 33554432;
8166
8521
  /**
@@ -8171,7 +8526,8 @@ window.__ModuleLoader__.load({
8171
8526
  * CDN edge was still cold). What is being paid is per-request latency, and the
8172
8527
  * host speaks HTTP/2 — the bytes are identical either way and the connection is
8173
8528
  * reused — so overlapping them buys wall-clock time and risks nothing but a
8174
- * little contention.
8529
+ * little contention. The same holds for the Bilibili edges the pictures come from
8530
+ * now (see `PROFILE_HOSTS` in ../profile).
8175
8531
  *
8176
8532
  * Four rather than all seven: a burst that wide competes for the same bandwidth
8177
8533
  * and makes the per-file timeout harder to reason about, and four already hides
@@ -8550,13 +8906,7 @@ window.__ModuleLoader__.load({
8550
8906
  const added = [];
8551
8907
  for (const dataUrl of dataUrls) {
8552
8908
  const slot = nextSlot();
8553
- const image = {
8554
- slot,
8555
- bgMode: "fit",
8556
- bgState: { ...DEFAULT_BG_STATE },
8557
- color: null
8558
- };
8559
- rule.images.push(image);
8909
+ rule.images.push(defaultImage(slot));
8560
8910
  added.push(slot);
8561
8911
  setImage(slot, dataUrl);
8562
8912
  writeImage(slot, dataUrl);
@@ -8632,6 +8982,28 @@ window.__ModuleLoader__.load({
8632
8982
  saveConfig();
8633
8983
  repaintIfMoved(id, winner);
8634
8984
  },
8985
+ /**
8986
+ * Store ONE image's three paint effects. Addressed by slot like the mode and
8987
+ * the framing: how a picture is exposed belongs to that picture, and a rule's
8988
+ * rotation can run a daylight photo next to a night shot.
8989
+ *
8990
+ * `normalizeRuleInPlace` is the clamp, so a drag can never write a value the
8991
+ * next load would trim; the layer's own preview path (`setWpOpacity` /
8992
+ * `setWpFilter`) has already painted the value for the frames of the gesture.
8993
+ */
8994
+ setImageEffects: (id, slot, patch) => {
8995
+ const rule = ruleById(id);
8996
+ const image = rule?.images.find((i) => i.slot === slot);
8997
+ if (rule === null || image === void 0) return;
8998
+ const winner = winnerId();
8999
+ if (patch.opacity !== void 0) image.opacity = patch.opacity;
9000
+ if (patch.brightness !== void 0) image.brightness = patch.brightness;
9001
+ if (patch.blur !== void 0) image.blur = patch.blur;
9002
+ normalizeRuleInPlace(rule);
9003
+ rulesRev++;
9004
+ saveConfig();
9005
+ repaintIfMoved(id, winner);
9006
+ },
8635
9007
  setRuleRotation: (id, patch) => {
8636
9008
  const rule = ruleById(id);
8637
9009
  if (rule === null) return;
@@ -8684,12 +9056,7 @@ window.__ModuleLoader__.load({
8684
9056
  const res = await fetchImageUrl(slot, url);
8685
9057
  if (!res.ok) return res;
8686
9058
  const winner = winnerId();
8687
- rule.images.push({
8688
- slot,
8689
- bgMode: "fit",
8690
- bgState: { ...DEFAULT_BG_STATE },
8691
- color: null
8692
- });
9059
+ rule.images.push(defaultImage(slot));
8693
9060
  setImage(slot, res.dataUrl ?? null);
8694
9061
  rulesRev++;
8695
9062
  persistConfig();
@@ -8786,30 +9153,28 @@ window.__ModuleLoader__.load({
8786
9153
  }
8787
9154
  },
8788
9155
  /**
8789
- * Fetch the project's recommended profile. Downloads only — applying it is
8790
- * separate, because the page has to ask before anything is destroyed and the
8791
- * question should describe what is actually in hand.
9156
+ * Fetch the recommended profile's wallpapers. Downloads only — applying them
9157
+ * is separate, because the page has to ask before anything is destroyed and
9158
+ * the question should describe what is actually in hand.
8792
9159
  *
8793
- * The profile is a DIRECTORY, so this is a pool rather than one call: the
8794
- * config names the slots and each wallpaper is one request, up to
8795
- * `PRESET_CONCURRENCY` at a time. Each file keeps its own mirror fallback,
8796
- * retry rounds (the node half's business, ./preset), validation and slot name
8797
- * in a failure.
9160
+ * The RULES are not fetched at all: they are compiled into the bundle
9161
+ * (./profile), so this is a set of picture transfers and nothing else. That
9162
+ * is why there is no manifest round, no shape gate and no "not a profile"
9163
+ * failure left — nothing remote can decide what this installs.
9164
+ *
9165
+ * Each slot is one request, up to `PRESET_CONCURRENCY` at a time, and each
9166
+ * file keeps its own host fallback, retry rounds (the node half's business,
9167
+ * ./preset), validation and slot name in a failure.
8798
9168
  *
8799
9169
  * STRICT: the first wallpaper that cannot be had stops the queue. What had
8800
9170
  * already arrived is kept for the next press (`presetHeld` below) — the
8801
9171
  * difference between retrying one file and retrying the lot. Nothing is
8802
- * touched here, so "the author renamed a file" arrives as an error naming the
8803
- * file rather than as a profile quietly missing a picture.
9172
+ * touched here, so a wrong URL arrives as an error naming the slot rather
9173
+ * than as a profile quietly missing a picture.
8804
9174
  */
8805
9175
  fetchRecommended: async (onProgress) => {
8806
- const head = await fetchPresetConfig();
8807
- if (!head.ok) return {
8808
- ok: false,
8809
- error: head.error
8810
- };
8811
- const slots = head.slots;
8812
- const key = presetKey(head.version, head.config);
9176
+ const slots = PROFILE_SLOTS;
9177
+ const key = RECOMMENDED_REVISION;
8813
9178
  const images = presetResume(presetHeld, key);
8814
9179
  let bytes = 0;
8815
9180
  for (const url of Object.values(images)) bytes += url.length;
@@ -8857,12 +9222,7 @@ window.__ModuleLoader__.load({
8857
9222
  presetHeld = null;
8858
9223
  return {
8859
9224
  ok: true,
8860
- preset: {
8861
- version: head.version,
8862
- exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
8863
- config: head.config,
8864
- images
8865
- }
9225
+ preset: recommendedThemeFile(images)
8866
9226
  };
8867
9227
  },
8868
9228
  /** Replace EVERYTHING with a downloaded profile: empty the store, then write it. */