跳转至

状态栏中的更新 — 设计规格

日期: 2026-06-02 状态: 已批准(头脑风暴)→ 可进入实施计划 分支: feat/updates-status-bar

问题 / 目标

自动更新界面目前是一个固定在右上角的浮动胶囊(UpdateBadge)。用户希望更新出现在底部状态栏(LogsFooter)中:一个常驻的版本指示器,外加一个“更新”标签页/面板,用于展示更新日志、stable/preview 渠道切换器以及更新/发布历史——这样更新就有了一个永久且可发现的位置,而不是一个转瞬即逝的浮动胶囊。

决策(来自头脑风暴)

  1. 位置: 在始终可见的 28px LogsFooter 栏右侧放置一个常驻版本芯片,并提供一个“更新”标签页,可将底部栏扩展为更新面板。点击芯片即可打开面板。
  2. 空闲行为: 芯片始终显示——处于最新状态时显示 v<current> ✓,并会在可更新 / 下载中 / 就绪 / 错误等状态之间切换。
  3. 数据源: GitHub Releases(实时),通过 Rust 命令获取。更新日志和“历史记录”统一为一个 Releases 列表(发布历史中正在运行的版本会标记为“current”)。这不是个人安装日志。
  4. 渠道切换器: 面板会提供一个可编辑的 stable/preview 切换器,与现有的“设置 → 关于”切换器共存;两者读写相同的 Rust get_update_channel / set_update_channel,并通过 store 中保存的单个 updateChannel 值保持同步。

