dsh-background-by-model 0.7.2 → 0.7.3

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,17 @@
1
1
  # Changelog
2
2
 
3
- > The full release history lives here; the two most recent releases (v0.7.2 / v0.7.1) are also listed in [README.md](./README.md#recent-optimizations).
3
+ > The full release history lives here; the two most recent releases (v0.7.3 / v0.7.2) are also listed in [README.md](./README.md#recent-optimizations).
4
+
5
+ ## v0.7.3
6
+
7
+ - **The switch itself became a setting: 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, a different rule winning) went through one hard-coded cross-fade whose length was read off a field no control could reach. The effect and its easing are global, because a rule has no notion of a transition: **cross-fade** (opacity), **instant**, **zoom** and **slide** (transform, on the incoming layer only — a property the wallpaper layer had never used, so neither new effect costs a byte of extra transfer). Instant and a `0` ms duration are the same hard cut rather than one of them being read as "unset", a `0` is kept as a real value rather than defaulted away, and `prefers-reduced-motion` is honoured as a **veto** rather than as a shorter animation. The decision is pure (`src/client/transition.ts`), which is what lets `check:transition` pin the failures that throw nothing: an effect whose start state equals its end state (visibly doing nothing), an effect that leaves the incoming layer scaled (which would crop every later wallpaper), and `fade` ceasing to be byte-for-byte the string earlier releases wrote.
8
+ - **The card carries a preview swatch, and that is not decoration** — a wallpaper fading into itself is invisible, so replaying the effect on the picture already on screen would show nothing at all: the one way this feature could look broken while working perfectly. The swatch replays the chosen effect at the duration a real switch would use right now, and picking an effect or an easing replays it at once.
9
+ - **There is exactly one duration, and the per-rule choice is gone** — the card's default was "Follow each rule", which named a value the interface cannot set: `rotate.fadeMs` had existed in the shape since 0.7.0 and no control ever reached it, so the option was a choice between a real setting and a phantom one. The slider IS the setting now, and the field it deferred to is gone with it. The removal is not a silent loss for the one kind of config that could carry it: that field could only ever be hand-edited, so `legacyFadeMs` folds the first deliberate value it finds into the global duration when a config that predates the setting is first read — values equal to the shipped 320 ms are ignored, so an ordinary config keeps the ordinary default, and an explicit `durationMs` always wins over a legacy field sitting beside it. `rules[].rotate.fadeMs` is exempt from the drift warning for the same reason: it is being migrated, not drifting.
10
+ - **The interface colour now changes over the same clock as the wallpaper** — a new theme colour is one rewrite of `body{--dsw-alias-…}`, which changed every surface in a single frame while the picture behind it blended: a snap in front of an animation. The fade is a **registration** problem, not a declaration one, because an unregistered custom property cannot interpolate: `src/client/palette-fade.ts` emits `@property` rules that turn the alias tokens into `<color>`s, plus a `transition` for the elements whose own declarations change with the palette — `body`, and the two surfaces that re-declare layer tokens instead of inheriting them (the settings dialog and the trajectory root). The two places the plugin paints **inline** (the app columns, and the chat/trajectory cards) do not read those tokens at all, so they carry the same duration on `background-color` directly. The duration, the easing and the vetoes come from the **same** `transitionPlan` the wallpaper uses, so "instant", a `0` ms duration and reduced motion cannot mean one thing for the picture and another for the interface. It is armed **only when the palette itself moved**: every other pass rewrites the same token block (a slider drag moves the alphas) and writes the sheet with no transition and `none` inline, because a live transition there would make the surface lag behind the pointer. A token the host does not publish is deliberately left unregistered — registration needs an `initial-value`, and a registered property that nothing else sets resolves to that value instead of to the `var(…, fallback)` chain the plugin's own stylesheet relies on, so registering it would replace a fallback with an invented colour.
11
+ - **A light↔dark flip switches instantly, and the wallpaper keeps animating (this one is a fix)** — interpolating between the two palettes walks the whole interface through mid-tones where the ink is wrong in both directions — dark text half-way into a dark palette, and driven by the **previous** scheme's ink — 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); light↔dark switches at once, which is what it did before the fade existed. The wallpaper is deliberately untouched by that rule: one image fading into another has no scheme to get wrong, so it keeps whatever the switch effect says. `paletteFadeAllowed` is the whole rule, pure and pinned in `check:transition`, and `check:repaint` asserts the call site keeps **both** halves of the question (the palette moved **and** the scheme held), because the call site is where a later edit would quietly drop one — and the surfaces the plugin paints inline take the same answer, so they cannot end up animating while the tokens do not.
12
+ - **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** (light) palette while every surface followed the new one: dark ink on a dark panel, unreadable, cleared only by reloading, and with nothing in the log to explain it. Measured from the reported screenshot rather than guessed: the panel was the plugin's own dark `bg-layer-2` — `hsl(229,59%,27%)` = `rgb(28,43,109)`, which at the configured 87% panel opacity composites to the sampled `rgb(23,36,91)` — while the label ink was the **light** branch token over it: `rgba(0,0,0,0.85)` on that navy is exactly `rgb(3,6,14)`, the value sampled from the glyph cores, and `rgba(0,0,0,0.7)` is exactly the `rgb(7,10,27)` sampled from the hint line. Two exact matches, one panel: the surface and the ink came from different palettes. The asymmetry is structural — every other surface has a second, synchronous writer (the dialog's layer tokens come from `applySettingsOverrides`, the columns' backgrounds from the view cards), so only the token stylesheet could go stale, and that one 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, parking every later update behind it, and a failure inside the write said nothing at all. Reloading was the only cure, which is exactly what the report said. A palette that moved is now written synchronously, in the same task as the surfaces it has to agree with, while the frame path is kept for what it was for (coalescing a slider drag, where the palette is already correct); a frame outstanding for more than a second is dropped instead of blocking forever, and the writer logs its failure instead of swallowing it.
13
+ - **The config shape moved to 5 and an export to format 6, and an older host still cannot eat a profile** — a schema-4 sanitizer REBUILDS the config from the keys it knows, so a stale host process would drop the effect, the easing and the duration on the first write after a refresh, silently and with nothing in the host log to explain it. The published `schema` therefore moves, the client's `>=` comparison puts such a host into the hold-writes path it already had, and `warnUnknownConfigKeys` compares the fixed-shape `transition` block one level down for the same reason. Theme exports move to format 6; 5 and older still import. That shape has never been released, so `5` still means what it meant when it was written — `SCHEMA_VERSION` guards a newer client against an older host process, and no host has ever announced 5 with the per-rule field.
14
+ - **Gates** — `check:transition` (`scripts/transition-check.ts`) joins `pnpm test`, which is now **six** checks: it pins the cross-fade string byte-for-byte, the hard-cut semantics of `instant` and a `0` duration, the reduced-motion veto, the landing state of every effect, the palette-fade CSS shape (including that `0` ms emits the registrations and no transition rule) and the scheme-flip table. `check:repaint` gained the three lines this bug was made of — the palette write must be synchronous when the palette moves, must not swallow its failure, and must 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 `transition` drift check, the announced schema of 5, and the legacy fold (a hand-tuned 1200, a deliberate `0`, the unremarkable 320, and an explicit global value winning). `check:ui` covers the new strings and the two new classes (`.dab-tr-preview`, `.dab-tr-preview-in`), and registers the effect/easing chip labels in `TABLE_KEYS` — they are reached through `Record<TransitionEffect, …>` tables, which is the documented mechanism for a label the compiler already guarantees exists.
4
15
 
5
16
  ## v0.7.2
6
17
 
package/CHANGELOG.zh.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # 更新日志
2
2
 
3
- > 这里保留完整的历史记录;最近两个版本(v0.7.2 / v0.7.1)同时列在 [README.zh.md](./README.zh.md#近期优化) 里。
3
+ > 这里保留完整的历史记录;最近两个版本(v0.7.3 / v0.7.2)同时列在 [README.zh.md](./README.zh.md#近期优化) 里。
4
+
5
+ ## v0.7.3
6
+
7
+ - **切换效果本身成了一个设置:四种效果、一种缓动、一个时长,都在「配置」页上** —— 这个插件做的每一次换图(切模型、轮换一步、手动「下一张」、节日接管、换成另一条规则生效)此前都走同一个写死的交叉淡入,而它的时长读的是一个没有任何控件能碰到的字段。效果和缓动是全局的,因为规则没有「过渡」这个概念:**淡入淡出**(opacity)、**直接切换**、**缩放淡入**、**平移推入**(transform,只作用在进来的那一层上 —— 壁纸层从来没用过这个属性,所以两种新效果一个字节的额外传输都不花)。「直接切换」和 `0` 毫秒是同一个硬切,而不是其中一个被读成「没设置」;`0` 是一个被保留的真实取值,不会被默认值顶掉;系统的「减少动态效果」是**否决**,而不是把动画缩短。判定是纯函数(`src/client/transition.ts`),正因如此 `check:transition` 才能钉住那些不会抛错的失败:起始状态与结束状态相同的效果(看上去什么都没做)、会把进来的那一层留在缩放状态上的效果(此后每一张壁纸都会被裁掉一块),以及 `fade` 不再逐字节等于早先版本写下的那个字符串。
8
+ - **卡片上带一个预览色块,而它不是装饰** —— 壁纸淡入它自己是什么都看不见的,所以「在已经在屏的那张图上重放效果」会什么都不显示:这是这个功能唯一可能「明明在工作却看起来坏了」的方式。色块按此刻一次真实切换会用的时长重放选中的效果,点效果或点缓动都会立刻重放一次。
9
+ - **时长只有一个,「跟随每条规则」删掉了** —— 那张卡的默认值是「跟随每条规则」,而它指向一个界面根本设不了的值:`rotate.fadeMs` 从 0.7.0 起就在格式里,却从来没有控件能到达它,所以那个选项是在一个真设置和一个幻影设置之间做选择。现在滑块本身就是那个设置,它让位的字段也一起删了。对唯一可能带上它的那种配置来说,这并不是一次无声的丢失:那个字段只可能被手工编辑,所以当一份早于这个设置的配置第一次被读取时,`legacyFadeMs` 会把找到的第一个「有意为之」的值折进全局时长 —— 等于出厂值 320 ms 的一律忽略(普通配置就保持普通默认值),而显式的 `durationMs` 永远胜过它旁边的旧字段。`rules[].rotate.fadeMs` 同样豁免漂移警告,理由相同:它是在被迁移,不是在漂移。
10
+ - **界面配色现在和壁纸走同一个时钟** —— 换一个主题色本质是一次 `body{--dsw-alias-…}` 的重写,它在一帧之内改掉每一个表面,而背后的图还在淡入淡出:这就是动画前面接了一下硬切。这个渐变是**注册**问题而不是声明问题 —— 未注册的自定义属性无法插值 —— 所以 `src/client/palette-fade.ts` 会发出把别名令牌变成 `<color>` 的 `@property` 规则,外加一条 `transition`,作用于「自身的声明会随调色板变化」的那些元素:`body`,以及两个**重新声明**层令牌而不是继承它们的表面(设置弹窗与轨迹页根节点)。插件**内联**绘制的两处(三栏列,以及对话/轨迹卡片)根本不读这些令牌,所以它们把同一个时长直接挂在 `background-color` 上。时长、缓动与两项否决都来自壁纸用的**同一个** `transitionPlan`,所以「直接切换」、`0` 毫秒和「减少动态效果」不可能对图是一个意思、对界面又是另一个意思。它**只在调色板确实变了**的时候武装:其它每一次 apply 都在重写同一段令牌(拖滑块改的是 alpha),那些写入不带过渡、内联写 `none`,因为那里挂一条活着的过渡会让表面跟不上指针。宿主没有公布的令牌是**刻意不注册**的 —— 注册需要 `initial-value`,而一个没有别的东西去设置它的已注册属性,会解析到那个初始值、而不是插件自己样式表依赖的 `var(…, fallback)` 链,注册它就等于用一个人为的颜色替换掉回退值。
11
+ - **浅↔深互切是立即切换,而壁纸照旧做动画(这一条是修复)** —— 在两套调色板之间插值会让整个界面走过中间调,而那里的墨色**两个方向都是错的**(深色文字走进一半的深色调色板,而且用的是**上一套**方案的墨色),所以浅↔深互切恰恰是渐变会让事情变糟的那一种情况。现在渐变只在**配色深浅不变**时武装(浅→浅、深→深);浅↔深立即切换,也就是渐变出现之前的行为。这条规则刻意不碰壁纸:一张图淡入另一张图没有「深浅」可以搞错,所以它照旧听切换效果的。`paletteFadeAllowed` 就是这条规则的全部,纯函数、由 `check:transition` 钉住;`check:repaint` 则要求调用点把问题的**两半**都问全(调色板变了**且**深浅没变),因为调用点正是后来某次编辑会悄悄丢掉其中一半的地方 —— 而插件内联绘制的那些表面拿到的是同一个答案,所以不可能出现「令牌不渐变、内联面板却在渐变」的半吊子状态。
12
+ - **墨色现在是同步写入的,而那是一个真实存在、且只有一个写入者的 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)`。两个精确吻合,一块面板:表面和墨色来自两套不同的调色板。这种不对称是结构性的 —— 其它每个表面都有第二个同步的写入者(弹窗的层令牌来自 `applySettingsOverrides`,三栏的背景来自 view cards),所以只有令牌样式表可能变旧,而这唯一的写入者只能从一个包在 `catch {}` 里的 `requestAnimationFrame` 回调到达:不在渲染的窗口会吞掉那一帧,而 id 仍然武装着,把此后每一次更新都堵在它后面;写入内部出错则什么都不说。重新加载是唯一的解药,而报告里说的正是这件事。现在调色板一旦移动,就在**与它必须保持一致的表面同一个任务里**同步写入;帧路径留给它本来要做的事(合并一次滑块拖动,那时调色板本来就是对的),积压超过一秒的帧会被丢掉而不是永久阻塞,写入者也会把失败记进日志而不是吞掉。
13
+ - **配置格式升到 5、导出格式升到 6,旧宿主依旧吃不掉配置** —— schema 4 的 sanitizer 会**按自己认识的键重建**配置,所以一个仍在运行的旧宿主进程会在刷新后的第一次写入时丢掉效果、缓动和时长,悄无声息,宿主日志里也什么都没有。因此公布的 `schema` 前进,客户端那句 `>=` 比较会把这样的宿主放进它本来就有的「扣住写入」分支,`warnUnknownConfigKeys` 也出于同样的理由往深一层比较固定形状的 `transition` 块。主题文件升到格式 6;5 及更早仍可导入。这个格式从未发布过,所以 `5` 仍然是它写下时的含义 —— `SCHEMA_VERSION` 防的是「新客户端配旧宿主进程」,而从来没有任何宿主用旧形状报出过 5。
14
+ - **门禁** —— `check:transition`(`scripts/transition-check.ts`)加入 `pnpm test`,现在是**六道**:它逐字节钉住交叉淡入的字符串、「直接切换」与 `0` 时长的硬切语义、「减少动态效果」的否决、每种效果的落点状态、配色渐变的 CSS 形状(包括 `0` 毫秒只发注册、不发过渡规则),以及浅↔深那张真值表。`check:repaint` 新增了这个 bug 由之构成的三行:调色板移动时写入必须是同步的、不许吞掉自己的失败、并且必须在令牌与内联两个调用点都把渐变问题的两半问全。`check:node` 覆盖出厂默认值、往返、恶意取值、被保留的 `0` 时长、`transition` 漂移检查、公布 schema 为 5,以及那次旧值折算(手工调过的 1200、有意的 `0`、平平无奇的 320、显式的全局值胜出)。`check:ui` 覆盖新文案和两个新类(`.dab-tr-preview`、`.dab-tr-preview-in`),并把效果/缓动胶囊的标签登记进 `TABLE_KEYS` —— 它们是通过 `Record<TransitionEffect, …>` 这类表到达的,而这正是「编译器已经保证存在的标签」的既定登记机制。
4
15
 
5
16
  ## v0.7.2
6
17
 
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.
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.
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
 
@@ -78,7 +78,7 @@ One interface, one config — only the current model differs:
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
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.
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 5, 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, and a color written before per-image colors is lifted onto every image that has none — 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, 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
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).
84
84
  - **Bilingual** — Full Chinese / English UI with automatic locale detection.
@@ -98,6 +98,19 @@ Per rule, and **off by default** — cycling spends real bandwidth, memory and b
98
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.
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
+ ### Switch effect (global)
102
+
103
+ What a wallpaper **change** looks like. It lives on the **Config** page, it is the same for every rule (a rule has no notion of a transition), and it covers every way the wallpaper changes: a model switch, a [rotation](#image-rotation) step, the manual **Next image**, and a [holiday](#holiday-backgrounds) taking over.
104
+
105
+ - **Four effects** — **Cross-fade** (the blend every earlier release performed, and therefore the default), **Instant** (a hard cut), **Zoom in** (the incoming image settles from 6% larger) and **Slide in** (it pushes in from the side). Zoom and slide animate `transform`, which the wallpaper layer had never used, so neither costs a byte of extra transfer.
106
+ - **Easing** — standard / linear / ease-out / ease-in-out. The row is not rendered while the effect is **Instant**: a hard cut has no curve to shape, and a control that does nothing is worse than no control.
107
+ - **One duration for every switch** — `0`–`3000 ms`, and `0` is a hard cut rather than "unset". It replaced the per-rule `rotate.fadeMs`, which from 0.7.0 on was a duration with **no control anywhere** — it could only be hand-edited — so a hand-tuned value is folded into this one when a config that predates the setting is first read (a value equal to the shipped 320 ms is ignored, so an ordinary config keeps the ordinary default).
108
+ - **`Instant` and `0` ms are the same hard cut** — two ways of asking for one thing, so both are honoured instead of one being read as "not set".
109
+ - **Reduced motion is a veto, not a shorter animation** — while `prefers-reduced-motion: reduce` is in force nothing animates at all, and the card says so rather than previewing something the interface will not do.
110
+ - **A preview swatch, because a wallpaper fading into itself is invisible** — replaying the effect on the wallpaper already on screen would show nothing, which is exactly the "I changed it and nothing happened" this plugin keeps designing out. The swatch replays the chosen effect with the duration a real switch would use right now, and picking a different effect replays it at once.
111
+ - **The interface colour changes on the same clock** — a theme colour is one rewrite of the token block, which used to change every surface in a single frame while the wallpaper faded. The alias tokens are registered as colours so they interpolate over this setting's duration and easing, including the surfaces that re-declare those tokens instead of inheriting them and the two the plugin paints inline. One deliberate exception: a **light↔dark flip switches instantly**, because that is a different palette rather than a tint of the same one, and interpolating between them walks the interface through mid-tones whose text is unreadable in both directions. The wallpaper keeps its own animation either way.
112
+ - **And it is not a slider** — the fade is armed only when the palette itself moved. A drag rewrites the same token block on every frame, and a live transition there would make the surface lag behind the pointer.
113
+
101
114
  ### Holiday backgrounds
102
115
 
103
116
  A small one, not a settings block: **one switch on the Config page and nothing else**. It is on out of the box, and on the day the background changes by itself and changes back the next morning.
@@ -135,7 +148,7 @@ Settings → **Theme** now has three tabs:
135
148
  | --- | --- | --- |
136
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 |
137
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) |
138
- | **Config** | — | Import / export the whole rule set (`dsh-background-by-model-theme.json`), plus the single [holiday background](#holiday-backgrounds) switch |
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 |
139
152
 
140
153
  The old **Color** tab is gone — the theme color is now a property of each rule. The old **Background** tab became **Model Background**.
141
154
 
@@ -145,7 +158,7 @@ Data directory: `~/.dsh/.dsh-background-by-model-data/` (Windows: `C:\Users\<you
145
158
 
146
159
  | File | Contents |
147
160
  | --- | --- |
148
- | `theme-config.json` | The rule list (each rule with its image list and rotation) plus the global Interface settings and the holiday overrides |
161
+ | `theme-config.json` | The rule list (each rule with its image list and rotation) plus the global Interface settings, the global switch effect and the holiday overrides |
149
162
  | `modelbg-<slot>` | One image, stored as raw bytes without a file extension — one file per image, so a rule with three images owns three slots |
150
163
  | `modelbg-h-midautumn` / `modelbg-h-nationalday` | Not used: both holiday wallpapers ship inside the package and are served straight out of it |
151
164
 
@@ -199,13 +212,14 @@ To mount a working copy into a profile instead, add it to the profile's `package
199
212
 
200
213
  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.
201
214
 
202
- `pnpm test` builds, typechecks and runs five checks (`tsdown && tsc -p tsconfig.json && node scripts/holiday-check.ts && node scripts/rotation-check.ts && node scripts/repaint-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:repaint` / `pnpm check:ui` / `pnpm check:node`:
215
+ `pnpm test` builds, typechecks and runs six 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/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:ui` / `pnpm check:node`:
203
216
 
204
217
  - **`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.
205
218
  - **`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.
219
+ - **`check:transition`** — the switch effect's decisions (`resolveFadeMs` / `transitionPlan`), which live in `src/client/transition.ts` and are the ones that fail without a sound: that the shipped default still produces **exactly** the transition string earlier releases wrote (`opacity 320ms ease`, opacity only — an unchanged property must not be put on the transition list), that `Instant`, a `0` duration, a `0` unified duration and a non-numeric duration each mean a hard cut, that reduced motion vetoes rather than shortens, that every effect starts somewhere other than where it ends (the "I picked the new effect and nothing happened" failure), and that every effect lands fully visible and **unscaled** — a zoom that never resets would crop every later wallpaper.
206
220
  - **`check:repaint`** — the decision that says whether an edit has to repaint the interface (`shouldRepaint`), including the case that kept failing in the field: an edit that **creates** the winner (a rule's first picture, its first or auto-extracted color) must repaint even though the edited rule was not the active one yet. It also fails if any of the write paths in `src/client/index.tsx` stops asking — so re-introducing the old `id === activeRuleId` test breaks the build's checks instead of the user's next upload — and if a rotation step stops re-emitting the interface palette (a theme color belongs to the image, so a step changes it).
207
221
  - **`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, and no dictionary entry has gone unreferenced.
208
- - **`check:node`** — the built node half driven through its real RPC handler with `DSH_HOME` redirected to a throwaway directory: the holiday block through the shared sanitizer, the packaged wallpapers served from an empty slot (without being copied into the data directory), a holiday slot refusing writes, deletes and URL fetches while a file dropped into it is ignored, an ordinary rule slot still writing, listing and deleting, a hand-edited config unable to redirect a holiday at another rule's image, and 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.
222
+ - **`check:node`** — the built node half driven through its real RPC handler with `DSH_HOME` redirected to a throwaway directory: the holiday block through the shared sanitizer, the packaged wallpapers served from an empty slot (without being copied into the data directory), a holiday slot refusing writes, deletes and URL fetches while a file dropped into it is ignored, 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.
209
223
 
210
224
  ## FAQ
211
225
 
@@ -251,9 +265,23 @@ Pressing it means leaving the system-theme state, so it has to hand out a starti
251
265
  **After switching back to "System theme" the interface stays light until I drag a slider — why?**
252
266
  The other half of the same hole: while it owns a color the plugin **forces** `body[data-ds-dark-theme]` (setting its marker for a dark palette, removing the attribute for a light one), and the host only rewrites that flag when it projects a **theme snapshot** — it does not watch the attribute. A flag dropped by a light skin therefore sticks, so the readback captured the host's light palette at the moment the color was cleared and nothing ever re-read it until an unrelated apply ran. Now the switch hands the flag back synchronously in the host's own boolean form, the scheme the host resolved (`ctx.theme`'s `active.colorScheme`, falling back to `html[data-ds-theme-source]` plus `prefers-color-scheme`) is pushed into the render layer, and the one-second watchdog re-reads the host palette while a rule has no color, repainting when it moved.
253
267
 
268
+ **I picked a new switch effect and the wallpaper still cuts instantly.**
269
+ Two things can hold it, and both are on the card: the effect is **Instant**, or the duration is `0` (an effect and a duration are two ways of asking for the same hard cut, and the effect wins). A third one is outside the plugin: if your system asks for **reduced motion**, nothing animates at all and the card says so.
270
+
254
271
  ## Recent Optimizations
255
272
 
256
- > Only the two most recent releases are listed here; older ones (v0.7.0 and earlier) live in [CHANGELOG.md](./CHANGELOG.md).
273
+ > Only the two most recent releases are listed here; older ones (v0.7.1 and earlier) live in [CHANGELOG.md](./CHANGELOG.md).
274
+
275
+ ### v0.7.3
276
+
277
+ - **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.
278
+ - **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.
279
+ - **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.
280
+ - **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.
281
+ - **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.
282
+ - **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.
283
+ - **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.
284
+ - **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`.
257
285
 
258
286
  ### v0.7.2
259
287
 
@@ -263,17 +291,6 @@ The other half of the same hole: while it owns a color the plugin **forces** `bo
263
291
  - **Every image of a batch upload gets its own color** — the extraction was addressed by POSITION (it only asked about `rule.images[length - 1]`), which is right for a one-file upload and silently wrong for a batch: the pictures before the last kept no color of their own and stayed on the system theme. It is now addressed by the slots the batch itself just appended, so which pictures get themed depends on neither where they landed nor which one the card selects.
264
292
  - **Copy trimmed** — the "Rules" heading, its priority sentence and the automatic-extraction sentence are gone (the panel's title and the `fallback` badge already say both), `Add images` / `Replace this image` / `Remove image` lose the noun they were repeating, and `Edit position` moved to the end of the layout row (it edits how the picture fills the area, and still appears in **fit** only).
265
293
 
266
- ### v0.7.1
267
-
268
- - **The theme color moved down to the image** — a rule is a rotation through several pictures, and pictures collected for different moods do not share one accent. Each image now carries its own `color`, and the wheel, the numeric inputs, the inspiration palette, **Extract from this image**, the eyedropper and **Follow system theme** all edit the image selected in the filmstrip. The controls say which image they are addressing and the strip marks the images that already have a color of their own, so "I changed it and nothing happened" cannot be the first thing this feature teaches.
269
- - **A rule's own color now means exactly one thing** — what a rule with **no image left** paints. It is not a default for the rule's pictures: an image with no color of its own follows the **system theme** rather than inheriting it, which is what makes a rotation through a green picture, a red one and a system-themed one expressible at all. Clearing one image's color therefore clears only that image, and the rule's color survives for the state where it is the only thing left to paint.
270
- - **Nothing loses its theme on upgrade** — an image entry written before this release has no `color` key at all, and that is the whole distinction the loader uses: a missing key means "never had a color of its own", so the rule's color is lifted onto it (once, in the shared sanitizer, so both halves lift identically); an explicit `null` means "cleared on purpose" and is left alone. The rule keeps its color as well, so emptying a themed rule still paints it.
271
- - **A rotation step re-emits the palette, and that is not a detail** — the step only repainted the wallpaper, so a per-image color would have appeared when *something else* re-ran an apply: dragging the color wheel, or switching models. That is the exact symptom this plugin already had once ("it only updates when I touch the color wheel"), so the palette apply is now one function called by both a model switch and every step of the rotation (the timer and the manual **Next image** button, which share one funnel).
272
- - **Holidays carry their palette per image too** — a festival's fixed color is written on the holiday *and* on its image and is forced from the definition on every read, so the shape is ready for a holiday that rotates through several pieces of art, each with its own palette, as a change to `HOLIDAYS` and its assets rather than a second code path in the render layer.
273
- - **A rule with no picture has exactly one upload target** — the big empty preview tile *is* the button now (click it, or drop files onto it), and the filmstrip is not rendered at all while the rule holds nothing: it contained nothing but a second "add image" tile, and a filmstrip is a list of pictures — a state with none does not have one. Nothing else moved: the labelled **Add images** / **From URL** buttons stay where they were, and with pictures present the preview is still deliberately inert (dropping on the strip adds, and *replace* stays an explicit labelled action, which is what keeps a preview from swallowing a click the user did not mean).
274
- - **The config shape moved to 4, and an older host still cannot eat a profile** — `images[].color` is unknown to a schema-3 sanitizer, which rebuilds each image entry from the keys it knows: the color would survive in memory until the next reload and then be gone, with nothing in the log to explain it. The version check is a plain `>=`, so a host announcing less holds every write and says so in the panel until DSH is restarted.
275
- - **The gates cover it** — `check:node` now pins the lift in both directions (an absent key inherits the rule's color, an explicit `null` does not), per-image independence through the file, holidays forcing both colors, and that a `null` is written as an explicit key rather than as an absent one (writing it as absent would silently re-inherit the rule's color on every load). `check:repaint` additionally fails if a rotation step ever stops re-emitting the palette.
276
-
277
294
  ## Star History
278
295
 
279
296
  [![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
 
@@ -78,7 +78,7 @@
78
78
  - **实时状态提示** — 「模型背景」页顶部会显示当前状态,例如「当前模型 deepseek-flash → 命中 · 规则 1」,方便当场验证规则写对没有;检测不到当前模型时会给出提示。
79
79
  - **外观随规则走,颜色随图走** — 壁纸(多张)、布局模式、透明度与模糊度是每条规则自己的属性;**取景和主题色则是每张图片自己的**。除「界面」页外没有任何全局外观项。
80
80
  - **一条规则多张图** — 每条规则持有一组有序图片,用胶片条展示:可一次加多张(多选文件、拖放,或一行一个网址批量粘贴)、排序、把某张设为第一张、**原地替换某一张**(替换会保住它在轮换里的位置)、逐张删除。不轮换时规则画的就是**第一张**。删掉最后一张只是让这条规则变空,而空不等于失效:只要它还留着自己的主题色,它就照旧参与匹配与兜底、只给界面配色而不铺壁纸;连主题色也没有(比如刚新增的规则)才会被匹配跳过。空规则是一个看得见、退得回的状态,而不是卡住出不来的死胡同。一张图都没有时胶片条会隐藏、上方那块大预览**就是**上传按钮,所以添加图片的地方只有一个、也很显眼。
81
- - **导入 / 导出** — 一键把**每一条规则连同它的全部图片**导出为 `dsh-background-by-model-theme.json`(版本 5,图片以 base64 内联),可随时导入还原。更早版本导出的文件同样能导入:规则唯一的那张图会被还原成它的图片列表,而在「主题色属于图片」之前写下的颜色会被抬升到每一张还没有自己颜色的图上——所以旧主题文件还原出来的观感与当初一模一样。
81
+ - **导入 / 导出** — 一键把**每一条规则连同它的全部图片**导出为 `dsh-background-by-model-theme.json`(版本 6,图片以 base64 内联),可随时导入还原。更早版本导出的文件同样能导入:规则唯一的那张图会被还原成它的图片列表,在「主题色属于图片」之前写下的颜色会被抬升到每一张还没有自己颜色的图上,而在[切换效果](#切换效果全局)之前写下的文件会拿到这项设置的出厂默认——所以旧主题文件还原出来的观感与当初一模一样。
82
82
  - **文件持久化** — 所有设置与图片保存到文件系统 `~/.dsh/.dsh-background-by-model-data/`,不依赖 `localStorage`。
83
83
  - **自动迁移** — 旧版单壁纸配置在首次读取时自动升级,详见 [CHANGELOG.zh.md 的 v0.3.0 条目](./CHANGELOG.zh.md#v030)。
84
84
  - **中英双语** — 完整的中英文界面,自动跟随语言设置。
@@ -98,6 +98,19 @@
98
98
  - **取景是逐图的,颜色也是** — 构图和配色都属于某一张图:每张图各自保留自己的取景与主题色,同时共享规则的布局、透明度与模糊度。胶片条会标出哪些图已经各自有颜色,调色区上方也会写明当前编辑的是哪一张。
99
99
  - **只加载用得到的图** — 启动时只读每条规则的**第一张**,其余的在展开卡片时、或者在轮换即将用到它时再读。所以「五条规则各十张图」不等于首帧之前先传五十张壁纸。
100
100
 
101
+ ### 切换效果(全局)
102
+
103
+ 壁纸**发生变化**时长什么样。它放在 **「配置」** 页,对所有规则是同一个(规则本身没有「切换效果」这个概念),并且覆盖壁纸变化的所有来源:切模型、[轮换](#多图轮换)换图、手动「下一张」,以及[节日](#节日特殊背景)接管。
104
+
105
+ - **四种效果** — **淡入淡出**(此前每个版本都在用的混合方式,因此是默认)、**直接切换**(硬切)、**缩放淡入**(新图从大 6% 收拢到位)、**平移推入**(新图从侧向推入)。缩放与平移动的是 `transform`,壁纸层此前从未用过它,所以两者都不增加一个字节的传输。
106
+ - **缓动** — 标准 / 匀速 / 先快后慢 / 两端慢。效果为**「直接切换」**时这一行不渲染:硬切没有曲线可调,而一个什么都不做的控件比没有这个控件更糟。
107
+ - **所有切换共用一个时长** — `0`–`3000 ms`,而 `0` 就是硬切,不会被当成「没设置」。它取代了每条规则的 `rotate.fadeMs`:那个字段从 0.7.0 起就**没有任何界面能改**(只能手改配置),所以手改过的值会在旧配置第一次被读取时折进这里(等于出厂 320ms 的值会被忽略,普通配置照旧拿默认值)。
108
+ - **「直接切换」和 0 秒是同一种硬切** — 同一个诉求的两种说法,所以两者都照做,而不是把其中一个读成「没设置」。
109
+ - **「减少动态效果」是否决而不是缩短动画** — 系统处于 `prefers-reduced-motion: reduce` 时完全不做动画,并且卡片会直说,而不是预览一个界面根本不会执行的效果。
110
+ - **有一个预览块,因为壁纸淡入到它自己身上是看不见的** — 拿已经在屏上的壁纸回放效果,什么都看不到,而这正是本插件一直在设计上规避的「我改了但什么都没发生」。预览块用**此刻真实切换会用的时长**回放所选效果,换一个效果也会立刻重放一次。
111
+ - **界面配色走同一个时钟** — 一次主题色变化就是一次 token 块重写,以前它会让所有表面在一帧里跳完,而壁纸却在淡入。现在这些 alias token 被注册成颜色、按本设置的时长与缓动插值,包括那些不继承而是自己重声明这些 token 的面,以及插件内联绘制的那两处。有一个刻意的例外:**浅↔深互切立即切换**——那是换了一整套调色板而不是同一套的色调变化,在两者之间插值会让界面经过一段两个方向的字色都读不清的中间态。壁纸不受影响,照旧有动画。
112
+ - **它不是滑块** — 只有调色板真的变了才武装渐变。拖动滑块会在每一帧重写同一个 token 块,那里若挂着过渡,表面就会滞后于指针。
113
+
101
114
  ### 节日特殊背景
102
115
 
103
116
  小小一个,不是一整块设置:**「配置」页上一个开关,别的什么都没有**。它默认就是开的;到了日子背景自己换,第二天早上自己换回。
@@ -135,7 +148,7 @@
135
148
  | --- | --- | --- |
136
149
  | **界面** | 全局,所有模型共用 | 主背景、左侧栏、右侧栏、卡片面板、输入框与控件、设置面板、对话文本框、轨迹页的透明度与模糊度 |
137
150
  | **模型背景** | 每条规则 | 有序规则列表、顶部实时状态,以及每条规则的图片(胶片条)、**逐图主题色**、布局模式、逐图取景、透明度、模糊度与[多图轮换](#多图轮换) |
138
- | **配置** | — | 导入 / 导出整套规则(`dsh-background-by-model-theme.json`),外加[节日特殊背景](#节日特殊背景)那一个开关 |
151
+ | **配置** | — | 全局[切换效果](#切换效果全局)、导入 / 导出整套规则(`dsh-background-by-model-theme.json`),外加[节日特殊背景](#节日特殊背景)那一个开关 |
139
152
 
140
153
  原先的 **「色彩」** 选项卡已删除——主题色改为每条规则自己的属性;原 **「背景」** 选项卡已改造成 **「模型背景」**。
141
154
 
@@ -145,7 +158,7 @@
145
158
 
146
159
  | 文件 | 内容 |
147
160
  | --- | --- |
148
- | `theme-config.json` | 规则列表(每条规则含图片列表与轮换设置)+ 全局「界面」设置 + 节日覆盖配置 |
161
+ | `theme-config.json` | 规则列表(每条规则含图片列表与轮换设置)+ 全局「界面」设置 + 全局切换效果 + 节日覆盖配置 |
149
162
  | `modelbg-<slot>` | 一张图片,保存为原始字节,无扩展名 —— 一张图一个文件,所以一条有三张图的规则占三个槽位 |
150
163
  | `modelbg-h-midautumn` / `modelbg-h-nationalday` | 不使用:两张节日壁纸都随包发布,直接从包内送出 |
151
164
 
@@ -199,13 +212,14 @@ pnpm run typecheck
199
212
 
200
213
  改完 `src/` 必须重新执行 `pnpm run bundle` 才会生效——挂载的 profile 加载的是 `lib/`。
201
214
 
202
- `pnpm test` 会构建、类型检查并跑五道校验(`tsdown && tsc -p tsconfig.json && node scripts/holiday-check.ts && node scripts/rotation-check.ts && node scripts/repaint-check.ts && node scripts/ui-strings-check.ts && node scripts/node-half-check.mjs`)。五道都不需要依赖、不需要转译器、也不需要浏览器,也可以单独跑:`pnpm check:holiday` / `pnpm check:rotation` / `pnpm check:repaint` / `pnpm check:ui` / `pnpm check:node`。
215
+ `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/ui-strings-check.ts && node scripts/node-half-check.mjs`)。六道都不需要依赖、不需要转译器、也不需要浏览器,也可以单独跑:`pnpm check:holiday` / `pnpm check:rotation` / `pnpm check:transition` / `pnpm check:repaint` / `pnpm check:ui` / `pnpm check:node`。
203
216
 
204
217
  - **`check:holiday`** — 两个节日窗口对照已知日期、北京时间的跨日边界(23:59 与 00:01)、1900–2100 年间的闰八月,以及 `pickHoliday` 允许节日接管背景之前必须通过的四道闸门。
205
218
  - **`check:rotation`** — 轮换的判定(`nextIndex` / `isRotating`):不会走越界、随机不会重复当前这张(在整个随机取值范围内穷举)、也不会把只有一张图的规则报成「正在轮换」。
219
+ - **`check:transition`** — 切换效果的判定(`resolveFadeMs` / `transitionPlan`),代码在 `src/client/transition.ts`,正是那些不报错、只是「看起来没生效」的部分:出厂默认仍然必须产出**与旧版本逐字符相同**的过渡串(`opacity 320ms ease`,且只动 opacity——没有变化的属性不能被写进过渡列表);`直接切换`、时长为 `0`、统一时长为 `0`、以及时长不是数字,四者都必须等于硬切;「减少动态效果」是否决而不是缩短;每个效果的起点都必须与终点不同(也就是「换了新效果却什么都没发生」);每个效果结束时都必须完全可见且**没有残留缩放**——一次没有复位的 zoom 会让之后每一张壁纸都被放大裁掉一圈。
206
220
  - **`check:repaint`** — 「这次编辑要不要重画界面」的判定(`shouldRepaint`),其中就包含实际踩过的那个坑:**这次编辑本身**制造出了生效规则(某条规则的第一张图、第一个颜色或自动提取出的颜色)时,即使被编辑的规则此刻还不是生效规则,也必须立刻重画。它同时还会在 `src/client/index.tsx` 里任何一条写入路径不再询问这个判定时报错——也就是说,把老的 `id === activeRuleId` 判断写回去,会是构建校验失败,而不是你下一次上传时界面不动;轮换步骤不再重发调色板(主题色属于图片,换图就是换配色)同样会被它拦下。
207
221
  - **`check:ui`** — 每个 `t('…')` 键在中英两本字典里都存在、两本字典键集完全一致、每个 `dab-…` 类在样式表里都有规则,且没有字典条目变成没人引用的死键。
208
- - **`check:node`** — 把构建好的 node 半边通过它真实的 RPC 处理器跑一遍,`DSH_HOME` 指向临时目录:节日配置块穿过共享的 sanitizer、内置壁纸从空槽位被送出(且不会因此写进数据目录)、节日槽位拒绝写入 / 删除 / 抓取而丢进去的文件被忽略、普通规则槽位照常写入和删除,以及手改过的配置无法把节日指向别的规则的图片。它还覆盖逐图主题色这套形状:读取时抬升的两个方向(缺键继承规则色、显式 `null` 不继承)、颜色经过文件往返后各自独立,以及 `null` 是写出键而不是省略。
222
+ - **`check:node`** — 把构建好的 node 半边通过它真实的 RPC 处理器跑一遍,`DSH_HOME` 指向临时目录:节日配置块穿过共享的 sanitizer、内置壁纸从空槽位被送出(且不会因此写进数据目录)、节日槽位拒绝写入 / 删除 / 抓取而丢进去的文件被忽略、普通规则槽位照常写入和删除,以及手改过的配置无法把节日指向别的规则的图片。它还覆盖逐图主题色这套形状:读取时抬升的两个方向(缺键继承规则色、显式 `null` 不继承)、颜色经过文件往返后各自独立,以及 `null` 是写出键而不是省略。切换效果同样在覆盖范围内:旧配置拿到的出厂默认、选定的效果 / 缓动 / 统一时长能原样穿过文件往返、非法值回退而夸张时长被夹紧、时长为 `0` 被保留而不是回退成默认,以及 `transition` 内部的字段漂移会被上报而不是悄悄丢掉。
209
223
 
210
224
  ## 常见问题
211
225
 
@@ -251,9 +265,23 @@ pnpm run typecheck
251
265
  **为什么「自定义颜色」点下去就有颜色了?**
252
266
  因为按下它就等于离开「跟随系统主题」,必须给出一个起始色。它先跑一次「从本图提取」(和「从本图提取」按钮同一条链路),取不到鲜明颜色时才退回默认色,所以不会再出现「点一下就从浅色跳成一块跟壁纸无关的深蓝」。
253
267
 
268
+ **我换了新的切换效果,壁纸还是直接跳过去。**
269
+ 有两件事会拦住它,而且两件都在那张卡上:效果是**「直接切换」**;或者时长是 `0`(效果和时长是同一个硬切的两种说法,而效果优先)。第三件不在插件里:系统开了**「减少动态效果」**时完全不做动画,卡片也会直说。
270
+
254
271
  ## 近期优化
255
272
 
256
- > 这里只列最近两个版本;更早的版本(v0.7.0 及以前)见 [CHANGELOG.zh.md](./CHANGELOG.zh.md)。
273
+ > 这里只列最近两个版本;更早的版本(v0.7.1 及以前)见 [CHANGELOG.zh.md](./CHANGELOG.zh.md)。
274
+
275
+ ### v0.7.3
276
+
277
+ - **切换效果现在是一个设置:四种效果、一种缓动、一个时长,都在「配置」页上** —— 这个插件做的每一次换图(切模型、轮换一步、手动「下一张」、节日接管)此前都走同一个写死的交叉淡入,而它的时长读的是一个没有任何控件能碰到的字段。**淡入淡出**、**直接切换**、**缩放淡入**、**平移推入**都作用在本来就有的那两层上 —— 缩放与平移只在进来的那一层上用 `transform`,而壁纸层从来没用过这个属性,所以两者都不花一个字节的额外传输 —— 「直接切换」与 `0` 秒是同一个硬切,系统的「减少动态效果」是**否决**而不是把动画缩短。
278
+ - **卡片上带一个预览色块,而它不是装饰** —— 壁纸淡入它自己是什么都看不见的,所以「在已经在屏的那张图上重放效果」会什么都不显示:这是这个功能唯一可能「明明在工作却看起来坏了」的方式。色块按此刻一次真实切换会用的时长重放选中的效果。
279
+ - **时长只有一个,「跟随每条规则」删掉了** —— 那个选项指向一个界面设不了的值(`rotate.fadeMs` 从 0.7.0 起就在格式里,却从来没有控件能到达它),所以它是在一个真设置和一个幻影设置之间做选择。滑块本身就是那个设置。手工编辑过的配置不会被无声丢掉:`legacyFadeMs` 在第一次读取时把找到的第一个「有意为之」的值折进全局时长(等于出厂值 320 ms 的忽略,显式的 `durationMs` 胜出),该字段也因「正在被迁移、而不是在漂移」而豁免漂移警告。
280
+ - **界面配色现在和壁纸走同一个时钟** —— 换一个主题色本质是一次 `body{--dsw-alias-…}` 的重写,而它以前在一帧之内改掉每个表面、背后的图却还在淡入淡出。这个渐变是**注册**问题(未注册的自定义属性无法插值),所以插件发出把别名令牌变成 `<color>` 的 `@property` 规则,外加一条 `transition`,作用于自身声明会随调色板变化的那些元素 —— `body`、设置弹窗与轨迹页根节点。它**内联**绘制的两处表面(三栏列、对话/轨迹卡片)根本不读这些令牌,所以把同一个时长直接挂在 `background-color` 上。时长、缓动与两项否决都来自壁纸用的**同一个** `transitionPlan`,而且只在**调色板确实变了**的时候武装 —— 其它每一次 apply(拖滑块改的是 alpha)写入的样式表不带过渡、内联写 `none`,因为那里挂一条活着的过渡会让表面跟不上指针。
281
+ - **浅↔深互切是立即切换,壁纸照旧做动画** —— 在两套调色板之间插值会让界面走过中间调,而那里的墨色**两个方向都是错的**,所以浅↔深恰恰是渐变会让事情变糟的那一种情况。渐变只在**配色深浅不变**时武装(浅→浅、深→深);壁纸刻意不受这条规则约束:一张图淡入另一张图没有「深浅」可以搞错。
282
+ - **墨色现在是同步写入的,而那是一个真实存在、且只有一个写入者的 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 却仍然武装着、把此后每一次更新都堵在它后面,写入内部出错则什么都不说。现在调色板一旦移动,就在**与它必须保持一致的表面同一个任务里**同步写入;帧路径留给它本来要做的事(合并一次滑块拖动),积压超过一秒的帧会被丢掉而不是永久阻塞,写入者也会把失败记进日志而不是吞掉。
283
+ - **配置格式升到 5、导出格式升到 6** —— schema 4 的 sanitizer 会**按自己认识的键重建**配置,所以一个旧宿主进程会在刷新后的第一次写入时丢掉效果、缓动与时长,悄无声息,宿主日志里也什么都没有。公布的 `schema` 随之前进,「扣住写入」分支会拦住这样的宿主,`warnUnknownConfigKeys` 也出于同样的理由往深一层比较固定形状的 `transition` 块。
284
+ - **门禁** —— `check:transition` 加入 `pnpm test`,现在是**六道**;`check:repaint` 钉住这个墨色 bug 由之构成的三行(调色板变化时同步写入、不许吞掉失败、在令牌与内联两个调用点都把渐变问题的两半问全);`check:node` 覆盖出厂默认值、往返、恶意取值、被保留的 `0` 时长、漂移检查与旧值折算;`check:ui` 覆盖新文案与新类,并把胶囊标签登记进 `TABLE_KEYS`。
257
285
 
258
286
  ### v0.7.2
259
287
 
@@ -263,17 +291,6 @@ pnpm run typecheck
263
291
  - **一次上传的多张图,每张都会各自取色** — 取色原本是**按下标**寻址的(只问 `rule.images[length - 1]`),对单张上传正确、对批量静默地错:最后一张之前的图没有自己的颜色、留在系统主题上。现在按**这一批自己刚追加的槽位**寻址,与它们落在哪个位置、卡片选中了哪张都无关。
264
292
  - **卡片文案精简** — 「规则列表」标题、优先级那句、自动取色旁边那句解释都删了(面板标题与 `兜底` 徽章已经说过);`添加图片` / `替换这张` / `移除图片` 收成 `添加` / `替换` / `移除`;`编辑位置` 移到**布局模式那一行的末尾**(它编辑的是图片如何填满这块区域,仍只在「适应」下出现)。
265
293
 
266
- ### v0.7.1
267
-
268
- - **主题色下放到图片** — 一条规则是一组图片的轮换,而这些图往往是为不同心情收的,不该共用同一个强调色。每张图现在带自己的 `color`,色轮、数值输入、灵感色板、「从本图提取」、吸管和「跟随系统主题」全都作用于**胶片条里选中的那张**。控件上方写明当前作用于哪张图,胶片条也标出哪些图已经各自有颜色,所以「我改了却没反应」不会是这个功能教给你的第一件事。
269
- - **规则自己的颜色只剩一个含义** — **一张图都没有时它画什么**。它不是该规则图片的默认值:没有自己颜色的图**跟随系统主题**,而不是去继承规则色——正是这一点才让「绿的、红的、跟随系统」这样一组轮换成立。所以清空某张图的颜色只影响那一张,而规则色会留在「只剩它能画」的那个状态里等着。
270
- - **升级不会丢主题色** — 这个版本之前写下的图片条目**根本没有** `color` 键,而这个区别正是加载器的全部依据:缺键 = 「从来没有自己的颜色」,于是规则色被抬升到它身上(只做一次,且在共享 sanitizer 里做,两半边行为一致);显式的 `null` = 「主动清空过」,原样保留。规则自身也留着那个颜色,所以把有主题色的规则清空后它照样能画。
271
- - **轮换每一步都会重发调色板,这不是细节** — 换图原本只重画壁纸,于是「逐图主题色」会等到**别的**事情顺手重跑一次 apply 才出现:拖动调色盘,或切换模型。这正是这个插件已经踩过一次的现象(「更新的不太及时,只有我动调色盘或者切模型的时候才出现」),所以调色板的施加被收成一个函数,模型切换和轮换的每一步(定时器与手动「下一张」共用一个出口)都走它。
272
- - **节日配色同样逐图** — 节日的固定配色同时写在节日条目和它的图片上、每次读取都由定义强制覆盖,于是「节日多图轮换、每张带自己的配色」只需要改 `HOLIDAYS` 和它的素材,而不需要在渲染层再开一条代码路径。
273
- - **没有图片的规则只剩一个上传入口** — 上方那块大预览**本身就是按钮**了(点它上传,也可以把文件拖上去),而胶片条在一张图都没有时**完全不渲染**:它里面除了一块「添加图片」再没有别的东西,而胶片条是一组图片的列表,一张图都没有的状态下它没有存在的理由。其它都没动:带文字的 **添加图片** / **从网址** 按钮还在原位;有图片时预览依旧刻意保持惰性(拖到胶片条是新增,「替换这张」仍是显式的带文字按钮——正是这一点让预览不会吞掉用户无意的点击)。
274
- - **配置格式升到 4,旧宿主依旧吃不掉配置** — `images[].color` 对 schema 3 的 sanitizer 是未知字段,而它会**按自己认识的键重建**每个图片条目:颜色会一直在内存里活到下一次重新加载,然后悄无声息地消失,日志里还什么都不会说。版本判断是简单的 `>=`,所以报出更低版本的宿主会让所有写入被扣住、面板里说明,直到重启 DSH。
275
- - **门禁覆盖到它** — `check:node` 现在双向钉住抬升(缺键继承规则色、显式 `null` 不继承)、逐图颜色经过文件往返后各自独立、节日两处颜色都被强制,以及 `null` 是**写出键**而不是省略键(写成省略就是每次加载都悄悄把规则色继承回来)。`check:repaint` 还会在轮换步骤不再重发调色板时报错。
276
-
277
294
  ## Star History
278
295
 
279
296
  [![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)