跨端: 修「深色下玻璃变灰纸板」+「主题选了不生效」(两个真 bug)

用户问「你的玻璃效果呢?」。我上手先取像素,结果是**两个一直存在的真 bug**,
不是观感偏好问题。两个都有实测数据。

## ① 深色下玻璃 alpha 没跟着翻 →「像深色卡片浮在灰纸板上」

### 实测

深色主题 + aurora 壁纸,同一行交替采样(`/tmp/g1.raw`,1008×2232):

    y=670   卡片缝隙 rgb(199,199,199)   卡片内 rgb(27,37,50)
    y=880   卡片缝隙 rgb(201,203,201)   卡片内 rgb(27,36,53)

缝隙是**浅灰 199**、比卡片还亮。而 199 恰好 = `0.78 × 255`
(壁纸层实测 x=4..24 为 rgb(0,1,3),近黑)—— 就是 `glassCardWall` 那层白纱。

### 根因:我把 WebUI 的一句话读漏了后半句

`Theme.glassCard` / `glassCardWall` 是**单一值、不分深浅**。我当初写的理由是
「基色恒为白,主题之间只差 alpha」——但 WebUI 原话是
「**基材恒为白**,主题之间**只差 alpha**」(`index.css:406`、`:1546`)。
我只抄了前半句,把「只差 alpha」误读成「alpha 也不用变」。

WebUI `.dark` 的实际取值:

    --glass-card-a:      0.06     ← 浅色 0.92
    --glass-card-wall-a: 0.04     ← 浅色 0.78

**深浅差 15 倍**:深色下白纱要几乎撤掉让深壁纸透上来,浅色下才用厚白纱盖亮壁纸。
我用同一个 0.92 配两套主题 ⇒ 深色下壁纸被糊成浅灰,**玻璃感与深色同时消失**。

### 修法

`Theme.glassCardFor(wall, dark?)` —— 与 `accentFor`/`textSubtleFor` 同一范式,
深浅从 `Theme.isDarkNow()`(`AppStorage` 那个发布键)读,不靠调用方自觉传。
新增 `glassCardDark #0FFFFFFF` / `glassCardWallDark #0AFFFFFF`。

改后同处实测:缝隙 rgb(3..16),壁纸透上来了;浅色档回归检查未变(244/239,厚白纱仍在)。

## ② 用户选的主题被无条件覆盖 →「选了深色但界面还是浅的」

### 实测

「我的」页选「深色」后:
· 按钮显示选中、服务端也记下 `theme:'dark'`(`GET /me/appearance` 确认);
· 但界面仍浅色,且文字浅色压浅底 —— 实测背景 `rgb(243,243,243)` /
  文字 `rgb(243,243,243)`,**对比度 ≈1.0:1,完全不可读**。

### 根因(两处叠加)

① `EntryAbility.onCreate` 有一句**无条件**的
   `setColorMode(COLOR_MODE_NOT_SET)`(= 跟随系统)—— 每次冷启都把用户的选择重置掉。
   它是目录迁移时抄进来的(`git log -S` 指向 `f9d757b chore: directory migration`),
   **没有注释说明为什么,也没有谁在用**。已删除(连带上游 import)。

② `MainPage.applyAppearance()` 算出了 `isDarkNow`、发布了 `AppStorage`,
   但**从来没调用过 `store.applyTheme(...)`** ⇒ 系统色彩模式根本没被设过。

### 修法

· 删掉 EntryAbility 那句无条件覆盖(附长注释说明为什么由页面负责);
· `applyAppearance()` 补上 `store.applyTheme(ctx, snap.theme)`。

★ **顺序**:`KEY_IS_DARK` 发布必须在 `applyTheme` **之前** ——
  因为 `glassCardFor()` 是从那个键读深浅的,反过来会让卡片用上一轮的深浅画一帧。
  `applyThemeNow()` 里同一处顺序也一并修正。

## 判据

`cross-client-theme` 的两条原本把 `Theme.glassCard`/`glassCardWall` 两个**字面量**钉死,
被我这次正确的改动撞红 —— 我没有放宽它们,而是改成钉**行为**:
· 卡片必须经 `glassCardFor(...)` 取色(入口存在);
· 四个令牌都要在 Theme 里存在;
· **深色档不得与浅色档同值**(同值 = 主题感知是假的)。

变异验证:① 深色档改回与浅色同值 → 3 条红;② 卡片改回不分深浅 → 5 条红;还原 → 21/21 绿。

## 验证状态

✓ 两个修复都在设备上取**像素**验证(不是看截图说"像了")
✓ `cross-client-theme` 21/21、全量套件 545 pass(3 红均为改动前既有:
  `harmony-admin` 两处 = 我上一轮 `SecuritySection` 拆分遗留、`build-stamp` = 产物戳过期)
✓ 编译通过、进程存活、无新 jscrash
This commit is contained in:
2026-09-21 18:46:19 +08:00
parent e29f38b151
commit cbb1e1ee62
6 changed files with 286 additions and 19 deletions

View File

