# Kindle Dashboard 多主题与方向方案 ## 0. 当前实现状态(2026-03-16) 以下能力已经落地,不再只是设计目标: - `calendar/` 网站已支持 `theme + orientation` 两维预览 - `mode=background` 下不会显示预览菜单,避免污染截图 - `calendar/` 已产出: - `dist/themes.json` - `dist/themes/.json` - `dist/themes///kindlebg.png` - 兼容输出仍保留: - `dist/kindlebg.png` - `dist/clock-region.json` - Kindle 已支持: - 拉取 `themes.json` 和主题级 JSON - 通过 `/mnt/us/dashboard/switch-theme.sh [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=&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 主题配置 `.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/.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. 拉取该主题对应的 `.json` 4. 确定当前要使用的 `orientation` 5. 从 `variants[orientation]` 里取出背景和时钟配置 6. 将 `THEME_ID` 和 `ORIENTATION` 写入本地 `theme.env` 7. 将主题 JSON 缓存在本地 8. 优先读取本地 `themes///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 [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` - 具体切换行为来自所选主题对应的 `.json` - 最终生效资源来自该主题该方向下的 variant ## 7. 分阶段落地与当前进度 ### 第一阶段:网站主题与方向预览(已完成) 目标: - 新增 `theme` 参数 - 新增 `orientation` 参数 - 增加顶部预览菜单 - 默认主题命名为 `default` - 新增 `paper` 和 `classic` - 每个主题都支持 `portrait` 和 `landscape` 结果: - 用户可以在网站上预览三套主题的两种方向 - `mode=background` 不显示预览控件 - 现有截图导出流程保持不变 ### 第二阶段:主题元数据发布(已完成) 目标: - `calendar/` 产出 `themes.json` - 每个主题产出自己的 `.json` - 每个主题在两个方向下都有独立背景图 URL 结果: - 主题列表、方向列表、时钟配置都由 `calendar/` 统一提供 - 主题配置里已同时提供 `background.path` 和 `background.url` ### 第三阶段:Kindle 端主题与方向切换(已完成) 目标: - Kindle 能拉取 `themes.json` - Kindle 能按选择主题拉取对应的 `.json` - Kindle 能切换 `portrait` 和 `landscape` - 切换后立即拉取新背景并全屏刷新 结果: - Kindle 端已具备多主题多方向切换能力 - 切换后会立即刷新 - SSH 命令切换已实机验证通过 ### 第四阶段:按需扩展主题能力(未开始) 只有在未来确实出现需求时,再考虑: - 为某些主题提供独立表盘素材 - 增加主题级本地资源同步 - 增加更复杂的切换交互 这一步不是当前必需项。 ## 8. 风险与约束 ### 8.1 预览和实机效果偏差 如果预览和导出用的不是同一套主题和方向配置,最终会出现: - 网页看起来正确 - Kindle 上时钟错位或背景不一致 因此必须保证: - 预览 - 导出 - Kindle 时钟绘制 都依赖同一套主题元数据和方向配置。 ### 8.2 方向切换后的缓存问题 如果主题或方向切换后没有立即拉取新背景,Kindle 可能继续显示旧缓存。 因此切换时必须同时处理: - 更新 `THEME_ID` - 更新 `ORIENTATION` - 拉取新的 `.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` 和主题级 `.json` 4. 每个主题配置里同时提供 `portrait` 和 `landscape` 两套 variant 5. Kindle 继续读取固定位置的 `themes.json` 6. 切换主题或方向时优先使用本地同步的主题背景,并继续使用现有本地时钟绘制 这条路线的优点是: - 不打断现有 `kindlebg.png` 生成流程 - 主题和方向都由 `calendar` 统一管理 - Kindle 侧实现最简化 - 预览和实机显示可以基于同一套配置对齐 - 即使远端主题图片还没发布完整,SSH 主题切换也能工作 - 当前时钟实现不依赖表盘和指针素材,第一阶段改造成本低