dsh-background-by-model 0.7.4 → 0.7.5

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 CHANGED
@@ -1,6 +1,16 @@
1
1
  # Changelog
2
2
 
3
- > The full release history lives here; the two most recent releases (v0.7.4 / v0.7.3) are also listed in [README.md](./README.md#recent-optimizations).
3
+ > The full release history lives here; the two most recent releases (v0.7.5 / v0.7.4) are also listed in [README.md](./README.md#recent-optimizations).
4
+
5
+ ## v0.7.5
6
+
7
+ - **The layout mode belongs to the picture, not the rule** — 适应 / 填充 / 拉伸 / 平铺 / 居中 was one value per rule, covering every picture of a rotation, which is the argument that moved the framing down in 0.7.1: a rotation through a landscape photo and a tall phone screenshot could not letterbox one and fill the other, so at least one of them always accepted the other's compromise. The mode now lives on `images[].bgMode`, read back from the image that is being painted (`rBgMode`), and the chip row under the filmstrip edits the image selected there through the same slot-addressed, sanitized, `repaintIfMoved`-closed write path the framing editor uses. The row is **disabled** rather than hidden on a rule with no picture: a rule starts empty, and a row that vanished there would leave the card's own "add an image" hint to explain a control the user has never seen. README and README.zh were updated everywhere they claimed the mode was the rule's.
8
+ - **`SCHEMA_VERSION` moves to 6, because both directions of this move are silent (a fix)** — a schema-5 host rebuilds every image entry from the keys it knows, so a per-image mode written to it would vanish on the first write (the symptom being a picture set to 拉伸 coming back 适应); and a schema-6 read of a schema-5 config has to **lift** the rule-level value onto each of its images, or every existing wallpaper would land on the default. `normalizeImage` grew the third parameter that carries it, resolved once in `normalizeRule`, and the pre-0.7 single `slot`/`bgState` pair goes through the same lift while `migrateLegacy` writes the new shape directly. `bgMode` joined `LEGACY_RULE_KEYS`, so the host log does not report the field it is deliberately consuming as drift. Verified against a copy of a real config before anything was touched: 填充 on the rule became 填充 on both of its images, on the way in and on disk, with no drift warning — and a hand-written pair of different modes round-trips unchanged.
9
+ - **A holiday's image is 填充 by definition** — full-bleed festival art must not letterbox. The mode is taken from the definition like the slot and the colours beside it, so a hand-edited value cannot repaint a festival; the framing next to it stays yours. `check:node` asserts that the definition wins.
10
+ - **The fade only animates what the palette in force writes (a fix)** — reported as "有时候切换背景时(图上为浅色切换为浅色),白色色块会渐变为黑色色块…白色渐变完黑色后立马切到白色", with the white value measured as `237, 243, 254`. That number is the answer: `#edf3fe` is `--dsw-static-deepseek-50`, which is what the host's **light** scheme paints `--dsw-specific-bubble` with — the user message bubble — and its dark-scheme value is `#2c2c2e`. The fade sheet had registered that name because `fadeTokenNames()` asks for the **dark** branch's token list (the superset) and handed the whole thing to the `:root` and `body` scopes: the light branch writes **35** of those **80** names, so the other 45 — the bubble, `--dsw-specific-selector` (the settings dropdown fills), the tip/tooltip/toast surfaces, the code-block header segments, the status colours, the toolbar and ghost button fills — still belong to the **host**. The host changes those on a schedule of its own, and this plugin is what provoked it: it disposes and re-registers its theme on every colour change, and the host's disposer resets the preference to the default — `system` — when the theme backing it goes away ("resets the preference to the default so the UI never keeps tokens of an unregistered theme", ui-theme's own `register()`), which on a dark desktop hands the whole interface to the host's dark palette for the length of that call, with the presenter's own forced style flush (`getComputedStyle(body).backgroundColor`) as the point where an armed transition picks the dark values up. The reported session runs its switch at `durationMs: 3000`, which is what turned a frame into a three-second ramp; the value then came back to the light one without an animation of its own, which is the "snaps back to white" half. Two changes, one per half of the mechanism: a scope's token is emitted only when the palette **in force** writes it (the new `written` input, passed as `Object.keys(tokens)` of the block being written — the registrations still cover all 80 names, and the dark palette, which writes every one of them, is unchanged), and a skin whose `colorScheme` already holds is not rebuilt at all (what the skin contributes is its scheme; its tokens are re-emitted by the plugin's own `!important` rule, and a same-scheme skin carries the same names either way). Measured on the reported palette (hue 192): `body`'s transition list goes from 80 names to 35, `--dsw-specific-bubble` and `--dsw-specific-selector` are out of it, and `--dsw-alias-bg-base`, `--dsw-alias-bg-layer-2` and the code plate stay in — nothing the plugin actually paints lost its fade.
11
+ - **The code block follows the palette, and its plate has an opacity of its own (a fix)** — reported as "代码块背景有点问题": dark green ink on a near-black plate inside a **light** interface. The cause is a `var()` substituted where it is DECLARED: the host's shiki sheet declares `--shiki-background: var(--dsw-alias-markdown-code-block)` (and `--shiki-foreground`) on `:root`, while the host itself — and therefore this plugin — declared those tokens on `body` only, so on the root element there was nothing to read but the fade registration's `initial-value`: the host's own colour, sampled while the plugin's sheet was muted. The plate stayed on the host's dark value (`#1b1b1c`) through every light palette while the ink, which does flip with the scheme (`body[data-ds-dark-theme]{--shiki-token-*}`), turned dark — `#2f9e44` green on `#1b1b1c`, about 2:1. The palette rule is now emitted for `:root` as well as `body`, and `:root` joins the fade scopes (only the element that DECLARES a token animates it, so without that scope the block jumped to the new tint while every surface around it faded); the plate and its banner are palette tints again — a constant can only ever match one of the two ink sets — with the light branch emitting the banner instead of silently borrowing the host's near-white. A new **`code`** opacity part drives both surfaces, each through its own plugin-owned variable (the writer sets one variable per token **name**, so a shared one would let the banner, written last, paint the plate as well); it defaults to opaque, which is what a code block is, and it is opacity-only, because frosting the wallpaper under the syntax only softens the plate.
12
+ - **Four small ones (a fix)** — the rotation switch no longer survives its own pictures: with rotation on, deleting images until one was left kept the switch reading "on", and the panel disables that switch below two images, so it could not be turned off either — the setting said one thing and the picture count said another, with no way to reconcile them. Removing an image now clears `enabled` when fewer than two are left, and `normalizeRule` enforces the same invariant for everything that does not pass through the panel (an imported profile, a config written back after a picture went away elsewhere); re-adding a picture finds the switch off, because rotation is a decision, not a consequence of a count. "切换时长" was printed twice, as a section heading and again as the slider's own label one line below it. The profile footer's package name and the brand tile are links to the project's repository — the two places a reader looks for "where does this come from" — both reading the one address in `repo.ts`, whose value `check:ui` compares against `package.json`'s `repository` so a renamed or moved project fails the gates instead of shipping a link that looks exactly like a working one.
13
+ - **Gates** — `pnpm test` is now **eight** checks. `check:code-plate` (the plate token is declared on `:root` as well as `body`, both branches emit the banner, and each surface reads its own variable) and `check:image-mode` (the lift, plus the wiring — wiring it wrongly fails silently, with every image painting the same way) join the suite; `check:rotation` gained the switch-and-count invariant on both sides, `check:ui` the repository-link agreement, `check:transition` the ownership rule (a registered name the palette does not write keeps its registration and gets no transition, and the written set is intersected with the registrations rather than unioned into them) and `check:repaint` the two lines that arm the fade.
4
14
 
5
15
  ## v0.7.4
6
16
 