@ -3317,12 +3317,24 @@ struct MainPage {
const store: AppearanceStore = AppearanceStore.getInstance();
const snap: AppearanceSnapshot = store.current();
snap.theme = next;
this.isDarkNow = next === 'dark';
/*
* ★★ 2026-09-21 顺序修正:**先发布、再应用主题**。
*
* 原来这里 `store.applyTheme()` 在 `AppStorage.setOrCreate(KEY_IS_DARK, …)`
* **之前**。而 2026-09-21 起 `Theme.glassCardFor()` 是**从那个键读深浅**的
* (`Theme.isDarkNow()` → `AppStorage.get(KEY_IS_DARK)`)。
*
* 后果:`setColorMode` 会触发一轮重渲染,那一轮重渲染里
* `glassCardFor()` 读到的还是**旧**的深浅 ⇒ 卡片用错 alha 画一帧,
* 下一轮才纠正。切主题本来就慢(260ms 淡出),这一帧错色容易被看见。
*
* ⇒ 顺序固定为:算 → 发布 → 应用(含重渲染)。
*/
AppStorage.setOrCreate(this.KEY_IS_DARK, this.isDarkNow);
/* 本机先生效(不等人看着);服务端写失败只影响"换设备带不带着走" */
store.applyTheme(ctx, next);
store.saveLocal(ctx, snap);
this.isDarkNow = next === 'dark';
/* 侧栏的主题快捷键也要把新状态发布出去(否则窗格里的浅底不跟着换) */
AppStorage.setOrCreate(this.KEY_IS_DARK, this.isDarkNow);
const client: ApiClient = ApiClient.getInstance(ctx);
new AppearanceApi(client).put(snap, store.wallpaper !== null).catch(() => {});
}
@ -3425,6 +3437,42 @@ struct MainPage {
const dark: boolean = isDarkMode(snap.theme, systemMode);
/* 侧栏底部那个主题按钮的图标跟着它走(WebUI `ThemeToggleButton` 同口径) */
this.isDarkNow = dark;
/*
* ★★ 2026-09-21 **把用户存的主题真正应用上去**(这里原来漏了这一步)。
*
* ── 实测到的症状 ──
*
* 「我的」页选「深色」后:
* · 按钮显示为选中(`appearanceTheme` 来自存储,是 'dark');
* · 服务端也记下了 `theme:'dark'`(实测 `GET /me/appearance` 返回 dark);
* · 但**界面仍是浅色**,而且文字是浅色压浅底 —— 实测
* 背景 `rgb(243,243,243)` / 文字 `rgb(243,243,243)`,对比度 ≈1.0:1,完全不可读。
*
* ── 根因(两处叠加)──
*
* ① `EntryAbility.onCreate` 里有一句**无条件**的
* `setColorMode(COLOR_MODE_NOT_SET)`(= 跟随系统)—— 每次冷启都把应用色彩模式
* 重置回"跟系统走",用户的选择被丢掉。(那句是目录迁移时抄进来的,
* 没有任何注释说明为什么,也没有谁在用它。)
* ② 本函数算出了 `isDarkNow`、发布了 `AppStorage`,但**从来没有调用
* `store.applyTheme(...)`** ⇒ 系统色彩模式没被设过。
*
* ⇒ 两件事一起修:EntryAbility 那句删掉(改由这里按存储值决定),
* 本函数在算完之后**真的应用一次**。
*
* ★ 为什么放在这里而不是"每次启动都设一遍默认值":
* 主题是**用户偏好**,权威在 `snap.theme`(本机缓存 + 服务端)。
* 启动时不读它就是在丢用户的设置。
*
* ★ 幂等:`setColorMode` 设同一个值不会有什么副作用
* (`applyAppearance` 在启动、切账号、环境变化时都会被调用)。
*
* ★★ 顺序很要緊:必须**先发布 `KEY_IS_DARK`、再应用主题**。
* 下一条语句就是 `AppStorage.setOrCreate(this.KEY_IS_DARK, dark)`,
* 而 `Theme.glassCardFor()` 是从那个键读深浅的(`Theme.isDarkNow()`)。
* 反过来的话,本函数内的卡片会用**上一次**的深浅重渲染一帧。
* 所以这里的布局是:算 dark → 发布 dark → 应用主题(含重渲染)。
*/
/*
* 把"当前是不是深色"**发布出去** —— 各窗格(通信/日历/联系/我的)里
* 有若干"品牌浅底"(`Theme.accentSoft`),深色下必须换成深色变体。
@ -3433,6 +3481,13 @@ struct MainPage {
* MainPage 算 → 写 → 窗格 `@StorageProp` 读。
*/
AppStorage.setOrCreate(this.KEY_IS_DARK, dark);
/*
* ★★ 上面那段长注释说的就是这一行:把用户存的主题**真正应用上去**。
* 位置在 `setOrCreate(KEY_IS_DARK, dark)` **之后” ——
* 因为 `Theme.glassCardFor()` 是从那个键读深浅的,先发布再重渲染,
* 这一次重渲染才会用对深浅。
*/
store.applyTheme(ctx, snap.theme);
/*
* 侧栏头像的前两字:当前**活跃账号**的展示名(不是 `accountName`,
* 那是收件箱筛选器选的)。拿不到就让它空着 —— 侧栏会显示 `?`。