dsh-background-by-model 0.7.7 → 0.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -1
- package/CHANGELOG.zh.md +21 -1
- package/README.md +21 -21
- package/README.zh.md +21 -21
- package/lib/client.js +171 -50
- package/lib/index.js +198 -49
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
> The full release history lives here; the two most recent releases (v0.7.
|
|
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.7.9 / v0.7.8).
|
|
4
|
+
|
|
5
|
+
## v0.7.9
|
|
6
|
+
|
|
7
|
+
- **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 three screenshots were read from the `main` branch, the festival art was pinned to a raw commit hash (`HOLIDAY_ASSETS_COMMIT = '58a9e107…'`), and the recommended profile followed `main` deliberately. Three references, three different answers to "which bytes is this build looking at", and only one of them was actually immune to the repository moving underneath it. `HOLIDAY_ASSETS_COMMIT` is now `HOLIDAY_ASSETS_REF = 'v0.1.0'` — renamed with the value, since it no longer names a commit — and `PRESET_ASSETS_REF` carries the same string, so one release of `HarmlessFunny/assets` is one version of every byte the plugin fetches: the festival art, the recommended profile and the README's images together, in a form a person can read, compare against the tags on GitHub and update in one place rather than a 40-character hash nobody can date.
|
|
8
|
+
|
|
9
|
+
- **The festival art's bytes do not move with the name.** `58a9e107` is an ancestor of `v0.1.0` and both `dsh-background-by-model/holiday/*.webp` files are **byte-identical** between the two revisions (the same blobs, `46ccfc89…` and `dc5080e4…`), so the download is the same download — the diff between the revisions is the profile arriving (seven `preset/modelbg-*` files, `preset.json` and `theme-config.json`, none of which existed at that commit) plus the three screenshots being re-shot. What did change is that the raw mirror carries the tag as well (`raw.githubusercontent.com/HarmlessFunny/assets/v0.1.0/…`), which is what keeps "both mirrors serve the same revision" true by VALUE rather than by convention.
|
|
10
|
+
|
|
11
|
+
- **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.
|
|
12
|
+
|
|
13
|
+
- **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.
|
|
14
|
+
|
|
15
|
+
## v0.7.8
|
|
16
|
+
|
|
17
|
+
- **A fresh install opens on one blank rule instead of the "no rules yet" screen** — the report was "图一是刚安装上的情况,什么也没有,我希望刚安装上就如图二一样创建一条空白的规则" (image one is a fresh install: nothing there; I want it to create one blank rule, like image two). The cause was two shapes being read as the same thing: 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`, the id `nextRuleId` mints for an empty list, so it is rule 1, the fallback and the tab the panel selects, with nothing downstream having to recognise it as seeded. A key that IS there and empty is still the user's own deletion and stays empty: a count cannot make that distinction, and without it the rule grows back on every read. The factory itself moved into `src/schema.ts` (`blankRule`), and the shipped default, `normalizeConfig` and the client's "add rule" now share one shape — the client's `newRule` is gone with it. The seeded rule is deliberately blank rather than half-built: with no picture and no colour of its own, `ruleCanPaint` makes matching skip it until something is filled in, instead of letting an empty background take the fallback away from rule 1.
|
|
18
|
+
|
|
19
|
+
- **The placeholder was not "too bright": the declaration never applied** — the report was "这个输入框的 placeholder 太亮了,让人以为真的有文字在里面" (this input's placeholder is too bright, it makes you think there really is text in there). `--dsw-alias-label-quaternary` is defined NOWHERE in the harness theme: an undefined custom property drops the declaration at computed-value time, and `color` is inherited, so the placeholder fell back to `inherit` — the input's own `label-primary`, the brightest ink in the panel, inside a box the user had not typed a character into. All three inputs that carry a placeholder (the rule's match string, the image URL, the rotation's custom seconds) now share one rule reading `--dsw-alias-label-caption`: one step weaker, and the same token `PLACEHOLDER_RULE` (`src/client/wallpaper.ts`) already hands the composer, so the plugin says one thing about what "empty" looks like. The dead name was in two more places: `.dab-brand-tag` wrote "quaternary with a fallback", so it has always painted tertiary and dropping the dead name changes no colour; `.dab-pick-n`'s `--dsw-mono` was missing a letter — the plugin's own variable is `--dab-mono`.
|
|
20
|
+
|
|
21
|
+
- **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.
|
|
22
|
+
|
|
23
|
+
- **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).
|
|
4
24
|
|
|
5
25
|
## v0.7.7
|
|
6
26
|
|
package/CHANGELOG.zh.md
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 这里保留完整的历史记录;[README.zh.md](./README.zh.md#近期优化) 的「近期优化」一节只列最近两个版本(v0.7.9 / v0.7.8)。
|
|
4
|
+
|
|
5
|
+
## v0.7.9
|
|
6
|
+
|
|
7
|
+
- **所有素材引用都钉到 assets 仓库的同一个发行 tag(`v0.1.0`),此前漂开的三处现在一致了** —— README 的三张截图读的是 `main` 分支,节日素材钉的是一个原始 commit 哈希(`HOLIDAY_ASSETS_COMMIT = '58a9e107…'`),而推荐配置是**刻意**跟着 `main` 的。三处引用,对「这个构建到底在看哪一份字节」给出了三种不同答案,而其中只有一处真的对仓库的变动免疫。现在 `HOLIDAY_ASSETS_COMMIT` 变成了 `HOLIDAY_ASSETS_REF = 'v0.1.0'`(值和名字一起改,因为它指的已经不是一个 commit),`PRESET_ASSETS_REF` 用同一个字符串,于是 `HarmlessFunny/assets` 的一个发行版就是插件所取的每一个字节的同一个版本 —— 节日素材、推荐配置和 README 的图片一起 —— 而且这个值人能读、能拿去和 GitHub 上的 tag 对照、也能在一处改完,而不是一个没人能对上日期的 40 位哈希。
|
|
8
|
+
|
|
9
|
+
- **节日素材的字节并没有跟着改名而变。** `58a9e107` 是 `v0.1.0` 的祖先,两张 `dsh-background-by-model/holiday/*.webp` 在这两个修订之间**逐字节相同**(blob 都是 `46ccfc89…` 和 `dc5080e4…`),所以下载还是同一次下载 —— 两个修订之间的差异是推荐配置到位(七个 `preset/modelbg-*`、`preset.json` 和 `theme-config.json`,这些在那个 commit 上还不存在)加上三张截图重拍。真正变了的是 raw 镜像也带上了 tag(`raw.githubusercontent.com/HarmlessFunny/assets/v0.1.0/…`),这才让「两个镜像服务同一个修订」是按**值**成立、而不是靠约定。
|
|
10
|
+
|
|
11
|
+
- **推荐配置是刻意放弃「不发版就能修」的** —— 它此前跟着分支,因为它要求不可变的理由和节日素材正好相反:推荐配置的失败是**响亮的**(用户正盯着那个按钮,会拿到一个点名缺哪个槽位的错误),而节日是在没人盯着的那一天静默失效;所以钉版本在这里什么都买不到,而分支买到了「改一个 JSON 就能修好一个不好的推荐」。现在把这个交易换掉了。分支同时还买来了「插件的字节可能在用户装上之后、点下去之前发生变化」;现在一个 tag 同时覆盖截图、节日素材和这份配置,所取的每一个字节都不会再这样。代价是写明的而不是等着被发现的:改一个 JSON 现在要花一个 `assets` 里的 tag、`src/index.ts` 里的一个新值,以及一次 npm 发版 —— 而 tag 不是 commit。它**可以**被重新指向,而 jsDelivr 把版本引用按不可变处理,所以重新指向要发一次 purge 请求而不是 push,于是「修正」的形态是发一个新 tag,而不是强推旧的。这也正是 `presetKey` 在这次改动后仍然保留的原因:一个没清过缓存的边缘节点仍然会回同一个 URL 的上一次字节,所以两次取「这份配置」依然不保证是同一个修订,恢复下载的那一次不能把两个修订拼在一起。
|
|
12
|
+
|
|
13
|
+
- **门禁** —— `check:node` 里有两条断言钉的是旧设计,而它的 `revision()` 辅助函数根本看不见 tag:它匹配的是 40 位 SHA,而这也正是 raw 镜像的引用当年能不靠 `@` 被取出来的唯一原因。现在它从仓库名旁边匹配(jsDelivr 形状是 `HarmlessFunny/assets@<ref>`,raw 形状是 `HarmlessFunny/assets/<ref>/`),于是两个镜像仍然可以互相比较;两条断言都要求 `v0.1.0` —— 节日素材那条变成「pinned to the assets release tag, never to a branch」,推荐配置那条变成「read at the assets release tag, not at a branch」,取代了此前**故意**钉住分支的那一条。`pnpm test` 十道全过,`lib/` 就是那一次运行的产物。
|
|
14
|
+
|
|
15
|
+
## v0.7.8
|
|
16
|
+
|
|
17
|
+
- **新装打开的就是一条空白规则,而不是「还没有规则」那一屏** —— 报告是「图一是刚安装上的情况,什么也没有,我希望刚安装上就如图二一样创建一条空白的规则」。成因是两种形状被读成了同一件事:完全没有配置文件时 `migrateLegacy` 返回的是 `rules: []`,而 `normalizeConfig` 把那个空数组当成「用户把规则删光了」。现在只有「`rules` 这个键根本不在」才意味着「从未配置过」,它换来一条空白规则 —— id 是 `r1`,也就是 `nextRuleId` 给空列表发的那个 id,所以它在面板上就是规则 1、就是兜底、就是默认选中的那张卡,下游没有任何分支需要认得它是被种出来的;而「键在、数组是空的」仍然是用户主动删光的结果,保持为空。按条数判断就没有这条区分,规则会在每次读盘时长回来。规则工厂本身挪进了 `src/schema.ts`(`blankRule`),出厂配置、`normalizeConfig` 和客户端的「新增规则」从此共用同一个形状,客户端的 `newRule` 随之删掉。种出来的那条规则是刻意「空」而不是「半成品」:它没有图、也没有自己的主题色,所以在被填上东西之前 `ruleCanPaint` 会让匹配跳过它,而不是让一个空背景顶掉规则 1 的兜底。
|
|
18
|
+
|
|
19
|
+
- **placeholder 不是「挑亮了」,是那条声明从来就没生效** —— 报告是「这个输入框的 placeholder 太亮了,让人以为真的有文字在里面」。`--dsw-alias-label-quaternary` 在整个 harness 主题里一个定义都没有:未定义的自定义属性会让这条声明在计算值阶段被丢掉,而 `color` 是继承属性,于是 placeholder 回落到 `inherit` —— 拿到输入框自己的 `label-primary`,也就是面板里最亮的墨色,写在一个用户一个字都没打过的框里。现在三个带 placeholder 的输入(规则匹配串、图片网址、轮换的自定义秒数)共读一条规则、统一用 `--dsw-alias-label-caption`:弱一档,也正是 `PLACEHOLDER_RULE`(`src/client/wallpaper.ts`)早就给输入框用的那个 token,所以插件对「空」这件事只有一个说法。同一个死名字还有两处:`.dab-brand-tag` 写的是「quaternary 加 fallback」,所以它一直画的就是 tertiary,去掉死名字不改变任何颜色;`.dab-pick-n` 的 `--dsw-mono` 则是少了个字母 —— 插件自己的变量是 `--dab-mono`。
|
|
20
|
+
|
|
21
|
+
- **推荐配置的下载按文件重试,而且失败的那一次会把已经下到的部分留给下一次** —— 此前一个文件只有两次机会(两个镜像各一次),七张壁纸里第六张碰上一次 502、一次连接被重置或者一次 CDN 抽风,前面六张就白下了,而用户唯一的办法 —— 再按一次按钮 —— 是从零开始。现在每个文件最多三轮、轮与轮之间先等 400 ms 再等 800 ms(`src/preset.ts` 的 `PRESET_ATTEMPTS` / `presetRetryDelayMs`),而且失败的那一次会把已经拿到的字节留在那里(`presetHeld`):下一次按按钮先重新取 manifest 与 config(两个小文件),只有当这份 config 与那些字节来自的那一份**逐值相同**时才复用(`presetKey`,把对象键排序后的规范形式 —— 上游把键顺序重建过一遍不该白白丢掉这些字节),于是「第六张失败」从重下七张变成只重下一张。三种失败**不重试**,每一种都有自己的理由:404/403 是「这个文件不在那儿」(配置跟随的是分支,作者改个文件名就必须**快**而响地报到槽位上,而不是再等两轮)、超限是「字节本身就是问题,下一轮还是这些字节」、超时是「六十秒的沉默不是天气」(两个镜像在同一轮里都已经问过了,而这一条正是把单个文件的最坏情况钉在三分钟而不是六分钟上的东西:两个镜像都不吭声的那一轮当场就结束,也就是原来那两分钟;只有「一个镜像沉默、另一个是瞬态失败」混在一起的那一轮,才会把一分钟带进下一轮)。**严格性一个字没改**:第一个拿不到的文件仍然让整次尝试失败、仍然报出槽位名 —— 变便宜的只是重试,不是失败。按钮上那个计数现在描述的是**整份配置**而不是这一次的传输,所以续传的那一次从 6/7 开始、到 7/7 结束(这也是这次改动**没有**碰界面任何一处的原因)。节日素材刻意**没有**跟着改成多轮:它是在启动时、当天第一次绘制之前抓的,一轮的等待会直接加到首帧上 —— 而它有缓存,此后每一次读取都会再试一次,按钮却没有「此后再读一次」这回事。
|
|
22
|
+
|
|
23
|
+
- **门禁** —— `pnpm test` 现在是**十道**(新增 `check:preset`)。`check:node` 新增:空数据目录读出来正好一条规则、那条规则是空的(无匹配串、无颜色、无图)、它是规则 1(因此兼任兜底)、显式写空的规则列表读回仍然为空(这几条就是新装那条规则的形状,以及那条不该被种回来的区分);`resetStore` 之后那条断言改成同一语义 —— 它原来钉的正是旧行为(「读到一个空配置」)。重试本身按**请求数**钉住:两个镜像同时 503 一次时,同一个文件被再问一遍并最终拿到(日志里三行:jsDelivr、raw、又是 jsDelivr)、托管处没有的文件仍然只花两次请求(每镜像一次、不等待)、永久 500 的镜像总共只花六次请求(三轮 × 两个镜像 —— 重试是有预算的)。`check:ui` 新增第 6 节:UI 里每一个 `var(--dsw-…)` 都必须是已安装主题确实发布过的 token —— 读不到主题(peer 依赖不在)时这一节跳过并说明,因为那是关于这台宿主的事实,不是关于这个仓库的;这一类失败本来不会在任何地方报错,声明被丢掉、属性悄悄回落到继承值,只有人眼看出来不对。新的 `check:preset` 钉住策略本身与两侧的接线:六个失败原因各自「值不值得再问一次」(三个终态正是能在本地廉价判定的那三个,而超时之所以不重试是因为同一轮里两个镜像都已经问过)、等待指数增长且有上限、`presetKey` 对同一个值的不同键顺序给出同一个键(而图片列表的顺序**算数**,因为它就是轮换顺序)、续传拿到的是副本而不是那条记录本身、以及两侧真的在用这些判定 —— node 半边循环轮次并按策略决定要不要再来一轮、客户端按「还缺哪些」而不是整个槽位表来铺并发池、失败时把已下到的字节留成 `presetHeld`、成功时清掉、以及体积上限**在文件被收下之前**判定(收下之后再判的话,超限的那一次会把每个槽位都留在手里,下一次续传于是无文件可抓、直接把一份超限的配置放行)。
|
|
4
24
|
|
|
5
25
|
## v0.7.7
|
|
6
26
|
|
package/README.md
CHANGED
|
@@ -49,19 +49,19 @@ Notes:
|
|
|
49
49
|
One interface, one config — only the current model differs:
|
|
50
50
|
|
|
51
51
|
<p align="center">
|
|
52
|
-
<img src="https://cdn.jsdelivr.net/gh/HarmlessFunny/assets@
|
|
52
|
+
<img src="https://cdn.jsdelivr.net/gh/HarmlessFunny/assets@v0.1.0/dsh-background-by-model/wallpaper-deepseek.webp" alt="Appearance under DeepSeek" width="880">
|
|
53
53
|
<br/>
|
|
54
54
|
<em>DeepSeek V4.1 Flash · matched the rule whose match string contains <code>deepseek</code>: a light wallpaper with a matching theme color</em>
|
|
55
55
|
</p>
|
|
56
56
|
|
|
57
57
|
<p align="center">
|
|
58
|
-
<img src="https://cdn.jsdelivr.net/gh/HarmlessFunny/assets@
|
|
58
|
+
<img src="https://cdn.jsdelivr.net/gh/HarmlessFunny/assets@v0.1.0/dsh-background-by-model/wallpaper-kimi.webp" alt="Appearance under Kimi" width="880">
|
|
59
59
|
<br/>
|
|
60
60
|
<em>Kimi K2.7 Code · matched the rule containing <code>kimi</code>: a dark wallpaper, a dark theme, and the whole interface reskinned with it</em>
|
|
61
61
|
</p>
|
|
62
62
|
|
|
63
63
|
<p align="center">
|
|
64
|
-
<img src="https://cdn.jsdelivr.net/gh/HarmlessFunny/assets@
|
|
64
|
+
<img src="https://cdn.jsdelivr.net/gh/HarmlessFunny/assets@v0.1.0/dsh-background-by-model/rule-editor.webp" alt="One rule's editor" width="660">
|
|
65
65
|
<br/>
|
|
66
66
|
<em>One rule on the Model Background tab · the wallpaper, its opacity and its blur belong to the rule; the layout mode, the framing and the theme color (wheel / HSL / RGB / extract from this image / pick from image) belong to the image it is editing</em>
|
|
67
67
|
</p>
|
|
@@ -120,7 +120,7 @@ A small one, not a settings block: **one switch on the Config page and nothing e
|
|
|
120
120
|
- **National Day** — October 1–7.
|
|
121
121
|
- **Mid-Autumn wins the overlap** — the two do collide (2025-10-06 was both); the single precise day is the better answer.
|
|
122
122
|
- **A holiday with no image falls through** to the model rules rather than blanking the wallpaper.
|
|
123
|
-
- **The art is downloaded, not shipped** — a compressed wallpaper per holiday (228 KB and 500 KB at native resolution, against 8.37 MB of source art), hosted in the project's [assets repository](https://github.com/HarmlessFunny/assets) rather than in the package: they were two thirds of the tarball for a picture that is looked at one day a year. The download is gated on the **calendar**, so a profile that is not on a holiday fetches nothing at all, and the first read on the day caches the file under `holiday-cache/` — every boot after that, and every year after that, is answered from disk and needs no network. The reference is pinned to a **
|
|
123
|
+
- **The art is downloaded, not shipped** — a compressed wallpaper per holiday (228 KB and 500 KB at native resolution, against 8.37 MB of source art), hosted in the project's [assets repository](https://github.com/HarmlessFunny/assets) rather than in the package: they were two thirds of the tarball for a picture that is looked at one day a year. The download is gated on the **calendar**, so a profile that is not on a holiday fetches nothing at all, and the first read on the day caches the file under `holiday-cache/` — every boot after that, and every year after that, is answered from disk and needs no network. The reference is pinned to a **release tag** (`v0.1.0`), never a branch, so reshuffling the repository's screenshots cannot break a shipped festival; a `jsDelivr` mirror is tried first and `raw.githubusercontent.com` second, and if both are unreachable the holiday simply does not take over — the model rules keep painting.
|
|
124
124
|
- **Neither the art nor the palette is swappable, by design** — an easter egg that asks to be configured is not an easter egg. A holiday slot is read-only: a file dropped into `modelbg-h-midautumn` / `modelbg-h-nationalday` is ignored by every read, and the plugin refuses to write, fetch into or delete those slots. The download lands in `holiday-cache/`, keyed by asset name and never by slot, so the store still never holds a `modelbg-h-*` file. The theme colors are constants in the holiday definition and are forced on every read — on the holiday **and on its image**, since a theme color lives on the image now — so a hand-edited `color` in `theme-config.json` cannot repaint a festival either. Its layout mode is the definition's **Fill** for the same reason (the art is full-bleed, and a stale `fit` would letterbox it), while the **framing** next to it stays yours. Theme exports therefore carry your rules only.
|
|
125
125
|
- **Turning it on** — the switch on the Config page, or `"holidays": { "enabled": true }` in the same file. Only a real `true` counts: the feature is on because a configuration says so, not because nothing has been said. The per-holiday switches inside it (中秋 / 国庆) are a different thing and still default to on — they are details of a feature that is already switched on.
|
|
126
126
|
|
|
@@ -161,11 +161,12 @@ So it is editable where it lives: the config is a few kilobytes of readable JSON
|
|
|
161
161
|
It is the only action in the panel that **destroys** something, and it is built around that:
|
|
162
162
|
|
|
163
163
|
- **You are asked second, not first.** The download happens before the confirmation, so the question can say what is actually in hand — *7 rules · 7 wallpapers · 2.9 MB* — instead of asking about a file that may never arrive. If the download fails, nothing has happened yet.
|
|
164
|
-
- **One missing wallpaper fails the whole thing**, and says which. A profile that quietly arrived without a picture — because a file was renamed where it is hosted — would be exactly the silent failure that pinning the festival art to a
|
|
164
|
+
- **One missing wallpaper fails the whole thing**, and says which. A profile that quietly arrived without a picture — because a file was renamed where it is hosted — would be exactly the silent failure that pinning the festival art to a release tag exists to prevent; naming the file keeps that failure loud, and that loudness is what makes a profile safe to read from a host this plugin does not control.
|
|
165
|
+
- **Every file retries on its own, and a failed attempt keeps what it fetched.** A file used to get exactly two chances (one per mirror), so with the first few wallpapers down and the next hitting a 502 or a CDN hiccup the whole profile failed and the button started from zero again. Each file now gets up to three rounds (400 ms and then 800 ms apart), while three failures are not retried at all — "the file is not there" (the same answer twice), "the bytes are over the cap" (the same bytes again), and "a full minute of silence" (both mirrors were already asked inside that same round, which is what keeps the worst case at three minutes rather than six). What did arrive is kept for the next press, so that press only fetches what is missing — as long as the config it re-fetches is **the same value** as the one those bytes came from (this profile is read at one version of the assets repository, but that version's tag can be re-pointed and an edge that has not been purged will still answer with the previous bytes, so two different revisions must not be glued together). The strictness is unchanged: the first file that cannot be had still fails the whole attempt, and still says which one.
|
|
165
166
|
- **The store is emptied before the profile is written.** `nextSlot` allocates slots as `m1`, `m2`, … — a counter, not a random token — so a downloaded profile and the store it lands in collide on those names as a matter of course. "Replace everything with this" is only true if the everything is genuinely gone, and this is also why there is no undo.
|
|
166
167
|
- **The wipe runs after the download, never before it.** An unreachable CDN must not cost you your configuration on the way to a profile that never arrived — which matters more now that the download is several transfers rather than one.
|
|
167
168
|
- **The festival art is not touched.** `holiday-cache/` is downloaded art that belongs to no slot; it is not part of anybody's configuration, and re-fetching it would be a network round trip for nothing. The config *does* name the two holiday slots (they are part of any config), and they are dropped from the request set rather than asked for: no `modelbg-h-*` file can exist.
|
|
168
|
-
- **It is referenced
|
|
169
|
+
- **It is referenced at the same release tag as the festival art** — and it used to be a branch, deliberately: a profile fails *loudly* (you are looking at the button when it happens) where a holiday fails silently, so pinning bought nothing there and a branch let a bad recommendation be corrected without a plugin release. What a branch also bought was a plugin whose bytes could change under you between the install and the click; with one tag covering the screenshots, the festival art and this profile, one release of the assets repository is now one version of everything the plugin fetches. The price is that correcting one JSON file costs a tag in `assets`, a value in `src/index.ts` and a release on npm — and that moving a tag does not reach jsDelivr's edge by itself (the reference is served as immutable, and the purge tool is the only way to re-point it), so a correction is a new tag rather than a force-push of the old one. The risk that is real — a profile written for a config shape this build cannot read — is what `preset.json` is for: it is refused before a single wallpaper is requested, and long before anything is deleted.
|
|
169
170
|
|
|
170
171
|
## Settings
|
|
171
172
|
|
|
@@ -239,7 +240,7 @@ To mount a working copy into a profile instead, add it to the profile's `package
|
|
|
239
240
|
|
|
240
241
|
After changing anything under `src/`, run `pnpm run bundle` again — the mounted profile loads `lib/`, so edits do not take effect until the bundle is rebuilt.
|
|
241
242
|
|
|
242
|
-
`pnpm test` builds, typechecks and runs
|
|
243
|
+
`pnpm test` builds, typechecks and runs ten checks (`tsdown && tsc -p tsconfig.json && node scripts/holiday-check.ts && node scripts/rotation-check.ts && node scripts/transition-check.ts && node scripts/repaint-check.ts && node scripts/code-plate-check.ts && node scripts/image-mode-check.ts && node scripts/rules-view-check.ts && node scripts/preset-check.ts && node scripts/ui-strings-check.ts && node scripts/node-half-check.mjs`). None of them needs a dependency, a transformer or a browser, and each can be run alone with `pnpm check:holiday` / `pnpm check:rotation` / `pnpm check:transition` / `pnpm check:repaint` / `pnpm check:code-plate` / `pnpm check:image-mode` / `pnpm check:rules` / `pnpm check:preset` / `pnpm check:ui` / `pnpm check:node`:
|
|
243
244
|
|
|
244
245
|
- **`check:holiday`** — the two holiday windows against known dates, the Beijing day boundary (23:59 vs 00:01), leap eighth months across 1900–2100, and the four gates `pickHoliday` applies before a holiday is allowed to take over the background.
|
|
245
246
|
- **`check:rotation`** — the rotation's decisions (`nextIndex` / `isRotating`): no walking off the end of a list, shuffle never repeating the image already on screen (across the whole support of the roll), and "rotating" never claimed for a rule with nothing to rotate.
|
|
@@ -248,8 +249,9 @@ After changing anything under `src/`, run `pnpm run bundle` again — the mounte
|
|
|
248
249
|
- **`check:code-plate`** — the code block's plate: that the surface tokens it reads are declared on **both** `:root` and `body` (a `var()` is substituted where it is *declared*, so a root-level consumer otherwise reads the host's colour and not the palette's), that the light branch emits the banner instead of borrowing the host's near-white, and that each of the two surfaces reads its **own** plugin variable — one shared variable would let the banner's write paint the plate too.
|
|
249
250
|
- **`check:image-mode`** — the 0.7.5 lift of the layout mode onto the image and, more to the point, the **wiring**: which image a control edits and which image the render layer reads. A mis-wired read does not throw, it just makes every picture in a rotation look the same.
|
|
250
251
|
- **`check:rules`** — the two decisions behind the tab strip, which are pure and live in `src/client/rules-view.ts` (which rule the editor shows, and where a deletion lands — its neighbour, never rule 1), plus the page structure the layout rests on: one editor drawn, no stacked per-card container, no per-card open state, a strip that can neither wrap nor grow, and its scrollbar block staying scoped to the plugin's own classes.
|
|
251
|
-
- **`check:
|
|
252
|
-
- **`check:
|
|
252
|
+
- **`check:preset`** — the two decisions behind the recommended profile's transfers (`src/preset.ts`: which failures are worth another round, and what a failed attempt leaves for the next one), plus the two halves actually using them. Each of the six failure reasons is 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); the wait grows and stays bounded; two key orders of one value are one revision while the image list's ORDER counts (it is the rotation); and a resume hands back a copy rather than the held record itself. The wiring is pinned with 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.
|
|
253
|
+
- **`check:ui`** — every `t('…')` key exists in both dictionaries, both dictionaries carry the same key set, every `dab-…` class has a rule in the stylesheet, no dictionary entry has gone unreferenced, and every `var(--dsw-…)` the UI reads is a token the installed theme actually publishes — an undefined custom property drops the declaration and the property quietly falls back to an inherited value, which is how a placeholder once painted in the ink of real text (with the theme bundle absent, that section skips with a notice).
|
|
254
|
+
- **`check:node`** — the built node half driven through its real RPC handler with `DSH_HOME` redirected to a throwaway directory, and `globalThis.fetch` replaced by a stub — the festival art lives on a CDN now, and this suite must not depend on one being up, nor assert today's bytes of a file it does not own. It covers: the holiday block through the shared sanitizer; nothing fetched merely for loading the plugin or reading the config; the art fetched on demand and cached under `holiday-cache/` keyed by **asset** rather than slot; a second read answered from disk with no second transfer; two simultaneous cold reads sharing one transfer; a dead first mirror falling through to the second with both mirrors read at the same release tag; every mirror down yielding "no bytes" rather than a throw, with nothing cached from a failed fetch; a mirror answering 200 with a non-image rejected rather than cached; a corrupt cache entry discarded and re-downloaded; a holiday slot refusing writes, deletes and URL fetches while a file dropped into it is ignored and no `modelbg-h-*` file is ever written; an ordinary rule slot still writing, listing and deleting; a hand-edited config unable to redirect a holiday at another rule's image; the per-image color shape — the read-time lift in both directions (an absent key inherits the rule's color, an explicit `null` does not), independent colors through the file, and a `null` written as a key rather than omitted; and the global switch effect — the shipped default for a config that predates it, a chosen effect/easing/unified duration surviving the round trip, hostile values falling back while an absurd duration clamps, a `0` duration kept rather than defaulted, and drift inside `transition` reported instead of silently dropped. The recommended profile is covered end to end from the node half's side: the manifest and the config fetched in that order and sanitized on arrival, the request set DERIVED from the rules with the holiday slots dropped from it, one request per wallpaper with the bytes sniffed rather than trusted from a header, a dead jsDelivr falling through to raw for a single file, the same file **asked again** when both mirrors stumble at once and arriving on that third request (three entries in the log), a file the host does not have still costing exactly two requests (one per mirror, no waiting), a permanently broken mirror costing exactly six (three rounds over two mirrors — the retry has a budget), and every way it can fail reported as a value rather than a throw — every mirror down, a mirror answering 500, a 200 carrying HTML, a manifest whose version is not a number, a profile written for a newer shape refused before a single wallpaper is asked for, a config that is not JSON, a config that is a JSON array, a config naming more images than the cap, and a mirror announcing an oversized body; plus the strict half (a named wallpaper the host does not have is an error, not a short profile), the two naming refusals that happen before any request at all (a holiday slot, and a slot name that is not a slot), and the reset: the config and every rule image gone, `holiday-cache/` untouched, a read afterwards yielding the same one blank rule rather than an error, and the profile then writing into the emptied store — with the fresh-install rule itself pinned first: a data directory with no config file reads as exactly one blank rule (`r1`, no match string, no colour, no image), while a rule list written explicitly empty still reads back empty, because those are two different answers.
|
|
253
255
|
|
|
254
256
|
## FAQ
|
|
255
257
|
|
|
@@ -300,22 +302,20 @@ Two things can hold it, and both are on the card: the effect is **Instant**, or
|
|
|
300
302
|
|
|
301
303
|
## Recent Optimizations
|
|
302
304
|
|
|
303
|
-
> Only the two most recent releases are listed here; older ones (v0.7.
|
|
305
|
+
> Only the two most recent releases are listed here; older ones (v0.7.7 and earlier) live in [CHANGELOG.md](./CHANGELOG.md).
|
|
304
306
|
|
|
305
|
-
### v0.7.
|
|
307
|
+
### v0.7.9
|
|
306
308
|
|
|
307
|
-
- **
|
|
308
|
-
- **The festival
|
|
309
|
-
- **The
|
|
309
|
+
- **Every asset reference is pinned to one release tag of the assets repository** — `v0.1.0`. Three references had drifted into three different states: the README's three screenshots were read from the `main` branch, the festival art was pinned to a raw commit hash (`58a9e107…`) that said nothing to a human, and the recommended profile followed `main` on purpose, so a plugin could be looking at bytes that changed after it was built. All three are now `…/assets@v0.1.0/…`, which is one version of the assets repository for every byte the plugin fetches — screenshots, festival art and the recommended profile together — and a value a person can read, compare against the tags on GitHub and update in one place.
|
|
310
|
+
- **The festival art's bytes do not move with it.** `58a9e107` is an ancestor of `v0.1.0` and the two `holiday/*.webp` files are byte-identical between them, so the download is unchanged; what changed is that the reference is now named the way the repository is named. The raw mirror carries the tag too (`raw.githubusercontent.com/HarmlessFunny/assets/v0.1.0/…`), so the fallback still cannot mix revisions.
|
|
311
|
+
- **The recommended profile deliberately gives up "fix it without a release"** — it used to follow a branch so a bad recommendation could be corrected in the assets repository instead of waiting for a plugin release, and the reasoning was that a profile fails *loudly* (you are looking at the button) where a holiday fails silently, so immutability bought nothing there. What it cost was a plugin whose bytes could change under you between the install and the click; with one tag covering the screenshots, the art and the profile, that is no longer true of any of them. Correcting one JSON file now costs a tag in `assets`, a value in `src/index.ts` and a release on npm. Note what a tag is **not**: it can be re-pointed, and jsDelivr serves a version reference as immutable, so re-pointing one means a purge request rather than a push — which is why `presetKey` survives this change, since an edge that has not been purged can still answer with the previous bytes of the same URL, and a press that resumes must not glue two revisions together.
|
|
312
|
+
- **`check:node` pins the tag on both features.** Its `revision()` helper used to look for a 40-character SHA, which cannot see a tag at all; it now lifts the reference out beside the repository name (`HarmlessFunny/assets@<ref>` in the jsDelivr shape, `HarmlessFunny/assets/<ref>/` in the raw one) so the two mirrors can still be compared, and the two assertions that used to read "a commit, never a branch" and "follows the branch" now both require `v0.1.0`.
|
|
310
313
|
|
|
311
|
-
### v0.7.
|
|
312
|
-
|
|
313
|
-
- **The rules page turns from a stack of self-growing cards into one tab strip and one editor** — every rule used to be a card that expanded itself, so the page was as tall as the sum of the cards and only got shorter once the user collapsed them by hand. Now every rule is a tab and exactly one editor is mounted, so the 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 of the first image, the match string, and whether the rule is the one in use and whether it rotates); the strip scrolls sideways rather than wrapping, at a fixed 34px with an elided match label, so switching rules cannot reflow the row. **The `+` and the "which rule" menu sit outside the scroller** — inside it, 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: it answers a question about the configuration as a whole, and on the rules page it pushed the strip a whole card down. The two decisions behind the strip are pure functions now (`src/client/rules-view.ts`), pinned by the new `check:rules`, which also pins the page structure the layout rests on.
|
|
314
|
-
- **The rotation card's controls were reshuffled, at your request** — the Next button and the "On screen 2 / 3" readout moved outside the rotation panel and are **always there**: that readout is the only place on the card that says which picture is showing, and it used to vanish together with the rotation switch. The only test left is `rotateNow`'s own — the active rule has two or more pictures — and below that the button is disabled and the readout is omitted rather than printing "0 / 0". **"Also step on a model switch" joined the order row** (all three answer "when does a picture change"); **"1 hour" is gone** from the dwell-time presets (the custom field still takes 3600 s); **the row of eight fixed preset swatches is deleted** (the wheel, the numeric inputs, Extract from this image, the eyedropper and Follow system theme all stay — the row was a shortcut into the same setter); and **the whole rotation block moved into the second column**, below the theme colour, where in the collapsed single-column card it used to be the tallest block on the card, sitting between the pictures and the colour controls that edit them.
|
|
315
|
-
- **The festival art stops shipping in the package: fetched on the day, cached by asset** — the two holiday wallpapers (228 KB + 500 KB) were two thirds of the entire tarball, for a picture looked at one day a year, with their binary history kept in `.git` forever on top of that. They now live in the project's own [`HarmlessFunny/assets`](https://github.com/HarmlessFunny/assets): the download is gated on the **calendar**, so a profile that is not on a holiday fetches nothing at all, and the first read on the day caches the file under `holiday-cache/` — every boot after that, and every year after that, comes off disk. The reference is **pinned to a commit, never a branch** — runtime art has to be immutable, or a later reshuffle of the documentation screenshots could silently kill a shipped feature on a day nobody is watching — with jsDelivr first and raw second, and with both mirrors unreachable the holiday simply does not take over and the model rules keep painting (the "does this slot actually have bytes" gate was written for exactly that). The cache is keyed by **asset** name and sits deliberately outside the slot namespace, so neither "a holiday slot is read-only" nor "the store never holds a `modelbg-h-*` file" moved an inch; a cache entry is **validated**, and one that is not an image is treated as a miss and re-downloaded. `globalThis.fetch` is replaced by a stub throughout `check:node`, so the suite does not need a CDN to be up.
|
|
316
|
-
- **A recommended profile you can install in one click** — the Config tab gained a third card that downloads the author's own configuration from the same assets repository and writes it in place of yours (see [Recommended profile](#recommended-profile)). **It is hosted in the store's own shape** — a `preset.json` shape gate, a readable `theme-config.json`, and one raw file per slot — which is what makes it editable where it lives: the config is 6.5 KB of JSON you can edit on GitHub, replacing a wallpaper is replacing a file, and publishing it is a `cp` out of the data directory. It was first written as a single theme file with every wallpaper inlined as base64, at 2.9 MB where 99.8% of the bytes were a picture; that is not a thing anyone can hand-edit, and the format was changed for exactly that reason. The plugin fetches the manifest and the config, derives the file list from `rules[].images[].slot` (no second place to keep in sync), and fetches the wallpapers one request each, four at a time — showing `3/7` on the button while it runs — so each file gets its own mirror fallback and its own validation. Applied through the same `applyThemeFile` a hand-picked file goes through, assembled on the client into the shape `exportTheme` writes; it is the only action in the panel that destroys anything, so it is the only one that asks, and the download happens **before** the question so the confirmation can state what is actually in hand (7 rules · 7 wallpapers · 2.9 MB). **The store is emptied first, deliberately:** `nextSlot` hands slots out as `m1`, `m2`, … — a counter, not a random token — so a downloaded profile and the store it lands in collide on those names as a matter of course, and "replace everything with this" is only true if the everything is actually gone; that is also why there is no undo. The wipe runs **after** the download and never before it, because an unreachable CDN must not cost the user their configuration on the way to a profile that never arrived, and a host too old to write this build's config shape is refused before anything is deleted. **One missing wallpaper fails the whole attempt and names the file** — a profile that quietly arrived short a picture is precisely the silent failure that pinning the festival art to a commit exists to prevent, so the loud failure is what makes a branch reference acceptable here; the two holiday slots a config legally names are dropped from the request set rather than asked for, since the store can never hold a `modelbg-h-*` file. `holiday-cache/` is left alone, and nothing about the profile is written to the store or cached to disk — the whole point of the button is to fetch what is there *now*.
|
|
317
|
-
- **The published package drops from 1.1 MB to about 220 kB** — the documentation screenshots (`example_img/`, three webp files, 220 KB) moved into the same assets repository and both READMEs reference them by absolute jsDelivr URL (an absolute URL keeps the images on the npm page unbroken just as well, without putting binaries in the tarball or their history in `.git`); `screenshots.json`, which nothing consumes, is deleted; and `lib/client.js.map` (a 624 KB source map, useless to anyone installing the package) is no longer published, though it stays in the repository. Thirteen files became ten.
|
|
314
|
+
### v0.7.8
|
|
318
315
|
|
|
316
|
+
- **A fresh install opens on one blank rule instead of the "no rules yet" screen** — with no config file at all the read used to produce an empty rule list, so the first thing a new user saw was "No rules yet — use 'Add rule' in the tab strip above": the panel telling them to press a control it had not given them. "The `rules` key is not there" (nothing has ever been configured) and "the key is there and the array is empty" (the user deleted every rule) are two different answers now, and only the first is filled in with one blank rule (id `r1`, so it is rule 1, the fallback, and the tab the panel selects); the second stays empty, so deleting the last rule is not undone by the next read. The rule is deliberately blank rather than half-built: with no picture and no theme colour of its own, matching skips it until something is filled in, instead of letting an empty background take the fallback away from rule 1.
|
|
317
|
+
- **The placeholder no longer looks like real text** — its `::placeholder` colour named `--dsw-alias-label-quaternary`, which is defined NOWHERE in this harness's theme: an undefined custom property drops the declaration at computed-value time, and `color` is inherited, so the placeholder fell back to the input's own `label-primary` — the brightest ink in the panel, inside a box nobody had typed in. All three inputs that carry one (the rule's match string, the image URL, the rotation's custom seconds) now read `--dsw-alias-label-caption`, the weak token the plugin already uses for input placeholders; two more dead names went with it (`.dab-pick-n`'s `--dsw-mono` was missing a letter — the plugin's own variable is `--dab-mono` — and `.dab-brand-tag`'s carried a fallback, so dropping it changes no colour).
|
|
318
|
+
- **The recommended profile retries per file, and a failed attempt keeps what it fetched** — a file used to get exactly two chances (one per mirror), so with the first few wallpapers down and the next hitting a 502 or a CDN hiccup the whole profile failed and pressing the button again started from zero. Every file now gets up to three rounds, 400 ms and then 800 ms apart; three failures are not retried — "the file is not there" (the same answer twice), "the bytes are over the cap" (the same bytes again), and "a full minute of silence" (both mirrors were already asked inside that same round, and this is what keeps the worst case at three minutes rather than six) — while everything else gets another round. What did arrive stays in hand, so the next press fetches only what is missing, as long as the config it re-fetches is **the same value** as the one those bytes came from (the profile is served from a branch, and two different pushes must not be glued together). The confirmation, the button's copy and where the progress sits are untouched; **so is the strictness** — the first file that cannot be had still fails the whole attempt and still names the slot.
|
|
319
319
|
|
|
320
320
|
## Star History
|
|
321
321
|
|
package/README.zh.md
CHANGED
|
@@ -49,19 +49,19 @@
|
|
|
49
49
|
同一套界面、同一份配置,只因为当前模型不同:
|
|
50
50
|
|
|
51
51
|
<p align="center">
|
|
52
|
-
<img src="https://cdn.jsdelivr.net/gh/HarmlessFunny/assets@
|
|
52
|
+
<img src="https://cdn.jsdelivr.net/gh/HarmlessFunny/assets@v0.1.0/dsh-background-by-model/wallpaper-deepseek.webp" alt="DeepSeek 下的外观" width="880">
|
|
53
53
|
<br/>
|
|
54
54
|
<em>DeepSeek V4.1 Flash · 命中匹配串含 <code>deepseek</code> 的规则:浅色壁纸 + 与图相配的主题色</em>
|
|
55
55
|
</p>
|
|
56
56
|
|
|
57
57
|
<p align="center">
|
|
58
|
-
<img src="https://cdn.jsdelivr.net/gh/HarmlessFunny/assets@
|
|
58
|
+
<img src="https://cdn.jsdelivr.net/gh/HarmlessFunny/assets@v0.1.0/dsh-background-by-model/wallpaper-kimi.webp" alt="Kimi 下的外观" width="880">
|
|
59
59
|
<br/>
|
|
60
60
|
<em>Kimi K2.7 Code · 命中匹配串含 <code>kimi</code> 的规则:深色壁纸 + 深色主题,整个界面跟着换肤</em>
|
|
61
61
|
</p>
|
|
62
62
|
|
|
63
63
|
<p align="center">
|
|
64
|
-
<img src="https://cdn.jsdelivr.net/gh/HarmlessFunny/assets@
|
|
64
|
+
<img src="https://cdn.jsdelivr.net/gh/HarmlessFunny/assets@v0.1.0/dsh-background-by-model/rule-editor.webp" alt="单条规则的编辑器" width="660">
|
|
65
65
|
<br/>
|
|
66
66
|
<em>「模型背景」页的单条规则 · 壁纸、透明度与模糊度属于这条规则;布局模式、取景与主题色(色轮 / HSL / RGB / 从本图提取 / 从图片取色)属于它正在编辑的那张图</em>
|
|
67
67
|
</p>
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
- **国庆节** — 公历 10 月 1–7 日。
|
|
121
121
|
- **两者重叠时中秋优先** — 它们确实会撞(2025-10-06 就是同一天),一个精确的单日比七天窗口里的某一天更值得采信。
|
|
122
122
|
- **没有图的节日会回落到模型规则**,而不是把背景刷空。
|
|
123
|
-
- **画是下载来的,不是随包发的** — 每个节日一张压缩过的壁纸(原生分辨率下 228 KB 与 500 KB,原图共 8.37 MB),放在项目自己的 [assets 仓库](https://github.com/HarmlessFunny/assets)里,而不是打进包里 —— 它们占了整个 tarball 的三分之二,而这张画一年只被看一天。下载由**日历**把关:不在节日的配置一点流量都不会有;当天第一次读取会把它缓存到 `holiday-cache
|
|
123
|
+
- **画是下载来的,不是随包发的** — 每个节日一张压缩过的壁纸(原生分辨率下 228 KB 与 500 KB,原图共 8.37 MB),放在项目自己的 [assets 仓库](https://github.com/HarmlessFunny/assets)里,而不是打进包里 —— 它们占了整个 tarball 的三分之二,而这张画一年只被看一天。下载由**日历**把关:不在节日的配置一点流量都不会有;当天第一次读取会把它缓存到 `holiday-cache/`,此后每一次启动、往后每一年,都直接读磁盘、不再联网。引用钉死在一个**发行 tag**(`v0.1.0`)上而不是分支,所以你以后整理仓库里的截图不可能弄坏一个已经发出去的节日;先走 `jsDelivr`、失败再走 `raw.githubusercontent.com`,两个都不通时这个节日就是不接管 —— 模型规则继续画,不会白屏。
|
|
124
124
|
- **图片和配色都不可替换,这是刻意的** — 一个要求用户去配置的彩蛋就不是彩蛋了。节日槽位是只读的:往 `modelbg-h-midautumn` / `modelbg-h-nationalday` 里放文件不会被任何一次读取采用,插件也拒绝写入、抓取和删除这两个槽位。下载落进 `holiday-cache/`,按**素材名**而不是槽位命名,所以数据目录里依然永远不会出现 `modelbg-h-*` 文件。主题色是节日定义里的常量、每次读取都强制覆盖——**节日条目和它的图片两处都覆盖**,所以手改 `theme-config.json` 里的 `color` 也换不掉节日配色。布局模式同理固定为定义里的**填充**(这些画本来就是满幅的,一个残留的 `fit` 会给它加黑边),而旁边的**取景**仍然是你自己的。导出的主题文件只包含你自己的规则。
|
|
125
125
|
- **想要它** — 打开「配置」页那个开关,或者在同一个文件里写 `"holidays": { "enabled": true }`。只有真正的 `true` 算数:这个功能是「配置说了要」才开的,不是「没人说话」就默认开。它内部每个节日(中秋 / 国庆)各自那个开关是另一回事,仍然默认开着 —— 那是一个已经打开的功能内部的细节。
|
|
126
126
|
|
|
@@ -161,11 +161,12 @@ preset/
|
|
|
161
161
|
它同时也是这个面板里**唯一会销毁东西**的操作,整个设计都在围着这一点转:
|
|
162
162
|
|
|
163
163
|
- **先下载,后询问。** 确认框出现在下载成功之后,所以它能如实说出手里到底有什么 —— *7 条规则 · 7 张壁纸 · 2.9 MB* —— 而不是让你先对「一个可能根本下不来的文件」做决定。下载失败时,什么都还没有发生。
|
|
164
|
-
- **少一张壁纸就整体失败,并且会说是哪一张。** 一份悄悄少了图的配置 —— 因为托管处的某个文件被改了名 ——
|
|
164
|
+
- **少一张壁纸就整体失败,并且会说是哪一张。** 一份悄悄少了图的配置 —— 因为托管处的某个文件被改了名 —— 正是「节日素材钉死发行 tag」那条契约要防的静默失效;把文件名报出来,这个失败就仍然是响亮的,而这份响亮正是「配置可以从一个插件控制不了的托管处读取」可以被接受的原因。
|
|
165
|
+
- **每张图都会自己重试,而失败的那一次会把已经下到的部分留给下一次。** 一个文件此前只有两次机会(两个镜像各一次),前面几张都下好了、下一张碰上一次 502 或一次 CDN 抽风,整份配置就失败,再按一次按钮又是从零开始。现在每个文件最多三轮(轮间 400 ms 再 800 ms),而「这个文件不在那儿」「字节本身就超限」「六十秒都没吭声」三种失败不重试 —— 前两种再问也是同一个答案,第三种是因为两个镜像在同一轮里都已经问过了,这一条也正是把最坏情况钉在三分钟而不是六分钟的东西。已经拿到的字节会留到下一次,于是下一次只补缺的那几张;前提是重新取到的配置与那些字节来自的那一份**逐值相同**(这份配置读的是 assets 仓库的一个版本,但那个版本的 tag 可以被重新指向,而没有清过缓存的边缘节点仍会回上一次的字节,所以两个不同的修订不许拼在一起)。严格性没变:第一个拿不到的文件仍然让整次失败、仍然会说是哪一个。
|
|
165
166
|
- **写入之前先清空。** `nextSlot` 分配槽位用的是 `m1`、`m2`…… 一个计数器而不是随机串,所以下载来的配置和它落进去的那个数据目录**必然**在这些名字上撞车。「用它替换一切」只有在那个「一切」真的没了的时候才成立 —— 这也是它没有撤销的原因。
|
|
166
167
|
- **清空发生在下载之后,绝不会在之前。** CDN 不通不该让你在等一个永远没来的推荐配置的路上,先把自己的配置弄丢 —— 现在下载是多次传输而不是一次,这一点更重要。
|
|
167
168
|
- **不碰节日素材。** `holiday-cache/` 是下载来的、不属于任何槽位的画,不是任何人配置的一部分;重下一遍只是白白一次网络往返。而 config **确实**会列出那两个节日槽位(任何一份 config 都有),它们会被从请求清单里剔掉而不是去请求:数据目录里永远不可能存在 `modelbg-h-*` 文件。
|
|
168
|
-
-
|
|
169
|
+
- **它和节日素材引用的是同一个发行 tag** —— 而它以前是分支,而且是刻意的:推荐配置的失败是**响亮的**(文件下不来时你正盯着那个按钮),节日的失败却是静默的,所以钉版本在这里什么都买不到,而分支能让一个不好的推荐不必等插件发版就能修正。但分支同时也买来了「插件的字节可能在装上之后、你点下去之前发生变化」;现在一个 tag 同时覆盖截图、节日素材和这份配置,assets 仓库的一个发行版就是插件所取的每一个字节的同一个版本。代价是:改一个 JSON 现在要花一个 `assets` 里的 tag、`src/index.ts` 里的一个值,以及一次 npm 发版;而且**移动**一个 tag 并不会自己抵达 jsDelivr 的边缘节点(版本引用是按不可变处理的,只有 purge 工具能重新指向它),所以修正是发一个新 tag,而不是强推旧的。真正存在的风险 —— 这份配置是写给一个本版本读不懂的配置形状的 —— 正是 `preset.json` 存在的理由:它在请求第一张壁纸之前、更在删除任何东西之前就被拒绝。
|
|
169
170
|
|
|
170
171
|
## 设置界面
|
|
171
172
|
|
|
@@ -239,7 +240,7 @@ pnpm run typecheck
|
|
|
239
240
|
|
|
240
241
|
改完 `src/` 必须重新执行 `pnpm run bundle` 才会生效——挂载的 profile 加载的是 `lib/`。
|
|
241
242
|
|
|
242
|
-
`pnpm test`
|
|
243
|
+
`pnpm test` 会构建、类型检查并跑十道校验(`tsdown && tsc -p tsconfig.json && node scripts/holiday-check.ts && node scripts/rotation-check.ts && node scripts/transition-check.ts && node scripts/repaint-check.ts && node scripts/code-plate-check.ts && node scripts/image-mode-check.ts && node scripts/rules-view-check.ts && node scripts/preset-check.ts && node scripts/ui-strings-check.ts && node scripts/node-half-check.mjs`)。十道都不需要依赖、不需要转译器、也不需要浏览器,也可以单独跑:`pnpm check:holiday` / `pnpm check:rotation` / `pnpm check:transition` / `pnpm check:repaint` / `pnpm check:code-plate` / `pnpm check:image-mode` / `pnpm check:rules` / `pnpm check:preset` / `pnpm check:ui` / `pnpm check:node`。
|
|
243
244
|
|
|
244
245
|
- **`check:holiday`** — 两个节日窗口对照已知日期、北京时间的跨日边界(23:59 与 00:01)、1900–2100 年间的闰八月,以及 `pickHoliday` 允许节日接管背景之前必须通过的四道闸门。
|
|
245
246
|
- **`check:rotation`** — 轮换的判定(`nextIndex` / `isRotating`):不会走越界、随机不会重复当前这张(在整个随机取值范围内穷举)、也不会把只有一张图的规则报成「正在轮换」。
|
|
@@ -248,8 +249,9 @@ pnpm run typecheck
|
|
|
248
249
|
- **`check:code-plate`** — 代码块底板:它读的那几个表面 token 必须在 `:root` **和** `body` 上都有声明(`var()` 是在**声明它的地方**就被替换的,只声明在 body 上的 token 被根元素消费时读到的是宿主的颜色而不是调色板的)、浅色分支也要发出 banner 而不是偷借宿主的近白色、以及两块表面各读**自己**的插件变量 —— 共用一个变量会让 banner 的写入把底板也一起画了。
|
|
249
250
|
- **`check:image-mode`** — 0.7.5 把布局模式下放到图片上的那次抬升,以及更要紧的**接线**:某个控件编辑的是哪张图、渲染层读的又是哪张图。接错了不会报错,只会让一轮轮换里的每张图长得一样。
|
|
250
251
|
- **`check:rules`** — 选项卡背后那两个纯判定(在 `src/client/rules-view.ts`:编辑器显示哪条规则、删除之后落在哪——落在邻居上,永远不是规则 1),以及布局所依赖的页面结构:只画一个编辑器、没有旧的堆叠容器、没有逐卡片的展开状态、胶片条既不换行也不长高,以及它的滚动条样式块仍然被限定在插件自己的类上。
|
|
251
|
-
- **`check:
|
|
252
|
-
- **`check:
|
|
252
|
+
- **`check:preset`** — 推荐配置那几次传输背后的两个判定(`src/preset.ts`:哪些失败值得再来一轮、失败的那一次给下一次留下什么),以及两侧**真的在用**它们:六个失败原因逐个判定(三个终态正是能在本地廉价判定的那三个;超时不重试是因为同一轮里两个镜像都已经问过)、等待指数增长且有上限、同一个值的不同键顺序给出同一个「同一份配置」而图片列表的顺序**算数**(它就是轮换顺序)、续传拿到的必须是副本而不是那条记录本身 —— 再加上接线(node 半边循环轮次并按策略决定、客户端按「还缺哪些」而不是整个槽位表铺并发池、失败留字节、成功清掉、体积上限在收下文件之前判)。
|
|
253
|
+
- **`check:ui`** — 每个 `t('…')` 键在中英两本字典里都存在、两本字典键集完全一致、每个 `dab-…` 类在样式表里都有规则、没有字典条目变成没人引用的死键,以及每一个 `var(--dsw-…)` 都必须是已安装主题确实发布过的 token —— 未定义的自定义属性会让声明被丢掉、属性悄悄回落到继承值,placeholder 因此曾经画成正文字的墨色(读不到主题时这一节跳过并说明)。
|
|
254
|
+
- **`check:node`** — 把构建好的 node 半边通过它真实的 RPC 处理器跑一遍,`DSH_HOME` 指向临时目录,并把 `globalThis.fetch` 换成桩 —— 节日画现在住在 CDN 上,这套校验不能依赖某个 CDN 是活的,也不该去断言一个不属于自己的文件今天有多少字节。它覆盖:节日配置块穿过共享的 sanitizer;仅仅加载插件或读取配置不会发起任何下载;画是按需下载并缓存到 `holiday-cache/`、按**素材名**而不是槽位命名;第二次读取直接读盘、不再传输;两次并发的冷读取共用一次传输;第一个镜像不通时回落到第二个,且两个镜像读的是同一个发行 tag;所有镜像都不通时答案是「没有字节」而不是抛异常,失败的抓取什么都不缓存;镜像返回 200 但内容不是图片时被拒绝而不是被缓存;损坏的缓存条目被丢弃并重新下载;节日槽位拒绝写入 / 删除 / 抓取而丢进去的文件被忽略、且数据目录里永远不会出现 `modelbg-h-*` 文件;普通规则槽位照常写入和删除;手改过的配置无法把节日指向别的规则的图片。它还覆盖逐图主题色这套形状:读取时抬升的两个方向(缺键继承规则色、显式 `null` 不继承)、颜色经过文件往返后各自独立,以及 `null` 是写出键而不是省略。切换效果同样在覆盖范围内:旧配置拿到的出厂默认、选定的效果 / 缓动 / 统一时长能原样穿过文件往返、非法值回退而夸张时长被夹紧、时长为 `0` 被保留而不是回退成默认,以及 `transition` 内部的字段漂移会被上报而不是悄悄丢掉。推荐配置从 node 半边这一侧被整条覆盖:manifest 与 config 按序下载并当场穿过 sanitizer、请求清单由规则推导且把节日槽位剔掉、一张壁纸一个请求且类型是嗅出来的而不是信 header、jsDelivr 对单个文件死了会回落到 raw、两个镜像同时抖一下时同一个文件会被**再问一遍**并最终拿到(三个请求,日志里看得见)、托管处没有的文件仍然只花两次请求(每镜像一次、不等待)、永久 500 的镜像总共只花六次请求(三轮 × 两个镜像 —— 重试是有预算的),以及它每一种失败都是返回值而不是抛异常 —— 所有镜像都不通、镜像回 500、回 200 但内容是 HTML、manifest 的版本不是数字、写给更新形状的配置在请求第一张壁纸之前就被拒绝、config 不是 JSON、config 是 JSON 数组、config 列出的图片数超限、镜像谎报一个超大的长度;再加上严格的那一半(config 点名而托管处没有的壁纸是一个错误,而不是一份短了的配置)、两次在任何请求之前就发生的拒绝(节日槽位、以及一个根本不是槽位的名字),以及新装那条规则:空数据目录读出来正好一条空白规则(`r1`、无匹配串、无颜色、无图),而显式写空的规则列表读回仍然为空 —— 这两者是不同的答案;清空那一步:配置和每一张规则图片都没了、`holiday-cache/` 原封不动、清空后再读拿到的是同一条空白规则而不是报错,以及随后能往空目录里写入这份配置。
|
|
253
255
|
|
|
254
256
|
## 常见问题
|
|
255
257
|
|
|
@@ -300,22 +302,20 @@ pnpm run typecheck
|
|
|
300
302
|
|
|
301
303
|
## 近期优化
|
|
302
304
|
|
|
303
|
-
> 这里只列最近两个版本;更早的版本(v0.7.
|
|
305
|
+
> 这里只列最近两个版本;更早的版本(v0.7.7 及以前)见 [CHANGELOG.zh.md](./CHANGELOG.zh.md)。
|
|
304
306
|
|
|
305
|
-
### v0.7.
|
|
307
|
+
### v0.7.9
|
|
306
308
|
|
|
307
|
-
-
|
|
308
|
-
-
|
|
309
|
-
-
|
|
309
|
+
- **所有素材引用都钉到 assets 仓库的同一个发行 tag** —— `v0.1.0`。三处引用此前漂成了三种状态:README 的三张截图读的是 `main` 分支,节日素材钉的是一个原始 commit 哈希(`58a9e107…`,对人来说什么都说明不了),而推荐配置是**刻意**跟着 `main` 的 —— 于是一个插件可能在读一份它构建之后才变过的字节。现在三处都是 `…/assets@v0.1.0/…`,插件所取的每一个字节(截图、节日素材、推荐配置)都来自 assets 仓库的同一个版本,而且这个值人能读、能拿去和 GitHub 上的 tag 对照、也能在一处改完。
|
|
310
|
+
- **节日素材的字节并没有跟着变。** `58a9e107` 是 `v0.1.0` 的祖先,两张 `holiday/*.webp` 在两者之间逐字节相同,所以下载行为不变;变的只是这个引用现在和仓库本身用同一种方式命名。raw 镜像也带 tag(`raw.githubusercontent.com/HarmlessFunny/assets/v0.1.0/…`),所以回落仍然不可能混版本。
|
|
311
|
+
- **推荐配置是刻意放弃「不发版就能修」的** —— 它此前跟着分支,是为了让一个不好的推荐能在 assets 仓库里直接修正、不必等插件发版,理由是推荐配置的失败是**响亮的**(你正盯着那个按钮),而节日的失败是静默的,所以不可变在这里什么都买不到。它买来的代价是:插件的字节可能在装上之后、你点下去之前发生变化;现在一个 tag 同时覆盖截图、节日素材和这份配置,三者都不再有这个问题。改一个 JSON 现在要花一个 `assets` 里的 tag、`src/index.ts` 里的一个值和一次 npm 发版。也要注意 tag **不是**什么:它可以被重新指向,而 jsDelivr 把版本引用按不可变处理,所以重新指向要发一次 purge 请求而不是 push —— 这也是 `presetKey` 在这次改动后仍然保留的原因:一个没清过缓存的边缘节点仍会回同一个 URL 的上一次字节,恢复下载的那一次不能把两个修订拼在一起。
|
|
312
|
+
- **`check:node` 在两个功能上都钉住了这个 tag。** 它的 `revision()` 辅助函数此前找的是 40 位 SHA,根本看不见 tag;现在它从仓库名旁边取出引用(jsDelivr 形状是 `HarmlessFunny/assets@<ref>`,raw 形状是 `HarmlessFunny/assets/<ref>/`),于是两个镜像仍然可以互相比较;两条断言里原来的「a commit, never a branch」和「follows the branch」现在都要求 `v0.1.0`。
|
|
310
313
|
|
|
311
|
-
### v0.7.
|
|
312
|
-
|
|
313
|
-
- **规则页从一摞会自己长大的卡片变成一条选项卡 + 一个编辑器** —— 此前每条规则是一张自展开的卡片,页面高度等于所有卡片之和,只有手动一张张收起才会变短。现在每条规则是一个选项卡、页面上只挂载一个编辑器,所以高度只跟「一张卡片 + 一条选项卡」有关,与规则条数无关。选项卡带上原本只在卡头才有的东西(序号、首图缩略图、匹配串,以及它是不是当前生效的那条、是否在轮换);条子横向滚动不换行、固定 34px、匹配串省略号截断,所以切换规则不会让整行抖动。**「+」和「切换哪条规则」挪到滚动区之外** —— 放在里面时,长列表里唯一能新增规则的控件会被滚走。匹配测试器搬到「资料」页:它回答的是关于整份配置的问题,而在规则页上它把选项卡整整推下去一张卡片的高度。背后两个判定抽成了纯函数(`src/client/rules-view.ts`),由新的 `check:rules` 钉住,同时钉住布局所依赖的页面结构。
|
|
314
|
-
- **轮换卡片的控件按你的要求重排了一遍** —— 「下一张」和「正在显示第 2 / 3 张」搬到了「轮换这组图」那一行的外侧、**常驻**:那句读数是这张卡片上唯一说明当前是哪张图的地方,此前它会跟着轮换开关一起消失。判据只剩一条 —— 当前生效的规则有两张以上图就可用(`rotateNow` 自己的判据),不足两张时按钮禁用、读数整句省略。**「切换模型时也换一张」挪到顺序那一行**(三者都在回答「什么时候换」);**「1 小时」从停留时长预设里删掉**(自定义输入照旧接受 3600 秒);**八个固定预设色板删掉**(滚轮、数值输入、从本图提取、吸管、跟随系统主题都还在,那一排只是同一个 setter 的快捷方式);**整个轮换块移到第二列、主题色之下** —— 折叠成单列的窄版里它此前是卡片最高的一块,正好夹在图片和编辑图片的颜色控件中间。
|
|
315
|
-
- **节日画不再随包发布:当天按需下载、按素材名缓存** —— 两张节日壁纸(228 KB + 500 KB)此前占了整个 tarball 的三分之二,为的是一张一年只被看一天的画,还要把二进制历史永久留在 `.git` 里。现在它们托管在项目自己的 [`HarmlessFunny/assets`](https://github.com/HarmlessFunny/assets):下载由**日历**把关,不在节日的配置一点流量都没有;当天第一次读取会把它缓存到 `holiday-cache/`,此后每次启动、往后每一年都直接读磁盘。引用**钉死在 commit 上而不是分支** —— 运行时素材必须不可变,否则以后整理一次文档截图就能让已发布的功能在某个没人盯着的那天静默失效;jsDelivr 主用、raw 兜底,两个都不通时这个节日就是不接管、模型规则继续画(那道「槽位里到底有没有字节」的闸门本来就是为这个写的)。缓存按**素材名**命名、刻意放在槽位命名空间之外,所以「节日槽位只读」「数据目录里永远没有 `modelbg-h-*`」两条不变量一个字没改;缓存条目会被**校验**,不是图片的按未命中处理并重下。`check:node` 里 `globalThis.fetch` 被整体换成桩,测试因此不依赖任何 CDN 是活的。
|
|
316
|
-
- **一键装上「推荐配置」** —— 「配置」页多了第三张卡,从同一个 assets 仓库下载作者本人正在用的那套配置并就地覆盖你的(见[推荐配置](#推荐配置))。**它托管在数据目录自己的形状里** —— 一个 `preset.json` 形状闸门、一份可读的 `theme-config.json`、加上每个槽位一个原始文件 —— 这正是它「在所在的地方就可编辑」的原因:配置是 6.5 KB 的 JSON,可以直接在 GitHub 上改;换一张壁纸就是替换一个文件;发布它就是从数据目录里 `cp` 一下。它一开始被写成单个主题文件、所有壁纸内联成 base64,2.9 MB 里 99.8% 是图片 —— 那不是任何人能手改的东西,格式就是为这个改的。插件先取 manifest 与 config,从 `rules[].images[].slot` 推出文件清单(没有第二处要同步),再一个文件一个请求地取壁纸(四路并发) —— 过程中按钮上显示 `3/7` —— 于是每个文件都有自己的镜像回退和校验。落地时走的还是本地文件那条 `applyThemeFile`,在客户端组装成 `exportTheme` 写出的形状;它是这个面板里唯一会销毁东西的操作,所以也是唯一会先问一句的,而下载发生在**提问之前**,确认框因此能如实说出手里到底有什么(7 条规则 · 7 张壁纸 · 2.9 MB)。**先清空再写入是刻意的**:`nextSlot` 分配槽位用的是 `m1`、`m2`…… 一个计数器而不是随机串,所以下载来的配置和它落进去的那个数据目录必然在这些名字上撞车,「用它替换一切」只有在那个「一切」真的没了的时候才成立 —— 这也是它没有撤销的原因。清空跑在下载**之后**、绝不在之前,因为 CDN 不通不该让你在等一个永远没来的推荐配置的路上先弄丢自己的配置;而一个旧到写不了这套配置形状的宿主,会在删除任何东西之前就被拒绝。**少一张壁纸就整体失败并报出是哪个文件** —— 一份悄悄少了图的配置,恰恰是「节日素材钉死 commit」那条契约要防的静默失效,所以这个响亮的失败正是分支引用在这里可以被接受的原因;config 合法列出的那两个节日槽位会被从请求清单里剔掉而不是去请求,因为数据目录里永远不可能有 `modelbg-h-*` 文件。`holiday-cache/` 不碰,配置也从不写进数据目录、从不落盘缓存 —— 那个按钮的全部意义就是拿到**此刻**的那一份。
|
|
317
|
-
- **发布包从 1.1 MB 瘦到约 220 kB** —— 文档截图(`example_img/`,三个 webp 共 220 KB)搬去同一个 assets 仓库、两份 README 改用 jsDelivr 绝对地址(绝对地址同样能让 npm 页面上的图不裂,还不用把二进制塞进包和 `.git` 历史);没有消费方的 `screenshots.json` 删掉;`lib/client.js.map`(624 KB 的源码映射,对装包的人没用)不再发布,仓库里仍然保留。13 个文件 → 10 个。
|
|
314
|
+
### v0.7.8
|
|
318
315
|
|
|
316
|
+
- **新装打开的就是一条空白规则,而不是「还没有规则」那一屏** —— 此前完全没有配置文件时读出来的是一份空的规则列表,于是第一次装上看到的是那句「还没有规则,点上面的『新增规则』新建一条」:界面在教用户去点一个它还没给出的控件。现在「`rules` 这个键根本不在」(从未配置过)和「键在、数组是空的」(用户把规则删光了)是两个不同的答案,只有前者会种出一条空白规则(id 是 `r1`,所以它就是规则 1、就是兜底、就是面板默认选中的那张卡),后者保持为空 —— 删掉最后一条规则不会被下一次读盘复活。这条规则刻意是「空」而不是「半成品」:它没有图、也没有自己的主题色,所以在被填上东西之前匹配会跳过它,而不是让一个空背景顶掉规则 1 的兜底。
|
|
317
|
+
- **placeholder 不再看起来像真的文字** —— 那条 `::placeholder` 颜色写的 `--dsw-alias-label-quaternary` 在这个 harness 的主题里**一个定义都没有**:未定义的自定义属性会让声明在计算值阶段被丢掉,而 `color` 是继承属性,于是 placeholder 拿到输入框自己的 `label-primary` —— 面板里最亮的墨色,写在一个用户一个字都没打过的框里。三个带 placeholder 的输入(规则匹配串、图片网址、轮换的自定义秒数)现在统一读 `--dsw-alias-label-caption`,也就是插件早就给输入框用的那个弱一档的 token;同一个死名字还有两处跟着修掉(`.dab-pick-n` 的 `--dsw-mono` 少了个字母,插件自己的变量是 `--dab-mono`;`.dab-brand-tag` 的那个本来就带 fallback,去掉它不改变颜色)。
|
|
318
|
+
- **推荐配置的下载会按文件重试,失败的那一次把已下到的部分留给下一次** —— 此前一个文件只有两次机会(两个镜像各一次),前面几张都下好了、下一张碰上一次 502 或者一次 CDN 抽风,整份配置就失败,而再按一次按钮是从零开始。现在每个文件最多三轮、轮间 400 ms 再 800 ms;三种失败不重试 ——「这个文件不在那儿」(再问也是同一个答案)、「字节本身就超限」(下一轮还是这些字节)、「六十秒都没吭声」(两个镜像在同一轮里都已经问过了,而这一条正是把最坏情况钉在三分钟而不是六分钟的东西),其余失败会再问一轮。失败的那一次把已经拿到的字节留在手里,下一次只补缺的那几张 —— 前提是重新取到的配置与那些字节来自的那一份**逐值相同**(这份配置服务在分支上,两次不同的推送不许拼在一起)。确认框、按钮文案、进度读数的位置一处没动;**严格性也没变**:第一个拿不到的文件仍然让整次失败、仍然报出是哪一个。
|
|
319
319
|
|
|
320
320
|
## Star History
|
|
321
321
|
|
package/lib/client.js
CHANGED
|
@@ -813,10 +813,48 @@ window.__ModuleLoader__.load({
|
|
|
813
813
|
function defaultHolidayRules() {
|
|
814
814
|
return HOLIDAYS.map(defaultHolidayRule);
|
|
815
815
|
}
|
|
816
|
-
/**
|
|
816
|
+
/**
|
|
817
|
+
* A rule with nothing in it yet: no picture, no match string, no color.
|
|
818
|
+
*
|
|
819
|
+
* Two states need this factory and they are the same state — a fresh install
|
|
820
|
+
* (see `freshThemeConfig` and `normalizeConfig`), and the "add rule" tile one
|
|
821
|
+
* entry further down the list.
|
|
822
|
+
*
|
|
823
|
+
* Such a rule is deliberately valid rather than half-built: it is skipped by
|
|
824
|
+
* matching until it has something of its own to paint (`ruleCanPaint` — a picture,
|
|
825
|
+
* or a theme color), which is what makes a rule nobody has filled in yet invisible
|
|
826
|
+
* instead of letting an empty background steal the fallback from rule 1.
|
|
827
|
+
*
|
|
828
|
+
* It used to be born with an allocated slot and an empty image entry, which is
|
|
829
|
+
* what put a blank tile at position 1 of a new rule's strip — and, worse, stopped
|
|
830
|
+
* the picture the user added next from being the first one (a rule paints its
|
|
831
|
+
* FIRST image). The slot is allocated when the first picture arrives.
|
|
832
|
+
*/
|
|
833
|
+
function blankRule(id) {
|
|
834
|
+
return {
|
|
835
|
+
id,
|
|
836
|
+
images: [],
|
|
837
|
+
match: "",
|
|
838
|
+
enabled: true,
|
|
839
|
+
color: null,
|
|
840
|
+
wallpaperOpacity: 1,
|
|
841
|
+
blur: 0,
|
|
842
|
+
rotate: defaultRotation()
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* A default config with independent nested objects (never hand out the shared ones).
|
|
847
|
+
*
|
|
848
|
+
* The rule list is seeded with one blank rule rather than left empty: "no rules at
|
|
849
|
+
* all" is not a state the panel should ever open in, because the first thing a new
|
|
850
|
+
* user has to do is name a match string and drop a picture in — and the drop target
|
|
851
|
+
* for that is the rule card, which an empty list has no way to show. The empty
|
|
852
|
+
* state is still reachable (deleting the last rule is a thing the user can mean),
|
|
853
|
+
* and is described by `ruleNoneHint`.
|
|
854
|
+
*/
|
|
817
855
|
function freshThemeConfig() {
|
|
818
856
|
return {
|
|
819
|
-
rules: [],
|
|
857
|
+
rules: [blankRule("r1")],
|
|
820
858
|
opacities: { ...DEFAULT_PART_OPACITIES },
|
|
821
859
|
blurs: { ...DEFAULT_PART_BLURS },
|
|
822
860
|
transition: defaultTransition(),
|
|
@@ -1090,7 +1128,7 @@ window.__ModuleLoader__.load({
|
|
|
1090
1128
|
/** Coerce an unknown persisted value into a valid ThemeConfig, falling back per-field. */
|
|
1091
1129
|
function normalizeConfig(raw) {
|
|
1092
1130
|
const r = raw ?? {};
|
|
1093
|
-
const rules = Array.isArray(r.rules) ? r.rules.map(normalizeRule).filter((x) => x !== null) : [];
|
|
1131
|
+
const rules = Array.isArray(r.rules) ? r.rules.map(normalizeRule).filter((x) => x !== null) : [blankRule("r1")];
|
|
1094
1132
|
const ops = r.opacities ?? {};
|
|
1095
1133
|
const bl = r.blurs ?? {};
|
|
1096
1134
|
const blurs = {};
|
|
@@ -1248,28 +1286,6 @@ window.__ModuleLoader__.load({
|
|
|
1248
1286
|
function holidayById(id) {
|
|
1249
1287
|
return cfg.holidays.items.find((r) => r.id === id) ?? null;
|
|
1250
1288
|
}
|
|
1251
|
-
/**
|
|
1252
|
-
* A fresh rule, holding NO image yet.
|
|
1253
|
-
*
|
|
1254
|
-
* It used to be born with an allocated slot and an empty entry, which is what put
|
|
1255
|
-
* a blank tile at position 1 of a new rule's strip — and, worse, stopped the
|
|
1256
|
-
* picture the user added next from being the first one (a rule paints its first
|
|
1257
|
-
* image). A rule with no pictures is a valid state: `ruleCanPaint` (./schema) skips
|
|
1258
|
-
* it — it has no image and no color yet — until either arrives, and the slot is
|
|
1259
|
-
* allocated when the first picture does.
|
|
1260
|
-
*/
|
|
1261
|
-
function newRule(id) {
|
|
1262
|
-
return {
|
|
1263
|
-
id,
|
|
1264
|
-
images: [],
|
|
1265
|
-
match: "",
|
|
1266
|
-
enabled: true,
|
|
1267
|
-
color: null,
|
|
1268
|
-
wallpaperOpacity: 1,
|
|
1269
|
-
blur: 0,
|
|
1270
|
-
rotate: defaultRotation()
|
|
1271
|
-
};
|
|
1272
|
-
}
|
|
1273
1289
|
/** Every slot any rule owns — the user's rules and the holiday entries alike. */
|
|
1274
1290
|
function takenSlots() {
|
|
1275
1291
|
const taken = /* @__PURE__ */ new Set();
|
|
@@ -1417,6 +1433,67 @@ window.__ModuleLoader__.load({
|
|
|
1417
1433
|
rotIndex = 0;
|
|
1418
1434
|
}
|
|
1419
1435
|
//#endregion
|
|
1436
|
+
//#region src/preset.ts
|
|
1437
|
+
/**
|
|
1438
|
+
* JSON with object keys in a fixed (sorted) order — the same string for the same
|
|
1439
|
+
* value, whatever order the object that carried it happened to build its keys in.
|
|
1440
|
+
*
|
|
1441
|
+
* `JSON.stringify` alone is not an identity for a value that came off the wire and
|
|
1442
|
+
* through a sanitizer: key order is an accident of how the object was assembled,
|
|
1443
|
+
* and a resume that missed on it would silently re-download everything (the safe
|
|
1444
|
+
* direction) while a resume that MATCHED on a value it should not have would be
|
|
1445
|
+
* the unsafe one — so this is deliberately total as well: anything JSON cannot
|
|
1446
|
+
* carry (a function, a `Symbol`, `undefined`) serializes as `null` and an
|
|
1447
|
+
* `undefined` property is skipped, exactly as `JSON.stringify` treats it.
|
|
1448
|
+
*/
|
|
1449
|
+
function canonical(value) {
|
|
1450
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
1451
|
+
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(",")}}`;
|
|
1452
|
+
return JSON.stringify(value) ?? "null";
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* The identity of one revision of the profile: the manifest's shape version plus
|
|
1456
|
+
* the config, in canonical form.
|
|
1457
|
+
*
|
|
1458
|
+
* The profile is served at ONE version of the assets repository, but a version
|
|
1459
|
+
* reference is only as fixed as the tag behind it: re-point the tag and every
|
|
1460
|
+
* edge that has not been purged keeps answering with the previous bytes of the
|
|
1461
|
+
* same URL. So "the profile" is still not guaranteed to be one fixed thing, and
|
|
1462
|
+
* the bytes a previous attempt is holding are only known to belong to what is
|
|
1463
|
+
* being installed when that config comes back the same. Order-insensitivity is
|
|
1464
|
+
* what lets this be a comparison of VALUES rather than of spellings —
|
|
1465
|
+
* `normalizeConfig` (./schema) rebuilds the config it reads, so pinning the key
|
|
1466
|
+
* to the exact key order the wire happened to use would make the hold useless
|
|
1467
|
+
* the moment anything upstream normalized differently.
|
|
1468
|
+
*/
|
|
1469
|
+
function presetKey(version, config) {
|
|
1470
|
+
return `${version}\n${canonical(config)}`;
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* The bytes of a failed attempt that may be reused for the revision `key` names.
|
|
1474
|
+
*
|
|
1475
|
+
* A fresh object every time, never the held record itself: the caller fills it in
|
|
1476
|
+
* as the rest of the profile arrives, and handing out the same mutable record
|
|
1477
|
+
* would let a re-fetch of one file edit the state the NEXT attempt resumes from.
|
|
1478
|
+
* An empty answer — nothing held, or held for a different revision — is the honest
|
|
1479
|
+
* one either way: the pool then asks for every slot the config names.
|
|
1480
|
+
*/
|
|
1481
|
+
function presetResume(held, key) {
|
|
1482
|
+
if (held === null || held.key !== key) return {};
|
|
1483
|
+
return { ...held.images };
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* The slots of `slots` that `images` does not hold yet, in the config's own order.
|
|
1487
|
+
*
|
|
1488
|
+
* Order matters in the only way it can here: it decides which file a worker takes
|
|
1489
|
+
* first, and the files are not the same size. Only names the CONFIG asked for are
|
|
1490
|
+
* ever returned, so a held picture nothing points at — a slot the author removed
|
|
1491
|
+
* from the profile — can never be fetched again or shipped.
|
|
1492
|
+
*/
|
|
1493
|
+
function presetMissing(slots, images) {
|
|
1494
|
+
return slots.filter((slot) => images[slot] === void 0);
|
|
1495
|
+
}
|
|
1496
|
+
//#endregion
|
|
1420
1497
|
//#region src/client/rotation.ts
|
|
1421
1498
|
/** Dwell-time presets the settings UI offers, in ms. */
|
|
1422
1499
|
const ROTATE_PRESETS = [
|
|
@@ -2168,7 +2245,10 @@ window.__ModuleLoader__.load({
|
|
|
2168
2245
|
.dab-brand:hover .dab-brand-name{color:var(--dsw-alias-brand-primary)}
|
|
2169
2246
|
.dab-brand-tile{width:30px;height:30px;flex:none;border-radius:9px;display:grid;place-items:center;color:var(--dsw-alias-label-primary-inverted);background:var(--dsw-alias-brand-primary);box-shadow:0 4px 14px -4px var(--dsw-alias-brand-primary)}
|
|
2170
2247
|
.dab-brand-name{font-size:13px;font-weight:650;letter-spacing:.01em;line-height:1.25}
|
|
2171
|
-
|
|
2248
|
+
/* The weak token was written as quaternary-WITH-a-fallback, so this tag has
|
|
2249
|
+
always painted tertiary: the dead name is dropped rather than being kept as
|
|
2250
|
+
decoration nothing resolves to (check:ui rejects the name outright now). */
|
|
2251
|
+
.dab-brand-tag{font-size:9px;letter-spacing:.16em;font-weight:600;color:var(--dsw-alias-label-tertiary);text-transform:uppercase}
|
|
2172
2252
|
.dab-nav-list{position:relative;display:flex;flex-direction:column;gap:4px}
|
|
2173
2253
|
.dab-nav-ind{position:absolute;left:0;right:0;top:0;height:38px;border-radius:11px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 13%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 25%,transparent);transition:transform .38s cubic-bezier(.22,1,.36,1)}
|
|
2174
2254
|
.dab-nav-item{position:relative;z-index:1;display:flex;align-items:center;gap:10px;height:38px;padding:0 12px;border:0;background:none;border-radius:11px;color:var(--dsw-alias-label-tertiary);font-size:13px;cursor:pointer;text-align:left;transition:color .22s ease}
|
|
@@ -2221,8 +2301,20 @@ window.__ModuleLoader__.load({
|
|
|
2221
2301
|
.dab-num::-webkit-outer-spin-button,.dab-num::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
|
2222
2302
|
.dab-num{-moz-appearance:textfield;appearance:textfield}
|
|
2223
2303
|
.dab-urlinput{flex:1;min-width:180px;height:34px;padding:0 12px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:12.5px;outline:none;transition:border-color .2s}
|
|
2224
|
-
.dab-urlinput::placeholder{color:var(--dsw-alias-label-quaternary)}
|
|
2225
2304
|
.dab-urlinput:focus{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent)}
|
|
2305
|
+
/* Placeholder text is the field's PREDICTION, never its content: the weak caption
|
|
2306
|
+
token, one step below the text it sits in, so an empty box cannot read as a
|
|
2307
|
+
filled one. All three inputs that carry one are listed here (the rule's match
|
|
2308
|
+
field lives further down, in the rule card) rather than each deciding for
|
|
2309
|
+
itself what "empty" looks like.
|
|
2310
|
+
This used to name --dsw-alias-label-quaternary, which this harness defines
|
|
2311
|
+
NOWHERE: an undefined token drops the declaration, and the placeholder then
|
|
2312
|
+
INHERITS the input's own label-primary — the brightest text in the panel,
|
|
2313
|
+
inside a field the user has not typed in. check:ui now fails on any --dsw-…
|
|
2314
|
+
name the installed theme does not publish, so that one cannot come back.
|
|
2315
|
+
label-caption is the token PLACEHOLDER_RULE (../wallpaper) already uses for the
|
|
2316
|
+
composer, so the plugin answers this question in one voice. */
|
|
2317
|
+
.dab-num::placeholder,.dab-urlinput::placeholder,.dab-rule-match::placeholder{color:var(--dsw-alias-label-caption)}
|
|
2226
2318
|
/* Prints the current hex on the color itself. Without the value this was an
|
|
2227
2319
|
* empty bordered rectangle sitting in the same column as the H/S/L number
|
|
2228
2320
|
* fields, which made it read as one more — blank — text input; nothing is ever
|
|
@@ -2407,7 +2499,7 @@ window.__ModuleLoader__.load({
|
|
|
2407
2499
|
.dab-pick-item:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
2408
2500
|
.dab-pick-item.is-active{color:var(--dsw-alias-brand-primary);font-weight:600;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
|
|
2409
2501
|
.dab-pick-match{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2410
|
-
.dab-pick-n{flex:none;font-family:var(--
|
|
2502
|
+
.dab-pick-n{flex:none;font-family:var(--dab-mono);font-size:10.5px;opacity:.7}
|
|
2411
2503
|
|
|
2412
2504
|
/* ── one rule editor (the tab panel's content) ─────────────────────────────── */
|
|
2413
2505
|
.dab-rule-panel{min-height:0}
|
|
@@ -2422,7 +2514,6 @@ window.__ModuleLoader__.load({
|
|
|
2422
2514
|
.dab-rule-num{font-family:var(--dab-mono);font-size:11px;font-weight:700;color:var(--dsw-alias-label-tertiary);min-width:16px}
|
|
2423
2515
|
.dab-rule-badge{font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent);border-radius:99px;padding:2px 8px}
|
|
2424
2516
|
.dab-rule-match{flex:1;min-width:150px;height:32px;padding:0 11px;border-radius:9px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-family:var(--dab-mono);font-size:12.5px;outline:none;transition:border-color .2s,box-shadow .2s}
|
|
2425
|
-
.dab-rule-match::placeholder{color:var(--dsw-alias-label-quaternary)}
|
|
2426
2517
|
.dab-rule-match:focus{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent)}
|
|
2427
2518
|
.dab-icon-btn{width:28px;height:28px;flex:none;display:grid;place-items:center;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;transition:color .2s,border-color .2s,background .2s}
|
|
2428
2519
|
.dab-icon-btn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-brand-primary)}
|
|
@@ -7792,6 +7883,20 @@ window.__ModuleLoader__.load({
|
|
|
7792
7883
|
applyActive();
|
|
7793
7884
|
return true;
|
|
7794
7885
|
};
|
|
7886
|
+
/**
|
|
7887
|
+
* What the last FAILED profile attempt already had in hand.
|
|
7888
|
+
*
|
|
7889
|
+
* Only a failure leaves anything here — a download that completes is applied or
|
|
7890
|
+
* thrown away whole, and holding megabytes of base64 for a re-click that a
|
|
7891
|
+
* second re-download would settle is not a trade this makes. What it buys is the
|
|
7892
|
+
* one case that had no answer at all before: a profile whose sixth of seven files
|
|
7893
|
+
* failed now costs ONE file on the next press instead of seven.
|
|
7894
|
+
*
|
|
7895
|
+
* Held across section re-injections on purpose (it is declared here, in `apply`,
|
|
7896
|
+
* not inside `sectionInject`): the retry is usually the next click of the same
|
|
7897
|
+
* button, but the panel can be closed and reopened between them.
|
|
7898
|
+
*/
|
|
7899
|
+
let presetHeld = null;
|
|
7795
7900
|
const sectionInject = (actions) => {
|
|
7796
7901
|
bound = actions;
|
|
7797
7902
|
sync();
|
|
@@ -7802,7 +7907,7 @@ window.__ModuleLoader__.load({
|
|
|
7802
7907
|
readModelFacts,
|
|
7803
7908
|
imageOf: (slot) => displayImageOf(slot),
|
|
7804
7909
|
addRule: () => {
|
|
7805
|
-
const rule =
|
|
7910
|
+
const rule = blankRule(nextRuleId());
|
|
7806
7911
|
cfg.rules.push(rule);
|
|
7807
7912
|
rulesRev++;
|
|
7808
7913
|
persistConfig();
|
|
@@ -8137,15 +8242,19 @@ window.__ModuleLoader__.load({
|
|
|
8137
8242
|
* request, up to `PRESET_CONCURRENCY` of them at a time. What the fan-out
|
|
8138
8243
|
* buys is wall-clock time — the real profile measured 8.3 s fetched one at
|
|
8139
8244
|
* a time against 4.8 s with four in flight — while each file keeps its own
|
|
8140
|
-
* mirror fallback, its own
|
|
8245
|
+
* mirror fallback, its own retry rounds (the node half's business, see
|
|
8246
|
+
* ./preset), its own validation, and its own slot name in a failure.
|
|
8141
8247
|
*
|
|
8142
8248
|
* STRICT, and the pool is what makes that a decision rather than a
|
|
8143
8249
|
* formality: the first wallpaper that cannot be had stops the queue, so the
|
|
8144
8250
|
* files nobody has asked for yet are never asked for. Whatever was already
|
|
8145
|
-
* in flight finishes and is thrown away
|
|
8146
|
-
*
|
|
8147
|
-
*
|
|
8148
|
-
*
|
|
8251
|
+
* in flight finishes and is thrown away — but what it FETCHED is kept for the
|
|
8252
|
+
* next press (`presetHeld` below), which is the difference between retrying
|
|
8253
|
+
* one file and retrying the lot. Nothing has been touched at this point, so
|
|
8254
|
+
* "the author renamed a file" arrives as an error naming the file rather
|
|
8255
|
+
* than as a profile that is quietly missing a picture — and that loudness
|
|
8256
|
+
* is what makes a reference the plugin does not fully control acceptable
|
|
8257
|
+
* here.
|
|
8149
8258
|
*/
|
|
8150
8259
|
fetchRecommended: async (onProgress) => {
|
|
8151
8260
|
const head = await fetchPresetConfig();
|
|
@@ -8154,17 +8263,22 @@ window.__ModuleLoader__.load({
|
|
|
8154
8263
|
error: head.error
|
|
8155
8264
|
};
|
|
8156
8265
|
const slots = head.slots;
|
|
8157
|
-
const
|
|
8266
|
+
const key = presetKey(head.version, head.config);
|
|
8267
|
+
const images = presetResume(presetHeld, key);
|
|
8268
|
+
let bytes = 0;
|
|
8269
|
+
for (const url of Object.values(images)) bytes += url.length;
|
|
8270
|
+
const missing = presetMissing(slots, images);
|
|
8271
|
+
/** Files already in hand from the attempt this one is resuming. */
|
|
8272
|
+
const held = slots.length - missing.length;
|
|
8158
8273
|
let next = 0;
|
|
8159
8274
|
let done = 0;
|
|
8160
|
-
let bytes = 0;
|
|
8161
8275
|
/** Latched by the first worker to fail, so the rest stop asking. */
|
|
8162
8276
|
let stopped = false;
|
|
8163
|
-
onProgress?.(
|
|
8277
|
+
onProgress?.(held, slots.length);
|
|
8164
8278
|
const worker = async () => {
|
|
8165
8279
|
for (;;) {
|
|
8166
|
-
if (stopped || next >=
|
|
8167
|
-
const slot =
|
|
8280
|
+
if (stopped || next >= missing.length) return null;
|
|
8281
|
+
const slot = missing[next++];
|
|
8168
8282
|
const got = await fetchPresetImage(slot);
|
|
8169
8283
|
if (!got.ok) {
|
|
8170
8284
|
stopped = true;
|
|
@@ -8173,21 +8287,28 @@ window.__ModuleLoader__.load({
|
|
|
8173
8287
|
slot
|
|
8174
8288
|
};
|
|
8175
8289
|
}
|
|
8176
|
-
|
|
8177
|
-
bytes += got.dataUrl.length;
|
|
8178
|
-
if (bytes > PRESET_TOTAL_MAX) {
|
|
8290
|
+
if (bytes + got.dataUrl.length > PRESET_TOTAL_MAX) {
|
|
8179
8291
|
stopped = true;
|
|
8180
8292
|
return { error: "too large" };
|
|
8181
8293
|
}
|
|
8182
|
-
|
|
8294
|
+
images[slot] = got.dataUrl;
|
|
8295
|
+
bytes += got.dataUrl.length;
|
|
8296
|
+
onProgress?.(held + ++done, slots.length);
|
|
8183
8297
|
}
|
|
8184
8298
|
};
|
|
8185
|
-
const failure = (await Promise.all(Array.from({ length: Math.min(PRESET_CONCURRENCY,
|
|
8186
|
-
if (failure !== null)
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8299
|
+
const failure = (await Promise.all(Array.from({ length: Math.min(PRESET_CONCURRENCY, missing.length) }, worker))).find((r) => r !== null) ?? null;
|
|
8300
|
+
if (failure !== null) {
|
|
8301
|
+
presetHeld = {
|
|
8302
|
+
key,
|
|
8303
|
+
images
|
|
8304
|
+
};
|
|
8305
|
+
return {
|
|
8306
|
+
ok: false,
|
|
8307
|
+
error: failure.error,
|
|
8308
|
+
slot: failure.slot
|
|
8309
|
+
};
|
|
8310
|
+
}
|
|
8311
|
+
presetHeld = null;
|
|
8191
8312
|
return {
|
|
8192
8313
|
ok: true,
|
|
8193
8314
|
preset: {
|
package/lib/index.js
CHANGED
|
@@ -213,6 +213,35 @@ function defaultHolidayRule(def) {
|
|
|
213
213
|
rotate: defaultRotation()
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* A rule with nothing in it yet: no picture, no match string, no color.
|
|
218
|
+
*
|
|
219
|
+
* Two states need this factory and they are the same state — a fresh install
|
|
220
|
+
* (see `freshThemeConfig` and `normalizeConfig`), and the "add rule" tile one
|
|
221
|
+
* entry further down the list.
|
|
222
|
+
*
|
|
223
|
+
* Such a rule is deliberately valid rather than half-built: it is skipped by
|
|
224
|
+
* matching until it has something of its own to paint (`ruleCanPaint` — a picture,
|
|
225
|
+
* or a theme color), which is what makes a rule nobody has filled in yet invisible
|
|
226
|
+
* instead of letting an empty background steal the fallback from rule 1.
|
|
227
|
+
*
|
|
228
|
+
* It used to be born with an allocated slot and an empty image entry, which is
|
|
229
|
+
* what put a blank tile at position 1 of a new rule's strip — and, worse, stopped
|
|
230
|
+
* the picture the user added next from being the first one (a rule paints its
|
|
231
|
+
* FIRST image). The slot is allocated when the first picture arrives.
|
|
232
|
+
*/
|
|
233
|
+
function blankRule(id) {
|
|
234
|
+
return {
|
|
235
|
+
id,
|
|
236
|
+
images: [],
|
|
237
|
+
match: "",
|
|
238
|
+
enabled: true,
|
|
239
|
+
color: null,
|
|
240
|
+
wallpaperOpacity: 1,
|
|
241
|
+
blur: 0,
|
|
242
|
+
rotate: defaultRotation()
|
|
243
|
+
};
|
|
244
|
+
}
|
|
216
245
|
/** Slot names reach the filesystem, so they are strictly whitelisted. */
|
|
217
246
|
const SLOT_RE = /^[A-Za-z0-9_-]{1,32}$/;
|
|
218
247
|
function clamp(n, lo, hi, def) {
|
|
@@ -472,7 +501,7 @@ function normalizeRightbarOpacity(v) {
|
|
|
472
501
|
/** Coerce an unknown persisted value into a valid ThemeConfig, falling back per-field. */
|
|
473
502
|
function normalizeConfig(raw) {
|
|
474
503
|
const r = raw ?? {};
|
|
475
|
-
const rules = Array.isArray(r.rules) ? r.rules.map(normalizeRule).filter((x) => x !== null) : [];
|
|
504
|
+
const rules = Array.isArray(r.rules) ? r.rules.map(normalizeRule).filter((x) => x !== null) : [blankRule("r1")];
|
|
476
505
|
const ops = r.opacities ?? {};
|
|
477
506
|
const bl = r.blurs ?? {};
|
|
478
507
|
const blurs = {};
|
|
@@ -492,6 +521,42 @@ function normalizeConfig(raw) {
|
|
|
492
521
|
holidays: normalizeHolidays(r.holidays)
|
|
493
522
|
};
|
|
494
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* Whether another round is worth asking for.
|
|
526
|
+
*
|
|
527
|
+
* Three reasons are terminal, and each for its own:
|
|
528
|
+
*
|
|
529
|
+
* - `missing` — the host says the file is not there. The profile follows a
|
|
530
|
+
* BRANCH (see `PRESET_ASSET_HOSTS` in ./index), so a renamed or deleted file
|
|
531
|
+
* has to reach the user as a loud failure NAMING the slot — and it has to
|
|
532
|
+
* reach them FAST: paying two more rounds for a file that is provably absent
|
|
533
|
+
* only makes the sentence later, on a button the user is watching.
|
|
534
|
+
* - `oversized` — the bytes are the problem, and the next round would serve the
|
|
535
|
+
* same bytes.
|
|
536
|
+
* - `timeout` — sixty seconds of silence is not weather. Both mirrors were
|
|
537
|
+
* already asked inside the SAME round, so a retry round would re-ask two hosts
|
|
538
|
+
* that each just spent a minute not answering. This is also what keeps the
|
|
539
|
+
* worst case for one file at three minutes instead of six: a round in which BOTH
|
|
540
|
+
* mirrors were silent is over immediately (two minutes, exactly what it cost
|
|
541
|
+
* before there were rounds at all), and only a round that mixed a silent mirror
|
|
542
|
+
* with a transient one can carry a minute into the next round.
|
|
543
|
+
*
|
|
544
|
+
* Everything else is a hiccup: a thrown connection, a 5xx, a 429, a 408, or a 200
|
|
545
|
+
* with no body.
|
|
546
|
+
*/
|
|
547
|
+
function presetRetryable(reason) {
|
|
548
|
+
return reason !== "missing" && reason !== "oversized" && reason !== "timeout";
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* The delay before the round that follows `failedAttempt` (1-based), in ms.
|
|
552
|
+
*
|
|
553
|
+
* Doubling rather than fixed: the second retry is the one that has to survive a
|
|
554
|
+
* mirror having a bad few hundred milliseconds, and it is still short enough that
|
|
555
|
+
* a user staring at "downloading" does not read it as a hang.
|
|
556
|
+
*/
|
|
557
|
+
function presetRetryDelayMs(failedAttempt) {
|
|
558
|
+
return 400 * 2 ** Math.max(0, failedAttempt - 1);
|
|
559
|
+
}
|
|
495
560
|
//#endregion
|
|
496
561
|
//#region src/index.ts
|
|
497
562
|
/**
|
|
@@ -538,23 +603,30 @@ const WALLPAPER_FETCH_TIMEOUT = 2e4;
|
|
|
538
603
|
* the `assets` repository beside the README screenshots and are fetched from a
|
|
539
604
|
* CDN the first time a holiday actually needs one (see `readHolidayAsset`).
|
|
540
605
|
*
|
|
541
|
-
* The reference is a **
|
|
606
|
+
* The reference is a **release tag**, never a branch. Runtime art has to be
|
|
542
607
|
* immutable: a branch reference would let a later reshuffle of the documentation
|
|
543
608
|
* screenshots silently kill a shipped feature, on a day nobody is watching, with
|
|
544
|
-
* nothing in the log — and jsDelivr
|
|
545
|
-
* exactly what a file that never changes wants.
|
|
546
|
-
*
|
|
609
|
+
* nothing in the log — and jsDelivr serves a version reference as immutable,
|
|
610
|
+
* which is exactly what a file that never changes wants. A tag says that to the
|
|
611
|
+
* CDN in the same breath as it says it to a human: `v0.1.0` is the reference the
|
|
612
|
+
* README's screenshots and the recommended profile are read at, so one release
|
|
613
|
+
* of the assets repository is one version of every byte this plugin fetches,
|
|
614
|
+
* where a raw commit hash was a value nobody could compare against anything.
|
|
615
|
+
* The cost is unchanged — new art means a new tag there and a new value here —
|
|
616
|
+
* with one addition: a tag that is MOVED does not reach jsDelivr's edge on its
|
|
617
|
+
* own (the reference is served as immutable), so re-pointing one means a purge
|
|
618
|
+
* request. Cutting a new tag is the cheap move.
|
|
547
619
|
*/
|
|
548
620
|
const HOLIDAY_ASSETS_REPO = "HarmlessFunny/assets";
|
|
549
|
-
const
|
|
621
|
+
const HOLIDAY_ASSETS_REF = "v0.1.0";
|
|
550
622
|
const HOLIDAY_ASSETS_DIR = "dsh-background-by-model/holiday";
|
|
551
623
|
/**
|
|
552
624
|
* Mirrors, tried in order. jsDelivr first because it is reachable from networks
|
|
553
625
|
* where `raw.githubusercontent.com` is not; raw second because a CDN can be
|
|
554
626
|
* blocked, rate-limited or down, and one dead mirror must not cost the easter
|
|
555
|
-
* egg. Every mirror serves the same
|
|
627
|
+
* egg. Every mirror serves the same version, so a fallback cannot mix revisions.
|
|
556
628
|
*/
|
|
557
|
-
const HOLIDAY_ASSET_HOSTS = [`https://cdn.jsdelivr.net/gh/${HOLIDAY_ASSETS_REPO}@${
|
|
629
|
+
const HOLIDAY_ASSET_HOSTS = [`https://cdn.jsdelivr.net/gh/${HOLIDAY_ASSETS_REPO}@${HOLIDAY_ASSETS_REF}/${HOLIDAY_ASSETS_DIR}`, `https://raw.githubusercontent.com/${HOLIDAY_ASSETS_REPO}/${HOLIDAY_ASSETS_REF}/${HOLIDAY_ASSETS_DIR}`];
|
|
558
630
|
/**
|
|
559
631
|
* Sub-directory of the store holding downloaded festival art.
|
|
560
632
|
*
|
|
@@ -587,22 +659,35 @@ const HOLIDAY_FETCH_TIMEOUT = 1e4;
|
|
|
587
659
|
* `rules[].images[].slot`, so adding a wallpaper is adding a rule entry and a
|
|
588
660
|
* file, with no second place to keep in sync.
|
|
589
661
|
*
|
|
590
|
-
* Referenced by
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
662
|
+
* Referenced by the same **release tag** as the festival art. This used to be a
|
|
663
|
+
* branch, and deliberately: the reason the art is immutable — a reshuffle of the
|
|
664
|
+
* assets repository would make a shipped feature disappear SILENTLY, on a day
|
|
665
|
+
* nobody is watching — does not apply to a profile, because a profile fails
|
|
666
|
+
* LOUDLY (the user is looking at the button and gets an error naming what went
|
|
667
|
+
* missing), and a branch bought the ability to correct a bad recommendation
|
|
668
|
+
* without a plugin release.
|
|
669
|
+
*
|
|
670
|
+
* That trade is now the other way round. What a branch also bought was a plugin
|
|
671
|
+
* whose bytes could change under the user between the build and the click, and
|
|
672
|
+
* with one tag covering the screenshots, the festival art and this profile, one
|
|
673
|
+
* release of the assets repository is one version of everything the plugin
|
|
674
|
+
* fetches — the same value in the README, in `HOLIDAY_ASSETS_REF` and here. The
|
|
675
|
+
* price is that correcting one JSON file now costs a tag in `assets`, a new
|
|
676
|
+
* value in this file and a release on npm, and that MOVING the tag does not
|
|
677
|
+
* reach jsDelivr's edge by itself (it is served as immutable, and the purge tool
|
|
678
|
+
* is the only way to re-point it) — so a correction is a new tag, never a
|
|
679
|
+
* force-push of the old one.
|
|
680
|
+
*
|
|
681
|
+
* The re-pointed-tag case is why `presetKey` survives this change: an edge that
|
|
682
|
+
* has not been purged can still answer with the previous bytes of the same URL,
|
|
683
|
+
* so two fetches of "the profile" are not guaranteed to be one revision, and a
|
|
684
|
+
* press that resumes must not glue them together. See `fetchPresetConfig`.
|
|
600
685
|
*
|
|
601
686
|
* The risk that IS real — a profile written for a config shape this build cannot
|
|
602
687
|
* read — is what `preset.json` exists for (see `fetchPresetConfig`).
|
|
603
688
|
*/
|
|
604
689
|
const PRESET_ASSETS_REPO = "HarmlessFunny/assets";
|
|
605
|
-
const PRESET_ASSETS_REF = "
|
|
690
|
+
const PRESET_ASSETS_REF = "v0.1.0";
|
|
606
691
|
const PRESET_ASSET_DIR = "dsh-background-by-model/preset";
|
|
607
692
|
/** The shape gate: `{"version": 6}` — see SCHEMA_VERSION in ./schema. */
|
|
608
693
|
const PRESET_MANIFEST = "preset.json";
|
|
@@ -661,12 +746,18 @@ const exists = async (p) => {
|
|
|
661
746
|
* layout mode, opacity, blur and framing, and the remaining legacy files
|
|
662
747
|
* (background video, upload temp) are removed. Idempotent: the rewritten config
|
|
663
748
|
* carries `rules`, so the next read short-circuits.
|
|
749
|
+
*
|
|
750
|
+
* The one case that is NOT a migration — a store with no config file at all — is
|
|
751
|
+
* handed through untouched rather than being filled in as `rules: []`. That empty
|
|
752
|
+
* list would be the same thing the sanitizer reads as "the user deleted every
|
|
753
|
+
* rule", and a fresh install is the opposite of that: `normalizeConfig` turns a
|
|
754
|
+
* config that names no rule list into the one blank rule the panel opens on.
|
|
664
755
|
*/
|
|
665
756
|
async function migrateLegacy(raw) {
|
|
666
757
|
const r = raw ?? {};
|
|
667
758
|
if (Array.isArray(r.rules)) return r;
|
|
668
759
|
const hasWallpaper = await exists(legacyWallpaperPath());
|
|
669
|
-
if (Object.keys(r).length === 0 && !hasWallpaper) return {
|
|
760
|
+
if (Object.keys(r).length === 0 && !hasWallpaper) return {};
|
|
670
761
|
if (hasWallpaper) {
|
|
671
762
|
const target = imagePath("m1");
|
|
672
763
|
try {
|
|
@@ -898,14 +989,20 @@ async function downloadHolidayAsset(asset) {
|
|
|
898
989
|
}
|
|
899
990
|
return null;
|
|
900
991
|
}
|
|
992
|
+
/** Wait, without pulling in `node:timers/promises` for one line. */
|
|
993
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
901
994
|
/**
|
|
902
|
-
* One asset over the wire, or
|
|
995
|
+
* One asset over the wire, or WHY it did not arrive.
|
|
903
996
|
*
|
|
904
997
|
* Never throws: a holiday that cannot be fetched is a holiday that does not paint
|
|
905
|
-
* today,
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
998
|
+
* today, and a profile file that cannot be fetched is a sentence on the button —
|
|
999
|
+
* neither is an error path the caller should have to wrap. What it answers with
|
|
1000
|
+
* instead is the reason, because the two callers treat those differently: the
|
|
1001
|
+
* profile asks `presetRetryable` whether the reason is weather (./preset) and the
|
|
1002
|
+
* festival art takes the null and waits for the next apply.
|
|
1003
|
+
*
|
|
1004
|
+
* The `content-length` check is a courtesy — a mirror that announces a huge body
|
|
1005
|
+
* never gets to send it — while the post-read size check is the one that holds.
|
|
909
1006
|
*
|
|
910
1007
|
* Deliberately says nothing about WHAT the bytes are: the festival art and the
|
|
911
1008
|
* recommended profile are both "a file from the assets repository", and the one
|
|
@@ -914,37 +1011,89 @@ async function downloadHolidayAsset(asset) {
|
|
|
914
1011
|
* asked for.
|
|
915
1012
|
*/
|
|
916
1013
|
async function fetchAsset(url, max, timeout) {
|
|
1014
|
+
const ctl = new AbortController();
|
|
1015
|
+
const timer = setTimeout(() => ctl.abort(), timeout);
|
|
917
1016
|
try {
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1017
|
+
const res = await fetch(url, {
|
|
1018
|
+
redirect: "follow",
|
|
1019
|
+
signal: ctl.signal
|
|
1020
|
+
});
|
|
1021
|
+
if (!res.ok) return {
|
|
1022
|
+
ok: false,
|
|
1023
|
+
reason: fetchFailureForStatus(res.status)
|
|
1024
|
+
};
|
|
1025
|
+
const announced = Number(res.headers.get("content-length") ?? "");
|
|
1026
|
+
if (isFinite(announced) && announced > max) return {
|
|
1027
|
+
ok: false,
|
|
1028
|
+
reason: "oversized"
|
|
1029
|
+
};
|
|
1030
|
+
const arr = await res.arrayBuffer();
|
|
1031
|
+
if (arr.byteLength > max) return {
|
|
1032
|
+
ok: false,
|
|
1033
|
+
reason: "oversized"
|
|
1034
|
+
};
|
|
1035
|
+
if (arr.byteLength === 0) return {
|
|
1036
|
+
ok: false,
|
|
1037
|
+
reason: "empty"
|
|
1038
|
+
};
|
|
1039
|
+
return {
|
|
1040
|
+
ok: true,
|
|
1041
|
+
buf: Buffer.from(arr)
|
|
1042
|
+
};
|
|
934
1043
|
} catch {
|
|
935
|
-
return
|
|
1044
|
+
return {
|
|
1045
|
+
ok: false,
|
|
1046
|
+
reason: ctl.signal.aborted ? "timeout" : "network"
|
|
1047
|
+
};
|
|
1048
|
+
} finally {
|
|
1049
|
+
clearTimeout(timer);
|
|
936
1050
|
}
|
|
937
1051
|
}
|
|
938
|
-
/**
|
|
1052
|
+
/**
|
|
1053
|
+
* The status codes worth another round: 5xx is a mirror having a bad moment, 429
|
|
1054
|
+
* is one asking to be left alone for a bit, 408 is one giving up on us.
|
|
1055
|
+
*
|
|
1056
|
+
* Every other non-2xx means the file is not there — jsDelivr answers a path
|
|
1057
|
+
* outside the repository with 403, both hosts answer a missing file with 404 — and
|
|
1058
|
+
* the profile follows a BRANCH, so that answer has to arrive fast and unchanged.
|
|
1059
|
+
*/
|
|
1060
|
+
function fetchFailureForStatus(status) {
|
|
1061
|
+
return status >= 500 || status === 429 || status === 408 ? "server" : "missing";
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* One festival picture, or null when no mirror served an image.
|
|
1065
|
+
*
|
|
1066
|
+
* One round, deliberately, unlike the profile's (see `fetchPresetFile`): this is
|
|
1067
|
+
* fetched during boot, on the day, before the first wallpaper is painted, and a
|
|
1068
|
+
* retry round would add its delay to that first paint. The art does not need one
|
|
1069
|
+
* either — it is cached once it arrives, and every later read tries again, while a
|
|
1070
|
+
* button press has no later read of its own.
|
|
1071
|
+
*/
|
|
939
1072
|
async function fetchHolidayAsset(url) {
|
|
940
|
-
const
|
|
941
|
-
return
|
|
1073
|
+
const got = await fetchAsset(url, HOLIDAY_FETCH_MAX, HOLIDAY_FETCH_TIMEOUT);
|
|
1074
|
+
return !got.ok || imageMimeOrNull(got.buf) === null ? null : got.buf;
|
|
942
1075
|
}
|
|
943
|
-
/**
|
|
1076
|
+
/**
|
|
1077
|
+
* One file out of the profile directory, or null when no mirror served it.
|
|
1078
|
+
*
|
|
1079
|
+
* Every mirror is asked in every round, so a round is "all the hosts there are"
|
|
1080
|
+
* and the rounds are the retry: the case this exists for is a mirror that answers
|
|
1081
|
+
* 502 on one round and correctly on the next, on the sixth of seven files, with
|
|
1082
|
+
* six good downloads behind it. A round in which every failure was terminal — the
|
|
1083
|
+
* file is not there, or it is too big — ends the attempts outright, so "the author
|
|
1084
|
+
* renamed a file" still costs exactly two requests and a sentence naming the slot
|
|
1085
|
+
* (see `presetRetryable`, ./preset).
|
|
1086
|
+
*/
|
|
944
1087
|
async function fetchPresetFile(name, max) {
|
|
945
|
-
for (
|
|
946
|
-
|
|
947
|
-
|
|
1088
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1089
|
+
let retryable = false;
|
|
1090
|
+
for (const host of PRESET_ASSET_HOSTS) {
|
|
1091
|
+
const got = await fetchAsset(`${host}/${name}`, max, PRESET_FETCH_TIMEOUT);
|
|
1092
|
+
if (got.ok) return got.buf;
|
|
1093
|
+
if (presetRetryable(got.reason)) retryable = true;
|
|
1094
|
+
}
|
|
1095
|
+
if (!retryable || attempt === 3) return null;
|
|
1096
|
+
await sleep(presetRetryDelayMs(attempt));
|
|
948
1097
|
}
|
|
949
1098
|
return null;
|
|
950
1099
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-background-by-model",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "Appearance plugin for DeepSeek Harness: an ordered list of model rules, each carrying its own wallpapers — every image with its own framing and theme color — plus layout mode, opacity and blur; the background follows the model you switch to, and a rule can rotate through several images on its own rhythm.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -60,9 +60,10 @@
|
|
|
60
60
|
"check:code-plate": "node scripts/code-plate-check.ts",
|
|
61
61
|
"check:image-mode": "node scripts/image-mode-check.ts",
|
|
62
62
|
"check:rules": "node scripts/rules-view-check.ts",
|
|
63
|
+
"check:preset": "node scripts/preset-check.ts",
|
|
63
64
|
"check:ui": "node scripts/ui-strings-check.ts",
|
|
64
65
|
"check:node": "node scripts/node-half-check.mjs",
|
|
65
|
-
"test": "tsdown && tsc -p tsconfig.json && node scripts/holiday-check.ts && node scripts/rotation-check.ts && node scripts/transition-check.ts && node scripts/repaint-check.ts && node scripts/code-plate-check.ts && node scripts/image-mode-check.ts && node scripts/rules-view-check.ts && node scripts/ui-strings-check.ts && node scripts/node-half-check.mjs"
|
|
66
|
+
"test": "tsdown && tsc -p tsconfig.json && node scripts/holiday-check.ts && node scripts/rotation-check.ts && node scripts/transition-check.ts && node scripts/repaint-check.ts && node scripts/code-plate-check.ts && node scripts/image-mode-check.ts && node scripts/rules-view-check.ts && node scripts/preset-check.ts && node scripts/ui-strings-check.ts && node scripts/node-half-check.mjs"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
|