package/CHANGELOG.zh.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # 更新日志
2
2
 
3
- > 这里保留完整的历史记录;最近两个版本(v0.7.4 / v0.7.3)同时列在 [README.zh.md](./README.zh.md#近期优化) 里。
3
+ > 这里保留完整的历史记录;最近两个版本(v0.7.5 / v0.7.4)同时列在 [README.zh.md](./README.zh.md#近期优化) 里。
4
+
5
+ ## v0.7.5
6
+
7
+ - **布局模式属于图片,不属于规则** —— 适应 / 填充 / 拉伸 / 平铺 / 居中 此前是每条规则一个值、覆盖一轮轮换里的每一张图;这跟 0.7.1 把取景下放是同一条理由:一轮里既有横构图照片又有竖屏截图时,没法让一张留黑边、另一张铺满,总得有一张接受另一张的妥协。现在模式存在 `images[].bgMode` 上,从**正在画的那张图**读回(`rBgMode`),胶片条下面那排胶囊编辑的也是那里选中的那张图 —— 走的是取景编辑器同一条按槽位寻址、过 sanitizer、以 `repaintIfMoved` 收尾的写入路径。规则还没有图片时这排胶囊是**禁用**而不是隐藏:新规则天生是空的,把它抽掉就等于让卡片自己那句「点这个上传」去解释一个用户从没见过的控件。README 与 README.zh 里凡是说「模式属于规则」的地方都改了。
8
+ - **配置格式升到 6,因为这次迁移两个方向都是无声的(修复)** —— schema 5 的宿主会按自己认识的键重建每一个图片条目,写进去的逐图模式会在第一次写入时消失(症状就是设成 拉伸 的图变回 适应);而 schema 6 读一份 schema 5 的配置时**必须**把规则级的值抬到它的每一张图上,否则所有现成壁纸都会掉到默认值。`normalizeImage` 多了一个承载它的参数,在 `normalizeRule` 里只解析一次;0.7 之前那个单 `slot`/`bgState` 老形态走同一条抬升路径,`migrateLegacy` 则直接写成新形状。`bgMode` 进了 `LEGACY_RULE_KEYS` —— 它是被**刻意消费**掉的迁移字段,不该在宿主日志里报成漂移。动手前先拿一份真实配置的副本验过:规则上的 填充 变成了它两张图各自的 填充,读入时与落盘后都是,drift 警告 0 条;手写的一对**不同**模式能原样读回。
9
+ - **节日的图按定义就是 填充** —— 满幅的节日画不该有黑边。模式和它旁边的槽位、颜色一样取**定义**、不取磁盘,所以手改过的值换不掉节日的模式;旁边的**取景**仍然是你自己的。`check:node` 钉住了「定义胜出」这条。
10
+ - **渐变只作用于当前调色板真正写入的东西(修复)** —— 报告是「有时候切换背景时(图上为浅色切换为浅色),白色色块会渐变为黑色色块…白色渐变完黑色后立马切到白色」,白色值量到的是 `237, 243, 254`。这个数字就是答案:`#edf3fe` 是 `--dsw-static-deepseek-50`,也就是宿主**浅色**方案画 `--dsw-specific-bubble` 用的值 —— 你自己那条消息的气泡 —— 它的深色方案值是 `#2c2c2e`。渐变样式表之所以注册了这个名字,是因为 `fadeTokenNames()` 要的是**深色**分支的 token 表(那份是超集),而 `fadeScopes()` 把整张表都交给了 `:root` 与 `body` 两个作用域:浅色分支只写其中 **35** 个(共 **80** 个),另外 45 个 —— 气泡、`--dsw-specific-selector`(设置里的下拉框底色)、提示与气泡框、代码块右上角那排小按钮、状态色、工具条与幽灵按钮的填充 —— 仍然归**宿主**。而这些名字宿主是按自己的时间表改的,触发者还是插件自己:它每次换配色都销毁并重新注册主题,而宿主的销毁器会把偏好重置为默认值 `system`(ui-theme 自己的 `register()`:「重置为默认偏好,这样界面不会留着已注销主题的 token」),在深色桌面上这就等于把整个界面交给宿主的深色调色板、直到那次调用结束;而 presenter 自己那句强制的样式刷新(`getComputedStyle(body).backgroundColor`)正是已经武装好的过渡抓住深色值的那一刻。报告里那次会话的切换时长是 `durationMs: 3000`,一帧的错色因此被拉成了三秒的渐变;值随后变回浅色且自身没有动画,这就是「立马切到白色」那一半。两处修改各治一半:作用域的 token 只在**当前调色板**确实写它时才发出(新增的 `written` 入参,传的就是本次要写的那一块的 `Object.keys(tokens)` —— 注册表仍然覆盖全部 80 个名字,而全部写入这 80 个的深色调色板完全没变),以及配色方案没变就**不再重建**皮肤(皮肤真正贡献的只是它的 `colorScheme`,token 本来就是我们自己用 `!important` 重发的,同一方案下两边名字集合完全一样)。在报告那套调色板上实测(色相 192):`body` 的过渡列表从 80 个名字降到 35 个,气泡与下拉框底色都不在里面了,而 `--dsw-alias-bg-base`、`--dsw-alias-bg-layer-2` 和代码块底板仍在 —— 插件真正绘制的表面一个都没丢掉渐变。
11
+ - **代码块跟着调色板走,而且它自己的底板有了独立透明度(修复)** —— 报告是「代码块背景有点问题」:**浅色**界面里,深绿色文字压在近黑色的底板上。成因是 `var()` 在**声明它的地方**就被替换了:宿主的 shiki 表在 `:root` 上声明 `--shiki-background: var(--dsw-alias-markdown-code-block)`(还有 `--shiki-foreground`),而宿主自己 —— 也因此包括本插件 —— 只在 `body` 上声明这些 token,于是根元素上除了过渡注册的 `initial-value` 什么也读不到:那是宿主自己的颜色,且是在插件样式表被静音时采样的。结果底板在每一套浅色调色板下都停在宿主的深色值(`#1b1b1c`),而会随方案翻转的墨色(`body[data-ds-dark-theme]{--shiki-token-*}`)变成了深色 —— `#2f9e44` 的绿压在 `#1b1b1c` 上,对比度约 2:1。现在调色板规则对 `:root` 与 `body` 一并发出,`:root` 也加入渐变作用域(只有**声明**该 token 的元素才会做过渡,少了这个作用域,代码块会在周围每块表面淡入时自己硬跳到新色);底板与它的 banner 重新是调色板的染色 —— 写死一个常量只可能匹配两套墨色中的一套 —— 且浅色分支也发出 banner,不再偷偷借宿主的近白色。新增 **`code`** 透明度档位驱动这两块表面,各自读自己的插件自有变量(写入者是按 token **名字**设变量的,共用一个变量会让后写的 banner 把底板也一起画了);它默认不透明 —— 代码块是拿来读的,不是拿来看穿的 —— 而且只做透明度:把语法底下的壁纸冻上一层霜,只会让底板更糊。
12
+ - **四个小修(修复)** —— 轮换开关不再「活过」它自己的图片:开着轮换把图片删到只剩一张时,开关仍显示「开」,而面板在少于两张时把这个开关禁用了,于是也关不掉 —— 设置说一套、图片数量说另一套,在下一张图到来之前无从调和。现在删图后不足两张就清掉 `enabled`,`normalizeRule` 对不经过面板的路径(导入的配置、别处删图后写回的配置)强制同一条不变量;重新加图时开关是关的,因为轮换是一个决定,不是数量的后果。「切换时长」被印了两遍(一个小节标题,隔一行又是滑块的标签)。资料页页脚的包名与品牌磁贴现在是指向项目仓库的链接 —— 这两处正是读者会去找「这东西哪来的」的地方 —— 两者都读 `repo.ts` 里那一个地址,`check:ui` 拿它和 `package.json` 的 `repository` 对比,于是改过名或搬过家的项目会让门禁失败,而不是发出去一个看起来完全正常的坏链接。
13
+ - **门禁** —— `pnpm test` 现在是**八道**。`check:code-plate`(底板 token 在 `:root` 与 `body` 上都有声明、两个分支都发 banner、每块表面读自己的变量)与 `check:image-mode`(抬升本身,外加接线 —— 接错了不会报错,只会每张图长得一样)加入套件;`check:rotation` 增加了开关与数量的不变量(两侧都验),`check:ui` 增加仓库链接一致性,`check:transition` 增加归属规则(注册了但当前调色板不写的名字保留注册、但不给它过渡;写入集合与注册集合是**求交**、绝不并集),`check:repaint` 钉住武装渐变的那两行。
4
14
 
5
15
  ## v0.7.4
6
16
 
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  > Forked from [`Tkingxiao/dsh-any-background`](https://github.com/Tkingxiao/dsh-any-background) and renamed to `dsh-background-by-model`.
11
11
 
12
- A **DeepSeek Harness** appearance plugin built around an **ordered list of model rules**. Each rule carries its own wallpaper, theme color, layout mode, framing, opacity and blur — switch the model, and the background switches with it, through the global [switch effect](#switch-effect-global) you choose on the Config page.
12
+ A **DeepSeek Harness** appearance plugin built around an **ordered list of model rules**. Each rule carries its own wallpapers and opacity and blur, and **each image carries its own layout mode, framing and theme color** — switch the model, and the background switches with it, through the global [switch effect](#switch-effect-global) you choose on the Config page.
13
13
 
14
14
  > **v0.3.0 is a breaking release.** Video wallpapers, generated dynamic backgrounds and the single global theme color were removed. See the [v0.3.0 entry in CHANGELOG.md](./CHANGELOG.md#v030).
15
15
 
@@ -63,7 +63,7 @@ One interface, one config — only the current model differs:
63
63
  <p align="center">
64
64
  <img src="example_img/rule-editor.webp" alt="One rule's editor" width="660">
65
65
  <br/>
66
- <em>One rule on the Model Background tab · image, layout mode, theme color (wheel / HSL / RGB / extract from this image / pick from image), background opacity and background blur are all properties of that rule alone</em>
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>
68
68
 
69
69
  <p align="center">
@@ -76,11 +76,11 @@ One interface, one config — only the current model differs:
76
76
 
77
77
  - **Ordered Rule List** — Add, remove, reorder and name rules. Each rule is matched by its own match string; the first hit wins and rule 1 is the fallback.
78
78
  - **Live Match Readout** — The top of the Model Background tab shows the current state, e.g. `Current model deepseek-flash → matched · Rule 1`, so you can verify a rule on the spot. If the current model can't be detected, a hint is shown instead.
79
- - **Per-rule Appearance** — Every rule owns its wallpapers, layout mode, opacity and blur, and **every image owns its framing and its theme color**. Nothing is shared globally except the Interface tab.
79
+ - **Per-rule Appearance** — Every rule owns its wallpapers, opacity and blur, and **every image owns its layout mode, its framing and its theme color**. Nothing is shared globally except the Interface tab.
80
80
  - **Several Images per Rule** — A rule holds an ordered set of images, shown as a filmstrip: add several at once (file picker, drag & drop, or a list of URLs, one per line), reorder them, promote one to first, **replace one in place** (which keeps its position in the rotation), or remove them one by one. The image a rule paints when nothing rotates is its **first**. Removing the last image simply leaves the rule empty — and empty is not the same as off: while the rule keeps a theme color of its own it still matches, still serves as the fallback, and paints the interface instead of a wallpaper; only a rule with neither (a freshly added one) is skipped by matching. So an emptied rule is a visible, reversible state rather than one you get stuck in. While it holds no picture the filmstrip is hidden and the big preview tile **is** the upload button, so there is exactly one obvious place to add one.
81
- - **Import / Export** — Export every rule **including all of its images** to a `dsh-background-by-model-theme.json` (format version 6, images inlined as base64) and restore it anywhere. Files written by earlier releases still import: each rule's single image is lifted into its image list, a color written before per-image colors is lifted onto every image that has none, and a file written before the [switch effect](#switch-effect-global) gets that setting's shipped default — so an older theme file restores its look unchanged.
81
+ - **Import / Export** — Export every rule **including all of its images** to a `dsh-background-by-model-theme.json` (format version 6, images inlined as base64) and restore it anywhere. Files written by earlier releases still import: each rule's single image is lifted into its image list, a color written before per-image colors is lifted onto every image that has none, a layout mode written when it was the rule's is lifted onto each of its images, and a file written before the [switch effect](#switch-effect-global) gets that setting's shipped default — so an older theme file restores its look unchanged.
82
82
  - **File-based Persistence** — All settings and images are stored on the filesystem under `~/.dsh/.dsh-background-by-model-data/`, not `localStorage`.
83
- - **Automatic Migration** — Old single-wallpaper configs are upgraded in place on first read. See the [v0.3.0 entry in CHANGELOG.md](./CHANGELOG.md#v030).
83
+ - **Automatic Migration** — Old single-wallpaper configs are upgraded in place on first read, and so is a layout mode that used to live on the rule. See the [v0.3.0 entry in CHANGELOG.md](./CHANGELOG.md#v030).
84
84
  - **Bilingual** — Full Chinese / English UI with automatic locale detection.
85
85
  - **Theme Watchdog** — Re-asserts the custom theme if the host resets it.
86
86
 
@@ -94,8 +94,8 @@ Per rule, and **off by default** — cycling spends real bandwidth, memory and b
94
94
  - **Also step on a model switch** — an optional second trigger: every time the model changes and lands on that rule, it steps once. That is the whole feature for a rule meant to show "a different picture every time" without any timer running.
95
95
  - **Only the active rule rotates** — one timer for the whole plugin, aimed at whichever rule the current model resolved to. It stops while the tab is hidden and resumes on a fresh interval; a single-image rule (and every [holiday](#holiday-backgrounds)) never schedules anything.
96
96
  - **Next image** — steps the wallpaper immediately, for when you want to check the set.
97
- - **What follows a switch is the rule — except the color, which travels with the picture** — layout mode, opacity and blur belong to the rule and stay put. The theme color belongs to the **image**, so a rotation can move through a green picture, a red one and a system-themed one, and the interface palette changes with the wallpaper. The cross-fade between two images is the same one a model switch already used.
98
- - **Framing is per image, and so is the color** — a crop and an accent both belong to a picture, so each image keeps its own while sharing the rule's layout, opacity and blur. The filmstrip marks the images that carry a color of their own, and the color controls always say which image they are editing.
97
+ - **What follows a switch is the rule — except everything that belongs to the picture** — opacity and blur belong to the rule and stay put. The **layout mode**, the **framing** and the **theme color** belong to the image, so a rotation can letterbox a tall screenshot and fill the landscape photo next to it, and can move through a green picture, a red one and a system-themed one with the interface palette following the wallpaper. The cross-fade between two images is the same one a model switch already used.
98
+ - **Layout mode, framing and color are per image** — 适应/填充 decides how *this* picture meets the viewport, a crop and an accent belong to it too, so each image keeps its own while sharing the rule's opacity and blur. The filmstrip marks the images that carry a color of their own, and the color controls always say which image they are editing.
99
99
  - **Only the images it needs are loaded** — boot reads each rule's first image; the rest stream in when a card is expanded or when the rotation is about to need one. Ten images per rule therefore do not mean ten wallpapers transferred before the first frame.
100
100
 
101
101
  ### Switch effect (global)
@@ -121,14 +121,14 @@ A small one, not a settings block: **one switch on the Config page and nothing e
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
123
  - **Built-in wallpapers** — Both holidays ship with a compressed wallpaper (228 KB and 500 KB at native resolution, against 8.37 MB of source art). Only the holiday that can paint *today* is ever fetched, so a profile that is not on a holiday transfers none of it.
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 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. Theme exports therefore carry your rules only.
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 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 off** — the switch on the Config page, or `"holidays": { "enabled": false }` in the same file.
126
126
 
127
- ### Per-rule settings
127
+ ### Per-rule and per-image settings
128
128
 
129
129
  - **Wallpaper** — Give a rule one or more images; each image is its own file. The filmstrip picks which one you are editing, and the "first" one is what the rule paints when no rotation is running. A rule may also hold **no** image: it then paints no wallpaper, but as long as it has a theme color of its own it still matches and still serves as the fallback (painting the interface alone), and only with neither is it skipped by matching until you give it something.
130
130
  - **Theme Color** — HSL wheel plus numeric input and an inspiration palette, with **Extract from this image** and an **eyedropper**. **The color belongs to the image**: those controls edit the one selected in the filmstrip, the strip shows which images already carry a color of their own, and an image that has none follows the system theme rather than inheriting its rule's. A rule with **no image at all** has only its own color left to edit — and that is exactly what such a rule paints. Generates the full CSS design-token set in real time. Following the system theme hands the palette back to the host while the wallpaper and the Interface-tab opacity/blur sliders keep working — the surface colors are read back from the host's own tokens and re-emitted with your alpha, so the wallpaper is never buried under an opaque plate.
131
- - **Layout Mode** — Fit / Fill / Stretch / Tile / Center.
131
+ - **Layout Mode** — Fit / Fill / Stretch / Tile / Center, and it is stored **per image**: a rotation can letterbox one picture and fill the next. On a rule that holds no picture the row is disabled (there is nothing to lay out) rather than hidden, so the card shows what becomes editable once you add one.
132
132
  - **Framing** — Drag to pan and scroll to zoom inside a viewport-proportional editor; **only editable in Fit mode**, and the committed framing stays consistent across window resizes and cross-monitor moves. Stored per image.
133
133
  - **Background Opacity** — `0–100%` for the rule's wallpaper layer.
134
134
  - **Background Blur** — `0–60 px`, applied to the wallpaper layer.
@@ -147,7 +147,7 @@ Settings → **Theme** now has three tabs:
147
147
  | Tab | Scope | Contents |
148
148
  | --- | --- | --- |
149
149
  | **Interface** | Global, shared by every model | Opacity & blur for the main background, left panel, right panel, cards & panels, input & controls, settings panel, conversation text box and trajectory page |
150
- | **Model Background** | Per rule | The ordered rule list, the live match readout, and each rule's images (filmstrip), theme color, layout mode, per-image framing, opacity, blur and [rotation](#image-rotation) |
150
+ | **Model Background** | Per rule, per image | The ordered rule list, the live match readout, and each rule's images (filmstrip), **per-image layout mode, framing and theme color**, plus the rule's opacity, blur and [rotation](#image-rotation) |
151
151
  | **Config** | — | The global [switch effect](#switch-effect-global), import / export of the whole rule set (`dsh-background-by-model-theme.json`), plus the single [holiday background](#holiday-backgrounds) switch |
152
152
 
153
153
  The old **Color** tab is gone — the theme color is now a property of each rule. The old **Background** tab became **Model Background**.
@@ -270,24 +270,21 @@ Two things can hold it, and both are on the card: the effect is **Instant**, or
270
270
 
271
271
  ## Recent Optimizations
272
272
 
273
- > Only the two most recent releases are listed here; older ones (v0.7.2 and earlier) live in [CHANGELOG.md](./CHANGELOG.md).
273
+ > Only the two most recent releases are listed here; older ones (v0.7.3 and earlier) live in [CHANGELOG.md](./CHANGELOG.md).
274
+
275
+ ### v0.7.5
276
+
277
+ - **The layout mode belongs to the picture, not the rule** — 适应 / 填充 / 拉伸 / 平铺 / 居中 was one value per rule, so a rotation through a landscape photo and a tall phone screenshot could not letterbox one and fill the other. It now lives on `images[].bgMode`, read back from the image being painted, and the chip row under the filmstrip edits the image selected there (disabled, not hidden, while a rule has no picture yet). `SCHEMA_VERSION` moves to **6** because both directions of this move are silent: a schema-5 host would drop a per-image mode on its first write (拉伸 coming back 适应), and a schema-6 read of a schema-5 config lifts the rule-level value onto every one of its images, so no existing wallpaper lands on the default. A holiday's image is 填充 from its definition, like the slot and the colours beside it; the framing stays yours.
278
+ - **The fade only animates what the palette in force writes (a fix)** — "有时候切换背景时(图上为浅色切换为浅色),白色色块会渐变为黑色色块…白色渐变完黑色后立马切到白色", with the white value measured as `237, 243, 254`. That number is the answer: `#edf3fe` = `--dsw-static-deepseek-50`, which is what the host's light scheme paints `--dsw-specific-bubble` with — the user message bubble — and its dark-scheme value is `#2c2c2e`. The fade had registered that name because it asks for the **dark** branch's token list (the superset) and hands the whole thing to the `:root` and `body` scopes, while the light branch writes only 35 of those 80 names: the other 45 (the bubble, the settings dropdown fills, the tip/tooltip/toast surfaces, the code-block header segments, the status colours) still belong to the **host**, and the plugin is what makes the host switch schemes for a moment — it disposes and re-registers its theme on every colour change, and the host's disposer resets the preference to `system`, which on a dark desktop paints the host's dark palette until that call finishes. With the reported 3000 ms switch, one frame of it became a three-second ramp. A scope's token is now emitted only when the palette in force writes it (the registrations still cover all 80 names, so the dark palette is unchanged), and a skin whose colour scheme already holds is not rebuilt at all. Measured on the reported palette: `body`'s transition list goes 80 → 35 names, the bubble and the dropdown fill are out of it, and `--dsw-alias-bg-base`, `--dsw-alias-bg-layer-2` and the code plate stay in.
279
+ - **The code block follows the palette, and its plate has an opacity of its own (a fix)** — "代码块背景有点问题": dark green ink on a near-black plate inside a **light** interface. The host's shiki sheet declares `--shiki-background: var(--dsw-alias-markdown-code-block)` on `:root`, while the token was declared on `body` only, so the root element had nothing to read but the fade registration's `initial-value` — the host's own colour, sampled once. The palette rule is emitted for `:root` too, `:root` joins the fade scopes, the plate and its banner are palette tints again, and a new **`code`** part drives both surfaces, each through its own variable, defaulting to opaque.
280
+ - **Four small ones** — the rotation switch no longer stays "on" when deleting images leaves fewer than two (the panel disables the switch there, so it could not be turned off either; `normalizeRule` enforces the same invariant for imports), "切换时长" is printed once instead of twice, and the profile footer's package name and the brand tile link to the repository, with `check:ui` comparing that one address against `package.json`.
281
+ - **Gates** — `pnpm test` is now **eight** checks: `check:code-plate` and `check:image-mode` join the suite, `check:rotation` gained the switch-and-count invariant, `check:ui` the repository-link agreement, `check:transition` the fade-ownership rule and `check:repaint` the two lines that arm the fade.
274
282
 
275
283
  ### v0.7.4
276
284
 
277
285
  - **The controls inside the dialog change colour on the wallpaper's clock instead of jumping when it finishes (a fix)** — on a theme-colour change the holiday switch's fill and the active **Slide in** / **Standard** chips held the **previous** colour for the whole switch and took the new one only once the background had finished: a three-second change that reads as "the interface ignores it, then hard-cuts at the end". The cause was this plugin's own doing: the fade armed the **full token list** on three elements, two of which (the settings dialog and the trajectory root) only **re-declare** the three layer tokens and inherit the rest. A transition on an *inherited* token targets the value that element saw when the fade was armed — still the old one, because the ancestor is itself interpolating — so it becomes a damped copy of the animation and drags its whole subtree behind it (measured: 113/255 of the way at t = 1500 ms on a 1200 ms transition, against frame-for-frame for a consumer with no transition of its own). The transition list is now per scope, and a scope only ever gets the tokens it declares.
278
286
  - **The surfaces painted from a colour of the plugin's own fade too now (a fix)** — the settings dialog's plate, the file-preview panel and the Cordis panel are not painted from a registered alias token but from a variable the plugin owns and writes inline on `<html>`, and an unregistered variable cannot interpolate at all: they snapped in one frame while every surface around them faded. A fade scope can now name a **standard property** (`background-color`), and each paint rule and its fade now share one selector constant so the two halves cannot drift apart. Measured over the real emitted CSS, one 3000 ms `ease` change: the plate, the right panel and the Cordis surface already read their **final** colour at the first sample and never moved, while all five surfaces now sit at exactly the same fraction of the way — 80.2% at t = 1500 ms, which is `ease(0.5)`.
279
287
 
280
- ### v0.7.3
281
-
282
- - **The switch is a setting now: four effects, an easing and one duration, on the Config page** — every wallpaper change this plugin performs (a model switch, a rotation step, the manual **Next image**, a holiday taking over) went through one hard-coded cross-fade whose length was read off a field no control could reach. **Cross-fade**, **instant**, **zoom** and **slide** all ride the two layers that already existed — zoom and slide use `transform` on the incoming layer only, a property the wallpaper layer had never used, so neither costs a byte of extra transfer — instant and a `0` s duration are the same hard cut, and **reduced motion** is a veto rather than a shorter animation.
283
- - **The card carries a preview swatch, and that is not decoration** — a wallpaper fading into itself is invisible, so replaying the effect on the picture already on screen would show nothing at all: the one way this feature could look broken while working. The swatch replays the chosen effect at the length a real switch would use right now.
284
- - **There is exactly one duration, and "Follow each rule" is gone** — the option named a value the interface cannot set (`rotate.fadeMs` had been in the shape since 0.7.0 and no control ever reached it), so it was a choice between a real setting and a phantom one. The slider IS the setting. A hand-edited config is not dropped in silence: `legacyFadeMs` folds the first deliberate value it finds into the global duration on the first read (a value equal to the shipped 320 ms is ignored, and an explicit `durationMs` wins), and the field is exempt from the drift warning because it is being migrated, not drifting.
285
- - **The interface colour now changes over the same clock as the wallpaper** — a new theme colour is one rewrite of `body{--dsw-alias-…}`, which used to change every surface in a single frame while the picture behind it blended. The fade is a **registration** problem (an unregistered custom property cannot interpolate), so the plugin emits `@property` rules that turn the alias tokens into `<color>`s, plus a `transition` for the elements whose own declarations change with the palette — `body`, the settings dialog and the trajectory root. The two surfaces it paints **inline** (the app columns, the chat/trajectory cards) do not read those tokens at all, so they carry the same duration on `background-color`. The duration, the easing and both vetoes come from the **same** `transitionPlan` the wallpaper uses, and it is armed **only when the palette itself moved** — every other pass (a slider drag moving the alphas) writes the sheet with no transition and `none` inline, because a live transition there would make the surface lag behind the pointer.
286
- - **A light↔dark flip switches instantly, and the wallpaper keeps animating** — interpolating between the two palettes walks the interface through mid-tones whose ink is wrong in both directions, so a scheme flip is exactly the case a fade makes worse. The fade is armed only while the **scheme** stays put (light→light, dark→dark); the wallpaper is deliberately untouched by that rule, because one image fading into another has no scheme to get wrong.
287
- - **The ink is written synchronously now, and that was a real bug with exactly one writer (a fix)** — switching a rule to a dark theme colour could leave a panel's text on the **previous** palette while every surface followed the new one: dark ink on a dark panel, unreadable, cleared only by reloading, with nothing in the log. Measured from the reported screenshot rather than guessed: the panel was the plugin's dark `bg-layer-2` (`hsl(229,59%,27%)` = `rgb(28,43,109)`, composited at 87% panel opacity to the sampled `rgb(23,36,91)`) while the ink was the **light** branch token over it (`rgba(0,0,0,0.85)` on that navy is exactly the `rgb(3,6,14)` sampled from the glyph cores, and `rgba(0,0,0,0.7)` exactly the `rgb(7,10,27)` of the hint line). Every other surface has a second, synchronous writer, so only the token stylesheet could go stale — and its single writer was reachable only from a `requestAnimationFrame` callback wrapped in `catch {}`: a window that is not rendering swallows the frame while the id stays armed and parks every later update behind it, and a failure inside the write said nothing at all. A palette that moved is now written in the same task as the surfaces it has to agree with, the frame path is kept for what it was for (coalescing a slider drag), a frame outstanding for over a second is dropped instead of blocking forever, and the writer logs its failure.
288
- - **The config shape moved to 5 and an export to format 6** — a schema-4 sanitizer rebuilds the config from the keys it knows, so a stale host process would drop the effect, the easing and the duration on the first write after a refresh, silently and with nothing in the host log. The published `schema` moves with it, the hold-writes path catches such a host, and `warnUnknownConfigKeys` compares the fixed-shape `transition` block one level down.
289
- - **Gates** — `check:transition` joins `pnpm test`, which is now **six** checks; `check:repaint` pins the three lines the ink bug was made of (write synchronously on a palette change, do not swallow the failure, ask both halves of the fade question at both the token and the inline call sites); `check:node` covers the shipped default, the round trip, hostile values, a kept `0` duration, the drift check and the legacy fold; `check:ui` covers the new strings and classes and registers the chip labels in `TABLE_KEYS`.
290
-
291
288
  ## Star History
292
289
 
293
290
  [![Star History Chart](https://api.star-history.com/chart?repos=HarmlessFunny/dsh-background-by-model&type=timeline&legend=bottom-right)](https://www.star-history.com/?repos=HarmlessFunny%2Fdsh-background-by-model&type=timeline&legend=bottom-right)
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  > 派生自 [`Tkingxiao/dsh-any-background`](https://github.com/Tkingxiao/dsh-any-background),并重命名为 `dsh-background-by-model`。
11
11
 
12
- 一个 **DeepSeek Harness** 外观插件,核心是一份**有序的模型规则列表**:每条规则自带壁纸(可多张,每张各有自己的取景与主题色)、布局模式、透明度与模糊度,切换模型即切换背景,切换过程走你在「配置」页选定的全局[切换效果](#切换效果全局)。
12
+ 一个 **DeepSeek Harness** 外观插件,核心是一份**有序的模型规则列表**:每条规则自带壁纸(可多张)、透明度与模糊度,**每张图各有自己的布局模式、取景与主题色**,切换模型即切换背景,切换过程走你在「配置」页选定的全局[切换效果](#切换效果全局)。
13
13
 
14
14
  > **v0.3.0 是破坏性更新**:视频背景、生成式背景与全局主题色均已被移除。详见 [CHANGELOG.zh.md 的 v0.3.0 条目](./CHANGELOG.zh.md#v030)。
15
15
 
@@ -63,7 +63,7 @@
63
63
  <p align="center">
64
64
  <img src="example_img/rule-editor.webp" alt="单条规则的编辑器" width="660">
65
65
  <br/>
66
- <em>「模型背景」页的单条规则 · 图片、布局模式、主题色(色轮 / HSL / RGB / 从本图提取 / 从图片取色)、背景透明度与背景模糊度,都是这一条规则自己的属性</em>
66
+ <em>「模型背景」页的单条规则 · 壁纸、透明度与模糊度属于这条规则;布局模式、取景与主题色(色轮 / HSL / RGB / 从本图提取 / 从图片取色)属于它正在编辑的那张图</em>
67
67
  </p>
68
68
 
69
69
  <p align="center">
@@ -76,11 +76,11 @@
76
76
 
77
77
  - **有序规则列表** — 可新增、删除、拖动排序并命名规则;每条规则用自己的匹配串参与匹配,先命中者生效,规则 1 兼任兜底。
78
78
  - **实时状态提示** — 「模型背景」页顶部会显示当前状态,例如「当前模型 deepseek-flash → 命中 · 规则 1」,方便当场验证规则写对没有;检测不到当前模型时会给出提示。
79
- - **外观随规则走,颜色随图走** — 壁纸(多张)、布局模式、透明度与模糊度是每条规则自己的属性;**取景和主题色则是每张图片自己的**。除「界面」页外没有任何全局外观项。
79
+ - **外观随规则走,逐图的三项随图走** — 壁纸(多张)、透明度与模糊度是每条规则自己的属性;**布局模式、取景和主题色则是每张图片自己的**。除「界面」页外没有任何全局外观项。
80
80
  - **一条规则多张图** — 每条规则持有一组有序图片,用胶片条展示:可一次加多张(多选文件、拖放,或一行一个网址批量粘贴)、排序、把某张设为第一张、**原地替换某一张**(替换会保住它在轮换里的位置)、逐张删除。不轮换时规则画的就是**第一张**。删掉最后一张只是让这条规则变空,而空不等于失效:只要它还留着自己的主题色,它就照旧参与匹配与兜底、只给界面配色而不铺壁纸;连主题色也没有(比如刚新增的规则)才会被匹配跳过。空规则是一个看得见、退得回的状态,而不是卡住出不来的死胡同。一张图都没有时胶片条会隐藏、上方那块大预览**就是**上传按钮,所以添加图片的地方只有一个、也很显眼。
81
- - **导入 / 导出** — 一键把**每一条规则连同它的全部图片**导出为 `dsh-background-by-model-theme.json`(版本 6,图片以 base64 内联),可随时导入还原。更早版本导出的文件同样能导入:规则唯一的那张图会被还原成它的图片列表,在「主题色属于图片」之前写下的颜色会被抬升到每一张还没有自己颜色的图上,而在[切换效果](#切换效果全局)之前写下的文件会拿到这项设置的出厂默认——所以旧主题文件还原出来的观感与当初一模一样。
81
+ - **导入 / 导出** — 一键把**每一条规则连同它的全部图片**导出为 `dsh-background-by-model-theme.json`(版本 6,图片以 base64 内联),可随时导入还原。更早版本导出的文件同样能导入:规则唯一的那张图会被还原成它的图片列表,在「主题色属于图片」之前写下的颜色会被抬升到每一张还没有自己颜色的图上,在布局模式下放到规则上时写下的值会被抬升到它的每一张图上,而在[切换效果](#切换效果全局)之前写下的文件会拿到这项设置的出厂默认——所以旧主题文件还原出来的观感与当初一模一样。
82
82
  - **文件持久化** — 所有设置与图片保存到文件系统 `~/.dsh/.dsh-background-by-model-data/`,不依赖 `localStorage`。
83
- - **自动迁移** — 旧版单壁纸配置在首次读取时自动升级,详见 [CHANGELOG.zh.md 的 v0.3.0 条目](./CHANGELOG.zh.md#v030)。
83
+ - **自动迁移** — 旧版单壁纸配置在首次读取时自动升级,曾经放在规则上的布局模式也会被抬升到它的每一张图上。详见 [CHANGELOG.zh.md 的 v0.3.0 条目](./CHANGELOG.zh.md#v030)。
84
84
  - **中英双语** — 完整的中英文界面,自动跟随语言设置。
85
85
  - **主题守护** — 宿主重置主题后自动重新激活自定义主题。
86
86
 
@@ -94,8 +94,8 @@
94
94
  - **切换模型时也换一张** — 可选的第二种触发方式:每次切换模型落到这条规则上就往前走一张。对「每次都想看到不一样」的规则来说,这就是全部功能,而且不用挂任何定时器。
95
95
  - **只有正在生效的规则会轮换** — 全插件只有一个定时器,始终对准当前模型解析出的那条规则。标签页不可见时它会停下、回来时重新计时;只有一张图的规则(以及所有[节日](#节日特殊背景))从不排期。
96
96
  - **下一张** — 立刻换一张,方便当场检查这组图。
97
- - **跟着规则走的是外观,跟着图片走的是颜色** — 布局模式、透明度、模糊度留在规则上、原地不动;**主题色属于图片**,所以一组图可以依次是绿的、红的、以及跟随系统主题的,界面配色跟着壁纸一起换。过渡用的就是切换模型时那套交叉淡入。
98
- - **取景是逐图的,颜色也是** — 构图和配色都属于某一张图:每张图各自保留自己的取景与主题色,同时共享规则的布局、透明度与模糊度。胶片条会标出哪些图已经各自有颜色,调色区上方也会写明当前编辑的是哪一张。
97
+ - **跟着规则走的是外观,跟着图片走的是布局、取景与颜色** — 透明度、模糊度留在规则上、原地不动;**布局模式、取景、主题色属于图片**,所以一组图可以一张高截图用适应、旁边那张横图用填充,也可以依次是绿的、红的、以及跟随系统主题的,界面配色跟着壁纸一起换。过渡用的就是切换模型时那套交叉淡入。
98
+ - **布局模式、取景、颜色都是逐图的** — 适应/填充决定的是**这一张**图怎么铺进这块区域,构图和配色也都属于某一张图:每张图各自保留自己的三项,同时共享规则的透明度与模糊度。胶片条会标出哪些图已经各自有颜色,调色区上方也会写明当前编辑的是哪一张。
99
99
  - **只加载用得到的图** — 启动时只读每条规则的**第一张**,其余的在展开卡片时、或者在轮换即将用到它时再读。所以「五条规则各十张图」不等于首帧之前先传五十张壁纸。
100
100
 
101
101
  ### 切换效果(全局)
@@ -121,14 +121,14 @@
121
121
  - **两者重叠时中秋优先** — 它们确实会撞(2025-10-06 就是同一天),一个精确的单日比七天窗口里的某一天更值得采信。
122
122
  - **没有图的节日会回落到模型规则**,而不是把背景刷空。
123
123
  - **内置壁纸** — 两个节日都自带一张压缩过的壁纸(原生分辨率下 228 KB 与 500 KB,原图共 8.37 MB)。只会去取**今天真能生效**的那一张,所以不在节日的配置一点都不会传。
124
- - **图片和配色都不可替换,这是刻意的** — 一个要求用户去配置的彩蛋就不是彩蛋了。节日槽位是只读的:往 `modelbg-h-midautumn` / `modelbg-h-nationalday` 里放文件不会被任何一次读取采用,插件也拒绝写入、抓取和删除这两个槽位。主题色是节日定义里的常量、每次读取都强制覆盖——**节日条目和它的图片两处都覆盖**,所以手改 `theme-config.json` 里的 `color` 也换不掉节日配色。导出的主题文件只包含你自己的规则。
124
+ - **图片和配色都不可替换,这是刻意的** — 一个要求用户去配置的彩蛋就不是彩蛋了。节日槽位是只读的:往 `modelbg-h-midautumn` / `modelbg-h-nationalday` 里放文件不会被任何一次读取采用,插件也拒绝写入、抓取和删除这两个槽位。主题色是节日定义里的常量、每次读取都强制覆盖——**节日条目和它的图片两处都覆盖**,所以手改 `theme-config.json` 里的 `color` 也换不掉节日配色。布局模式同理固定为定义里的**填充**(这些画本来就是满幅的,一个残留的 `fit` 会给它加黑边),而旁边的**取景**仍然是你自己的。导出的主题文件只包含你自己的规则。
125
125
  - **不想要它** — 关掉「配置」页那个开关,或者在同一个文件里写 `"holidays": { "enabled": false }`。
126
126
 
127
- ### 每条规则自带的外观
127
+ ### 规则与图片各自的设置
128
128
 
129
129
  - **背景图片** — 一条规则可以放一张或多张图,每张图是独立文件。胶片条决定你正在编辑哪一张;不轮换时规则画的是**第一张**。规则也可以是**一张图都没有**的:这时它不再铺壁纸,但只要它有自己的主题色就照旧参与匹配与兜底(只是把界面按那个颜色配色),连主题色也没有才会被匹配跳过,直到你给它一点内容。
130
130
  - **主题色** — HSL 色轮 + 数值输入 + 灵感色板,外加「从本图提取」和吸管「从图片取色」。**颜色属于图片**:这些控件编辑的是胶片条里选中的那张,胶片条上会标出哪些图已经各自有颜色,而没有自己颜色的图**跟随系统主题**、不会去继承规则的颜色。**一张图都没有**的规则只剩它自己的主题色可编辑——那正是这种规则唯一能画的东西。实时生成整套 CSS 设计令牌。跟随系统主题时,界面配色交还宿主,而壁纸与「界面」页的透明度、模糊照旧生效——表面颜色是从宿主自己的令牌读出来后套上你的透明度的,所以壁纸不会被一块不透明底板盖住。
131
- - **布局模式** — 适应 / 填充 / 拉伸 / 平铺 / 居中。
131
+ - **布局模式** — 适应 / 填充 / 拉伸 / 平铺 / 居中,并且**按图分别保存**:同一组轮换里可以一张适应、一张填充。规则还没有图片时这一行是**禁用**而不是隐藏(没有图可布局),这样卡片本身就说明了加上图片后会多出什么可调项。
132
132
  - **取景** — 在视口比例的编辑器中拖动平移、滚轮缩放;**仅「适应」模式下可编辑**,提交的构图在窗口缩放、跨屏移动后保持一致。按图分别保存。
133
133
  - **背景透明度** — `0–100%`,作用于该规则的壁纸层。
134
134
  - **背景模糊** — `0–60 px`,作用于壁纸层。
@@ -147,7 +147,7 @@
147
147
  | 选项卡 | 作用范围 | 内容 |
148
148
  | --- | --- | --- |
149
149
  | **界面** | 全局,所有模型共用 | 主背景、左侧栏、右侧栏、卡片面板、输入框与控件、设置面板、对话文本框、轨迹页的透明度与模糊度 |
150
- | **模型背景** | 每条规则 | 有序规则列表、顶部实时状态,以及每条规则的图片(胶片条)、**逐图主题色**、布局模式、逐图取景、透明度、模糊度与[多图轮换](#多图轮换) |
150
+ | **模型背景** | 每条规则、每张图片 | 有序规则列表、顶部实时状态,以及每条规则的图片(胶片条)、**逐图布局模式 / 取景 / 主题色**,加上规则自己的透明度、模糊度与[多图轮换](#多图轮换) |
151
151
  | **配置** | — | 全局[切换效果](#切换效果全局)、导入 / 导出整套规则(`dsh-background-by-model-theme.json`),外加[节日特殊背景](#节日特殊背景)那一个开关 |
152
152
 
153
153
  原先的 **「色彩」** 选项卡已删除——主题色改为每条规则自己的属性;原 **「背景」** 选项卡已改造成 **「模型背景」**。
@@ -270,24 +270,21 @@ pnpm run typecheck
270
270
 
271
271
  ## 近期优化
272
272
 
273
- > 这里只列最近两个版本;更早的版本(v0.7.2 及以前)见 [CHANGELOG.zh.md](./CHANGELOG.zh.md)。
273
+ > 这里只列最近两个版本;更早的版本(v0.7.3 及以前)见 [CHANGELOG.zh.md](./CHANGELOG.zh.md)。
274
+
275
+ ### v0.7.5
276
+
277
+ - **布局模式属于图片,不属于规则** —— 适应 / 填充 / 拉伸 / 平铺 / 居中 此前是每条规则一个值,于是一轮里既有横构图照片又有竖屏截图时,没法让一张留黑边、另一张铺满。现在它存在 `images[].bgMode` 上、从**正在画的那张图**读回,胶片条下面那排胶囊编辑的也是那里选中的图(规则还没有图片时是**禁用**而不是隐藏)。`SCHEMA_VERSION` 因此升到 **6**,因为两个方向都是无声的:schema 5 的宿主会在第一次写入时丢掉逐图模式(设成 拉伸 的图变回 适应),而 schema 6 读老配置时会把规则级的值抬到每一张图上,现成壁纸不会掉到默认值。节日的图按**定义**就是 填充(和它旁边的槽位、颜色同一条规矩),旁边的取景仍然是你自己的。
278
+ - **渐变只作用于当前调色板真正写入的东西(修复)** —— 「有时候切换背景时,白色色块会渐变为黑色色块…白色渐变完黑色后立马切到白色」,白色值量到 `237, 243, 254`。那就是 `#edf3fe` = `--dsw-static-deepseek-50`,也就是宿主浅色方案的 `--dsw-specific-bubble`(你自己的消息气泡;深色方案是 `#2c2c2e`)。渐变之所以注册了这个名字,是因为它要的是**深色**分支的 token 表、又把整张表交给了 `:root`/`body` —— 浅色分支只写这 80 个名字里的 35 个,剩下 45 个(气泡、设置里的下拉框底色、提示与气泡框、代码块右上角小按钮、状态色)仍然归**宿主**;而让宿主短暂换方案的正是插件自己:每次换配色都销毁并重新注册主题,宿主的销毁器把偏好重置为 `system`,在深色桌面上那一瞬间画的就是宿主的深色调色板。配上报告里 3000ms 的切换时长,一帧的错色就成了三秒的渐变。现在只有当前调色板真正写入的 token 才会拿到过渡(注册表仍是全部 80 个,深色调色板完全没变),配色方案没变也不再重建皮肤。实测:`body` 的过渡列表 80 → 35 个名字,气泡与下拉框出局,`--dsw-alias-bg-base` / `-layer-2` / 代码块底板仍在。
279
+ - **代码块跟着调色板走,底板有了自己的透明度(修复)** —— 「代码块背景有点问题」:浅色界面里深绿文字压在近黑底板上。宿主的 shiki 表在 `:root` 上声明 `--shiki-background: var(--dsw-alias-markdown-code-block)`,而该 token 只声明在 `body` 上,于是根元素只能读到过渡注册的 `initial-value` —— 采样一次的宿主颜色。现在调色板规则对 `:root` 一并发出(`:root` 也加入渐变作用域),底板与 banner 重新是调色板的染色,并新增 **`code`** 档位驱动这两块表面、各读自己的变量,默认不透明。
280
+ - **四个小修** —— 删图删到不足两张时轮换开关不再卡在「开」(面板在那里本来就把它禁用了,于是也关不掉;`normalizeRule` 对导入的配置强制同一条不变量);「切换时长」只印一次;资料页页脚的包名与品牌磁贴现在是指向仓库的链接,`check:ui` 拿那一个地址和 `package.json` 对比。
281
+ - **门禁** —— `pnpm test` 现在是**八道**:`check:code-plate` 与 `check:image-mode` 加入,`check:rotation` 增加开关不变量,`check:ui` 增加链接一致性,`check:transition` 增加渐变归属规则,`check:repaint` 钉住武装渐变的那两行。
274
282
 
275
283
  ### v0.7.4
276
284
 
277
285
  - **弹窗里的控件现在和壁纸走同一个时钟,而不是等它切完再跳(修复)** —— 换主题色那一下,「节日特殊背景」的开关填充和「平移推入」「标准」这些胶囊按钮会**整整一套切换时长**停在上一套颜色上,直到背景切完才瞬间换成新的:一次 3 秒的换图看起来就是"界面先不管它、末尾来一下硬切"。成因是插件自己造成的:淡入把**整张 token 表**挂到了三个元素上,而其中两个(设置弹窗与轨迹页根节点)**只重新声明**三个 layer token、其余全靠继承。给一个*继承来的* token 挂过渡,该元素的过渡目标就是武装那一刻它看到的值 —— 祖先正在插值,那个值还是旧的 —— 于是它变成一条被阻尼的过渡、整棵子树跟着走(实测:1200ms 过渡在 t=1500ms 只走到 113/255,而只继承、自己没有过渡的消费者是逐帧跟随)。现在过渡表按作用域给,每个作用域只拿自己声明的 token。
278
286
  - **用插件自有颜色画的那几块表面,现在也淡入了(修复)** —— 设置弹窗底板、文件预览面板、Cordis 面板都不是用注册过的 alias token 画的,而是用插件自有、内联写在 `<html>` 上的变量;未注册的变量根本不能插值,所以它们在一帧之内硬切,而周围每一块都在渐变。现在淡入作用域可以点一个**标准属性**(`background-color`),并且每条上色规则与它的淡入共用同一个选择器常量,两半不可能再走岔。拿真实产出的 CSS 实测、跑一次 3000ms `ease`:底板、右侧面板、Cordis 面板在第一次采样时就已经是**终色**并且再没动过,而现在五块表面每一次采样都处在同一进度 —— t=1500ms 时 80.2%,恰好是 `ease(0.5)`。
279
287
 
280
- ### v0.7.3
281
-
282
- - **切换效果现在是一个设置:四种效果、一种缓动、一个时长,都在「配置」页上** —— 这个插件做的每一次换图(切模型、轮换一步、手动「下一张」、节日接管)此前都走同一个写死的交叉淡入,而它的时长读的是一个没有任何控件能碰到的字段。**淡入淡出**、**直接切换**、**缩放淡入**、**平移推入**都作用在本来就有的那两层上 —— 缩放与平移只在进来的那一层上用 `transform`,而壁纸层从来没用过这个属性,所以两者都不花一个字节的额外传输 —— 「直接切换」与 `0` 秒是同一个硬切,系统的「减少动态效果」是**否决**而不是把动画缩短。
283
- - **卡片上带一个预览色块,而它不是装饰** —— 壁纸淡入它自己是什么都看不见的,所以「在已经在屏的那张图上重放效果」会什么都不显示:这是这个功能唯一可能「明明在工作却看起来坏了」的方式。色块按此刻一次真实切换会用的时长重放选中的效果。
284
- - **时长只有一个,「跟随每条规则」删掉了** —— 那个选项指向一个界面设不了的值(`rotate.fadeMs` 从 0.7.0 起就在格式里,却从来没有控件能到达它),所以它是在一个真设置和一个幻影设置之间做选择。滑块本身就是那个设置。手工编辑过的配置不会被无声丢掉:`legacyFadeMs` 在第一次读取时把找到的第一个「有意为之」的值折进全局时长(等于出厂值 320 ms 的忽略,显式的 `durationMs` 胜出),该字段也因「正在被迁移、而不是在漂移」而豁免漂移警告。
285
- - **界面配色现在和壁纸走同一个时钟** —— 换一个主题色本质是一次 `body{--dsw-alias-…}` 的重写,而它以前在一帧之内改掉每个表面、背后的图却还在淡入淡出。这个渐变是**注册**问题(未注册的自定义属性无法插值),所以插件发出把别名令牌变成 `<color>` 的 `@property` 规则,外加一条 `transition`,作用于自身声明会随调色板变化的那些元素 —— `body`、设置弹窗与轨迹页根节点。它**内联**绘制的两处表面(三栏列、对话/轨迹卡片)根本不读这些令牌,所以把同一个时长直接挂在 `background-color` 上。时长、缓动与两项否决都来自壁纸用的**同一个** `transitionPlan`,而且只在**调色板确实变了**的时候武装 —— 其它每一次 apply(拖滑块改的是 alpha)写入的样式表不带过渡、内联写 `none`,因为那里挂一条活着的过渡会让表面跟不上指针。
286
- - **浅↔深互切是立即切换,壁纸照旧做动画** —— 在两套调色板之间插值会让界面走过中间调,而那里的墨色**两个方向都是错的**,所以浅↔深恰恰是渐变会让事情变糟的那一种情况。渐变只在**配色深浅不变**时武装(浅→浅、深→深);壁纸刻意不受这条规则约束:一张图淡入另一张图没有「深浅」可以搞错。
287
- - **墨色现在是同步写入的,而那是一个真实存在、且只有一个写入者的 bug(修复)** —— 把一条规则换成深色主题色时,面板的文字可能留在**上一套**调色板上,而所有表面都已经跟到新的一套:深色面板上的深色墨色,读不出来,只有重新加载才能清掉,日志里什么都不会说。这是从报告里的截图**量出来**的:面板是插件自己的深色 `bg-layer-2`(`hsl(229,59%,27%)` = `rgb(28,43,109)`,在 87% 面板透明度下合成为采样到的 `rgb(23,36,91)`),而墨色是画在它上面的**浅色**分支令牌(`rgba(0,0,0,0.85)` 压在那块藏青上恰好就是字形核心采样到的 `rgb(3,6,14)`,`rgba(0,0,0,0.7)` 恰好是提示行采样到的 `rgb(7,10,27)`)。其它每个表面都有第二个同步的写入者,所以只有令牌样式表可能变旧 —— 而它唯一的写入者只能从一个包在 `catch {}` 里的 `requestAnimationFrame` 回调到达:不在渲染的窗口会吞掉那一帧,id 却仍然武装着、把此后每一次更新都堵在它后面,写入内部出错则什么都不说。现在调色板一旦移动,就在**与它必须保持一致的表面同一个任务里**同步写入;帧路径留给它本来要做的事(合并一次滑块拖动),积压超过一秒的帧会被丢掉而不是永久阻塞,写入者也会把失败记进日志而不是吞掉。
288
- - **配置格式升到 5、导出格式升到 6** —— schema 4 的 sanitizer 会**按自己认识的键重建**配置,所以一个旧宿主进程会在刷新后的第一次写入时丢掉效果、缓动与时长,悄无声息,宿主日志里也什么都没有。公布的 `schema` 随之前进,「扣住写入」分支会拦住这样的宿主,`warnUnknownConfigKeys` 也出于同样的理由往深一层比较固定形状的 `transition` 块。
289
- - **门禁** —— `check:transition` 加入 `pnpm test`,现在是**六道**;`check:repaint` 钉住这个墨色 bug 由之构成的三行(调色板变化时同步写入、不许吞掉失败、在令牌与内联两个调用点都把渐变问题的两半问全);`check:node` 覆盖出厂默认值、往返、恶意取值、被保留的 `0` 时长、漂移检查与旧值折算;`check:ui` 覆盖新文案与新类,并把胶囊标签登记进 `TABLE_KEYS`。
290
-
291
288
  ## Star History
292
289
 
293
290
  [![Star History Chart](https://api.star-history.com/chart?repos=HarmlessFunny/dsh-background-by-model&type=timeline&legend=bottom-right)](https://www.star-history.com/?repos=HarmlessFunny%2Fdsh-background-by-model&type=timeline&legend=bottom-right)