Files
kindle-calendar/dash/docs/theme-switching-plan.zh.md
2026-03-17 10:37:27 +08:00

676 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Kindle Dashboard 多主题与方向方案
## 0. 当前实现状态2026-03-16
以下能力已经落地,不再只是设计目标:
- `calendar/` 网站已支持 `theme + orientation` 两维预览
- `mode=background` 下不会显示预览菜单,避免污染截图
- `calendar/` 已产出:
- `dist/themes.json`
- `dist/themes/<theme-id>.json`
- `dist/themes/<theme-id>/<orientation>/kindlebg.png`
- 兼容输出仍保留:
- `dist/kindlebg.png`
- `dist/clock-region.json`
- Kindle 已支持:
- 拉取 `themes.json` 和主题级 JSON
- 通过 `/mnt/us/dashboard/switch-theme.sh <theme-id> [orientation]` 立即切换
- 优先使用本地已同步的主题背景图
- 优先使用本地已同步的主题索引和主题 JSON
- 本地没有对应背景图时,再回退到远端 `background.url`
- 同步脚本 `scripts/sync-layered-clock-to-kindle.sh` 会先批量导出全部主题背景,再把 `/mnt/us/dashboard/themes/` 同步到设备
### 0.1 横向主题的抓屏评审约定
`fbgrab` 抓到的是 Kindle 的原始 framebuffer 图,尺寸始终是纵向的 `1072 x 1448`
这意味着:
- `portrait` 主题下,`physical.png``raw.png` 相同
- `landscape` + `logo_right` 主题下,日常评审应优先看 `physical.png`
- `raw.png` 只用于排查原始 framebuffer 坐标
- `physical.png` 等于把 raw 图按设备实际摆放方向逆时针旋转 90 度后的结果
仓库里已经补了一个辅助脚本:
```sh
sh scripts/capture-kindle-screen.sh kindle landscape-check
```
它会同时输出两张图:
- `tmp/landscape-check-raw.png`
- `tmp/landscape-check-physical.png`
其中:
- `raw.png` 用于排查 framebuffer 原始坐标
- `physical.png` 用于按 Kindle 实际摆放方向评审横向主题
## 1. 背景
当前链路已经稳定分成两部分:
- `calendar/` 负责渲染网页和导出背景图
- `dash/` 运行在 Kindle 上,负责拉取背景图并在本地重画时钟
这次方案先明确两个概念:
- `theme`:视觉主题,例如 `default``paper``classic`
- `orientation`:显示方向,例如 `portrait``landscape`
其中方向不是主题名的一部分,而是每个主题都必须包含的两套样式:
- `portrait`
- Kindle 竖向摆放
- 设备外部面板上的 `kindle` logo 在下方
- `landscape`
- Kindle 横向摆放
- 设备外部面板上的 `kindle` logo 在右侧
也就是说,后续系统模型应当是:
- 每个主题都包含纵向和横向两套显示样式
- Kindle 端仍然保持“拉背景图片 + 绘制时钟”的职责
- 时钟的位置、尺寸和绘制参数都由 `calendar/` 提供
## 2. 目标
本方案的目标是:
1. 访问网站时,可以切换主题和方向进行预览
2. 网站预览效果应尽量与 Kindle 最终显示效果一致
3. 当前背景生成流程继续可用,不因预览能力被破坏
4. `calendar/` 提供统一的主题清单 JSON作为主题与方向配置的单一真相源
5. Kindle 只需要读取固定位置的主题清单和主题配置 JSON
6. Kindle 切换主题或方向后,应立即拉取对应背景并刷新
7. 时钟的位置、尺寸和绘制参数由 `calendar` 的主题配置决定Kindle 不自行决定
非目标:
- 不在 Kindle 上重新渲染整张页面
- 不让 Kindle 维护主题路径规则
- 不让 Kindle 自行推导时钟区域布局
- 不要求第一阶段就做“本地表盘素材包”体系
## 3. 设计原则
### 3.1 `calendar/` 是单一真相源
主题系统的单一真相源放在 `calendar/` 侧。
建议由 `calendar/` 产出:
- 一个主题清单文件,例如 `themes.json`
- 每个主题自己的配置文件,例如 `default.json``paper.json`
- 每个主题在不同方向下的背景图资源
其中 `themes.json` 至少描述:
- 有哪些主题
- 每个主题叫什么名字
- 每个主题的配置 JSON 在哪里
- 默认主题是谁
- 默认方向是谁
这样做的好处是:
- 主题路径由 `calendar` 管理,不在 Kindle 端写死
- 新增主题时Kindle 无需改代码里的路径规则
- 网站预览和 Kindle 切换都依赖同一套主题元数据
### 3.2 `theme` 和 `orientation` 分开建模
不建议把“横向主题”“纵向主题”作为主题名本身。
推荐模型是:
- `theme`
- `orientation`
例如:
- `theme=default` + `orientation=portrait`
- `theme=default` + `orientation=landscape`
- `theme=paper` + `orientation=portrait`
这样更符合实际含义:
- `theme` 代表视觉风格
- `orientation` 代表设备摆放方向与对应布局
### 3.3 网站预览与导出解耦
主题和方向预览只是网站访问时的交互能力,不应破坏当前导出主流程。
当前已明确三条链路:
- 预览链路:用户访问 `/?mode=full&theme=<id>&orientation=<orientation>`,通过页面菜单切换主题和方向
- 兼容导出链路:`export:background` 继续生成默认背景图,保持兼容
- 批量导出链路:`export:themes` 一次性生成全部主题和方向背景
也就是说:
- 预览能力已经落地
- 当前 `kindlebg.png` 导出链路继续保留
- 多主题多方向导出已经接入
### 3.4 Kindle 侧最简化
Kindle 侧只保留最小能力:
- 拉取背景图
- 读取主题 JSON
- 根据当前方向读取对应 variant
- 按 JSON 提供的时钟参数绘制时钟
Kindle 不负责:
- 推导主题路径
- 决定时钟区域坐标
- 维护布局规则
推荐让 Kindle 只认识一个固定入口,例如:
```text
https://shell.biboer.cn:20001/themes.json
```
然后从这个 JSON 中知道:
- 当前有哪些主题
- 默认主题和默认方向是什么
- 每个主题的配置 JSON 在哪里
### 3.5 当前时钟实现已经足够轻量
当前 Kindle 侧时钟是本地几何绘制,不依赖外部表盘或指针素材文件。
这意味着第一阶段主题与方向切换只需要关注:
- 背景图 URL
- 时钟区域位置和尺寸
- 本地绘制时钟所需的参数
不需要先引入复杂的“主题素材包”机制。
## 4. 数据约定
### 4.1 主题清单 `themes.json`
建议由 `calendar/` 在固定位置提供主题清单,例如:
```json
{
"updatedAt": "2026-03-16T10:00:00+08:00",
"defaultThemeId": "default",
"defaultOrientation": "portrait",
"themes": [
{
"id": "default",
"label": "Default",
"configUrl": "https://shell.biboer.cn:20001/themes/default.json",
"orientations": ["portrait", "landscape"]
},
{
"id": "paper",
"label": "Paper",
"configUrl": "https://shell.biboer.cn:20001/themes/paper.json",
"orientations": ["portrait", "landscape"]
},
{
"id": "classic",
"label": "Classic",
"configUrl": "https://shell.biboer.cn:20001/themes/classic.json",
"orientations": ["portrait", "landscape"]
}
]
}
```
第一阶段建议先保留当前主题并命名为 `default`,然后新增:
- `paper`
- `classic`
### 4.2 主题配置 `<theme-id>.json`
每个主题提供一份主题级配置,内部包含两个方向的 variant例如
```json
{
"id": "default",
"label": "Default",
"variants": {
"portrait": {
"devicePlacement": "logo_bottom",
"background": {
"path": "themes/default/portrait/kindlebg.png",
"url": "https://shell.biboer.cn:20001/themes/default/portrait/kindlebg.png",
"refreshIntervalMinutes": 120
},
"clock": {
"x": 347,
"y": 55,
"width": 220,
"height": 220,
"faceRadiusRatio": 0.47,
"faceStroke": 3,
"tickOuterInset": 6,
"majorTickLength": 14,
"minorTickLength": 7,
"majorTickThickness": 4,
"minorTickThickness": 2,
"hourLengthRatio": 0.48,
"minuteLengthRatio": 0.72,
"hourThickness": 9,
"minuteThickness": 5,
"centerRadius": 7
}
},
"landscape": {
"devicePlacement": "logo_right",
"background": {
"path": "themes/default/landscape/kindlebg.png",
"url": "https://shell.biboer.cn:20001/themes/default/landscape/kindlebg.png",
"refreshIntervalMinutes": 120
},
"clock": {
"x": 795,
"y": 659,
"width": 220,
"height": 220,
"faceRadiusRatio": 0.47,
"faceStroke": 3,
"tickOuterInset": 6,
"majorTickLength": 14,
"minorTickLength": 7,
"majorTickThickness": 4,
"minorTickThickness": 2,
"hourLengthRatio": 0.48,
"minuteLengthRatio": 0.72,
"hourThickness": 9,
"minuteThickness": 5,
"centerRadius": 7,
"rotationDegrees": 90
}
}
}
}
```
说明:
- 背景图真实路径由主题配置提供,不在 Kindle 端拼接猜测
- `background.path` 是设备本地相对路径,用于优先读取已同步的本地背景
- 每个方向都有自己的背景图和时钟区域
- `landscape``clock` 已经是 Kindle 设备坐标,不再是网页坐标
- `landscape``rotationDegrees=90` 表示 Kindle 本地绘制时钟时,表盘和指针再顺时针旋转 90 度
- 时钟坐标和大小来自 `calendar` 中该主题该方向下的时钟占位,不允许 Kindle 任意改
- 后续如果某个主题在不同方向下需要不同绘制参数,也由该 JSON 一起提供
### 4.3 远端配置边界
远端主题配置只有两类:
- `themes.json`
- 每个主题自己的 JSON例如 `themes/default.json`
除此之外:
- 背景图属于资源文件,不属于主题配置
- Kindle 本地缓存属于运行时状态,不属于主题配置
### 4.4 Kindle 本地状态
Kindle 本地只需要保存少量状态:
```text
/mnt/us/dashboard/local/theme.env
/mnt/us/dashboard/local/state/themes.json
/mnt/us/dashboard/local/state/current-theme.json
/mnt/us/dashboard/local/state/theme-runtime.env
/mnt/us/dashboard/local/state/background-updated-at
```
其中:
- `theme.env` 记录当前 `THEME_ID``ORIENTATION`
- `themes.json` 是最近一次同步到本地的主题清单缓存
- `current-theme.json` 是当前主题配置缓存
- `theme-runtime.env` 是 Kindle 实际消费的运行时快照,包含背景路径和时钟参数
例如:
```sh
export THEME_ID=default
export ORIENTATION=portrait
```
### 4.5 Kindle 本地主题资源
当前实现中,主题背景图会随部署一起同步到 Kindle
```text
/mnt/us/dashboard/themes/default/portrait/kindlebg.png
/mnt/us/dashboard/themes/default/landscape/kindlebg.png
/mnt/us/dashboard/themes/paper/portrait/kindlebg.png
/mnt/us/dashboard/themes/paper/landscape/kindlebg.png
/mnt/us/dashboard/themes/classic/portrait/kindlebg.png
/mnt/us/dashboard/themes/classic/landscape/kindlebg.png
```
Kindle 拉背景时的规则是:
1. 先读 `theme-runtime.env` 中的 `BACKGROUND_PATH`
2. 如果本地对应文件存在,直接从 `/mnt/us/dashboard/themes/...` 拷贝
3. 如果本地不存在,再回退到 `BACKGROUND_URL`
主题元数据同步的规则是:
1. 先读本地 `/mnt/us/dashboard/themes.json`
2. 再读本地 `/mnt/us/dashboard/themes/<theme-id>.json`
3. 本地 bundle 不存在时,才回退到远端 URL
## 5. `calendar/` 侧方案
### 5.1 网站访问时增加主题与方向预览菜单
预览菜单只出现在网站访问场景,不出现在背景截图结果里。
建议行为:
1. 用户访问 `/?mode=full` 时,在页面顶部显示两个可选菜单
2. 一个菜单切换 `theme`
3. 一个菜单切换 `orientation`
4.`mode=background` 下不显示这些菜单,避免影响截图导出
建议 URL 形态如下:
- `/?mode=full&theme=default&orientation=portrait`
- `/?mode=full&theme=default&orientation=landscape`
- `/?mode=full&theme=paper&orientation=portrait`
### 5.2 预览效果与 Kindle 效果对齐
主题预览不能只是“网页版好看”,还需要尽量接近 Kindle 最终效果。
建议做到:
- 预览和导出使用同一套主题配置
- 预览和导出使用同一套方向布局配置
- Kindle 侧按主题 JSON 中当前方向的参数绘制,避免预览和实机错位
这样评审主题时,看到的页面效果和 Kindle 最终效果才是一致的。
### 5.3 现有导出流程保持兼容
当前默认导出链路继续保留:
```text
calendar/dist/kindlebg.png
calendar/dist/dashboard-manifest.json
calendar/dist/clock-region.json
```
兼容规则建议如下:
- 如果导出脚本没有显式传入 `theme`,默认导出 `default`
- 如果导出脚本没有显式传入 `orientation`,默认导出 `portrait`
- 现有 `export:background` 的行为保持不变
- 默认导出继续只产出 `default + portrait`
这样可以保证:
- 网站预览菜单先落地
- 当前生产链路不被打断
- Kindle 当前单主题纵向使用方式继续可用
### 5.4 多主题多方向导出已作为附加能力落地
当前 `export:themes` 会生成如下产物:
```text
calendar/dist/themes.json
calendar/dist/themes/default.json
calendar/dist/themes/default/portrait/kindlebg.png
calendar/dist/themes/default/landscape/kindlebg.png
calendar/dist/themes/paper.json
calendar/dist/themes/paper/portrait/kindlebg.png
calendar/dist/themes/paper/landscape/kindlebg.png
calendar/dist/themes/classic.json
calendar/dist/themes/classic/portrait/kindlebg.png
calendar/dist/themes/classic/landscape/kindlebg.png
```
这里的关键点是:
- 原有 `calendar/dist/kindlebg.png` 继续保留
- 多主题多方向目录和 `themes.json` 是新增能力,不是替换能力
- 主题和方向的路径管理由 `calendar` 决定,不在 Kindle 侧固化为固定模式
- 同步到 Kindle 时,会把整个 `calendar/dist/themes/` 目录一起带上
- `landscape` 的网页先按 `1448x1072` 渲染,再顺时针旋转 90 度,写成 `1072x1448` 的 Kindle 设备图
- 因此 `landscape``kindlebg.png` 是设备向产物,不是给桌面直接平铺预览的横图
## 6. Kindle 侧方案
### 6.1 主题发现方式
Kindle 只需要知道一个固定地址:
```text
themes.json 的 URL
```
推荐两种同步方式并存:
- 被动同步Kindle 每天拉取一次 `themes.json`
- 主动同步:用户进入主题切换流程时,先即时拉取最新 `themes.json`
- 如果设备上已经有本地同步的 `themes.json` 和主题 JSON本地 bundle 优先
这样能兼顾:
- 平时低频更新
- 切换主题时拿到最新主题清单
### 6.2 主题与方向切换流程
主题切换时建议执行以下流程:
1. 拉取最新 `themes.json`
2. 根据用户选择的主题 ID 找到对应 `configUrl`
3. 拉取该主题对应的 `<theme-id>.json`
4. 确定当前要使用的 `orientation`
5.`variants[orientation]` 里取出背景和时钟配置
6.`THEME_ID``ORIENTATION` 写入本地 `theme.env`
7. 将主题 JSON 缓存在本地
8. 优先读取本地 `themes/<theme>/<orientation>/kindlebg.png`
9. 如果本地没有对应背景,再回退拉取 `background.url`
10. 立即执行一次全屏刷新
11. 之后继续按当前分钟级时钟刷新逻辑运行
这里有两个关键要求:
- 主题或方向切换后,不等待下一个两小时周期
- 而是立即拉取一次对应的新背景
### 6.3 时钟绘制保持现有模型
当前 Kindle 时钟是本地几何绘制,已经满足主题切换第一阶段需求。
因此 Kindle 侧仍然保持:
- 拉背景图片
- 读取主题 JSON
- 读取当前方向的 variant
- 用本地 `lua + fbink` 画时钟
不需要在第一阶段增加:
- 表盘图片素材同步
- 指针图片素材同步
- 复杂的本地主题资源目录
如果未来某个主题确实需要独立表盘素材,再在第二阶段扩展。
### 6.4 切换入口
当前已经有一个可直接 SSH 调用的入口:
```sh
/mnt/us/dashboard/switch-theme.sh <theme-id> [orientation]
```
例如:
```sh
ssh kindle '/mnt/us/dashboard/switch-theme.sh default landscape'
ssh kindle '/mnt/us/dashboard/switch-theme.sh paper portrait'
```
如果本地刚改过页面样式,推荐先同步一次:
```sh
sh scripts/sync-layered-clock-to-kindle.sh kindle
```
KUAL 动态主题入口仍然是后续可选项,不是当前必需项。
如果后续要补 KUAL目标交互仍然可以保持
1. 用户点击“Theme”
2. 设备根据 `themes.json` 获取当前可用主题清单
3. 用户选择具体主题
4. 用户选择 `portrait``landscape`
5. 立即切换到该主题该方向
不管具体 UI 形态如何,数据契约都应保持一致:
- 主题列表来自 `themes.json`
- 具体切换行为来自所选主题对应的 `<theme-id>.json`
- 最终生效资源来自该主题该方向下的 variant
## 7. 分阶段落地与当前进度
### 第一阶段:网站主题与方向预览(已完成)
目标:
- 新增 `theme` 参数
- 新增 `orientation` 参数
- 增加顶部预览菜单
- 默认主题命名为 `default`
- 新增 `paper``classic`
- 每个主题都支持 `portrait``landscape`
结果:
- 用户可以在网站上预览三套主题的两种方向
- `mode=background` 不显示预览控件
- 现有截图导出流程保持不变
### 第二阶段:主题元数据发布(已完成)
目标:
- `calendar/` 产出 `themes.json`
- 每个主题产出自己的 `<theme-id>.json`
- 每个主题在两个方向下都有独立背景图 URL
结果:
- 主题列表、方向列表、时钟配置都由 `calendar/` 统一提供
- 主题配置里已同时提供 `background.path``background.url`
### 第三阶段Kindle 端主题与方向切换(已完成)
目标:
- Kindle 能拉取 `themes.json`
- Kindle 能按选择主题拉取对应的 `<theme-id>.json`
- Kindle 能切换 `portrait``landscape`
- 切换后立即拉取新背景并全屏刷新
结果:
- Kindle 端已具备多主题多方向切换能力
- 切换后会立即刷新
- SSH 命令切换已实机验证通过
### 第四阶段:按需扩展主题能力(未开始)
只有在未来确实出现需求时,再考虑:
- 为某些主题提供独立表盘素材
- 增加主题级本地资源同步
- 增加更复杂的切换交互
这一步不是当前必需项。
## 8. 风险与约束
### 8.1 预览和实机效果偏差
如果预览和导出用的不是同一套主题和方向配置,最终会出现:
- 网页看起来正确
- Kindle 上时钟错位或背景不一致
因此必须保证:
- 预览
- 导出
- Kindle 时钟绘制
都依赖同一套主题元数据和方向配置。
### 8.2 方向切换后的缓存问题
如果主题或方向切换后没有立即拉取新背景Kindle 可能继续显示旧缓存。
因此切换时必须同时处理:
- 更新 `THEME_ID`
- 更新 `ORIENTATION`
- 拉取新的 `<theme-id>.json`
- 拉取或读取当前方向对应的新背景图
- 重置背景更新时间戳或直接覆盖缓存背景
如果设备本地没有同步主题背景,而远端对应 PNG 也还没发布完成Kindle 可能拉到 HTML 或错误页而不是图片。
因此当前推荐做法是:
- 同步到 Kindle 时总是一起同步 `/mnt/us/dashboard/themes/`
- 设备优先使用本地主题背景
- 远端 `background.url` 只作为回退
### 8.3 主题清单缓存过旧
如果 Kindle 长时间只读本地缓存,可能看不到新主题或新方向配置。
因此建议:
- 日常低频同步一次 `themes.json`
- 用户进入切换流程时再主动刷新一次
## 9. 当前推荐结论
推荐采用下面这条路线:
1. 继续保留默认 `kindlebg.png` 导出链路,默认仍是 `default + portrait`
2. 同时保留 `export:themes` 批量导出,作为多主题切换的资源来源
3.`calendar` 继续维护 `themes.json` 和主题级 `<theme-id>.json`
4. 每个主题配置里同时提供 `portrait``landscape` 两套 variant
5. Kindle 继续读取固定位置的 `themes.json`
6. 切换主题或方向时优先使用本地同步的主题背景,并继续使用现有本地时钟绘制
这条路线的优点是:
- 不打断现有 `kindlebg.png` 生成流程
- 主题和方向都由 `calendar` 统一管理
- Kindle 侧实现最简化
- 预览和实机显示可以基于同一套配置对齐
- 即使远端主题图片还没发布完整SSH 主题切换也能工作
- 当前时钟实现不依赖表盘和指针素材,第一阶段改造成本低