2026-03-29-localmusic2-player-refactor-design.md 11 KB

LocalMusic2 播放器拆分设计

背景

当前 LocalMusic.ets 已演变为内容展示、播放控制、底部播放条、全屏播放页、卡片联动混杂在一起的大文件,单文件规模超过两万行,继续在原文件上实现音乐卡片和播放器能力会持续放大耦合风险。

仓库内虽然已有 LocalMusic2.ets,但它目前本质上仍是 LocalMusic 的复制体,尚未形成真正可维护的新架构。

本次设计目标是在 不修改 LocalMusic 的前提下,以 LocalMusic2 为新入口,完成播放器职责拆分,为音乐卡片、底部播放条和独立播放页提供清晰边界。

目标

  • NewIndex 挂载底部播放条和全屏播放浮层,成为播放器宿主。
  • LocalMusic2 只负责音乐分类与列表展示,不再承担播放器 UI 宿主职责。
  • 全屏播放页抽离为独立的 MusicPlayerBuilder.ets
  • 播放控制逻辑抽离为独立控制类,统一管理队列、播放状态、进度、歌词、卡片同步。
  • 首版必须兼容本地、歌单、WebDAV、Navidrome、发现页等现有来源。
  • 播放页交互优先复用当前 LocalMusic / LocalMusic2 已验证过的交互能力。

非目标

  • 不在本轮重写底层 Ijk 播放内核。
  • 不在本轮重做现有播放页视觉设计。
  • 不在本轮修改 LocalMusic.ets 的现有行为。
  • 不追求一次性移除所有 AppStorage 依赖,允许首版保留必要兼容镜像。

核心架构

1. NewIndex 作为播放器宿主

NewIndex.ets 负责:

  • 挂载底部播放条组件。
  • 挂载全屏播放浮层。
  • 管理 isShowPlay 这类纯 UI 显示状态。
  • 处理返回键优先关闭播放浮层。
  • 响应音乐卡片的 OPEN_PLAYER 动作。

NewIndex 不再承担具体播放逻辑,也不直接维护播放队列。

2. LocalMusic2 作为内容页

LocalMusic2.ets 负责:

  • 本地音乐分类展示。
  • 音乐列表、歌单、搜索、定位当前歌曲等内容能力。
  • 用户点击歌曲或歌单后,组装统一的播放请求并发给播放控制器。

LocalMusic2 不再负责:

  • 底部播放条 UI。
  • 全屏播放页 UI。
  • isShowPlay 的宿主级管理。
  • 音乐卡片命令消费。

3. MusicPlayerBuilder 作为独立播放页 UI

新增 MusicPlayerBuilder.ets,负责:

  • 复用当前播放页交互与视觉。
  • 读取播放控制器提供的状态进行渲染。
  • 将播放/暂停、切歌、拖动进度、上滑切歌、下滑关闭等操作转发给播放控制器或宿主。

该文件不作为状态源头,只做播放器 UI 呈现层。

4. MusicPlaybackController 作为唯一播放控制中台

新增 MusicPlaybackController.ets,负责:

  • 当前歌曲、当前队列、当前索引。
  • 播放、暂停、上一首、下一首、seek。
  • 播放模式、进度、时长、歌词、封面。
  • 多数据源队列切换。
  • 音乐卡片状态同步。
  • 播放状态对底部播放条与播放页的统一输出。

控制器是唯一播放状态源,避免多个页面各自维护一份播放器状态。

文件拆分方案

新增文件

修改文件

数据模型

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 中的卡片消息入口继续保留:

  • PLAY_OR_PAUSE
  • PLAY_PREVIOUS
  • PLAY_NEXT
  • OPEN_PLAYER

新职责分配

  • PLAY_OR_PAUSEPLAY_PREVIOUSPLAY_NEXT 交给 MusicPlaybackController 执行。
  • OPEN_PLAYER 交给 NewIndex 宿主处理,直接展开全屏播放浮层。

卡片状态更新

每次以下状态变化后,由 MusicPlaybackController 负责更新:

  • 当前歌曲变化。
  • 播放状态变化。
  • 封面变化。

更新流程为:

  1. 写入 MusicCardPlaybackStore.ets
  2. 调用 MusicCardFormManager.ets 刷新所有卡片。

迁移顺序

阶段 1:抽控制器与模型

  • 新建 PlaybackRequestPlaybackStateMusicPlaybackController
  • 优先迁移现有核心方法,例如播放、暂停、切歌、队列装载、歌单/网盘请求处理。
  • 暂时保留原有 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@ConsumeeventHub、控制器新状态源并存的过渡期会增加短期复杂度。
  • 多来源播放链路分散,回归验证量较大。

取舍

  • 首版优先保证职责分离和行为兼容,不追求一次性清理所有历史状态通道。
  • 先把“谁负责什么”理顺,再逐步减少页面直接持有的播放器细节。
  • 先复用现有交互和动画,避免在架构迁移阶段同时引入新的 UI 回归风险。

结论

本次重构采用“NewIndex 做宿主、LocalMusic2 做内容页、MusicPlayerBuilder 做播放页、MusicPlaybackController 做唯一控制中台”的拆法。

这是在不修改 LocalMusic 的约束下,兼顾现有交互复用、多来源兼容、音乐卡片接入和后续可维护性的最稳方案。后续实施时应严格按照迁移顺序推进,避免再次把播放器能力回流到内容页中。