# 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` 的约束下,兼顾现有交互复用、多来源兼容、音乐卡片接入和后续可维护性的最稳方案。后续实施时应严格按照迁移顺序推进,避免再次把播放器能力回流到内容页中。