dsh-background-by-model 0.7.10 → 0.8.0-test.1
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 +81 -90
- package/CHANGELOG.zh.md +76 -86
- package/README.md +87 -124
- package/README.zh.md +94 -131
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,128 +1,119 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
> The full release history lives here; the **Recent optimizations** section of [README.md](./README.md#recent-optimizations) lists only the two most recent releases (v0.
|
|
3
|
+
> The full release history lives here; the **Recent optimizations** section of [README.md](./README.md#recent-optimizations) lists only the two most recent releases (v0.8.0 / v0.7.10).
|
|
4
4
|
|
|
5
|
-
## v0.
|
|
5
|
+
## v0.8.0
|
|
6
6
|
|
|
7
|
-
- **The
|
|
7
|
+
- **The bundle is built when the tarball is packed, instead of being committed** — `lib/` was ~1 MB of generated JavaScript in every release and effectively all of this repository's growth, while keeping it in step with `src/` was something a person had to remember. There is no `prepublishOnly` hook here, so a stale bundle shipped in silence and nothing in either flow would have noticed. It is now gitignored and built by `prepack`, which npm runs for both `npm pack` and `npm publish`, so what is published is always built from the tagged source. Nothing changes for anyone installing the package: the same three files arrive, built a minute earlier.
|
|
8
|
+
- **The `github:` install route is gone, deliberately** — `dsh plugin --profile web add github:HarmlessFunny/dsh-background-by-model` fetched a tree that no longer contains `lib/` and has nothing in it that builds one, so it could only produce a plugin that would not load. npm is the only source now and the `npx` form follows it; the third method, a working copy built from source, says up front that the build is a step you have to run.
|
|
9
|
+
- **Releases are published from GitHub Actions over OIDC, so the versions carry build provenance** — the npm token is out of the release path entirely, and npm attaches a provenance attestation binding each version to the commit and the workflow that produced it. Every version up to v0.7.10 was uploaded by hand and can claim nothing of the kind; from here they are checkable rather than merely asserted.
|
|
10
|
+
- **The README and this file are cut back to the register of the v0.3.x entries** — a feature is introduced in a sentence, the ten checks are one line each, and the FAQ keeps all fifteen questions with each answer cut to the sentence carrying the fact. Nothing a user needs was removed; what went was the narration of how each change was arrived at.
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
## v0.7.10
|
|
10
13
|
|
|
11
|
-
- **
|
|
14
|
+
- **The festival art's cache is keyed by the release the plugin pins, so an updated plugin is answered by a download instead of by the previous generation's bytes** — the cache lived at `holiday-cache/<asset>` and `readHolidayAsset` returned any file that existed and was a valid image, with no version comparison, no network and no log line. Pointing a build at a new release of the assets repository therefore changed the URL it would have fetched and nothing about what it returned. `holidayCachePath` now carries the tag (`holiday-cache/v0.1.0/mid-autumn.webp`), which makes the release part of the file's identity: a build pinning a new tag looks in a directory nobody has written to, so the first holiday after an update is a cache miss, and `ls holiday-cache/` answers which generations are on this disk.
|
|
15
|
+
- **A superseded generation is dropped, after the new art is on disk and never before it.** A versioned key on its own would leave every generation lying there forever — about 730 KB each, including assets a later release stops shipping at all. `pruneHolidayCache` runs at the end of a successful cache write and removes the generation directories that are not this build's tag, so a pinned release that turns out to be unreachable cannot cost a user the art they already had. It is best-effort, and `resetStore` still leaves every generation alone.
|
|
16
|
+
- **Gates** — `check:node` derives both URL assertions and the cache path from one `REF`, and pins that art missing from the pinned generation is re-downloaded rather than read stale, that a superseded generation is gone once the new art has landed, and that the generation in use is left alone. `pnpm test` is ten checks, all passing.
|
|
12
17
|
|
|
13
18
|
## v0.7.9
|
|
14
19
|
|
|
15
|
-
- **Every asset reference is pinned to one release tag of the assets repository (`v0.1.0`), and the three that had drifted apart now agree** — the README's
|
|
16
|
-
|
|
17
|
-
- **The
|
|
18
|
-
|
|
19
|
-
- **The recommended profile gives up "correct it without a release", deliberately** — it followed a branch because its case for immutability was the opposite of the festival art's: a profile fails LOUDLY (the user is looking at the button and gets an error naming the missing slot) where a holiday fails silently on a day nobody is watching, so pinning bought nothing there and a branch bought the ability to fix a bad recommendation by editing one JSON file. That is what is traded away. A branch also bought a plugin whose bytes could change under a user between the build and the click, and with one tag covering the screenshots, the art and the profile, no fetched byte can do that any more. The price is named rather than left to be discovered: correcting one JSON file now costs a tag in `assets`, a new value in `src/index.ts` and a release on npm — and a tag is not a commit. It **can** be re-pointed, while jsDelivr serves a version reference as immutable, so re-pointing one takes a purge request instead of a push, which makes a correction a new tag rather than a force-push of the old one. That is exactly why `presetKey` survives this change: an edge that has not been purged can still answer with the previous bytes of the same URL, so two fetches of "the profile" are still not guaranteed to be one revision, and a press that resumes must not glue them together.
|
|
20
|
-
|
|
21
|
-
- **Gates** — `check:node` had pinned the old design in two assertions, and its `revision()` helper could not see a tag at all: it matched a 40-character SHA, which is the only reason the raw mirror's reference could be lifted out without an `@`. It now matches beside the repository name (`HarmlessFunny/assets@<ref>` in the jsDelivr shape, `HarmlessFunny/assets/<ref>/` in the raw one), so the two mirrors stay comparable, and both assertions require `v0.1.0` — the holiday art's became "pinned to the assets release tag, never to a branch" and the profile's became "read at the assets release tag, not at a branch", replacing the assertion that used to pin the branch on purpose. `pnpm test` is ten checks, all passing, and `lib/` is that run's output.
|
|
20
|
+
- **Every asset reference is pinned to one release tag of the assets repository (`v0.1.0`), and the three that had drifted apart now agree** — the README's screenshots read `main`, the festival art was pinned to a raw commit hash (`HOLIDAY_ASSETS_COMMIT`), and the recommended profile followed `main` deliberately: three references, three answers to "which bytes is this build looking at". It is now `HOLIDAY_ASSETS_REF = 'v0.1.0'`, with `PRESET_ASSETS_REF` carrying the same string, so one release of `HarmlessFunny/assets` is one version of every byte the plugin fetches — the art, the profile and the screenshots together — in a form a person can read, compare against the tags on GitHub and update in one place.
|
|
21
|
+
- **The festival art's bytes do not move with the name.** `58a9e107` is an ancestor of `v0.1.0` and both `holiday/*.webp` files are byte-identical between the two revisions, so the download is the same download. What did change is that the raw mirror carries the tag as well, which keeps "both mirrors serve the same revision" true by value rather than by convention.
|
|
22
|
+
- **The recommended profile gives up "correct it without a release", deliberately** — it followed a branch because its case for immutability was the opposite of the festival art's: a profile fails loudly (the user is looking at the button) where a holiday fails silently on a day nobody is watching. That is what is traded away: correcting one JSON file now costs a tag in `assets`, a new value in `src/index.ts` and a release on npm. And because jsDelivr serves a version reference as immutable, a re-pointed tag takes a purge request instead of a push — which is exactly why `presetKey` survives, since an edge that has not been purged can still answer with the previous bytes of the same URL.
|
|
23
|
+
- **Gates** — `check:node`'s `revision()` helper matched a 40-character SHA and could not see a tag at all; it now matches beside the repository name (`assets@<ref>` and `assets/<ref>/`), so the two mirrors stay comparable, and both assertions require `v0.1.0` instead of pinning a branch. `pnpm test` is ten checks, all passing.
|
|
22
24
|
|
|
23
25
|
## v0.7.8
|
|
24
26
|
|
|
25
|
-
- **A fresh install opens on one blank rule instead of the "no rules yet" screen** —
|
|
26
|
-
|
|
27
|
-
- **The
|
|
28
|
-
|
|
29
|
-
- **The recommended profile now retries per file, and a failed attempt hands what it fetched to the next one** — a file used to get exactly two chances (one per mirror), so a single 502, a reset connection or a CDN hiccup on the sixth of seven wallpapers threw away the six good downloads behind it, and the user's only recourse — pressing the button again — started from zero. Every file now gets up to three rounds, waiting 400 ms and then 800 ms between them (`PRESET_ATTEMPTS` / `presetRetryDelayMs` in `src/preset.ts`), and an attempt that fails holds the bytes it did get (`presetHeld`): the next press re-fetches the manifest and the config first (two small files) and reuses those bytes only while the config comes back **the same value** (`presetKey`, a canonical form with object keys sorted — a config the wire rebuilt in another key order must not cost the hold for nothing) — which turns "the sixth file failed" into one file to fetch instead of seven. Three failures are NOT retried, each for its own reason: 404/403 is "the file is not there" (the profile follows a branch, so an author renaming a file has to reach the slot **fast** and loudly rather than after two more rounds), oversized is "the bytes are the problem and the next round serves the same bytes", and a timeout is "sixty seconds of silence is not weather" (both mirrors were already asked inside the same round — and that is what keeps the worst case for one file at three minutes instead of six: a round in which BOTH mirrors were silent is over immediately, at the same two minutes this cost before there were rounds at all, and only a round mixing a silent mirror with a transient one can carry a minute into the next round). **The strictness is unchanged**: the first file that cannot be had still fails the whole attempt and still names the slot — what got cheaper is the retry, not the failure. The counter on the button now describes the **profile** rather than this attempt's transfers, so a resumed attempt opens on 6/7 and finishes on 7/7 (which is also why this change touches **no** interface code). The festival art deliberately does NOT get the rounds: it is fetched during boot, before the first wallpaper is painted, where a round's delay lands straight on the first frame — and it has a cache, with every later read trying again, while a button has no later read of its own.
|
|
30
|
-
|
|
31
|
-
- **Gates** — `pnpm test` is **ten** checks now (the new `check:preset`). `check:node` gains: a data directory with no config file reads as exactly one rule; that rule is blank (no match string, no colour, no image); it is rule 1, and therefore the fallback; and a rule list written explicitly empty still reads back empty — the shape of the fresh-install rule, and the distinction that keeps it from being seeded back. The assertion after `resetStore` was rewritten to the same meaning, since it used to pin the old behaviour ("an emptied store reads as an empty config"). The retry itself is pinned **by request count**: with both mirrors answering 503 once, the same file is asked again and arrives (three lines in the log — jsDelivr, raw, jsDelivr again); a file the host does not have still costs exactly two requests (one per mirror, no waiting); and a permanently broken mirror costs exactly six (three rounds over two mirrors — the retry has a budget). `check:ui` gains section 6: every `var(--dsw-…)` the UI reads must be a token the installed theme actually publishes — with the theme bundle absent (peer dependencies not installed) the section skips with a notice, because that is a fact about this host and not about this repository; this class of failure reports itself nowhere, the declaration is dropped and the property quietly falls back to an inherited value. The new `check:preset` pins the policy and the wiring on both sides: each of the six failure reasons judged on its own (the three terminal ones are the three that can be decided locally and cheaply, and a timeout is not retried because both mirrors were already asked inside the same round), a wait that grows and stays bounded, a revision key that treats two key orders of one value as one revision while the image list's ORDER counts (it is the rotation), a resume that hands back a copy rather than the held record itself, and the two halves actually using it — the node half looping over the round budget and asking the policy, the client sizing its pool by what is missing rather than by the whole slot list, holding what arrived on failure and clearing it on success, and applying the size cap **before** a file is kept (applied after, the attempt that blows it holds every slot, and the resume then has nothing left to fetch and passes an oversized profile straight through).
|
|
27
|
+
- **A fresh install opens on one blank rule instead of the "no rules yet" screen** — with no config file at all `migrateLegacy` answered `rules: []`, and `normalizeConfig` read that empty array as "the user deleted every rule". Only a **missing** `rules` key now means "nothing has ever been configured", and it is answered with one blank rule — id `r1`, so it is rule 1, the fallback and the tab the panel selects. A key that is there and empty is still the user's own deletion and stays empty. The factory moved into `src/schema.ts` (`blankRule`) and is shared by the shipped default, the sanitizer and the client's "add rule".
|
|
28
|
+
- **The placeholder was not "too bright": the declaration never applied** — `--dsw-alias-label-quaternary` is defined nowhere in the harness theme, so the declaration was dropped at computed-value time and the placeholder fell back to `inherit`, the input's own `label-primary`. All three placeholder inputs now read `--dsw-alias-label-caption`, the token `PLACEHOLDER_RULE` already hands the composer. Two more dead names went with it: `.dab-brand-tag`'s dead alias, and `.dab-pick-n`'s `--dsw-mono` for `--dab-mono`.
|
|
29
|
+
- **The recommended profile retries per file, and a failed attempt hands what it fetched to the next one** — a file used to get exactly two chances (one per mirror), so a single 502 on the sixth of seven wallpapers threw away the six good downloads behind it. Every file now gets up to three rounds, waiting 400 ms and then 800 ms, and an attempt that fails holds the bytes it did get, reusing them only while the config comes back as the same value (`presetKey`). 404/403, oversized and timeout are not retried, each for its own reason. The strictness is unchanged — the first file that cannot be had still fails the whole attempt and names the slot — and the button's counter now describes the profile, so a resumed attempt opens on 6/7.
|
|
30
|
+
- **Gates** — `pnpm test` is ten checks (the new `check:preset`). `check:node` pins the fresh-install rule and the distinction that keeps it from being seeded back, and pins the retry **by request count**: three lines for one transient 503, two requests for a file the host does not have, six for a permanently broken mirror. `check:ui` gains a section requiring every `var(--dsw-…)` the UI reads to be a token the installed theme actually publishes, skipping with a notice when the theme bundle is absent.
|
|
32
31
|
|
|
33
32
|
## v0.7.7
|
|
34
33
|
|
|
35
|
-
- **A fresh install starts on the author's own profile, and it needs no network** — the shipped
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
- **
|
|
40
|
-
|
|
41
|
-
- **The percentage pill in each Interface card is gone** — the number was already on the slider row directly below it, so the card head is the icon and the name again and `.dab-part-badge` goes with it, stylesheet rule included.
|
|
42
|
-
|
|
43
|
-
- **Gates** — `pnpm test` is still nine checks; the defaults above moved `check:code-plate` (the code plate ships on the profile's `81%`) and `check:node`, whose new assertions are written **by value** so that a constant changed without the profile — or the profile changed without the constants — fails a check instead of drifting out of step: the appearance numbers a config with no file at all comes up with, an explicit `null` preview panel reading back as itself, a number of its own surviving, a hand-edit that is not a number landing on the shipped `45%`, and the festival switch through both booleans and a non-boolean.
|
|
34
|
+
- **A fresh install starts on the author's own profile, and it needs no network** — the shipped Interface defaults now ARE the numbers `preset/theme-config.json` carries: main background `0%`, left panel `10%`, right panel `45%`, cards `100%`, code `81%`, inputs `88%` with `60 px` of blur, settings `100%`, conversation text `35%`, trajectory `73%`. Every one of those values used to be chosen per key for **safety** rather than for looks, which is why a new install opened on a pale approximation of the host. The constants live in `src/schema.ts` under a note that they are copied from a live data directory.
|
|
35
|
+
- **`rightbarOpacity` keeps its "never owned" state while a fresh install starts owned** — the preview panel's `null` means "follow `opacities.bg`", and every release so far has written it, because a panel the user never dragged is persisted as an explicit `null`. `normalizeRightbarOpacity` therefore hands the shipped `45%` only to a key that is not there at all; treating `null` as missing would have silently repainted the preview panel of every existing configuration.
|
|
36
|
+
- **The festival switch ships OFF** — a first install paints what the model rules say and nothing else, and the single switch on the Config page is how the feature is asked for. The recommended profile still turns it **ON**. The sanitizer is now "a real boolean is honoured, anything else takes the shipped default", so the absent field of a config written before the feature existed and junk from a hand-edit both mean OFF.
|
|
37
|
+
- **The percentage pill in each Interface card is gone** — the number was already on the slider row directly below it, so the card head is the icon and the name again and `.dab-part-badge` goes with it.
|
|
38
|
+
- **Gates** — `pnpm test` is still nine checks; the new defaults moved `check:code-plate` and `check:node`, whose assertions are written **by value** so that a constant changed without the profile — or the profile changed without the constants — fails a check instead of drifting out of step.
|
|
44
39
|
|
|
45
40
|
## v0.7.6
|
|
46
41
|
|
|
47
|
-
- **The rules page turns from a stack of self-growing cards into one tab strip and one editor** —
|
|
48
|
-
|
|
49
|
-
- **The
|
|
50
|
-
|
|
51
|
-
- **The
|
|
52
|
-
|
|
53
|
-
- **A recommended profile, installed in one click** — the Config page gained a third card that downloads the author's own configuration from the same [`HarmlessFunny/assets`](https://github.com/HarmlessFunny/assets) repository and writes it in place of yours. **It is hosted in the store's own shape**: `preset/preset.json` (the shape gate), `preset/theme-config.json` (6.5 KB of readable JSON) and `preset/modelbg-<slot>` (one raw file per slot). It was first written as a single theme file with every wallpaper inlined as base64 — 2.9 MB where 99.8% of the bytes were a picture, which is not a document anyone can hand-edit, since changing one rule's `match` string means finding it inside one line of several hundred thousand characters. That is why the format was changed: editing the config is now editing 6.5 KB of JSON on GitHub, replacing a wallpaper is replacing a file, and publishing the whole thing is a `cp` out of `~/.dsh/.dsh-background-by-model-data/`. On the node side a new `fetchPresetConfig` (fetch the manifest and the config, run the SHARED sanitizer on this side, then derive which files to ask for) and `fetchPresetImage` (one file by slot, magic-byte checked, returned as a data URL) replace the old single-document `fetchPreset`, both reusing the general `fetchAsset` the previous release split out of the festival-art downloader. **The request set is derived from the config rather than from a second manifest**, so adding a wallpaper is adding a rule entry and a file with nothing to keep in sync — which also means an untrusted config decides how many times this process goes to the network, so every slot name is checked against `SLOT_RE` (in `normalizeImage`) and the count is capped besides. **The two holiday slots a config legally names are dropped rather than requested**: `holidays.items` always carries `h-midautumn` / `h-nationalday`, no `modelbg-h-*` file can ever exist (a hard assertion added by the previous release), and asking for one would fail the whole feature on a file the plugin itself guarantees is not there. The client fetches **one file per request**, four of them in flight at a time — measured at 8.3 s sequential against 4.8 s concurrent for the real profile, since what is being paid is per-request latency over HTTP/2 — showing `3/7` on the button; each file therefore gets its own mirror fallback and its own validation. **One missing wallpaper fails the whole attempt and names the slot** — a profile that quietly arrived short a picture is precisely the silent failure that pinning the festival art to a commit exists to prevent, and naming it is what keeps a branch reference acceptable here. It still lands through the same `applyThemeFile` a hand-picked file goes through (assembled on the client into the shape `exportTheme` writes), so the version gate, the sanitizer and the wipe are one path; `preset.json` is what lets the version gate fire before a single wallpaper is requested. It remains the only destructive action in the panel: the download happens before the question (so the confirmation can state what is actually in hand), the wipe happens only after the download succeeds (an unreachable CDN must not cost the user their configuration on the way to a profile that never arrived), a host too old to write this shape is refused before anything is deleted, `holiday-cache/` is untouched, and nothing is cached to disk. `check:node`'s stub grew a "serve a whole directory by file name, a name it does not hold is a 404" mode, covering: the manifest and config fetched in order and sanitized on arrival, the request set derived from the rules with the holiday slot dropped, one request per wallpaper with its type sniffed rather than trusted from a header, a fallback to raw for a single file, a named wallpaper the host does not have being an error rather than a short profile, the two refusals that happen before any request at all (a holiday slot, a malformed slot name), a manifest version that is not a number, a profile written for a newer shape refused before anything is fetched, a config that is not JSON, one that is a JSON array, one naming more images than the cap, and a mirror announcing an oversized body.
|
|
54
|
-
|
|
55
|
-
- **The published package drops from 1.1 MB to about 220 kB** — besides the two festival pictures above, two other things were taking up room that the people downloading this plugin should not be paying for. **The documentation screenshots moved out**: `example_img/` (three webp files, 220 KB) now lives in the same assets repository under `dsh-background-by-model/`, and both READMEs reference it by absolute jsDelivr URL — the screenshots were put into `files` in the first place so the images on the npm page would not break, and an absolute URL does that too without putting binaries in the tarball or their history in `.git`. `screenshots.json` is deleted: nothing in the whole DSH installation or `~/.dsh` consumes it. **`lib/client.js.map` is no longer published**: it is a source map for the browser bundle, 624 KB, useless to anyone installing the package, and it stays in the repository (anyone debugging should be debugging a local build anyway). Ten files, about 220 kB packed / 690 kB unpacked, against thirteen files and 1.1 MB / 2.0 MB before. (Both figures are deliberately approximate — this entry and the two READMEs are themselves part of the number, so they nudge it every time they are edited. What the number says is the point: after the pictures left, the tarball is code and documentation and very little else.)
|
|
42
|
+
- **The rules page turns from a stack of self-growing cards into one tab strip and one editor** — every rule is a tab and exactly one editor is mounted, so the page height is a function of one card plus the strip, whatever the rule count. The tabs carry what only the card head used to show: the number, a thumbnail, the match string, and the two states worth knowing unopened (whether it is in use, and whether it rotates or is off); the strip scrolls sideways rather than wrapping, for the same reason. The `+` and the "which rule" menu sit **outside** the scroller, because inside, the only control that can add a rule scrolled away from the end of a long list. The match tester moved to the profile page, which is what it is about. The two decisions behind the strip are pure and now live in `src/client/rules-view.ts`, pinned by the new `check:rules`.
|
|
43
|
+
- **The rotation card's controls were reshuffled, at your request** — the Next button and the "On screen 2 / 3" readout moved out of the rotation switch's gate (that readout is the only place the card says which picture is showing, and it vanished with the switch); "also step on a model switch" joined the order row, where all three answer "when does a picture change"; "1 hour" is gone from the dwell presets; the row of eight fixed swatches is deleted with its only readers (the `PALETTE` table and the `.dab-swatch*` rules); and the whole rotation block moved into the second column, below the theme colour, out of the collapsed card's worst spot.
|
|
44
|
+
- **The festival art stops shipping in the package: it is fetched on the day, and cached by asset** — the two wallpapers (228 KB + 500 KB) were two thirds of the tarball, for a picture that is looked at one day a year. They now live in [`HarmlessFunny/assets`](https://github.com/HarmlessFunny/assets), and the node half downloads, validates and caches one on the first read of the day under `~/.dsh/.dsh-background-by-model-data/holiday-cache/`. The reference is pinned to a commit, never a branch; `cdn.jsdelivr.net` is tried first with raw as the fallback, both at the same revision; a failed download is a normal state, so the model rules keep painting; and a cache entry that is not an image is treated as a miss rather than trusted. The cache stays keyed by **asset** name, deliberately outside the slot namespace, so `writeImage` still refuses a holiday slot.
|
|
45
|
+
- **A recommended profile, installed in one click** — a third card on the Config page downloads the author's own configuration from the same assets repository: `preset/preset.json` (the shape gate), `preset/theme-config.json` (6.5 KB of readable JSON) and one `preset/modelbg-<slot>` file per wallpaper. It was first written as a single theme file with every wallpaper inlined as base64 — 2.9 MB in which 99.8% of the bytes were a picture, and not a document anyone can hand-edit. The request set is derived from the config rather than from a second manifest, so every slot name is checked against `SLOT_RE` and the count is capped; the two holiday slots a config legally names are dropped rather than requested; and one missing wallpaper fails the whole attempt and names the slot. It remains the only destructive action in the panel: the download happens before the question, and the wipe only after the download succeeds.
|
|
46
|
+
- **The published package drops from 1.1 MB to about 220 kB** — the documentation screenshots moved to the same assets repository and both READMEs reference them by absolute jsDelivr URL (which is what putting them in `files` was for), `screenshots.json` is deleted since nothing consumes it, and `lib/client.js.map` (624 KB) is no longer published though it stays in the repository. Ten files, about 220 kB packed / 690 kB unpacked, against thirteen files and 1.1 MB / 2.0 MB before.
|
|
56
47
|
|
|
57
48
|
## v0.7.5
|
|
58
49
|
|
|
59
|
-
- **The layout mode belongs to the picture, not the rule** — 适应 / 填充 / 拉伸 / 平铺 / 居中 was one value per rule
|
|
60
|
-
- **`SCHEMA_VERSION` moves to 6, because both directions of this move are silent (a fix)** — a schema-5 host rebuilds every image entry from the keys it knows, so a per-image mode written to it would vanish on the first write
|
|
61
|
-
- **A holiday's image is 填充 by definition** — full-bleed festival art must not letterbox. The mode
|
|
62
|
-
- **The fade only animates what the palette in force writes (a fix)** — reported as
|
|
63
|
-
- **The code block follows the palette, and its plate has an opacity of its own (a fix)** — reported as
|
|
64
|
-
- **Four small ones (a fix)** —
|
|
65
|
-
- **Gates** — `pnpm test` is now **eight** checks. `check:code-plate`
|
|
50
|
+
- **The layout mode belongs to the picture, not the rule** — 适应 / 填充 / 拉伸 / 平铺 / 居中 was one value per rule covering every picture of a rotation, so a landscape photo and a tall screenshot could not be framed differently. The mode now lives on `images[].bgMode`, read back from the image being painted, and the chip row under the filmstrip edits the image selected there. The row is **disabled** rather than hidden on a rule with no picture. README and README.zh were corrected everywhere they claimed the mode was the rule's.
|
|
51
|
+
- **`SCHEMA_VERSION` moves to 6, because both directions of this move are silent (a fix)** — a schema-5 host rebuilds every image entry from the keys it knows, so a per-image mode written to it would vanish on the first write; and a schema-6 read of a schema-5 config has to **lift** the rule-level value onto each of its images, or every existing wallpaper would land on the default. `bgMode` joined `LEGACY_RULE_KEYS`, so the host log does not report the field it is deliberately consuming as drift.
|
|
52
|
+
- **A holiday's image is 填充 by definition** — full-bleed festival art must not letterbox. The mode comes from the definition like the slot and the colours beside it, so a hand-edited value cannot repaint a festival; the framing next to it stays yours.
|
|
53
|
+
- **The fade only animates what the palette in force writes (a fix)** — reported as a white block fading to black and snapping back, with the white value measured as `237, 243, 254`. That number is the answer: `#edf3fe` is `--dsw-static-deepseek-50`, the host's light-scheme user bubble, and the fade sheet had registered it because `fadeTokenNames()` asks for the **dark** branch's token list while the light branch writes only 35 of its 80 names. The other 45 still belong to the host, which changes them on a schedule of its own — and this plugin is what provoked it, by disposing and re-registering its theme on every colour change. A scope's token is now emitted only when the palette **in force** writes it, and a skin whose `colorScheme` already holds is not rebuilt at all.
|
|
54
|
+
- **The code block follows the palette, and its plate has an opacity of its own (a fix)** — reported as dark green ink on a near-black plate inside a **light** interface. The host's shiki sheet substitutes `var(--dsw-alias-markdown-code-block)` where it is DECLARED, on `:root`, while the plugin declared those tokens on `body` only, so on the root element there was nothing to read but the host's own colour. The palette rule is now emitted for `:root` as well, `:root` joins the fade scopes, and the plate and its banner are palette tints again. A new **`code`** opacity part drives both surfaces, each through its own plugin-owned variable; it defaults to opaque, which is what a code block is.
|
|
55
|
+
- **Four small ones (a fix)** — with rotation on, deleting images until one was left kept the switch reading "on" while the panel disabled it below two images, so it could not be turned off either; removing an image now clears `enabled` when fewer than two are left, and `normalizeRule` enforces the same invariant for everything that does not pass through the panel. "切换时长" was printed twice, as a section heading and again as the slider's label. And the profile footer's package name and the brand tile are links to the project's repository, both reading the one address in `repo.ts`, which `check:ui` compares against `package.json`.
|
|
56
|
+
- **Gates** — `pnpm test` is now **eight** checks. `check:code-plate` and `check:image-mode` join the suite; `check:rotation` gains the switch-and-count invariant, `check:ui` the repository-link agreement, `check:transition` the ownership rule, and `check:repaint` the two lines that arm the fade.
|
|
66
57
|
|
|
67
58
|
## v0.7.4
|
|
68
59
|
|
|
69
|
-
- **The controls inside the dialog now change colour on the wallpaper's clock instead of jumping when it finishes (a fix)** — on a theme-colour change, the controls in the settings dialog
|
|
70
|
-
- **What holds is an intermediate element, not "inheritance cannot see an animated value"** —
|
|
71
|
-
- **The fix: the transition list is per scope, and a scope only gets the tokens it declares** — `paletteFadeCss`
|
|
72
|
-
- **
|
|
73
|
-
- **
|
|
74
|
-
- **
|
|
75
|
-
- **
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- **The
|
|
80
|
-
- **
|
|
81
|
-
- **
|
|
82
|
-
- **
|
|
83
|
-
- **
|
|
84
|
-
- **The
|
|
85
|
-
- **
|
|
60
|
+
- **The controls inside the dialog now change colour on the wallpaper's clock instead of jumping when it finishes (a fix)** — on a theme-colour change, the controls in the settings dialog held the **previous** colour for the whole duration and only took the new one once the background had finished. Measured in a real session: `body`'s own token interpolated correctly across the three seconds, while the active chip's `background-color` was bit-for-bit the OLD value at every sample and only reached the new one after the animation ended.
|
|
61
|
+
- **What holds is an intermediate element, not "inheritance cannot see an animated value"** — a consumer that merely inherits follows the declaring element frame for frame, which an isolated repro confirms. The plugin armed the **full token list** on three elements, two of which only re-declare the three layer tokens: give an inherited token a transition and that element's transition targets the value it saw when the fade was armed, so it becomes a damped copy of the animation and drags its whole subtree behind it.
|
|
62
|
+
- **The fix: the transition list is per scope, and a scope only gets the tokens it declares** — `paletteFadeCss` takes `scopes: { selector, tokens }[]` instead of a list of selectors: `body` carries the full list, and the dialog and the trajectory root each carry only the three layer tokens they re-declare. That is not a "do less work" optimisation; a scope handed a name it does not declare copies the lag above into its whole subtree.
|
|
63
|
+
- **Three surfaces were still snapping, and they are the ones painted from a colour of the plugin's OWN (a fix)** — classifying every pixel of the middle frame against the first and the last puts the settings dialog's own plate on the final colour while every card inside it is still on the first. A card that fades while the plate behind it does not is a different paint path: the plate is painted from `--dsh-any-bg-settings-surface`, a plugin-owned variable written inline, and an **unregistered** variable cannot interpolate at all. The same shape exists twice more — the file-preview panel and the Cordis panel.
|
|
64
|
+
- **The fix: a scope may name a STANDARD property, and the paint rule and the fade share one selector** — `PaletteFadeScope` gained `properties?: string[]`, emitted into the same `transition` list and deliberately not filtered against the registered names. `fadeScopes()` carries `background-color` for the settings plate and the file-preview panel and `--dsw-specific-menu` for the Cordis panel, and both paint rules and their fade scopes now read one shared constant each, so the half that paints and the half that fades cannot drift apart. Measured in Chromium against the real emitted CSS, all five surfaces sit at the same fraction of the way at every sample — 80.2% at t = 1500 ms, exactly `ease(0.5)`.
|
|
65
|
+
- **One phenomenon recorded that is NOT the cause** — the probe also shows the fade sheet being rewritten without its `transition` a frame or so after it is armed, and a transition that has already started is not cancelled by removing the declaration. It is not what froze the dialog, but it does mean an element whose transition would only start later gets nothing at all.
|
|
66
|
+
- **Gates** — `check:transition` gained the two halves of the fade shape: a scope is never given a token it only inherits, a scope with nothing of its own emits no rule, a scope carrying only a standard property still emits its rule and rides the same duration and easing, and `0` ms disarms the property too. `check:repaint` gained the pairing itself: each plugin-painted surface must be painted from its own variable **and** have a fade scope naming the same shared constant, so a fourth one added without a fade becomes a failing check rather than a screenshot nobody took.
|
|
67
|
+
|
|
68
|
+
## v0.7.3
|
|
69
|
+
|
|
70
|
+
- **The switch itself became a setting: four effects, an easing and one duration, on the Config page** — every wallpaper change this plugin performs went through one hard-coded cross-fade whose length was read off a field no control could reach. The effects are **cross-fade**, **instant**, **zoom** and **slide** (the last two transform the incoming layer only, so neither costs a byte of extra transfer); instant and a `0` ms duration are the same hard cut rather than one being read as "unset", a `0` is kept as a real value, and `prefers-reduced-motion` is honoured as a **veto** rather than as a shorter animation. The decision is pure (`src/client/transition.ts`), which is what lets `check:transition` pin the failures that throw nothing.
|
|
71
|
+
- **The card carries a preview swatch, and that is not decoration** — a wallpaper fading into itself is invisible, so replaying the effect on the picture already on screen would show nothing at all: the one way this feature could look broken while working perfectly. The swatch replays the chosen effect at the duration a real switch would use right now.
|
|
72
|
+
- **There is exactly one duration, and the per-rule choice is gone** — the card's default was "Follow each rule", which named `rotate.fadeMs`, a field that had existed in the shape since 0.7.0 and that no control ever reached. The slider IS the setting now, and `legacyFadeMs` folds the first deliberate value it finds into the global duration when a config that predates the setting is first read; an explicit `durationMs` always wins over a legacy field sitting beside it.
|
|
73
|
+
- **The interface colour now changes over the same clock as the wallpaper** — the fade is a **registration** problem, not a declaration one, because an unregistered custom property cannot interpolate: `src/client/palette-fade.ts` emits `@property` rules that turn the alias tokens into `<color>`s, plus a `transition` for the elements whose own declarations change with the palette. It is armed **only when the palette itself moved**, because a live transition on a slider drag would make the surface lag behind the pointer, and a token the host does not publish is deliberately left unregistered.
|
|
74
|
+
- **A light↔dark flip switches instantly, and the wallpaper keeps animating (this one is a fix)** — interpolating between the two palettes walks the whole interface through mid-tones where the ink is wrong in both directions, so the fade is armed only while the **scheme** stays put. The wallpaper is deliberately untouched by that rule: one image fading into another has no scheme to get wrong, so it keeps whatever the switch effect says.
|
|
75
|
+
- **The ink is written synchronously now, and that was a real bug with exactly one writer (a fix)** — switching a rule to a dark theme colour could leave a panel's text on the **previous** palette while every surface followed the new one: dark ink on a dark panel, cleared only by reloading, with nothing in the log. Every other surface has a second, synchronous writer, so only the token stylesheet could go stale — and its one writer was reachable only from a `requestAnimationFrame` callback wrapped in `catch {}`, where a window that is not rendering swallows the frame. A palette that moved is now written synchronously, in the same task as the surfaces it has to agree with, and the writer logs its failure instead of swallowing it.
|
|
76
|
+
- **The config shape moved to 5 and an export to format 6, and an older host still cannot eat a profile** — a schema-4 sanitizer REBUILDS the config from the keys it knows, so a stale host process would drop the effect, the easing and the duration on the first write, silently. Theme exports move to format 6; 5 and older still import.
|
|
77
|
+
- **Gates** — `check:transition` joins `pnpm test`, which is now **six** checks. `check:repaint` gained the three lines this bug was made of, `check:node` the announced schema and the legacy fold, and `check:ui` the new strings and the two new classes.
|
|
86
78
|
|
|
87
79
|
## v0.7.2
|
|
88
80
|
|
|
89
|
-
- **The filmstrip IS the control: drag to reorder, double-click to show** — the order used to be reachable only through a pair of ↑ / ↓ arrows and a "make first" button in the row below, which is a second and worse way to say "put this one there"
|
|
90
|
-
- **The order and "which one is on screen" no longer interfere with each other (this one is a fix)** — both old actions
|
|
91
|
-
- **The tile that is on screen lost its ring and kept its dot** — `.is-sel` already rings the tile
|
|
92
|
-
- **Every image of a batch upload gets its own color** — a theme color belongs to an IMAGE,
|
|
93
|
-
- **Copy trimmed, and the framing button moved** — the "Rules" heading and the
|
|
94
|
-
- **Gates** — `check:repaint` gained the batch section
|
|
81
|
+
- **The filmstrip IS the control: drag to reorder, double-click to show** — the order used to be reachable only through a pair of ↑ / ↓ arrows and a "make first" button in the row below, which is a second and worse way to say "put this one there". The drop point is a 2px bar drawn INSIDE the tile it precedes, because a bar that took up room would push around the very tiles the pointer is being measured against. Which picture is on screen is the strip's OTHER gesture, and it writes nothing at all — the image being shown is runtime state, not a setting, so the next boot starts at the rule's first image again.
|
|
82
|
+
- **The order and "which one is on screen" no longer interfere with each other (this one is a fix)** — both old actions ended by moving the image index onto the image they had just moved, so tidying the list swapped the wallpaper as well. `moveRuleImageTo(id, slot, to)` replaces both: it remembers the SLOT being painted before the splice and puts the index back on it afterwards.
|
|
83
|
+
- **The tile that is on screen lost its ring and kept its dot** — `.is-sel` already rings the tile open for editing, so a second ring put two "selected" tiles in a row of near-identical thumbnails. Being on screen is the dot, and nothing else.
|
|
84
|
+
- **Every image of a batch upload gets its own color** — a theme color belongs to an IMAGE, and the extraction was addressed by POSITION: `addRuleImages` appended the whole batch and then asked for a color for the last entry alone, so every picture before it kept no color of its own and stayed on the system theme. It is now addressed by the slots the batch itself just appended.
|
|
85
|
+
- **Copy trimmed, and the framing button moved** — the "Rules" heading and the two explanatory sentences are gone (the panel's title and the `fallback` badge already say it), the labels lost their nouns (`Image 1 / 5`, `Add` / `Replace` / `Remove`), and `Edit position` moved to the end of the layout row — what it opens edits how the picture fills the area, which is what those five modes are about.
|
|
86
|
+
- **Gates** — `check:repaint` gained the batch section, and `moveRuleImageTo` joins the actions required to sample the winner and close with `repaintIfMoved` while the two deleted actions leave it. `check:ui` covers the new strings and the new class.
|
|
95
87
|
|
|
96
88
|
## v0.7.1
|
|
97
89
|
|
|
98
|
-
- **The theme color moved down to the image** — a rule is a rotation through several pictures, and pictures collected for different moods do not share one accent
|
|
99
|
-
- **A rule's own `color` now means exactly one thing: what an image-less rule paints** — it is deliberately NOT a default for the rule's pictures. An image with no color of its own follows the **system theme** rather than inheriting the rule's, which is what makes a rotation through a green picture, a red one and a system-themed one expressible at all
|
|
100
|
-
- **Nothing loses its theme on upgrade, and the distinction is one key** — an image entry written before this release has no `color` key at all, and that is the whole test
|
|
101
|
-
- **A rotation step re-emits the interface palette, and that is the bug this feature would otherwise ship with** — the step only repainted the wallpaper, so a per-image color existed in the config and in the panel while the interface behind it kept the previous picture's palette until
|
|
102
|
-
- **Holidays carry their palette per image too** — a festival's fixed color is written on the holiday *and* on its image, and `normalizeHolidayRule` forces both from the definition on every read
|
|
103
|
-
- **The config shape moved to 4, and an older host still cannot eat a profile** — `images[].color` is unknown to a schema-3 `normalizeImage`, which REBUILDS each image entry from the keys it knows
|
|
104
|
-
- **A rule with no picture has exactly one upload target** — the big empty preview tile *is* the button now (a real `<button>`, so it is focusable and keyboard-reachable; click it or drop files onto it), and the filmstrip is not rendered at all while the rule holds nothing
|
|
105
|
-
- **Gates** — `check:node` gained the per-image color shape end to end through the real RPC handler
|
|
90
|
+
- **The theme color moved down to the image** — a rule is a rotation through several pictures, and pictures collected for different moods do not share one accent, so every image now carries its own `color: [h, s, l] | null` and the whole color section edits the image selected in the filmstrip. The controls gained one line naming their target ("Applies to the selected image: 2 / 3") and the filmstrip marks the images that already carry a color of their own, because a section that silently retargets is exactly how "I changed the color and nothing happened" starts. No new control and no layout change.
|
|
91
|
+
- **A rule's own `color` now means exactly one thing: what an image-less rule paints** — it is deliberately NOT a default for the rule's pictures. An image with no color of its own follows the **system theme** rather than inheriting the rule's, which is what makes a rotation through a green picture, a red one and a system-themed one expressible at all. Clearing one image's color therefore clears only that image; the rule keeps its own, ready for the state where it is the only thing left to paint.
|
|
92
|
+
- **Nothing loses its theme on upgrade, and the distinction is one key** — an image entry written before this release has no `color` key at all, and that missing key is the whole test: it means "this picture never had a color of its own", so the rule's color is lifted onto it once in the shared sanitizer. An explicit `null` means "cleared on purpose" and is never re-inherited over, which is why every write emits the key even when the value is null.
|
|
93
|
+
- **A rotation step re-emits the interface palette, and that is the bug this feature would otherwise ship with** — the step only repainted the wallpaper, so a per-image color existed in the config and in the panel while the interface behind it kept the previous picture's palette until something else re-ran an apply. The palette apply is now one function (`applyPalette`) called by both a model switch and every step of the rotation. `pnpm check:repaint` fails if either path stops going through it.
|
|
94
|
+
- **Holidays carry their palette per image too** — a festival's fixed color is written on the holiday *and* on its image, and `normalizeHolidayRule` forces both from the definition on every read. The shape is therefore ready for a holiday that rotates through several pieces of art, as a change to `HOLIDAYS` and its assets rather than a second code path in the render layer.
|
|
95
|
+
- **The config shape moved to 4, and an older host still cannot eat a profile** — `images[].color` is unknown to a schema-3 `normalizeImage`, which REBUILDS each image entry from the keys it knows, so the color would live in memory until the next reload and then be gone. `SCHEMA_VERSION = 4` is published on every `read`, and a host announcing less lands in the hold-writes path. Exports move to format 5.
|
|
96
|
+
- **A rule with no picture has exactly one upload target** — the big empty preview tile *is* the button now (a real `<button>`, so it is focusable and keyboard-reachable; click it or drop files onto it), and the filmstrip is not rendered at all while the rule holds nothing, because a filmstrip is a list of pictures. The drop handler moves up with it rather than disappearing, and with pictures present the preview is still deliberately inert.
|
|
97
|
+
- **Gates** — `check:node` gained the per-image color shape end to end through the real RPC handler, `check:repaint` the palette-on-a-rotation-step section, and `check:ui` the new strings and the two new classes.
|
|
106
98
|
|
|
107
99
|
## v0.7.0
|
|
108
100
|
|
|
109
101
|
- **Multi-image rotation, per rule — and off by default** — a rule now owns an ordered list of images (`images: [{ slot, bgState }]`) and can cycle them on its own rhythm: 10 s / 30 s / 1 min / 5 min / 30 min / 1 h, or any custom dwell from 5 s to 24 h, **in order** or **shuffle**. It ships OFF because rotating spends real bandwidth, memory and battery, and a default that quietly animates a wallpaper nobody asked to animate is a default that decided for the user.
|
|
110
|
-
- **A second trigger that needs no timer at all** — *also step on a model switch* advances the image once every time the model changes and lands on that rule.
|
|
111
|
-
- **Shuffle never lands on the image already on screen** — the picker is uniform over every index *except* the current one, because a tick that leaves the picture unchanged is indistinguishable from a tick that never fired
|
|
112
|
-
- **One timer for the plugin, aimed at the active rule, re-armed by signature** — a timer per rule would burn wakeups on rules that are not painting anything, and only one rule can be on screen. It is
|
|
113
|
-
- **The picture changes; the rule does not** — theme color, layout mode, wallpaper opacity and blur stay the rule's, and the transition is the same two-layer cross-fade a model switch already used.
|
|
114
|
-
- **Framing moved onto the image, where it belongs** — a crop is a property of a picture: two wallpapers of different aspect ratios share the rule's mode, color, opacity and blur, but one `fit` framing computed for the first is simply wrong for the second. Each image carries its own `bgState
|
|
115
|
-
- **The single thumbnail became a filmstrip** — add several images at once (
|
|
116
|
-
- **A rule may hold NO image, and that has to be a real state** — the first cut
|
|
117
|
-
- **A rule with no image left keeps painting from its theme color** — an empty list
|
|
118
|
-
- **Every edit that can move the winner takes effect at once, and only an edit that cannot may skip the repaint** — `color`, `enabled` and `match` all feed which rule wins, but each write path asked its own narrow question ("is this the rule that is painting right now?")
|
|
119
|
-
- **An
|
|
120
|
-
- **
|
|
121
|
-
- **
|
|
122
|
-
- **
|
|
123
|
-
- **Older configs still load, and older exports still import** — `normalizeRule` accepts both shapes: a rule's single `slot`/`bgState` pair is lifted into `images: [{ slot, bgState }]` on read, duplicates within a rule are dropped, and an unusable slot is dropped. Theme exports move to **format version 4** and now carry, and restore, every image of every rule rather than just the first; version 3 files still import.
|
|
102
|
+
- **A second trigger that needs no timer at all** — *also step on a model switch* advances the image once every time the model changes and lands on that rule. Nothing is scheduled, nothing wakes up, and the image only changes when the user does something.
|
|
103
|
+
- **Shuffle never lands on the image already on screen** — the picker is uniform over every index *except* the current one, because a tick that leaves the picture unchanged is indistinguishable from a tick that never fired, which is the one failure this feature could produce with no error anywhere. `pnpm check:rotation` walks the whole support of the roll for lists of 2, 3 and 7 images rather than sampling it once.
|
|
104
|
+
- **One timer for the plugin, aimed at the active rule, re-armed by signature** — a timer per rule would burn wakeups on rules that are not painting anything, and only one rule can be on screen. It is re-armed only when the signature of its own inputs changes (rule id, enabled flag, interval, order, image count, tab visibility), so the dozens of full re-applies a slider drag triggers can never restart the dwell. A hidden tab stops the rotation with the pixels and resumes on a fresh interval.
|
|
105
|
+
- **The picture changes; the rule does not** — theme color, layout mode, wallpaper opacity and blur stay the rule's, and the transition is the same two-layer cross-fade a model switch already used. `fadeMs: 0` is kept as a real value rather than read as "absent".
|
|
106
|
+
- **Framing moved onto the image, where it belongs** — a crop is a property of a picture: two wallpapers of different aspect ratios share the rule's mode, color, opacity and blur, but one `fit` framing computed for the first is simply wrong for the second. Each image carries its own `bgState`, and the background editor edits the image selected in the strip.
|
|
107
|
+
- **The single thumbnail became a filmstrip** — add several images at once (multi-select, drag & drop, or one URL per line, fetched sequentially so N downloads do not interleave with no progress to show), click one to edit it, reorder it, promote it to **first**, **replace this image** in place (which keeps its position in the rotation, so it is labelled apart from adding), or remove it.
|
|
108
|
+
- **A rule may hold NO image, and that has to be a real state** — the first cut refused to remove a rule's last picture and cleared its bytes "instead", which left the entry behind: a blank tile at position 1 that could never be deleted. Removing the last image now empties the list, a brand-new rule starts in that state, and a hand-edited list whose entries are all unusable lands in it rather than being conjured back into a broken slot.
|
|
109
|
+
- **A rule with no image left keeps painting from its theme color** — the first cut made an empty list mean "this rule is off", so a rule whose pictures had all been removed stopped matching altogether and the models it used to match were quietly handed to the **next** rule. Usability is now *an image, or a theme color of its own* (`ruleCanPaint`): such a rule still matches, still serves as the fallback, and simply paints the interface instead of a wallpaper. The card's copy now says which of the three real states it is in.
|
|
110
|
+
- **Every edit that can move the winner takes effect at once, and only an edit that cannot may skip the repaint** — `color`, `enabled` and `match` all feed which rule wins, but each write path asked its own narrow question ("is this the rule that is painting right now?"), so the edits that are not already winning — exactly the ones that **create** the winner — landed in the config and the readout while the interface kept the previous rule. Every path now samples the winner before its mutation and compares it after (`repaintIfMoved`), and `pnpm check:repaint` fails if any of the nine write paths stops asking it.
|
|
111
|
+
- **An emptied rule and an upload after it** — the picture you add is the picture you are then looking at: the card moves its selection to the first newly added image, and the strip, the big preview and the position readout all follow.
|
|
112
|
+
- **Only the images that are needed are loaded** — boot hydrates each rule's **first** image, which is exactly what it hydrated per rule before multi-image existed, plus today's holiday and the image the current model paints. The rest arrive when a card is expanded or when the rotation is about to need one; hydrating everything would mean twenty full-size data URLs over this plugin's RPC channel before the first frame.
|
|
113
|
+
- **An older host process can no longer eat a profile — the check is a version comparison, which is why the shape moved to 3** — the browser half and the node half are loaded separately, so updating the package and merely refreshing the page can pair a NEW client bundle with a host process still running the PREVIOUS node half, and that older sanitizer knows nothing about `images` and drops every rule that has one. The node half therefore publishes the shape it writes, and a client that finds an older answer **holds every config write**, logs once and says so in the panel: "restart DSH". Edits that silently do not persist are worse than edits that visibly wait for a restart; edits that silently delete rules are worse than both. The number means "**an empty image list is legal**" (`SCHEMA_VERSION = 3`), not merely "rules carry `images`", so any host announcing less than that lands in the hold-writes state.
|
|
114
|
+
- **Older configs still load, and older exports still import** — `normalizeRule` accepts both shapes: a rule's single `slot`/`bgState` pair is lifted into `images: [{ slot, bgState }]` on read, duplicates within a rule are dropped, and an unusable slot is dropped. Theme exports move to **format version 4** and carry every image of every rule rather than just the first; version 3 files still import.
|
|
124
115
|
- **Per-image slots are independent, and removing a rule releases only what it owns** — a rule with three images owns three `modelbg-<slot>` files; `nextSlot` scans every slot in the config (rules *and* holiday entries) so a second image can never collide with an existing one, and deleting a rule reclaims each of its slots only when nothing else references it.
|
|
125
|
-
- **The node half reports nested drift too, and migration writes the new shape** — `warnUnknownConfigKeys` now compares `rules[].rotate` and `rules[].images[]` one level deeper, which is where a field that only the client bundle knows would otherwise disappear in silence, while the legacy `slot`/`bgState` names are explicitly exempt because the sanitizer lifts them on purpose.
|
|
116
|
+
- **The node half reports nested drift too, and migration writes the new shape** — `warnUnknownConfigKeys` now compares `rules[].rotate` and `rules[].images[]` one level deeper, which is where a field that only the client bundle knows would otherwise disappear in silence, while the legacy `slot`/`bgState` names are explicitly exempt because the sanitizer lifts them on purpose.
|
|
126
117
|
|
|
127
118
|
## v0.6.0
|
|
128
119
|
|