Преглед изворни кода

docs(card): 补充音乐卡片设计

onecold пре 4 месеци
родитељ
комит
5973249912
1 измењених фајлова са 548 додато и 0 уклоњено
  1. 548 0
      docs/superpowers/specs/2026-04-02-music-card-design.md

+ 548 - 0
docs/superpowers/specs/2026-04-02-music-card-design.md

@@ -0,0 +1,548 @@
+# 音乐卡片设计
+
+## 背景
+
+当前项目已经具备基础播放控制抽象:
+
+- `MusicPlaybackController` 负责统一播放动作入口。
+- `PlaybackCoordinator` 负责将控制动作转发到实际播放运行时。
+- `PlaybackStateBridge` 已经维护当前歌曲、队列、索引、播放状态、进度与时长。
+
+但项目中还没有音乐卡片能力,`entry/src/main/module.json5` 也尚未声明 `form` 类型扩展能力。
+
+参考实现里有两条路线:
+
+- `KDVideo`:单一音乐播放卡片体系,使用 `FormExtensionAbility + preferences 持久化快照 + updateForm 主动更新`。
+- `musicCard` 官方样例:多类型卡片平台,使用数据库维护卡片实例,并承载播放、歌词、推荐等多类卡片。
+
+结合当前项目结构与近期播放器控制收敛方向,首版音乐卡片应优先复用 `KDVideo` 的轻量链路,而不是引入官方样例那种重型卡片平台。
+
+## 本阶段目标
+
+新增 3 种音乐卡片:
+
+1. 普通播放卡片
+2. 中等播放卡片
+3. 歌词卡片
+
+完成标准:
+
+- 用户可从桌面添加上述 3 种卡片。
+- 三种卡片都能显示当前播放歌曲的封面、标题、歌手和播放状态。
+- 普通卡片和中等卡片支持上一首、播放暂停、下一首。
+- 歌词卡片支持封面、两行歌词、上一首、播放暂停、下一首。
+- 点击卡片主体可回到应用并打开当前播放器视图。
+- 卡片在歌曲切换、播放暂停、进度推进时能同步刷新。
+- 无歌词时,歌词卡片自动回退显示歌名和歌手,不出现空白。
+
+非目标:
+
+- 首版不做拖动进度条。
+- 首版不做收藏、喜欢、推荐歌单等扩展能力。
+- 首版不做整页滚动歌词,仅展示两行歌词。
+- 首版不引入卡片数据库,不建设统一多业务卡片平台。
+- 首版不处理锁屏卡片。
+
+## 方案对比
+
+### 方案一:轻量单链路卡片方案
+
+做法:
+
+- 新增一个 `FormExtensionAbility`。
+- 三种卡片共用一套播放卡片快照和动作协议。
+- 通过 `preferences` 保存快照和 formId 列表。
+- 播放状态变化时主动 `updateForm`。
+
+优点:
+
+- 与当前项目现状最匹配。
+- 对现有播放架构侵入较小。
+- 后续继续推进播放器控制层解耦时不会推翻重来。
+
+缺点:
+
+- 需要补齐卡片基础设施与一层卡片数据适配。
+
+### 方案二:直接依赖 LocalMusic 页面状态
+
+做法:
+
+- 让卡片更新逻辑尽量绑定 `LocalMusic` 页面内部字段和生命周期。
+
+优点:
+
+- 初看接入路径最短。
+
+缺点:
+
+- 页面耦合过重。
+- 一旦 `LocalMusic` 生命周期变化或后续继续拆播放器,卡片很容易失效。
+- 不利于卡片成为稳定的系统级入口。
+
+### 方案三:官方样例式多卡片平台
+
+做法:
+
+- 参考 `musicCard` 样例建设数据库、统一工具类、多类卡片管理框架。
+
+优点:
+
+- 扩展性最好。
+
+缺点:
+
+- 对当前项目明显过重。
+- 第一版投入和改动面过大。
+- 会把“先把播放器卡片做出来”这个目标拖慢。
+
+## 推荐方案
+
+采用方案一,即“轻量单链路卡片方案”。
+
+核心原则:
+
+- 控制动作统一走 `MusicPlaybackController`。
+- 卡片展示数据统一由一套独立的播放卡片快照提供。
+- 卡片实例管理通过 `FormExtensionAbility` 完成。
+- 不把卡片直接绑定到 `LocalMusic` 页面生命周期。
+
+## 目标架构
+
+### 1. FormExtensionAbility
+
+新增 `MusicCardFormAbility`,负责:
+
+- `onAddForm` 时返回初始卡片数据。
+- 记录 formId。
+- `onUpdateForm` 时按最新快照刷新卡片。
+- `onFormEvent` 时处理卡片发回的动作。
+- `onRemoveForm` 时清理失效 formId。
+
+### 2. MusicCardManager
+
+新增统一卡片管理器,负责:
+
+- 从当前播放状态构建卡片快照。
+- 将卡片快照转换为三种卡片共用的 binding data。
+- 主动更新所有已注册 form。
+- 节流歌词卡片刷新频率。
+- 处理 form 更新失败后的 formId 清理。
+
+它是卡片层的唯一更新入口。
+
+### 3. MusicCardSnapshotStore
+
+使用 `preferences` 持久化卡片快照,保证:
+
+- 卡片首次添加时能拿到最近一次播放状态。
+- 应用未完全拉起时仍能显示上次有效内容。
+
+### 4. MusicCardFormStore
+
+使用 `preferences` 保存 formId 列表,负责:
+
+- 新增 formId
+- 删除 formId
+- 读取当前所有 formId
+
+### 5. 现有播放层的职责
+
+`PlaybackStateBridge` 继续承担基础播放状态桥接职责:
+
+- `currentSong`
+- `currentQueue`
+- `currentQueueIndex`
+- `isPlaying`
+- `positionMs`
+- `durationMs`
+
+`LocalMusic` 仍然负责真实播放过程中的状态推进和歌词解析,但它不直接成为卡片宿主。
+
+卡片侧通过 `MusicCardManager` 从现有播放状态中派生专用显示数据。
+
+## 建议文件布局
+
+建议新增以下文件:
+
+- `entry/src/main/ets/entryformability/MusicCardFormAbility.ets`
+- `entry/src/main/ets/common/player/MusicCardConstants.ets`
+- `entry/src/main/ets/common/player/MusicCardSnapshotStore.ets`
+- `entry/src/main/ets/common/player/MusicCardFormStore.ets`
+- `entry/src/main/ets/common/player/MusicCardManager.ets`
+- `entry/src/main/ets/common/player/MusicCardSnapshot.ets`
+- `entry/src/main/ets/widget/pages/MusicPlayerWidgetCard.ets`
+- `entry/src/main/ets/widget/pages/MusicPlayerWidgetWideCard.ets`
+- `entry/src/main/ets/widget/pages/MusicPlayerWidgetLyricCard.ets`
+- `entry/src/main/resources/base/profile/form_config.json`
+
+需要修改的现有文件:
+
+- `entry/src/main/module.json5`
+- `entry/src/main/ets/entryability/EntryAbility.ets`
+- `entry/src/main/ets/view/LocalMusic.ets`
+- 可能还包括现有歌词工具类或封面工具类的少量扩展
+
+## 卡片数据模型
+
+建议新增一份专门的卡片快照,不直接暴露播放器内部所有字段。
+
+建议字段:
+
+- `title`
+- `artist`
+- `coverPath`
+- `hasCoverImage`
+- `isPlaying`
+- `hasSong`
+- `currentPositionMs`
+- `durationMs`
+- `currentTimeText`
+- `durationTimeText`
+- `lyricLine1`
+- `lyricLine2`
+- `hasLyric`
+- `playType`
+- `filePath`
+- `updatedAtMs`
+
+字段原则:
+
+- 卡片只拿自己需要的显示字段。
+- 不把完整队列、复杂页面状态、动画状态塞进卡片快照。
+- 歌词卡片展示只保留两行,避免把整份歌词文本塞进卡片层。
+
+## 三种卡片设计
+
+### 1. 普通播放卡片
+
+定位:
+
+- 紧凑型播放控制卡片。
+
+展示内容:
+
+- 小封面
+- 歌名
+- 歌手
+- 上一首
+- 播放暂停
+- 下一首
+
+交互:
+
+- 点卡片主体:打开应用并进入播放器
+- 点按钮:直接控制播放
+
+说明:
+
+- 第一版不展示进度条。
+- 第一版不展示歌词。
+
+### 2. 中等播放卡片
+
+定位:
+
+- 信息更完整的播放控制卡片。
+
+展示内容:
+
+- 更大的封面
+- 歌名
+- 歌手
+- 可选时间信息或播放状态文案
+- 上一首
+- 播放暂停
+- 下一首
+
+交互:
+
+- 与普通卡片一致
+
+说明:
+
+- 第一版与普通卡片共用同一套动作与数据协议。
+- 差异主要体现在布局和视觉密度。
+
+### 3. 歌词卡片
+
+定位:
+
+- 增强版播放控制卡片。
+
+展示内容:
+
+- 封面
+- 当前歌词
+- 下一句歌词
+- 上一首
+- 播放暂停
+- 下一首
+
+歌词规则:
+
+- 有逐行歌词时:显示当前行和下一行。
+- 只有当前行没有下一行时:第二行留空或回退为歌手名。
+- 无歌词时:回退显示歌名和歌手。
+
+说明:
+
+- 首版不做多行滚动歌词。
+- 首版不做歌词区域滚动动画。
+- 重点保证“准、稳、不卡”。
+
+## 歌词更新策略
+
+歌词卡片是本方案最容易引发频繁刷新的部分,必须节流。
+
+推荐策略:
+
+- 播放进度仍由 `LocalMusic` 在现有链路里持续推进。
+- `LocalMusic` 在推进进度后,调用 `MusicCardManager` 的节流更新入口。
+- `MusicCardManager` 基于当前歌曲和当前进度,解析出“当前行 + 下一行”。
+- 只有歌词行内容发生变化、或播放态发生变化、或歌曲发生变化时,才实际调用 `updateForm`。
+
+建议节流频率:
+
+- 普通状态:800ms 到 1000ms 一次
+- 若歌词行未变化:不更新卡片
+
+这样可以兼顾显示及时性和系统开销。
+
+## 封面策略
+
+封面优先级:
+
+1. 当前歌曲可用的本地封面路径
+2. 已缓存的缩略图路径
+3. 默认占位图
+
+建议复用现有封面能力:
+
+- `CoverThumbCache`
+- 当前歌曲已有的 `pixelMapPath` 或相关封面字段
+
+首版原则:
+
+- 本地和已缓存封面优先
+- 不在卡片层新建复杂下载流程
+- 远端封面若无法稳定直出,则先回退占位图
+
+## 卡片动作协议
+
+三种卡片统一使用一套动作常量。
+
+建议动作:
+
+- `play_pause`
+- `prev_song`
+- `next_song`
+- `open_player`
+- `sync_register`
+
+建议参数:
+
+- `formId`
+- `action`
+- `source`
+
+协议规则:
+
+- 控制按钮通过 `postCardAction(call)` 回到应用。
+- 主体点击通过 `postCardAction(router)` 或统一路由动作打开播放器。
+- `sync_register` 用于卡片出现时补登记 formId,避免首次添加或恢复后 formId 丢失。
+
+## 动作回流链路
+
+推荐链路:
+
+1. 卡片按钮触发 `postCardAction`
+2. `EntryAbility` 接收 call 事件
+3. `EntryAbility` 解析动作
+4. `EntryAbility` 调用 `MusicPlaybackController`
+5. 控制器驱动当前播放运行时执行动作
+6. 播放状态变化后回写 `PlaybackStateBridge`
+7. `MusicCardManager` 刷新所有卡片
+
+主体点击链路:
+
+1. 卡片发送 `open_player`
+2. `EntryAbility` 设置打开播放器所需状态或触发既有事件
+3. 应用拉起后展示当前播放器视图
+
+## 与当前项目的集成点
+
+### 1. module.json5
+
+需要新增一个 `extensionAbilities` 节点:
+
+- 名称建议为 `MusicCardFormAbility`
+- 类型为 `form`
+- 元数据指向 `$profile:form_config`
+
+### 2. form_config.json
+
+需要声明 3 个 form:
+
+- 普通播放卡片
+- 中等播放卡片
+- 歌词卡片
+
+建议都设置为动态卡片,关闭定时更新,走主动刷新:
+
+- `isDynamic: true`
+- `updateEnabled: false`
+
+原因:
+
+- 音乐卡片依赖实时播放状态
+- 主动刷新比定时刷新更准确
+
+### 3. EntryAbility
+
+需要增加:
+
+- 卡片动作 call handler 注册
+- `open_player` 路由处理
+- 必要时对 formId 的兜底登记
+
+### 4. LocalMusic
+
+需要增加:
+
+- 在歌曲切换、播放暂停、进度推进时调用 `MusicCardManager`
+- 在歌词行变化时推动歌词卡片刷新
+
+不建议增加:
+
+- 不把卡片逻辑直接写成大段页面内分支
+- 不让 `LocalMusic` 自己维护 formId 列表
+
+## 分阶段实施建议
+
+### 阶段一:打通基础卡片链路
+
+目标:
+
+- 新增 `FormExtensionAbility`
+- 新增 `form_config.json`
+- 跑通普通播放卡片
+- 跑通按钮控制和打开播放器
+
+验收:
+
+- 可添加普通卡片
+- 点击上一首、播放暂停、下一首生效
+- 切歌后卡片自动刷新
+
+### 阶段二:补齐中等播放卡片
+
+目标:
+
+- 复用同一套数据模型和动作协议
+- 完成中等卡布局
+
+验收:
+
+- 中等卡片可正常显示和控制
+- 与普通卡共用同一套更新链路
+
+### 阶段三:补齐歌词卡片
+
+目标:
+
+- 打通歌词两行提取
+- 实现节流更新
+- 实现歌词缺失回退
+
+验收:
+
+- 歌词卡片随播放进度切换两行歌词
+- 无歌词时稳定显示歌名和歌手
+- 更新频率稳定,不造成明显卡顿
+
+### 阶段四:稳定性清理
+
+目标:
+
+- 清理无效 formId
+- 处理卡片恢复、应用重启、播放中断等场景
+- 统一日志与错误兜底
+
+验收:
+
+- 移除卡片后不会持续报错
+- 应用重启后卡片能显示最近状态
+
+## 风险与应对
+
+### 1. 歌词刷新过频
+
+风险:
+
+- `updateForm` 调用过于频繁,影响性能和稳定性。
+
+应对:
+
+- 对歌词卡片做节流。
+- 仅在歌词行变化时刷新。
+
+### 2. 封面来源不稳定
+
+风险:
+
+- 远端或临时路径封面在卡片中不可直接显示。
+
+应对:
+
+- 首版优先本地缓存图或缩略图。
+- 无法稳定获取时回退占位图。
+
+### 3. 播放器状态与卡片状态分裂
+
+风险:
+
+- 页面内局部状态和卡片快照来源不一致。
+
+应对:
+
+- 卡片只认 `MusicCardManager` 构建的统一快照。
+- 不允许三种卡片各自维护不同的数据来源。
+
+### 4. LocalMusic 耦合仍偏重
+
+风险:
+
+- 卡片更新时机如果分散写在 `LocalMusic` 多处,后续维护成本会很高。
+
+应对:
+
+- 抽一个集中入口,例如 `notifyPlaybackCardStateChanged()`。
+- 歌曲切换、播放态切换、进度推进都收敛到统一通知方法。
+
+## 测试与验证
+
+首版应重点做真机冒烟验证。
+
+必须验证:
+
+- 添加三种卡片是否成功
+- 普通卡片播放控制是否正常
+- 中等卡片播放控制是否正常
+- 歌词卡片是否显示两行歌词
+- 无歌词歌曲是否正确回退
+- 切歌后封面、标题、歌手是否同步
+- 暂停恢复后卡片状态是否同步
+- 点击卡片主体是否能进入播放器
+- 删除卡片后是否仍有 form 更新报错
+- 应用重启后卡片是否能显示最近一次播放状态
+
+## 结论
+
+本项目的音乐卡片应采用“`KDVideo` 轻量链路 + 当前项目播放控制抽象”的组合方案:
+
+- 不直接照搬官方样例的重型多卡片平台
+- 不把卡片强绑到 `LocalMusic` 页面生命周期
+- 先用一套统一快照和统一动作协议支撑 3 种卡片
+
+这样既能尽快落地普通卡、中等卡、歌词卡,又能与当前播放器解耦方向保持一致,避免后续返工。