本设计所基于的现有代码

  • frontend/src/components/LogsFooter.jsx / .css — 底部栏。固定定位,z-index: 40,折叠时 28px,可展开 180–720px。标签页来自 SOURCES 数组,渲染为 .logs-footer__pill 按钮;当前标签页持久化到 localStorage omnivoice.logs.active。主体根据当前标签页渲染;Notifications 标签页是非日志自定义标签页主体的先例。设置 CSS 变量 --logs-footer-height。
  • frontend/src/components/UpdateBadge.jsx / .css — 当前的浮动胶囊,挂在 App.jsx(<UpdateBadge/>)。渲染 available / downloading / ready / error 状态(idle + checking → null)。当前 main 分支在错误状态上增加了关闭(X)按钮、dismissUpdate store action 和 update.dismiss 键——这些必须保留。
  • frontend/src/store/updaterSlice.ts — updateStatus | updateVersion | updateNotes | updateProgress | updateError;setter setUpdateChecking/Available/Idle/Progress/Ready/Error + dismissUpdate。不持久化。 没有历史记录字段。
  • frontend/src/utils/updater.js — isTauri()、currentChannel()(invoke('get_update_channel'))、checkForUpdate(store)(invoke('check_update',{channel}))、installUpdate(store)(invoke('install_update',{channel}),监听 update://progress、relaunch())。
  • frontend/src/utils/updateChannel.js — UPDATE_CHANNELS = ['stable','preview']、normalizeChannel()。
  • frontend/src/pages/Settings.jsx(About 标签页)— 现有的渠道 Segmented(changeChannel → set_update_channel)、端点显示、检查更新。
  • Rust(src-tauri/src)— 命令 get_update_channel、set_update_channel、check_update、install_update。新增: list_releases。
  • CHANGELOG.md 存在于仓库根目录,但未在界面中展示(本设计不使用它;发布说明来自 GitHub)。

架构

A. 状态栏芯片 — UpdateStatusChip

新组件渲染在 LogsFooter 顶部栏的右侧(靠近 Discord/捐赠按钮组)。订阅 updaterSlice。状态 → 呈现:

updateStatus 芯片显示
idle(已是最新) v<current> ✓(弱化/变暗)
available ⬆ <version> · Update(点击行在面板中安装)
downloading ↺ Updating <pct>%
ready ↺ Restart
error ⚠ Failed · Retry(+ 保留 dismiss)
checking 短暂的 … Checking(或保持之前的芯片)
  • 点击 → 打开底部栏的 Updates 标签页(openTo('updates'))。
  • 当前版本来源: 挂载时通过 @tauri-apps/api/app 的 getVersion() 获取,并存储在 updaterSlice.appVersion 中(回退方案:在非 Tauri/开发环境中版本未知时隐藏芯片)。芯片仅作指示器;主要操作在面板中(可通过打开面板从芯片进入安装;重试/重启也可以内联操作,以保留当前的一键行为——详见下方已解决的开放问题)。

B. 更新面板 — UpdatesPanel

当 active === 'updates' 时,渲染为底部栏主体。各区块自上而下为:

  1. 实时行 —— 与芯片状态对应,并带有可操作控件:
  2. available → Update available · <v> + 安装(当 dubStep === 'generating' 时禁用,并显示与现在相同的 toast)。
  3. downloading → 进度条 + 百分比;ready → 重启;error → 消息 + 重试 + 关闭;idle → Up to date · v<current> + 立即检查。
  4. 渠道 —— Segmented stable/preview 控件绑定到 store 中的 updateChannel;onChange → set_update_channel + 重新获取 releases。与 Settings 保持同步。
  5. 发布列表 —— 可滚动;每一行包含:版本、日期、prerelease 标签、可展开的说明。正在运行的版本会标记为 current。已处理加载中与空/离线状态(见数据部分)。

C. 数据 — list_releases Rust 命令 + releasesSlice

  • Rust list_releases(channel) -> Vec<ReleaseInfo>:通过现有的 reqwest 发起 GET https://api.github.com/repos/{owner}/{repo}/releases(无需认证;设置 User-Agent)。映射为 { version, name, date, prerelease, notes }。Stable 渠道会过滤掉 prerelease;preview 会包含它们。按最新优先排序。使用短期内存缓存(例如 5 分钟),避免频繁重复请求。
  • 前端 临时 releasesSlice(不持久化):releases、releasesStatus: 'idle'|'loading'|'loaded'|'error'、loadReleases(channel)(调用该命令;失败时设置 error)。在面板首次打开以及渠道切换时懒加载。
  • 离线 / 失败: 面板显示“无法加载发布列表(离线?)”+ 重试按钮。实时更新流程(check_update/install_update)以及应用的其余部分不受影响。非 Tauri/开发环境 → releases 不可用,面板显示同样的空状态;实时更新与现在一样不执行任何操作。

D. 移除 / 移动

  • 从 App.jsx 中移除浮动的 <UpdateBadge/>。
  • UpdateBadge 的状态逻辑移入 UpdateStatusChip(包括 dismiss(忽略)逻辑 + dub-busy(配音忙碌)门控)。UpdateBadge.jsx/.css 退役(或重命名为 chip)。现有 update.* i18n 键被复用。

状态 / 存储

  • updaterSlice:新增 appVersion: string | null + setAppVersion。其余保持不变(仍然不持久化)。
  • 新增 updateChannelSlice(或并入 updaterSlice):updateChannel: 'stable'|'preview'、setUpdateChannel(ch)(通过 Rust 写入)。设置与面板均绑定到此处 → 单一事实来源,自动同步。
  • 新增 releasesSlice(瞬态):releases + status + loadReleases。

i18n(硬性规则)

新的 updates.* 键添加到全部 21 个语言文件(强制对等;CJK 守卫保持绿色——不能有硬编码的面向用户字符串)。尽量复用 update.* 和 about.channel_*。新键(指示性):updates.tab、updates.up_to_date、updates.check_now、updates.current、updates.releases、updates.prerelease、updates.load_error、updates.retry_load、updates.installed_version。

跨平台一致性(严格规则)

chip + 面板均为纯前端;list_releases 是平台无关的 Rust。该功能默认开启,在 macOS / Windows / Linux 上表现一致。没有按操作系统门控的默认行为。Updater 本身仍仅限 Tauri(在 dev/web 中为 no-op)——chip 在这些环境中降级为隐藏/仅显示版本,且在各平台上表现一致。

测试

  • vitest:覆盖全部六种状态的 chip 状态→呈现映射;使用 mock releases 数组渲染面板(当前版本标记、按通道过滤 prerelease);通道切换调用 set_update_channel 并触发重新加载;releases 加载错误 → 离线空状态;安装时的 dub-busy 门控。
  • 保持现有 updater.test.js(#216 守卫)+ updaterSlice.test.ts 通过。
  • i18n 对等测试 + CJK 守卫必须通过。

范围之外(YAGNI)

  • 个人安装历史记录(“你在日期 Y 安装了 X”)。
  • 针对单个 release 的手动下载 / 回滚 / 降级。
  • 在现有纯文本/pre-wrap 处理之外,对更新说明做富 Markdown 渲染。
  • 自动刷新/轮询 releases 列表(仅在打开时和通道变更时获取)。

开放问题——已解决

  • chip 与面板在一键操作上的分工: chip 显示状态;Restart/Retry 仍可从 chip 一键操作(保留现有行为);Install 会打开面板(这是会产生实际影响的操作,且先显示更新说明更合适)。
  • Changelog 与历史记录: 统一并入 Releases 列表(决定以 GitHub 作为来源)。
  • 版本未知(dev/web)时的版本 chip: 隐藏。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 om/VoiceStudio