Ver código fonte

docs(player): 添加 LocalMusic2 播放器拆分设计

onecold 4 meses atrás
pai
commit
532995b14c

+ 281 - 0
docs/superpowers/specs/2026-03-29-localmusic2-player-refactor-design.md

@@ -0,0 +1,281 @@
+# LocalMusic2 播放器拆分设计
+
+## 背景
+
+当前 [LocalMusic.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/view/LocalMusic.ets) 已演变为内容展示、播放控制、底部播放条、全屏播放页、卡片联动混杂在一起的大文件,单文件规模超过两万行,继续在原文件上实现音乐卡片和播放器能力会持续放大耦合风险。
+
+仓库内虽然已有 [LocalMusic2.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/musicCard/LocalMusic2.ets),但它目前本质上仍是 `LocalMusic` 的复制体,尚未形成真正可维护的新架构。
+
+本次设计目标是在 **不修改 `LocalMusic`** 的前提下,以 `LocalMusic2` 为新入口,完成播放器职责拆分,为音乐卡片、底部播放条和独立播放页提供清晰边界。
+
+## 目标
+
+- `NewIndex` 挂载底部播放条和全屏播放浮层,成为播放器宿主。
+- `LocalMusic2` 只负责音乐分类与列表展示,不再承担播放器 UI 宿主职责。
+- 全屏播放页抽离为独立的 [MusicPlayerBuilder.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/player/MusicPlayerBuilder.ets)。
+- 播放控制逻辑抽离为独立控制类,统一管理队列、播放状态、进度、歌词、卡片同步。
+- 首版必须兼容本地、歌单、WebDAV、Navidrome、发现页等现有来源。
+- 播放页交互优先复用当前 `LocalMusic` / `LocalMusic2` 已验证过的交互能力。
+
+## 非目标
+
+- 不在本轮重写底层 Ijk 播放内核。
+- 不在本轮重做现有播放页视觉设计。
+- 不在本轮修改 [LocalMusic.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/view/LocalMusic.ets) 的现有行为。
+- 不追求一次性移除所有 `AppStorage` 依赖,允许首版保留必要兼容镜像。
+
+## 核心架构
+
+### 1. NewIndex 作为播放器宿主
+
+[NewIndex.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/pages/NewIndex.ets) 负责:
+
+- 挂载底部播放条组件。
+- 挂载全屏播放浮层。
+- 管理 `isShowPlay` 这类纯 UI 显示状态。
+- 处理返回键优先关闭播放浮层。
+- 响应音乐卡片的 `OPEN_PLAYER` 动作。
+
+`NewIndex` 不再承担具体播放逻辑,也不直接维护播放队列。
+
+### 2. LocalMusic2 作为内容页
+
+[LocalMusic2.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/musicCard/LocalMusic2.ets) 负责:
+
+- 本地音乐分类展示。
+- 音乐列表、歌单、搜索、定位当前歌曲等内容能力。
+- 用户点击歌曲或歌单后,组装统一的播放请求并发给播放控制器。
+
+`LocalMusic2` 不再负责:
+
+- 底部播放条 UI。
+- 全屏播放页 UI。
+- `isShowPlay` 的宿主级管理。
+- 音乐卡片命令消费。
+
+### 3. MusicPlayerBuilder 作为独立播放页 UI
+
+新增 [MusicPlayerBuilder.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/player/MusicPlayerBuilder.ets),负责:
+
+- 复用当前播放页交互与视觉。
+- 读取播放控制器提供的状态进行渲染。
+- 将播放/暂停、切歌、拖动进度、上滑切歌、下滑关闭等操作转发给播放控制器或宿主。
+
+该文件不作为状态源头,只做播放器 UI 呈现层。
+
+### 4. MusicPlaybackController 作为唯一播放控制中台
+
+新增 [MusicPlaybackController.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/player/MusicPlaybackController.ets),负责:
+
+- 当前歌曲、当前队列、当前索引。
+- 播放、暂停、上一首、下一首、seek。
+- 播放模式、进度、时长、歌词、封面。
+- 多数据源队列切换。
+- 音乐卡片状态同步。
+- 播放状态对底部播放条与播放页的统一输出。
+
+控制器是唯一播放状态源,避免多个页面各自维护一份播放器状态。
+
+## 文件拆分方案
+
+### 新增文件
+
+- [MusicPlaybackController.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/player/MusicPlaybackController.ets)
+  用于集中承接现有播放器主逻辑。
+- [MusicPlayerBuilder.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/player/MusicPlayerBuilder.ets)
+  用于承接全屏播放页 Builder。
+- [MiniPlayerBar.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/player/MiniPlayerBar.ets)
+  用于承接底部播放条。
+- [PlaybackRequest.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/player/model/PlaybackRequest.ets)
+  定义统一播放请求。
+- [PlaybackState.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/player/model/PlaybackState.ets)
+  定义统一播放状态快照。
+
+### 修改文件
+
+- [NewIndex.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/pages/NewIndex.ets)
+  接入播放器宿主、底部播放条、全屏浮层。
+- [LocalMusic2.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/musicCard/LocalMusic2.ets)
+  收缩为内容页,并把播放入口改为发请求。
+- [EntryAbility.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/entryability/EntryAbility.ets)
+  保持音乐卡片消息分发入口不变。
+- [MusicCardPlaybackStore.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/musicCard/MusicCardPlaybackStore.ets)
+  继续作为卡片快照存储。
+- [MusicCardFormManager.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/musicCard/MusicCardFormManager.ets)
+  继续负责卡片刷新。
+
+## 数据模型
+
+### PlaybackRequest
+
+统一播放入口模型至少包含:
+
+- `sourceType`:来源类型,例如本地、歌单、WebDAV、Navidrome、发现页。
+- `playlistId`:来源队列标识。
+- `playlistName`:当前队列展示名称。
+- `songs`:已拿到的歌曲对象列表。
+- `songFilePaths`:只拿到路径时的回填列表。
+- `startIndex`:起播索引。
+- `playType`:请求时希望应用的播放模式。
+- `openPlayer`:是否自动展开全屏播放页。
+
+### PlaybackState
+
+统一状态快照至少包含:
+
+- 当前歌曲。
+- 当前队列。
+- 当前索引。
+- 播放状态。
+- 当前进度与总时长。
+- 当前封面。
+- 当前歌词。
+- 是否可切上一首/下一首。
+- 当前来源上下文。
+
+## 数据流设计
+
+### 页面发起播放
+
+所有页面统一走以下链路:
+
+1. `LocalMusic2`、歌单页、WebDAV 页面、发现页等组装 `PlaybackRequest`。
+2. 调用 `MusicPlaybackController.play(request)`。
+3. 控制器解析来源、刷新队列、设置索引、启动播放。
+4. 如 `request.openPlayer === true`,控制器通知宿主展开全屏播放页。
+
+这样页面层不再直接调用 `setShowPlayTrue()`、`doPlay()`、`startPlayOrResumePlay()` 等底层方法组合。
+
+### 宿主层显示控制
+
+`NewIndex` 负责以下 UI 宿主逻辑:
+
+- 收到控制器的展开请求后设置 `isShowPlay = true`。
+- 收到关闭请求或返回键事件时设置 `isShowPlay = false`。
+- 将底部播放条和全屏播放浮层统一挂载在宿主层,而不是内容页层。
+
+现有 `dismissPlayerView` 事件仍可保留作为过渡期兼容,但最终应由宿主层统一管理,不再让内容页充当播放器宿主。
+
+### 状态同步策略
+
+首版采用“双轨同步”:
+
+- `MusicPlaybackController` 内部状态是主状态。
+- 关键状态镜像到 `AppStorage`,保持现有组件兼容。
+
+首版保留的兼容字段包括:
+
+- `currentSong`
+- `progressValue`
+- `CONTROL_PlayStatus`
+- `cover`
+
+后续稳定后再逐步收紧 `AppStorage` 依赖。
+
+## 音乐卡片联动
+
+### 保留现有入口
+
+[EntryAbility.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/entryability/EntryAbility.ets) 中的卡片消息入口继续保留:
+
+- `PLAY_OR_PAUSE`
+- `PLAY_PREVIOUS`
+- `PLAY_NEXT`
+- `OPEN_PLAYER`
+
+### 新职责分配
+
+- `PLAY_OR_PAUSE`、`PLAY_PREVIOUS`、`PLAY_NEXT` 交给 `MusicPlaybackController` 执行。
+- `OPEN_PLAYER` 交给 `NewIndex` 宿主处理,直接展开全屏播放浮层。
+
+### 卡片状态更新
+
+每次以下状态变化后,由 `MusicPlaybackController` 负责更新:
+
+- 当前歌曲变化。
+- 播放状态变化。
+- 封面变化。
+
+更新流程为:
+
+1. 写入 [MusicCardPlaybackStore.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/musicCard/MusicCardPlaybackStore.ets)。
+2. 调用 [MusicCardFormManager.ets](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/musicCard/MusicCardFormManager.ets) 刷新所有卡片。
+
+## 迁移顺序
+
+### 阶段 1:抽控制器与模型
+
+- 新建 `PlaybackRequest`、`PlaybackState`、`MusicPlaybackController`。
+- 优先迁移现有核心方法,例如播放、暂停、切歌、队列装载、歌单/网盘请求处理。
+- 暂时保留原有 UI Builder,不立刻大规模移动展示层。
+
+### 阶段 2:抽播放页
+
+- 将 `LocalMusic2` 中的大播放页 Builder 迁移到 `MusicPlayerBuilder.ets`。
+- 由 `NewIndex` 挂载全屏浮层。
+- 复用当前动画、上滑切歌、下滑关闭、播放列表 Sheet 等交互。
+
+### 阶段 3:抽底部播放条
+
+- 将底部播放条迁移到 `MiniPlayerBar.ets`。
+- `NewIndex` 统一挂载普通模式和 HiCar 模式播放条。
+- 播放条动作全部走控制器。
+
+### 阶段 4:收缩 LocalMusic2
+
+- 删除 `LocalMusic2` 中播放器宿主职责。
+- 删除对 `isShowPlay` 的直接控制。
+- 删除卡片命令监听与播放页关闭事件监听。
+- 只保留内容展示与播放请求发起能力。
+
+### 阶段 5:多来源回归
+
+- 统一验证本地、歌单、WebDAV、Navidrome、发现页播放流程。
+- 补齐来源上下文切换时的队列同步与状态恢复。
+
+## 错误处理
+
+- 当 `PlaybackRequest` 中只有路径列表时,控制器负责回填缺失歌曲对象;回填失败的项目跳过并记录日志。
+- 当来源队列为空时,不展开播放页,直接给出 Toast 提示。
+- 当卡片刷新失败时,沿用现有 `MusicCardFormManager` 行为,记录日志并移除失效 formId。
+- 当宿主层未挂载完成时,卡片命令可先写入挂起动作队列,待宿主或控制器注册后消费。
+
+## 测试与验收
+
+首版以手工回归为主,必须覆盖:
+
+- 本地音乐列表点击播放。
+- 歌单点击播放。
+- WebDAV 点击播放。
+- Navidrome 点击播放。
+- 发现页点击播放。
+- 底部播放条播放、暂停、上一首、下一首。
+- 全屏播放页展开、关闭、拖动进度、上下滑动交互。
+- 音乐卡片播放、暂停、切歌、打开播放页。
+- 返回键在播放页打开时优先关闭浮层。
+
+如果本轮新增可稳定编写的 Hypium 用例,可优先覆盖控制器层的纯逻辑部分,例如:
+
+- `PlaybackRequest` 队列构建。
+- 来源切换后的索引与状态恢复。
+- 卡片状态快照更新逻辑。
+
+## 风险与取舍
+
+### 风险
+
+- 现有播放器逻辑大量依赖页面成员变量,首轮迁移时容易出现漏迁。
+- `AppStorage`、`@Consume`、`eventHub`、控制器新状态源并存的过渡期会增加短期复杂度。
+- 多来源播放链路分散,回归验证量较大。
+
+### 取舍
+
+- 首版优先保证职责分离和行为兼容,不追求一次性清理所有历史状态通道。
+- 先把“谁负责什么”理顺,再逐步减少页面直接持有的播放器细节。
+- 先复用现有交互和动画,避免在架构迁移阶段同时引入新的 UI 回归风险。
+
+## 结论
+
+本次重构采用“`NewIndex` 做宿主、`LocalMusic2` 做内容页、`MusicPlayerBuilder` 做播放页、`MusicPlaybackController` 做唯一控制中台”的拆法。
+
+这是在不修改 `LocalMusic` 的约束下,兼顾现有交互复用、多来源兼容、音乐卡片接入和后续可维护性的最稳方案。后续实施时应严格按照迁移顺序推进,避免再次把播放器能力回流到内容页中。