2 커밋 7c85db2039 ... 5dcb17bd51

작성자 SHA1 메시지 날짜
  onecold 5dcb17bd51 抽离播放页成独立播放页开始慢慢改 4 달 전
  Codex 2b3494a63f docs(player): 补充独立播放页一期设计 4 달 전

+ 214 - 0
docs/superpowers/specs/2026-04-06-independent-player-page-phase1-design.md

@@ -0,0 +1,214 @@
+# 独立播放页第一阶段设计
+
+## 背景
+
+当前 `entry/src/main/ets/pages/NewIndex.ets` 打开的是独立播放页 `entry/src/main/ets/view/player/MusiPlayerView.ets`,但该页面目前只保留了封面、标题和三个基础按钮,缺少原播放页中的进度条、当前时间、总时长、歌词主体等核心内容。
+
+现象上表现为:
+
+- 播放/暂停图标与实际播放状态可能不同步。
+- 点击上一首/下一首后,封面、歌名、歌手等信息更新不完整。
+- 页面缺少进度条、时间展示、歌词主体,用户感知上和原播放页差距过大。
+
+仓库中原完整播放页仍存在于 `entry/src/main/ets/view/LocalMusic.ets` 的 `IjkMusicPlayerView()` 链路中,但当前需求不是回退到原页面,而是继续保留独立播放页方向,并把原播放页的核心体验逐步迁移到 `MusiPlayerView.ets`。
+
+## 本阶段目标
+
+第一阶段只恢复“核心可用”体验:
+
+- 封面、歌名、歌手在切歌后正确更新。
+- 播放/暂停按钮图标跟随真实播放状态刷新。
+- 进度条、当前时间、总时长恢复展示并持续更新。
+- 歌词主体恢复展示,并随播放进度滚动。
+- 整体页面布局尽量接近原播放页的主骨架。
+
+## 非目标
+
+本阶段不纳入以下功能:
+
+- 更多弹层、播放页更多菜单。
+- 歌词设置面板。
+- 复制歌词、分享歌词。
+- 单行歌词模式。
+- 频谱、长按倍速、复杂手势分支。
+- PIP/蓝牙歌词/投播特化 UI。
+
+这些能力后续如果继续推进独立播放页,可在第二阶段单独迁移。
+
+## 现状与问题归因
+
+`MusiPlayerView.ets` 当前直接通过少量 `@StorageLink` 消费状态:
+
+- `currentSong`
+- `CONTROL_PlayStatus`
+- `progressValue`
+- `cover`
+
+这只能支撑最基础的静态展示,无法完整复用原播放页能力。原播放页中的关键信息还依赖以下链路:
+
+- `playbackPositionMs`、`playbackDurationMs` 驱动当前时间和总时长。
+- `currentSong.lyricContent` 与 `LyricController` 驱动歌词 UI。
+- 播放页内部还维护了当前显示页、歌词滚动位置、封面/歌词切换等展示状态。
+
+因此当前问题不是单个按钮实现问题,而是独立播放页缺少完整的播放态输入和歌词渲染链路。
+
+## 设计概览
+
+保持 `entry/src/main/ets/view/player/MusiPlayerView.ets` 作为独立播放页根组件,不回退到 `LocalMusic.ets` 直接渲染原页面,但补齐独立页自身需要的状态消费和展示层。
+
+页面第一阶段按四层组织:
+
+1. 顶部封面信息层:封面、歌名、歌手。
+2. 中部主体切换层:`Swiper` 两页,第一页展示封面主体,第二页展示歌词主体。
+3. 底部播放控制层:进度条、当前时间、总时长、上一首、播放/暂停、下一首。
+4. 页面内部状态层:当前 `Swiper` 页、点光按钮状态、歌词控制器、派生时间文本。
+
+## 状态输入设计
+
+独立播放页继续以宿主同步到 `AppStorage` 的状态为唯一数据来源,不直接依赖 `LocalMusic.ets` 页面实例字段。
+
+### 直接消费的宿主状态
+
+- `currentSong`
+- `cover`
+- `CONTROL_PlayStatus`
+- `progressValue`
+- `playbackPositionMs`
+- `playbackDurationMs`
+
+### 页面内部派生状态
+
+- `currentTime`
+- `totalTime`
+- 当前 `Swiper` 页索引
+- `LyricController`
+- 点光按钮的 `PointLightOptions`
+
+### 派生规则
+
+- `currentTime` 由 `playbackPositionMs` 转换为 `mm:ss` 或 `hh:mm:ss` 文本。
+- `totalTime` 由 `playbackDurationMs` 转换为时间文本。
+- 当 `currentSong` 或 `currentSong.lyricContent` 变化时,重新解析歌词并更新 `LyricController`。
+- 当 `playbackPositionMs` 变化时,同步刷新 `currentTime`,并推动歌词滚动位置。
+
+## 页面结构设计
+
+### 1. 顶部封面信息层
+
+保留当前独立页的封面背景模糊与主体封面展示,但视觉结构尽量靠拢原播放页:
+
+- 中心展示大封面。
+- 封面下方展示歌名。
+- 歌名下方展示歌手。
+
+封面、歌名、歌手全部直接绑定 `currentSong` 和 `cover`,不保留临时静态文案。
+
+### 2. 中部主体切换层
+
+恢复与原播放页相同的双页结构:
+
+- 第 1 页:封面主体视图。
+- 第 2 页:歌词主体视图。
+
+本阶段只保留最基础的左右切换,不引入更多操作按钮和歌词设置入口。
+
+### 3. 底部播放控制层
+
+底部控制区复用已经独立出的 `entry/src/main/ets/view/player/PlayerControls.ets`,由 `MusiPlayerView.ets` 负责传入:
+
+- `progressValue`
+- `currentTime`
+- `totalTime`
+- `controlPlayStatus`
+- 上一首/播放暂停/下一首回调
+- `onSeek`
+
+这样可以避免在 `MusiPlayerView.ets` 里再次手写一份完整底部控制 UI,也让独立页后续扩展更稳定。
+
+## 歌词设计
+
+本阶段仅恢复“歌词主体显示 + 随进度滚动”。
+
+### 数据来源
+
+优先使用 `currentSong?.lyricContent`。
+
+如果当前歌曲没有内嵌歌词,则歌词区域展示空态提示,不在第一阶段继续接本地 `.lrc` 自动读取、复制歌词、歌词设置等历史能力。
+
+### 控制器
+
+在 `MusiPlayerView.ets` 内创建独立的 `LyricController`,不复用 `LocalMusic.ets` 持有的控制器实例。
+
+### 更新时机
+
+- `currentSong` 切换时:清空旧歌词,解析新歌词并喂给 `LyricController`。
+- `playbackPositionMs` 更新时:调用歌词位置更新逻辑,推动高亮行滚动。
+- 切换到歌词页时:立即同步一次当前位置,避免页面首次切到歌词页时高亮延迟。
+
+## 交互设计
+
+本阶段保留以下基础交互:
+
+- 点击上一首。
+- 点击播放/暂停。
+- 点击下一首。
+- 拖动进度条 seek。
+- 左右切换封面页和歌词页。
+
+本阶段不迁移以下交互:
+
+- 更多菜单。
+- 歌词长按复制。
+- 歌词设置弹层。
+- 长按倍速。
+
+## UI 对齐策略
+
+目标不是逐像素复刻 `LocalMusic.ets`,而是优先恢复用户可感知的主结构一致性。
+
+第一阶段要求:
+
+- 背景继续使用当前歌曲封面模糊。
+- 中部保持“封面/歌词”二页切换结构。
+- 底部恢复时间与进度条。
+- 控制按钮保留当前已验证可用的独立页按钮实现方式。
+
+允许与原页存在的差异:
+
+- 暂不引入更多弹层按钮与歌词设置按钮。
+- 暂不接入频谱和单行歌词。
+- 暂不迁移原页中与特定设备场景绑定的复杂分支。
+
+## 实现步骤
+
+1. 在 `MusiPlayerView.ets` 中补充 `playbackPositionMs`、`playbackDurationMs` 的 `@StorageLink`。
+2. 在页面内部新增时间文本派生逻辑,统一生成 `currentTime` 与 `totalTime`。
+3. 将当前简化中部区域改为 `Swiper`,分别承载封面页与歌词页。
+4. 在 `MusiPlayerView.ets` 中接入 `LyricController`、歌词解析与歌词位置更新逻辑。
+5. 使用 `PlayerControls.ets` 替换当前简化版底部控制区,接入进度与时间。
+6. 复查上一首、播放/暂停、下一首在切歌后是否带动封面、标题、艺术家、歌词与进度一起刷新。
+
+## 验收标准
+
+满足以下条件即可认为第一阶段完成:
+
+- 点击播放/暂停后,按钮图标立即与真实播放状态一致。
+- 点击上一首/下一首后,封面、歌名、歌手同步刷新。
+- 进度条能随播放推进更新。
+- 当前时间与总时长正确显示。
+- 有歌词的歌曲能展示歌词并随进度滚动。
+- 无歌词的歌曲展示空态,页面无异常报错。
+
+## 风险与约束
+
+### 风险 1:歌词链路从原页面剥离时依赖过深
+
+原 `LocalMusic.ets` 中歌词逻辑与设置项、复制歌词、蓝牙歌词、PIP 文本节点等深度耦合。第一阶段必须明确截断边界,只迁移播放页主歌词显示所需的最小链路。
+
+### 风险 2:独立页继续直接堆代码会再次失控
+
+本阶段虽然继续使用 `MusiPlayerView.ets` 作为根组件,但要优先复用已有的独立子组件,例如 `PlayerControls.ets`,避免把原 `LocalMusic.ets` 中的大段 UI 直接整块复制过来。
+
+### 风险 3:状态来源混乱
+
+独立页必须只消费宿主同步后的播放状态,不能一部分走 `AppStorage`,另一部分再去借 `LocalMusic.ets` 实例变量,否则后续仍会出现状态不同步问题。

+ 15 - 0
entry/src/main/ets/common/player/MusicCardManager.ets

@@ -14,6 +14,7 @@ import { MusicCardFormStore } from './MusicCardFormStore'
 import { MusicCardSnapshotStore } from './MusicCardSnapshotStore'
 
 const TAG = 'MusicCardManager'
+const KILLCARD_TRACE = '[KILLCARD_TRACE]'
 const DEFAULT_TIME_TEXT = '00:00'
 const MUSIC_CARD_OPEN_PLAYER_ACTION = 'open_player'
 const MUSIC_CARD_PLAY_PAUSE_ACTION = 'play_pause'
@@ -194,10 +195,15 @@ export class MusicCardManager {
       `coverImageName=${snapshot.coverImageName}, coverPath=${snapshot.coverPath}, hasCoverImage=${snapshot.hasCoverImage}, ` +
       `hasSong=${snapshot.hasSong}, isPlaying=${snapshot.isPlaying}, positionMs=${snapshot.currentPositionMs}, ` +
       `durationMs=${snapshot.durationMs}, lyricLine0=${snapshot.lyricLine0}, lyricLine1=${snapshot.lyricLine1}, lyricLine2=${snapshot.lyricLine2}`)
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} notifyPlaybackStateChanged title=${snapshot.title}, isPlaying=${snapshot.isPlaying}, ` +
+      `position=${snapshot.currentPositionMs}, duration=${snapshot.durationMs}, filePath=${snapshot.filePath}, ` +
+      `coverPath=${snapshot.coverPath}, hasSong=${snapshot.hasSong}`)
     snapshot.updatedAtMs = Date.now()
     const saved = MusicCardSnapshotStore.writeSnapshot(snapshot, context)
     if (!saved) {
       Logger.error(TAG, 'notifyPlaybackStateChanged failed to write snapshot')
+      Logger.error(TAG, `${KILLCARD_TRACE} notifyPlaybackStateChanged failed writeSnapshot=false`)
       return
     }
     this.updateAllForms(context)
@@ -259,12 +265,15 @@ export class MusicCardManager {
     const abilityContext = this.resolveAbilityContext(context)
     if (!abilityContext) {
       Logger.warn(TAG, 'musicCard updateAllForms skipped because abilityContext missing')
+      Logger.warn(TAG, `${KILLCARD_TRACE} updateAllForms skip abilityContext-missing`)
       return
     }
     const formIds = MusicCardFormStore.readFormIds(abilityContext)
     Logger.info(TAG, `musicCard updateAllForms formIds=${JSON.stringify(formIds)}`)
+    Logger.info(TAG, `${KILLCARD_TRACE} updateAllForms formIds=${JSON.stringify(formIds)}`)
     if (formIds.length === 0) {
       Logger.warn(TAG, 'musicCard updateAllForms skipped because no formIds')
+      Logger.warn(TAG, `${KILLCARD_TRACE} updateAllForms skip no-formIds`)
       return
     }
     const snapshot = MusicCardSnapshotStore.readSnapshot(abilityContext)
@@ -274,12 +283,17 @@ export class MusicCardManager {
       `coverImageName=${payload.coverImageName}, coverPath=${payload.coverPath}, hasCoverImage=${payload.hasCoverImage}, ` +
       `hasSong=${payload.hasSong}, isPlaying=${payload.isPlaying}, currentPositionMs=${payload.currentPositionMs}, ` +
       `durationMs=${payload.durationMs}, lyricLine0=${payload.lyricLine0}, lyricLine1=${payload.lyricLine1}, lyricLine2=${payload.lyricLine2}`)
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} updateAllForms payload title=${payload.title}, isPlaying=${payload.isPlaying}, ` +
+      `position=${payload.currentPositionMs}, duration=${payload.durationMs}, coverPath=${payload.coverPath}, ` +
+      `hasSong=${payload.hasSong}`)
     for (let i = 0; i < formIds.length; i++) {
       const formId = formIds[i]
       if (!formId) {
         continue
       }
       Logger.info(TAG, `musicCard updateAllForms update formId=${formId}`)
+      Logger.info(TAG, `${KILLCARD_TRACE} updateAllForms update formId=${formId}`)
       const formCoverData = MusicCardFormCoverResolver.buildFormCoverData(payload.coverPath)
       const bindingPayload = new MusicCardFormBindingData()
       bindingPayload.formId = formId
@@ -304,6 +318,7 @@ export class MusicCardManager {
       const bindingData = formBindingData.createFormBindingData(bindingPayload)
       void Promise.resolve(formProvider.updateForm(formId, bindingData)).catch((error: Object): void => {
         Logger.warn(TAG, `musicCard updateAllForms failed formId=${formId}, error=${error}`)
+        Logger.warn(TAG, `${KILLCARD_TRACE} updateAllForms failed formId=${formId}, error=${error}`)
         MusicCardFormStore.removeFormId(abilityContext, formId)
       })
     }

+ 18 - 1
entry/src/main/ets/common/player/MusicCardSnapshotStore.ets

@@ -4,6 +4,7 @@ import Logger from '../util/Logger'
 import { createEmptyMusicCardSnapshot, MusicCardSnapshot } from './MusicCardSnapshot'
 
 const TAG = 'MusicCardSnapshotStore'
+const KILLCARD_TRACE = '[KILLCARD_TRACE]'
 const STORE_NAME = 'music_card_snapshot_store'
 const SNAPSHOT_KEY = 'music_card_snapshot'
 
@@ -65,6 +66,7 @@ export class MusicCardSnapshotStore {
   static readSnapshot(context?: common.Context): MusicCardSnapshot {
     const preferences = resolvePreferences(context)
     if (!preferences) {
+      Logger.warn(TAG, `${KILLCARD_TRACE} readSnapshot fallback empty because preferences-missing`)
       return createEmptyMusicCardSnapshot()
     }
     let raw = ''
@@ -75,16 +77,24 @@ export class MusicCardSnapshotStore {
       }
     } catch (error) {
       Logger.error(TAG, `readSnapshot failed: ${formatError(error as Object)}`)
+      Logger.error(TAG, `${KILLCARD_TRACE} readSnapshot failed error=${formatError(error as Object)}`)
       return createEmptyMusicCardSnapshot()
     }
     if (!raw || raw.length === 0) {
+      Logger.info(TAG, `${KILLCARD_TRACE} readSnapshot empty payload`)
       return createEmptyMusicCardSnapshot()
     }
     try {
       const parsed = JSON.parse(raw) as MusicCardSnapshot
-      return mergeSnapshot(parsed)
+      const snapshot = mergeSnapshot(parsed)
+      Logger.info(TAG,
+        `${KILLCARD_TRACE} readSnapshot title=${snapshot.title}, isPlaying=${snapshot.isPlaying}, ` +
+        `position=${snapshot.currentPositionMs}, duration=${snapshot.durationMs}, ` +
+        `filePath=${snapshot.filePath}, updatedAt=${snapshot.updatedAtMs}`)
+      return snapshot
     } catch (error) {
       Logger.error(TAG, `parseSnapshot failed: ${formatError(error as Object)}`)
+      Logger.error(TAG, `${KILLCARD_TRACE} parseSnapshot failed error=${formatError(error as Object)}`)
       return createEmptyMusicCardSnapshot()
     }
   }
@@ -92,15 +102,22 @@ export class MusicCardSnapshotStore {
   static writeSnapshot(snapshot: MusicCardSnapshot, context?: common.Context): boolean {
     const preferences = resolvePreferences(context)
     if (!preferences) {
+      Logger.warn(TAG, `${KILLCARD_TRACE} writeSnapshot failed because preferences-missing`)
       return false
     }
     try {
       const payload = JSON.stringify(snapshot ?? createEmptyMusicCardSnapshot())
       preferences.putSync(SNAPSHOT_KEY, payload)
       preferences.flushSync()
+      const safeSnapshot = snapshot ?? createEmptyMusicCardSnapshot()
+      Logger.info(TAG,
+        `${KILLCARD_TRACE} writeSnapshot title=${safeSnapshot.title}, isPlaying=${safeSnapshot.isPlaying}, ` +
+        `position=${safeSnapshot.currentPositionMs}, duration=${safeSnapshot.durationMs}, ` +
+        `filePath=${safeSnapshot.filePath}, updatedAt=${safeSnapshot.updatedAtMs}`)
       return true
     } catch (error) {
       Logger.error(TAG, `writeSnapshot failed: ${formatError(error as Object)}`)
+      Logger.error(TAG, `${KILLCARD_TRACE} writeSnapshot failed error=${formatError(error as Object)}`)
       return false
     }
   }

+ 0 - 3
entry/src/main/ets/common/util/PlayerPageOpenDispatchHelper.ets

@@ -1,3 +0,0 @@
-export function resolvePlayerPageOpenDelayMs(mType: number): number {
-  return mType === 0 ? 0 : 32
-}

+ 0 - 10
entry/src/main/ets/controller/MusicPlaybackController.ets

@@ -19,8 +19,6 @@ export interface MusicPlaybackControllerActions {
 }
 
 export interface MusicPlaybackHostActions {
-  showPlayerView: () => void | Promise<void>
-  dismissPlayerView: () => void | Promise<void>
   openPlayList: () => void | Promise<void>
 }
 
@@ -589,14 +587,6 @@ export class MusicPlaybackController {
     await this.playbackCoordinator.seekTo(value, source)
   }
 
-  public async showPlayerView(): Promise<void> {
-    await this.hostActions?.showPlayerView?.()
-  }
-
-  public async dismissPlayerView(): Promise<void> {
-    await this.hostActions?.dismissPlayerView?.()
-  }
-
   public async openPlayList(): Promise<void> {
     await this.hostActions?.openPlayList?.()
   }

+ 48 - 2
entry/src/main/ets/entryability/EntryAbility.ets

@@ -38,6 +38,7 @@ import { MusicCardFormStore } from '../common/player/MusicCardFormStore';
 import { requestPlaybackPlayerOpen } from '../playback/PlaybackHostState';
 import { PlaybackRestoreCoordinator } from '../playback/PlaybackRestoreCoordinator';
 import { BackgroundAudioPlaybackHost } from '../playback/BackgroundAudioPlaybackHost';
+import { savePendingPlaybackActivation } from '../playback/PlaybackActivationStore';
 
 const MUSIC_CARD_ACTION_PATTERN: RegExp = /"ttmusic_music_card_action"\s*:\s*"([^"]*)"/;
 const MUSIC_CARD_FORM_ID_STRING_PATTERN: RegExp = /"ttmusic_music_card_form_id"\s*:\s*"([^"]*)"/;
@@ -45,6 +46,7 @@ const MUSIC_CARD_FORM_ID_NUMBER_PATTERN: RegExp = /"ttmusic_music_card_form_id"\
 const MUSIC_CARD_SOURCE_PATTERN: RegExp = /"ttmusic_music_card_source"\s*:\s*"([^"]*)"/;
 const MUSIC_CARD_SEEK_POSITION_STRING_PATTERN: RegExp = /"ttmusic_music_card_seek_position_ms"\s*:\s*"([^"]*)"/;
 const MUSIC_CARD_SEEK_POSITION_NUMBER_PATTERN: RegExp = /"ttmusic_music_card_seek_position_ms"\s*:\s*(-?\d+)/;
+const KILLCARD_TRACE = '[KILLCARD_TRACE]';
 
 class EmptyRpcParcelable implements rpc.Parcelable {
     marshalling(_dataOut: rpc.MessageSequence): boolean {
@@ -82,6 +84,7 @@ export default class EntryAbility extends UIAbility {
                 Logger.warn('EntryAbility', `[MusicCast] call skipped because action empty payload=${rawText}`);
                 return new EmptyRpcParcelable();
             }
+            this.savePendingMusicCardActivationIfNeeded(action, source);
             this.dispatchMusicCardAction(action, 'call', seekPositionMs);
         } catch (error) {
             const err = error as Error;
@@ -116,7 +119,7 @@ export default class EntryAbility extends UIAbility {
             return;
         }
         if (action === 'open_player') {
-            void MusicPlaybackController.getInstance().showPlayerView();
+            requestPlaybackPlayerOpen();
             return;
         }
         if (action === MusicCardActionConstants.ACTION_SEEK_TO) {
@@ -293,6 +296,7 @@ export default class EntryAbility extends UIAbility {
     private handleMusicCardActionFromWant(want: Want): void {
         const parameters = want.parameters as Object | undefined;
         if (parameters === undefined) {
+            Logger.info('EntryAbility', `${KILLCARD_TRACE} handleMusicCardActionFromWant skip parameters-undefined`);
             return;
         }
         const parametersText: string = JSON.stringify(parameters);
@@ -300,35 +304,61 @@ export default class EntryAbility extends UIAbility {
         const source: string = this.resolveMusicCardSourceFromText(parametersText);
         const formId: string = this.resolveMusicCardFormIdFromText(parametersText);
         const seekPositionMs: string = this.resolveMusicCardSeekPositionFromText(parametersText);
+        Logger.info('EntryAbility',
+            `${KILLCARD_TRACE} handleMusicCardActionFromWant action=${action}, source=${source}, ` +
+            `formId=${formId}, seek=${seekPositionMs}, params=${parametersText}`);
         this.registerMusicCardFormIdIfNeeded(formId, source);
         if (StrUtil.isEmpty(action)) {
+            Logger.info('EntryAbility', `${KILLCARD_TRACE} handleMusicCardActionFromWant skip action-empty`);
             return;
         }
         if (shouldIgnoreWidgetWantAction(action, source)) {
             Logger.info('EntryAbility',
                 `[MusicCast] want ignored because call will handle action=${action}, source=${source}, formId=${formId}`);
+            Logger.info('EntryAbility',
+                `${KILLCARD_TRACE} handleMusicCardActionFromWant ignored action=${action}, source=${source}, formId=${formId}`);
             return;
         }
+        this.savePendingMusicCardActivationIfNeeded(action, source);
         Logger.info('EntryAbility',
             `[MusicCast] want enter action=${action}, source=${source}, formId=${formId}, seek=${seekPositionMs}`);
         this.dispatchMusicCardAction(action, 'want', seekPositionMs);
     }
 
+    private savePendingMusicCardActivationIfNeeded(action: string, source: string): void {
+        const pendingActivation = PlaybackRestoreCoordinator.resolvePendingMusicCardActivation(
+            action,
+            source,
+            PlaybackCoordinator.getInstance().hasRuntime()
+        );
+        if (!pendingActivation) {
+            return;
+        }
+        savePendingPlaybackActivation(pendingActivation);
+        Logger.info('EntryAbility',
+            `[MusicCast] save pending activation action=${action}, source=${source}, pending=${pendingActivation}`);
+    }
+
     /**
      * 根据动作类型把音乐卡片控制命令路由到前台运行时或后台播放宿主。
      * 支持播放/暂停、上一首、下一首、打开播放器以及拖动进度等动作。
      */
     private dispatchMusicCardAction(action: string, source: string, seekPositionMs: string = ''): void {
         Logger.info('EntryAbility', `[MusicCast] dispatch action=${action}, source=${source}, seek=${seekPositionMs}`);
+        Logger.info('EntryAbility',
+            `${KILLCARD_TRACE} dispatchMusicCardAction action=${action}, source=${source}, ` +
+            `seek=${seekPositionMs}, hasRuntime=${PlaybackCoordinator.getInstance().hasRuntime()}`);
         const playbackController: MusicPlaybackController = MusicPlaybackController.getInstance();
         const playbackCoordinator: PlaybackCoordinator = PlaybackCoordinator.getInstance();
         const backgroundHost = BackgroundAudioPlaybackHost.getInstance();
         if (action === MusicCardActionConstants.ACTION_PLAY_PAUSE) {
             if (playbackCoordinator.hasRuntime()) {
                 Logger.info('EntryAbility', '[MusicCast] dispatch -> runtime playOrPause');
+                Logger.info('EntryAbility', `${KILLCARD_TRACE} dispatchMusicCardAction -> runtime playOrPause`);
                 void playbackController.playOrPause();
             } else {
                 Logger.info('EntryAbility', '[MusicCast] dispatch -> background host playOrPause');
+                Logger.info('EntryAbility', `${KILLCARD_TRACE} dispatchMusicCardAction -> background playOrPause`);
                 void backgroundHost.handleControlAction(action);
             }
             return;
@@ -336,9 +366,11 @@ export default class EntryAbility extends UIAbility {
         if (action === MusicCardActionConstants.ACTION_PREVIOUS) {
             if (playbackCoordinator.hasRuntime()) {
                 Logger.info('EntryAbility', '[MusicCast] dispatch -> runtime playPrevious');
+                Logger.info('EntryAbility', `${KILLCARD_TRACE} dispatchMusicCardAction -> runtime playPrevious`);
                 void playbackController.playPrevious();
             } else {
                 Logger.info('EntryAbility', '[MusicCast] dispatch -> background host playPrevious');
+                Logger.info('EntryAbility', `${KILLCARD_TRACE} dispatchMusicCardAction -> background playPrevious`);
                 void backgroundHost.handleControlAction(action);
             }
             return;
@@ -346,29 +378,38 @@ export default class EntryAbility extends UIAbility {
         if (action === MusicCardActionConstants.ACTION_NEXT) {
             if (playbackCoordinator.hasRuntime()) {
                 Logger.info('EntryAbility', '[MusicCast] dispatch -> runtime playNext');
+                Logger.info('EntryAbility', `${KILLCARD_TRACE} dispatchMusicCardAction -> runtime playNext`);
                 void playbackController.playNext();
             } else {
                 Logger.info('EntryAbility', '[MusicCast] dispatch -> background host playNext');
+                Logger.info('EntryAbility', `${KILLCARD_TRACE} dispatchMusicCardAction -> background playNext`);
                 void backgroundHost.handleControlAction(action);
             }
             return;
         }
         if (action === MusicCardActionConstants.ACTION_OPEN_PLAYER) {
             Logger.info('EntryAbility', '[MusicCast] dispatch -> routeToMusicPlayerPage');
+            Logger.info('EntryAbility', `${KILLCARD_TRACE} dispatchMusicCardAction -> routeToMusicPlayerPage`);
             this.routeToMusicPlayerPage();
             return;
         }
         if (action === MusicCardActionConstants.ACTION_SEEK_TO) {
             if (!playbackCoordinator.hasRuntime()) {
                 Logger.info('EntryAbility', `[MusicCast] dispatch -> background host seekTo=${seekPositionMs}`);
+                Logger.info('EntryAbility',
+                    `${KILLCARD_TRACE} dispatchMusicCardAction -> background seekTo=${seekPositionMs}`);
                 void backgroundHost.handleControlAction(action, seekPositionMs);
                 return;
             }
             if (StrUtil.isEmpty(seekPositionMs)) {
                 Logger.warn('EntryAbility', `[MusicCast] seek ignored because seek empty, source=${source}`);
+                Logger.warn('EntryAbility',
+                    `${KILLCARD_TRACE} dispatchMusicCardAction seek ignored empty source=${source}`);
                 return;
             }
             Logger.info('EntryAbility', `[MusicCast] dispatch -> runtime seekTo=${seekPositionMs}`);
+            Logger.info('EntryAbility',
+                `${KILLCARD_TRACE} dispatchMusicCardAction -> runtime seekTo=${seekPositionMs}`);
             void playbackController.seekTo(seekPositionMs, 'music-card-widget');
             return;
         }
@@ -486,10 +527,15 @@ export default class EntryAbility extends UIAbility {
         const openRoute = PlaybackRestoreCoordinator.resolveMusicCardOpenRoute(
             PlaybackCoordinator.getInstance().hasRuntime()
         );
+        Logger.info('EntryAbility',
+            `${KILLCARD_TRACE} routeToMusicPlayerPage openRoute=${openRoute}, ` +
+            `hasRuntime=${PlaybackCoordinator.getInstance().hasRuntime()}`);
         if (openRoute === 'show_player_now') {
-            void MusicPlaybackController.getInstance().showPlayerView();
+            Logger.info('EntryAbility', `${KILLCARD_TRACE} routeToMusicPlayerPage -> showPlayerNow`);
+            requestPlaybackPlayerOpen();
             return;
         }
+        Logger.info('EntryAbility', `${KILLCARD_TRACE} routeToMusicPlayerPage -> requestPlaybackPlayerOpen`);
         requestPlaybackPlayerOpen();
     }
 

+ 7 - 0
entry/src/main/ets/entryformability/MusicCardFormAbility.ets

@@ -17,6 +17,7 @@ const FORM_ID_STRING_PATTERN = /"ohos\.extra\.param\.key\.form_identity"\s*:\s*"
 const FORM_ID_NUMBER_PATTERN = /"ohos\.extra\.param\.key\.form_identity"\s*:\s*(-?\d+)/
 const FORM_ID_TRUE_PATTERN = /"ohos\.extra\.param\.key\.form_identity"\s*:\s*true/
 const FORM_ID_FALSE_PATTERN = /"ohos\.extra\.param\.key\.form_identity"\s*:\s*false/
+const KILLCARD_TRACE = '[KILLCARD_TRACE]'
 
 class MusicCardColdStartParameters {
   ttmusic_music_card_form_id: string = ''
@@ -33,6 +34,7 @@ export default class MusicCardFormAbility extends FormExtensionAbility {
   onAddForm(want: Want): formBindingData.FormBindingData {
     const formId = this.resolveFormIdFromWant(want)
     Logger.info(TAG, `musicCard onAddForm formId=${formId}`)
+    Logger.info(TAG, `${KILLCARD_TRACE} onAddForm formId=${formId}`)
     MusicCardFormStore.addFormId(this.context, formId)
     return formBindingData.createFormBindingData(this.buildPayload(formId))
   }
@@ -43,6 +45,7 @@ export default class MusicCardFormAbility extends FormExtensionAbility {
    */
   onUpdateForm(formId: string): void {
     Logger.info(TAG, `musicCard onUpdateForm formId=${formId}`)
+    Logger.info(TAG, `${KILLCARD_TRACE} onUpdateForm formId=${formId}`)
     MusicCardFormStore.addFormId(this.context, formId)
     const bindingData = formBindingData.createFormBindingData(this.buildPayload(formId))
     void Promise.resolve(formProvider.updateForm(formId, bindingData)).catch((error: Object): void => {
@@ -97,6 +100,10 @@ export default class MusicCardFormAbility extends FormExtensionAbility {
       `coverImageName=${source.coverImageName}, coverPath=${source.coverPath}, hasCoverImage=${source.hasCoverImage}, ` +
       `hasSong=${source.hasSong}, isPlaying=${source.isPlaying}, currentPositionMs=${source.currentPositionMs}, ` +
       `durationMs=${source.durationMs}, lyricLine1=${source.lyricLine1}, lyricLine2=${source.lyricLine2}`)
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} buildPayload formId=${formId}, title=${source.title}, isPlaying=${source.isPlaying}, ` +
+      `position=${source.currentPositionMs}, duration=${source.durationMs}, coverPath=${source.coverPath}, ` +
+      `coverImageName=${source.coverImageName}, hasSong=${source.hasSong}`)
     const formCoverData = MusicCardFormCoverResolver.buildFormCoverData(source.coverPath)
     const payload = new MusicCardFormBindingData()
     payload.formId = formId

+ 5 - 7
entry/src/main/ets/lyric/parse/LyricParser.ts

@@ -1,7 +1,6 @@
 import { IParser } from './IParser';
 import { Lyric } from '../bean/Lyric';
 import { LyricLine } from '../bean/LyricLine';
-import { printD, printW } from '../extensions/Extension';
 import { LyricWord } from '../bean/LyricWord';
 
 /**
@@ -35,7 +34,7 @@ export class LyricParser implements IParser {
 
         // 如果没有时间标签,作为纯文本歌词处理
         if (!hasValidTimeTag) {
-            printD("检测到纯文本歌词(无时间标签),不添加时间标签");
+            // printD("检测到纯文本歌词(无时间标签),不添加时间标签");
             return this.parsePlainTextLyric(src);
         }
 
@@ -43,7 +42,7 @@ export class LyricParser implements IParser {
             let line = src[i]
             if (line == "" || line == "\n" || line == "\r" || line == "\r\n" || line == "[Verse]" || line == "[Chorus]"
                 || line == "[PreChorus]" || line == "[PreChorus]"||line == "[Bridge]") {
-                printW("the lyric line is empty, carriage return or line feed, line index= " + i)
+                // printW("the lyric line is empty, carriage return or line feed, line index= " + i)
                 continue
             }
 
@@ -54,7 +53,7 @@ export class LyricParser implements IParser {
                 return tagPattern.test(line);
             });
             if (shouldIgnore) {
-                printW(`the lyric line contains ignored tag, line index= ${i}`);
+                // printW(`the lyric line contains ignored tag, line index= ${i}`);
                 continue;
             }
             // 修改后的标签检测逻辑,修复英文歌词的时候,部分歌词没有显示出来。
@@ -121,7 +120,6 @@ export class LyricParser implements IParser {
                     // [01:05.49][02:08.40]看不穿 是你失落的魂魄
                     let spr = line.split(']');
                     if (spr.length <= 1) {
-                        printW("the lyric line is no timestamp, line index= " + i)
                         continue
                     }
                     // parse text
@@ -319,7 +317,7 @@ export class LyricParser implements IParser {
         // 支持多种分隔符:`.` 或 `:`
         const parts = timeString.split(/[:.]/);
         if (parts.length  < 2) {
-            printW(`Invalid timeline format: ${timeString}`);
+            // printW(`Invalid timeline format: ${timeString}`);
             return 0;
         }
 
@@ -395,7 +393,7 @@ export class LyricParser implements IParser {
             lyricLines.push(new LyricLine(line, -1, -1))
         }
 
-        printD(`纯文本歌词解析完成,共 ${lyricLines.length} 行歌词`)
+        // printD(`纯文本歌词解析完成,共 ${lyricLines.length} 行歌词`)
         // 标记为纯文本歌词
         let result = new Lyric(artist, title, album, by, offset, lyricLines, true)
         return result

+ 1 - 1
entry/src/main/ets/lyric/view/LyricView2.ets

@@ -1,4 +1,3 @@
-import { duration2text } from '../extensions/Extension';
 import { LyricController } from '../LyricController';
 import { Lyric } from '../bean/Lyric';
 import { ListAdapter } from '../extensions/ListAdapter';
@@ -6,6 +5,7 @@ import { LyricLine } from '../bean/LyricLine';
 import { LyricWord } from '../bean/LyricWord';
 import { transverter, TransverterType, TransverterLanguage } from "@nutpi/chinese_transverter"
 import { LengthMetrics } from '@kit.ArkUI';
+import { duration2text } from '../extensions/Extension';
 
 /**
  * A component to display the lyric with scroll animation.

+ 71 - 546
entry/src/main/ets/pages/NewIndex.ets

@@ -63,6 +63,7 @@ import { PlayingIndicator } from '../view/PlayingIndicator';
 import { FindView } from '../view/FindView';
 import { hdsEffect } from '@kit.UIDesignKit';
 import { MiniPlayerBar } from '../view/MiniPlayerBar';
+import { MusiPlayerView } from '../view/player/MusiPlayerView';
 import {
   resolveMiniPlayerMorphTarget,
 } from '../common/util/PlayerDismissHelper';
@@ -79,8 +80,7 @@ import {
   ensurePlaybackHostStorageDefaults,
   requestPlaybackPlayerOpen,
   requestPlaybackPlaylistOpen,
-  setPlaybackRuntimeReady,
-  showPlaybackPlayer
+  setPlaybackRuntimeReady
 } from '../playback/PlaybackHostState';
 import { getRegisteredPlaybackRuntime } from '../playback/PlaybackRuntimeRegistry';
 import { BackgroundAudioPlaybackHost } from '../playback/BackgroundAudioPlaybackHost';
@@ -88,6 +88,7 @@ import { BackgroundAudioPlaybackHost } from '../playback/BackgroundAudioPlayback
 // import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
 
 const TAG = 'NewIndex'; // 日志标签
+const KILLCARD_TRACE = '[KILLCARD_TRACE]'
 const FIND_DEFAULT_HOME_PROMPT_COUNT_KEY = 'find_default_home_prompt_count'
 const FIND_DEFAULT_HOME_PROMPT_HANDLED_KEY = 'find_default_home_prompt_handled'
 const FIND_DEFAULT_HOME_PROMPT_TARGET_COUNT = 5
@@ -100,13 +101,14 @@ const FIND_DEFAULT_HOME_PROMPT_TARGET_COUNT = 5
 @Entry
 @Component
 struct NewIndex {
+  @State isShowMusicPlayView:boolean = false
   //背景流光控制器
   @State bgController: hdsEffect.ShaderEffectController|undefined = deviceInfo.sdkApiVersion>=20
     &&canIUse("SystemCapability.UIDesign.HDSComponent.Core")?
     new hdsEffect.ShaderEffectController():undefined;
   @State defalut_home_type:number = 0//首页默认类型 可支持首页 媒体库 歌单 网盘
   @State isDetailView: boolean = false; // 是否在艺术家/专辑详情视图
-  @StorageLink('isShowPlay') @Watch('syncMiniPlayerVisibility') isShowPlay: boolean = false;
+  @Provide @Watch('syncMiniPlayerVisibility') isShowPlay: boolean = false;
   @Provide @Watch('onMiniPlayerControlStatusChanged') CONTROL_PlayStatus: number = PlayStatus.INIT;
   @Provide @Watch('onMiniPlayerProgressChanged') progressValue: number = 0;
   @State isShowPrecious: boolean = false //播控条显示上一首按钮
@@ -133,7 +135,6 @@ struct NewIndex {
   @State isMiniPlayerMounted: boolean = true;
   @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0;
   @Provide @Watch('onMiniPlayerCoverChanged') cover: string | undefined = '';
-  // @Provide currentSong: VideoItem | undefined = undefined;
   @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined;
   @StorageLink('CONTROL_PlayStatus') @Watch('onHostMiniPlayerControlStatusChanged')
   hostControlPlayStatus: number = PlayStatus.INIT;
@@ -245,13 +246,6 @@ struct NewIndex {
   private isMiniPlayerModeTransitioning: boolean = false;
   private pendingPlayerPageOpenTimer: number = -1;
   private readonly playbackHostActions: MusicPlaybackHostActions = {
-    showPlayerView: (): void => {
-      LogUtil.info(TAG, '[MiniState] hostActions.showPlayerView -> requestPlaybackPlayerOpen')
-      requestPlaybackPlayerOpen()
-    },
-    dismissPlayerView: (): void => {
-      showPlaybackPlayer(false)
-    },
     openPlayList: (): void => {
       requestPlaybackPlaylistOpen()
     }
@@ -344,8 +338,12 @@ struct NewIndex {
   onBackPress(): boolean | void {
     console.info('onecold onBackPress isDetailView = '+ this.isDetailView);
     console.info('onecold onBackPress mType = '+  this.mType);
+    if (this.isShowMusicPlayView) {
+      this.isShowMusicPlayView = false
+      return true
+    }
     if (this.isShowPlay) {
-      void this.playbackController.dismissPlayerView();
+      this.isShowPlay = false
       return true
     }
     if (this.mType === 4) {
@@ -1229,7 +1227,8 @@ struct NewIndex {
       miniPlayerOrbScale: this.miniPlayerOrbScale,
       miniPlayerOrbTranslateX: this.miniPlayerOrbTranslateX,
       onOpenPlayer: (): void => {
-        this.setShowPlayTrue()
+        this.isShowMusicPlayView = true
+
       },
       onCollapseToOrb: (): void => {
         this.collapseMiniPlayerToOrb()
@@ -1253,577 +1252,103 @@ struct NewIndex {
   }
 
   build() {
-    SideBarContainer(SideBarContainerType.AUTO) {
-      Column() {
-        this.getLeftView()
-      }
-      .backgroundColor(Color.Transparent)
-      Stack() {
-        this.ContentBuild()
-        if (this.isMiniPlayerMounted) {
-          this.MiniPlayerBarBuilder()
-        }
-      }
-      .alignContent(Alignment.Bottom)
-      .width('100%')
-      .height('100%')
-
-      .gesture(SwipeGesture({ direction: SwipeDirection.Horizontal }).onAction((event: GestureEvent) => {
-        if (event) {//手势返回
-          this.getUIContext()?.animateTo({ duration: 555 }, () => {
-            this.isShowDrawer = !this.isShowDrawer
-            if(this.isShowDrawer)
-              this.offsetX = 0
-          })
+    Stack({ alignContent: Alignment.TopStart }) {
+      SideBarContainer(SideBarContainerType.AUTO) {
+        Column() {
+          this.getLeftView()
         }
-      }))
-    }
-    .showControlButton(false)
-    .minContentWidth(0)
-    .sideBarWidth(260)
-    .autoHide(true)
-    .showSideBar($$this.isShowDrawer)
-    .onChange((value: boolean) => {
-      this.isShowDrawer = value
-    })
-
-  }
-
-  @Builder
-  centerName() {
-    Column() {
-      Text(this.currentSong?.name)
-        .fontSize(16)
-        .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
-        .maxLines(1)
-        .fontColor(Color.White)
-        .fontWeight(FontWeight.Bolder);
-      Row() {
-        Text(this.currentSong?.artist)
-          .margin({ top: 2 })
-          .fontSize(13)
-          .textAlign(TextAlign.Start)
-          .maxLines(1)
-          .fontWeight(FontWeight.Bold)
-          .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
-          .fontColor(Color.White)
-      }
-      .visibility(this.currentSong?.artist? Visibility.Visible:Visibility.None)
-    }
-    .alignItems(HorizontalAlign.Center)
-  }
-
-  setShowPlayTrue(){
-    if (this.isMiniPlayerModeTransitioning) {
-      return
-    }
-    this.playbackController.showPlayerView()
-  }
+        .backgroundColor(Color.Transparent)
+        Stack() {
+          this.ContentBuild()
 
-  // 右侧圆球内部内容:封面、暗罩、白色环形进度和中心播放状态指示。
-  @Builder
-  private buildMiniPlayerOrbButtonContent(orbSize: number) {
-    Stack({ alignContent: Alignment.Center }) {
-      Image(StrUtil.isNotEmpty(this.cover) ? this.cover as string : $r('app.media.alt'))
-        .width('100%')
-        .height('100%')
-        .objectFit(ImageFit.Cover)
-        .alt($r('app.media.alt'))
-        .borderRadius(100)
-      Column()
-        .width('100%')
-        .height('100%')
-        .backgroundColor('#66000000')
+          if (this.isMiniPlayerMounted) {
+            this.MiniPlayerBarBuilder()
+          }
 
-      Progress({
-        value: Math.floor(this.progressValue),
-        total: 100,
-        type: ProgressType.Ring,
-      })
-        .color(Color.White)
+          if (this.isShowMusicPlayView) {
+            Stack() {
+              this.MusicPlayBuilder()
+            }
+            .width('100%')
+            .height('100%')
+            .zIndex(99)
+            .transition(TransitionEffect.asymmetric(
+              TransitionEffect.opacity(1),
+              TransitionEffect.OPACITY
+            ))
+          }
+        }
+        .alignContent(Alignment.Bottom)
         .width('100%')
         .height('100%')
-        .style({ strokeWidth: 3 })
-
-      if (this.CONTROL_PlayStatus === PlayStatus.PLAY) {
-        PlayingIndicator({
-          isActive: true,
-          indicatorSize: Math.max(16, Math.floor(orbSize * 0.34)),
-          indicatorColor: '#FFFFFF'
-        })
-      }
-    }
-    .width('100%')
-    .height('100%')
-    .clip(true)
-    .borderRadius(100)
-  }
 
-  // 右侧圆球外层按钮,负责点光、缩放、位移和点击展开。
-  @Builder
-  private MiniPlayerOrbControl() {
-    PointLightContentButton({
-      pointColor: this.themeColor,
-      buttonRadius: this.resolveMiniPlayerOrbSize() / 2,
-      pointLightHeight: 96,
-      pressScale: 0.94,
-      useShadow: true,
-      builder: (): void => {
-        this.buildMiniPlayerOrbButtonContent(this.resolveMiniPlayerOrbSize())
+        .gesture(SwipeGesture({ direction: SwipeDirection.Horizontal }).onAction((event: GestureEvent) => {
+          if (event) {//手势返回
+            this.getUIContext()?.animateTo({ duration: 555 }, () => {
+              this.isShowDrawer = !this.isShowDrawer
+              if(this.isShowDrawer)
+                this.offsetX = 0
+            })
+          }
+        }))
       }
-    })
-      .width(this.resolveMiniPlayerOrbSize())
-      .height(this.resolveMiniPlayerOrbSize())
-      .opacity(this.miniPlayerOrbOpacity)
-      .borderRadius(this.resolveMiniPlayerOrbSize() / 2)
-      .scale({ x: this.miniPlayerOrbScale, y: this.miniPlayerOrbScale, centerX: '50%', centerY: '50%' })
-      .translate({ x: this.miniPlayerOrbTranslateX })
-      .onClick((): void => {
-        this.handleMiniPlayerOrbTap()
+      .showControlButton(false)
+      .minContentWidth(0)
+      .sideBarWidth(260)
+      .autoHide(true)
+      .showSideBar($$this.isShowDrawer)
+      .onChange((value: boolean) => {
+        this.isShowDrawer = value
       })
-  }
-
-  @Builder
-  PlayController() {
-    Row() {
-      this.playConLeft()
-      this.playConRigth()
-    }
-    .width('100%')
-    .height(this.bottomBarHeight)
-    .scale({ x: 1, y: this.miniPlayerContentScaleY, centerX: '50%', centerY: '50%' })
-    .translate({ y: this.miniPlayerContentTranslateY })
-    .hitTestBehavior(HitTestMode.Transparent)
-    .zIndex(2)
-    .opacity(0.9)
-    .onClick(()=>{
-      this.setShowPlayTrue()
-    })
-    .onTouch((event: TouchEvent): void => {
-      if (event.type === TouchType.Down) {
-        this.pointLightOptions = {
-          color: this.themeColor,
-          intensity: 1,
-          height: 60
-        }
-      } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
-        this.pointLightOptions = undefined
-      }
-    })
-    .visualEffect( deviceInfo.sdkApiVersion >= 20
-      ? new hdsEffect.HdsEffectBuilder()
-        .pointLight({
-          options: this.pointLightOptions,
-          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-        })
-        .buildEffect()
-      : undefined)
-    .padding({
-      left: 16,
-      right: 16
-    })
-
-
-  }
-  @Builder
-  private HiCarPlayController() {
-    Stack({ alignContent: Alignment.Center }) {
-      this.hiCarLeadingCluster()
-      this.hiCarCenterInfo()
-    }
-    .opacity(0.9)
-    .width('100%')
-    .height(this.bottomBarHeight)
-    .hitTestBehavior(HitTestMode.Transparent)
-    .zIndex(2)
-    .onClick(()=>{
-      this.setShowPlayTrue()
-    })
-    .onTouch((event: TouchEvent): void => {
-      if (event.type === TouchType.Down) {
-        this.pointLightOptions = {
-          color: this.themeColor,
-          intensity: 1,
-          height: 60
-        }
-      } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
-        this.pointLightOptions = undefined
-      }
-    })
-    .visualEffect(deviceInfo.sdkApiVersion >= 20
-      ? new hdsEffect.HdsEffectBuilder()
-        .pointLight({
-          options: this.pointLightOptions,
-          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-        })
-        .buildEffect()
-      : undefined)
-    .padding({
-      left: 16,
-      right: 16
-    })
-  }
-
-  @Builder
-  private hiCarLeadingCluster() {
-    Row() {
-      this.hiCarCoverControl()
-      this.hiCarPlayConRigth()
-      Row()
-        .layoutWeight(1)
     }
     .width('100%')
     .height('100%')
-    .justifyContent(FlexAlign.Start)
-    .alignItems(VerticalAlign.Center)
-  }
 
-  // 迷你播放条与 HiCar 模式复用同一个封面按钮,统一点击反馈与发光效果。
-  @Builder
-  private buildMiniPlayerCoverContent() {
-    Stack({ alignContent: Alignment.Center }) {
-      Image(StrUtil.isNotEmpty(this.currentSong?.pixelMapPath) ?
-        this.currentSong?.pixelMapPath : $r('app.media.alt'))
-        .width(48)
-        .height(48)
-        .objectFit(ImageFit.Contain)
-        .alt($r('app.media.alt'))
-        .fillColor(this.themeColor)
-        .borderRadius(8)
-        .shadow({
-          radius: 15,
-          type: ShadowType.BLUR,
-          color: 'on_primary'
-        })
-    }
-    .width(48)
-    .height(48)
   }
 
+  //新的播放页,这里引入外部的单独播放页组件
   @Builder
-  private miniPlayerCoverButton() {
-    PointLightContentButton({
-      pointColor: this.themeColor,
-      buttonRadius: 10,
-      pointLightHeight: 88,
-      pressScale: 0.92,
-      useShadow: false,
-      builder: () => {
-        this.buildMiniPlayerCoverContent()
+  MusicPlayBuilder() {
+    MusiPlayerView({
+      onClose: (): void => {
+        this.isShowMusicPlayView = false
       }
     })
-      .width(48)
-      .height(48)
-      .margin({ left: 5 })
-      .zIndex(3)
-      .onClick((): void => {
-        this.collapseMiniPlayerToOrb()
-      })
-  }
-
-  @Builder
-  playConLeft() {
-    Row() {
-      this.miniPlayerCoverButton()
-
-      Column() {
-        Text(this.currentSong?.name)
-          .fontSize(16)
-          .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
-          .maxLines(1)
-          .fontColor(Color.White)
-          .fontWeight(FontWeight.Bolder);
-        Row() {
-          Text(this.currentSong?.artist)
-            .margin({ top: 2 })
-            .fontSize(13)
-            .textAlign(TextAlign.Start)
-            .maxLines(1)
-            .fontWeight(FontWeight.Bold)
-            .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
-            .fontColor(Color.White)
-        }
-        .visibility(this.currentSong?.artist? Visibility.Visible:Visibility.None)
-      }
-      .padding({left:8})
-      .alignItems(HorizontalAlign.Start)
-      .margin({ right: 22 })
-      .onClick((): void => {
-        this.setShowPlayTrue()
-      })
-
-    }
-    .padding({ right: 18 })
-    .layoutWeight(1)
-    .alignItems(VerticalAlign.Center)
-    .justifyContent(FlexAlign.Start)
-
-  }
-
-  @Builder
-  private hiCarCoverControl() {
-    this.miniPlayerCoverButton()
   }
-
   @Builder
-  private hiCarCenterInfo() {
+  centerName() {
     Column() {
       Text(this.currentSong?.name)
         .fontSize(16)
-        .textOverflow({ overflow: TextOverflow.MARQUEE })
+        .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
         .maxLines(1)
-        .textAlign(TextAlign.Center)
         .fontColor(Color.White)
-        .fontWeight(FontWeight.Bolder)
+        .fontWeight(FontWeight.Bolder);
       Row() {
         Text(this.currentSong?.artist)
           .margin({ top: 2 })
           .fontSize(13)
-          .textAlign(TextAlign.Center)
+          .textAlign(TextAlign.Start)
           .maxLines(1)
           .fontWeight(FontWeight.Bold)
-          .textOverflow({ overflow: TextOverflow.MARQUEE })
+          .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
           .fontColor(Color.White)
       }
-      .visibility(this.currentSong?.artist ? Visibility.Visible : Visibility.None)
+      .visibility(this.currentSong?.artist? Visibility.Visible:Visibility.None)
     }
-    .width('42%')
     .alignItems(HorizontalAlign.Center)
-    .onClick((): void => {
-      this.setShowPlayTrue()
-    })
-  }
-
-
-  @Builder
-  private buildPlayPreviousControlContent() {
-    Row() {
-      SymbolGlyph($r('sys.symbol.backward_end_fill'))
-        .fontSize(28)
-        .fontColor([Color.White])
-    }
-    .width('100%')
-    .height('100%')
-    .justifyContent(FlexAlign.Center)
-    .alignItems(VerticalAlign.Center)
-  }
-
-  @Builder
-  private buildPlayToggleControlContent() {
-    Row() {
-      Stack() {
-        Progress({
-          value: Math.floor(this.progressValue),
-          type: ProgressType.Ring,
-        })
-          .color(this.themeColor)
-          .height(38)
-          .aspectRatio(CommonConstants.ASPECT_RATIO)
-        Image(this.CONTROL_PlayStatus === PlayStatus.PLAY ? $r('app.media.hm_pause') : $r('app.media.hm_play2'))
-          .height(36)
-          .width(36)
-          .fillColor(Color.White)
-      }
-    }
-    .width('100%')
-    .height('100%')
-    .justifyContent(FlexAlign.Center)
-    .alignItems(VerticalAlign.Center)
-  }
-
-  @Builder
-  private buildPlayNextControlContent() {
-    Row() {
-      SymbolGlyph($r('sys.symbol.forward_end_fill'))
-        .fontSize(28)
-        .fontColor([Color.White])
-    }
-    .width('100%')
-    .height('100%')
-    .justifyContent(FlexAlign.Center)
-    .alignItems(VerticalAlign.Center)
   }
 
-  @Builder
-  private buildPlayListControlContent() {
-    Row() {
-      SymbolGlyph($r('sys.symbol.music_note_list'))
-        .fontSize(28)
-        .fontColor([Color.White])
-    }
-    .width('100%')
-    .height('100%')
-    .justifyContent(FlexAlign.Center)
-    .alignItems(VerticalAlign.Center)
-  }
-
-  @Builder
-  playConRigth() {
-    Row() {
-      PointLightContentButton({
-        pointColor: this.themeColor,
-        buttonRadius: 22,
-        pointLightHeight: 56,
-        pressScale: 0.88,
-        builder: () => {
-          this.buildPlayPreviousControlContent()
-        }
-      })
-        .width(44)
-        .height(44)
-        .visibility(this.isShowPrecious ? Visibility.Visible : Visibility.None)
-        .displayPriority(2)
-        .onClick(() => {
-          if (this.isMiniPlayerModeTransitioning) {
-            return
-          }
-          this.getUIContext().getHostContext()!.eventHub.emit('playPrevious');
-        })
-      // 播放进度条与播放键共用一个点光点击区域
-      PointLightContentButton({
-        pointColor: this.themeColor,
-        buttonRadius: 23,
-        pointLightHeight: 62,
-        pressScale: 0.9,
-        builder: () => {
-          this.buildPlayToggleControlContent()
-        }
-      })
-        .width(46)
-        .height(46)
-        .displayPriority(3)
-        .onClick(() => {
-          if (this.isMiniPlayerModeTransitioning) {
-            return
-          }
-          this.getUIContext().getHostContext()!.eventHub.emit('playOrPause');
-        })
-
-      PointLightContentButton({
-        pointColor: this.themeColor,
-        buttonRadius: 22,
-        pointLightHeight: 56,
-        pressScale: 0.88,
-        builder: () => {
-          this.buildPlayNextControlContent()
-        }
-      })
-        .width(44)
-        .height(44)
-        .displayPriority(2)
-        .onClick(() => {
-          if (this.isMiniPlayerModeTransitioning) {
-            return
-          }
-          this.getUIContext().getHostContext()!.eventHub.emit('playNext');
-        })
-
-      PointLightContentButton({
-        pointColor: this.themeColor,
-        buttonRadius: 21,
-        pointLightHeight: 52,
-        pressScale: 0.88,
-        builder: () => {
-          this.buildPlayListControlContent()
-        }
-      })
-        .width(42)
-        .height(42)
-        .displayPriority(1)
-        .onClick(() => {
-          if (this.isMiniPlayerModeTransitioning) {
-            return
-          }
-          void this.playbackController.openPlayList()
-        })
+  setShowPlayTrue(){
+    this.isShowPlay = true
+    if (this.isMiniPlayerModeTransitioning) {
+      LogUtil.info(TAG, `${KILLCARD_TRACE} setShowPlayTrue skip transitioning=true`)
+      return
     }
-    .margin({left:5})
-    .justifyContent(FlexAlign.End)
-    .alignItems(VerticalAlign.Center)
-  }
+    LogUtil.info(TAG, `${KILLCARD_TRACE} setShowPlayTrue before isShowPlay=${this.isShowPlay}`)
 
-  @Builder
-  private hiCarPlayConRigth() {
-    Row({space:8}) {
-      PointLightContentButton({
-        pointColor: this.themeColor,
-        buttonRadius: 22,
-        pointLightHeight: 56,
-        pressScale: 0.88,
-        builder: () => {
-          this.buildPlayPreviousControlContent()
-        }
-      })
-        .width(44)
-        .height(44)
-        .visibility(this.isShowPrecious ? Visibility.Visible : Visibility.None)
-        .displayPriority(2)
-        .onClick(() => {
-          if (this.isMiniPlayerModeTransitioning) {
-            return
-          }
-          this.getUIContext().getHostContext()!.eventHub.emit('playPrevious');
-        })
-      PointLightContentButton({
-        pointColor: this.themeColor,
-        buttonRadius: 23,
-        pointLightHeight: 62,
-        pressScale: 0.9,
-        builder: () => {
-          this.buildPlayToggleControlContent()
-        }
-      })
-        .width(46)
-        .height(46)
-        .displayPriority(3)
-        .onClick(() => {
-          if (this.isMiniPlayerModeTransitioning) {
-            return
-          }
-          this.getUIContext().getHostContext()!.eventHub.emit('playOrPause');
-        })
-
-      PointLightContentButton({
-        pointColor: this.themeColor,
-        buttonRadius: 22,
-        pointLightHeight: 56,
-        pressScale: 0.88,
-        builder: () => {
-          this.buildPlayNextControlContent()
-        }
-      })
-        .width(44)
-        .height(44)
-        .displayPriority(2)
-        .onClick(() => {
-          if (this.isMiniPlayerModeTransitioning) {
-            return
-          }
-          this.getUIContext().getHostContext()!.eventHub.emit('playNext');
-        })
-
-      PointLightContentButton({
-        pointColor: this.themeColor,
-        buttonRadius: 21,
-        pointLightHeight: 52,
-        pressScale: 0.88,
-        builder: () => {
-          this.buildPlayListControlContent()
-        }
-      })
-        .width(42)
-        .height(42)
-        .displayPriority(1)
-        .onClick(() => {
-          if (this.isMiniPlayerModeTransitioning) {
-            return
-          }
-          void this.playbackController.openPlayList()
-        })
-    }
-    .margin({ left: 16 })
-    .justifyContent(FlexAlign.Start)
-    .alignItems(VerticalAlign.Center)
+    requestPlaybackPlayerOpen()
+    LogUtil.info(TAG, `${KILLCARD_TRACE} setShowPlayTrue after requestPlaybackPlayerOpen isShowPlay=${this.isShowPlay}`)
   }
 
 

+ 107 - 0
entry/src/main/ets/playback/BackgroundAudioPlaybackHost.ets

@@ -27,6 +27,7 @@ import { MusicPlaybackController } from '../controller/MusicPlaybackController'
 import { PlaybackRuntime } from '../controller/PlaybackCoordinator'
 import { VideoItem } from '../viewmodel/VideoItem'
 import { PlaybackSnapshotStore } from './PlaybackSnapshotStore'
+import { PlaybackViewState, PlaybackViewStateCenter } from './PlaybackViewStateCenter'
 import {
   BackgroundAudioControlDecision,
   BackgroundAudioControlKind,
@@ -37,6 +38,7 @@ import {
 import { PlaybackSnapshot, PlaybackSnapshotSong, resolvePlaybackSnapshotProgressValue } from './model/PlaybackSnapshot'
 
 const TAG = 'BackgroundAudioHost'
+const KILLCARD_TRACE = '[KILLCARD_TRACE]'
 const PLAYER_ID = 'audioIjkId'
 const PROGRESS_INTERVAL_MS = 1000
 const PLAYBACK_VERIFY_DELAY_MS = 200
@@ -46,6 +48,15 @@ interface PlaybackSnapshotWriter {
   write(snapshot: PlaybackSnapshot): void
 }
 
+export class BackgroundAudioPlaybackViewState {
+  currentSong: VideoItem | undefined = undefined
+  controlPlayStatus: number = PlayStatus.INIT
+  progressValue: number = 0
+  playbackPositionMs: number = 0
+  playbackDurationMs: number = 0
+  cover: string | undefined = ''
+}
+
 export class BackgroundAudioPlaybackHost {
   private static instance: BackgroundAudioPlaybackHost
 
@@ -64,6 +75,7 @@ export class BackgroundAudioPlaybackHost {
   private creatingPlaybackSession: boolean = false
   private playbackSessionCallbacksRegistered: boolean = false
   private snapshotStore: PlaybackSnapshotWriter = new PlaybackSnapshotStore()
+  private viewStateObservers: Array<(state: BackgroundAudioPlaybackViewState) => void> = []
   private readonly runtime: PlaybackRuntime = {
     playQueue: async (queue: VideoItem[], startIndex: number, source: string, playType?: number): Promise<void> => {
       await this.playQueue(queue, startIndex, source, playType)
@@ -101,6 +113,33 @@ export class BackgroundAudioPlaybackHost {
     return this.runtime
   }
 
+  public getPlaybackViewState(): BackgroundAudioPlaybackViewState {
+    const state = new BackgroundAudioPlaybackViewState()
+    state.currentSong = this.currentSong
+    state.controlPlayStatus = this.resolveCurrentPlayStatus()
+    state.cover = this.currentSong?.pixelMapPath ?? ''
+
+    if (this.player && this.currentSong) {
+      const positionMs = Math.max(0, this.player.getCurrentPosition())
+      const durationMs = this.resolveDurationMs()
+      state.playbackPositionMs = positionMs
+      state.playbackDurationMs = durationMs
+      state.progressValue = resolvePlaybackSnapshotProgressValue(positionMs, durationMs, 100)
+      return state
+    }
+
+    return state
+  }
+
+  public subscribePlaybackViewState(callback: (state: BackgroundAudioPlaybackViewState) => void): void {
+    this.viewStateObservers.push(callback)
+    callback(this.getPlaybackViewState())
+  }
+
+  public unsubscribePlaybackViewState(callback: (state: BackgroundAudioPlaybackViewState) => void): void {
+    this.viewStateObservers = this.viewStateObservers.filter((observer): boolean => observer !== callback)
+  }
+
   public async playQueue(queue: VideoItem[], startIndex: number, source: string, playType?: number): Promise<void> {
     if (queue.length <= 0) {
       Logger.warn(TAG, `[MusicCast] playQueue ignored empty queue source=${source}`)
@@ -420,7 +459,9 @@ export class BackgroundAudioPlaybackHost {
         `[MusicCast] verifyPlaybackStarted source=${source}, token=${token}, isPlaying=${isPlayingNow}, position=${currentPosition}`)
       if (isPlayingNow && currentPosition > 0) {
         this.isPlaying = true
+        this.syncHostStorage(PlayStatus.PLAY)
         this.syncPlaybackSessionState(PlayStatus.PLAY)
+        this.publishSnapshot(true)
         return
       }
       Logger.warn(TAG,
@@ -437,7 +478,9 @@ export class BackgroundAudioPlaybackHost {
         Logger.info(TAG,
           `[MusicCast] verifyPlaybackStarted afterRetry source=${source}, token=${token}, ` +
           `isPlaying=${retryPlaying}, position=${retryPosition}`)
+        this.syncHostStorage(retryPlaying ? PlayStatus.PLAY : PlayStatus.PAUSE)
         this.syncPlaybackSessionState(retryPlaying ? PlayStatus.PLAY : PlayStatus.PAUSE)
+        this.publishSnapshot(retryPlaying)
       }, PLAYBACK_VERIFY_RETRY_DELAY_MS)
     }, PLAYBACK_VERIFY_DELAY_MS)
   }
@@ -715,8 +758,13 @@ export class BackgroundAudioPlaybackHost {
   }
 
   private publishSnapshot(isPlaying: boolean): void {
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} publishSnapshot enter isPlaying=${isPlaying}, ` +
+      `currentSong=${this.currentSong?.name ?? ''}, currentIndex=${this.currentIndex}, ` +
+      `queueLength=${this.queue.length}, contextReady=${this.context !== undefined}`)
     this.persistPlaybackSnapshot(isPlaying)
     if (!this.context) {
+      Logger.warn(TAG, `${KILLCARD_TRACE} publishSnapshot skip context-missing`)
       return
     }
     const options = new MusicCardPlaybackStateOptions()
@@ -726,6 +774,10 @@ export class BackgroundAudioPlaybackHost {
     options.durationMs = this.resolveDurationMs()
     options.lyricText = this.currentSong?.lyricContent
     options.coverPath = this.currentSong?.pixelMapPath
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} publishSnapshot notifyCard song=${this.currentSong?.name ?? ''}, ` +
+      `isPlaying=${options.isPlaying}, position=${options.positionMs}, duration=${options.durationMs}, ` +
+      `cover=${options.coverPath ?? ''}`)
     MusicCardManager.getInstance().notifyPlaybackStateChanged(
       this.context,
       MusicCardManager.getInstance().buildSnapshotFromPlaybackState(options)
@@ -738,6 +790,10 @@ export class BackgroundAudioPlaybackHost {
       `[MiniState] persistPlaybackSnapshot song=${snapshot.currentSongName}, index=${snapshot.currentIndex}, ` +
       `isPlaying=${snapshot.isPlaying}, shouldResume=${snapshot.shouldResumeWhenActivated}, ` +
       `position=${snapshot.positionMs}, duration=${snapshot.durationMs}`)
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} persistPlaybackSnapshot song=${snapshot.currentSongName}, index=${snapshot.currentIndex}, ` +
+      `isPlaying=${snapshot.isPlaying}, shouldResume=${snapshot.shouldResumeWhenActivated}, ` +
+      `position=${snapshot.positionMs}, duration=${snapshot.durationMs}, queueLength=${snapshot.queue.length}`)
     this.snapshotStore.write(snapshot)
   }
 
@@ -828,9 +884,17 @@ export class BackgroundAudioPlaybackHost {
     AppStorage.setOrCreate('cover', this.currentSong?.pixelMapPath ?? '')
     AppStorage.setOrCreate('currentSongName', this.currentSong?.name ?? '')
     AppStorage.setOrCreate('currentSongArtist', this.currentSong?.artist ?? '')
+    this.notifyPlaybackViewState(
+      this.buildPlaybackViewState(status, progressValue, positionMs, durationMs)
+    )
     Logger.info(TAG,
       `[MusicCast] syncHostStorage status=${status}, currentSong=${this.currentSong?.name ?? ''}, ` +
       `cover=${this.currentSong?.pixelMapPath ?? ''}`)
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} syncHostStorage status=${status}, isPlaying=${isPlaying}, ` +
+      `currentSong=${this.currentSong?.name ?? ''}, currentIndex=${this.currentIndex}, ` +
+      `progress=${progressValue}, position=${positionMs}, duration=${durationMs}, ` +
+      `cover=${this.currentSong?.pixelMapPath ?? ''}`)
   }
 
   private syncPlaybackDisplayState(progressValue: number, positionMs: number, durationMs: number): void {
@@ -838,6 +902,49 @@ export class BackgroundAudioPlaybackHost {
     AppStorage.setOrCreate('playbackPositionMs', positionMs)
     AppStorage.setOrCreate('playbackDurationMs', durationMs)
     AppStorage.setOrCreate('playbackIsFavorite', this.currentSong?.isFav === 1)
+    this.notifyPlaybackViewState(
+      this.buildPlaybackViewState(this.resolveCurrentPlayStatus(), progressValue, positionMs, durationMs)
+    )
+  }
+
+  private resolveCurrentPlayStatus(): number {
+    if (this.isPlaying) {
+      return PlayStatus.PLAY
+    }
+    if (this.isPrepared || this.currentSong !== undefined) {
+      return PlayStatus.PAUSE
+    }
+    return PlayStatus.INIT
+  }
+
+  private buildPlaybackViewState(controlPlayStatus: number, progressValue: number, positionMs: number,
+    durationMs: number): BackgroundAudioPlaybackViewState {
+    const state = new BackgroundAudioPlaybackViewState()
+    state.currentSong = this.currentSong
+    state.controlPlayStatus = controlPlayStatus
+    state.progressValue = progressValue
+    state.playbackPositionMs = positionMs
+    state.playbackDurationMs = durationMs
+    state.cover = this.currentSong?.pixelMapPath ?? ''
+    return state
+  }
+
+  private notifyPlaybackViewState(state: BackgroundAudioPlaybackViewState): void {
+    Logger.info(TAG,
+      `[PLAYER_DETACH] host notify song=${state.currentSong?.name ?? ''}, status=${state.controlPlayStatus}, ` +
+      `position=${state.playbackPositionMs}, duration=${state.playbackDurationMs}, progress=${state.progressValue}, ` +
+      `cover=${state.cover ?? ''}, observers=${this.viewStateObservers.length}`)
+    const playbackViewState = new PlaybackViewState()
+    playbackViewState.currentSong = state.currentSong
+    playbackViewState.controlPlayStatus = state.controlPlayStatus
+    playbackViewState.progressValue = state.progressValue
+    playbackViewState.playbackPositionMs = state.playbackPositionMs
+    playbackViewState.playbackDurationMs = state.playbackDurationMs
+    playbackViewState.cover = state.cover
+    PlaybackViewStateCenter.getInstance().publish('background-audio-host', playbackViewState)
+    for (let i = 0; i < this.viewStateObservers.length; i++) {
+      this.viewStateObservers[i](state)
+    }
   }
 
   private toggleCurrentSongFavoriteState(): void {

+ 0 - 8
entry/src/main/ets/playback/PlaybackHostState.ets

@@ -1,9 +1,6 @@
 import { PlayStatus } from '../common/PlayStatus'
 
 export function ensurePlaybackHostStorageDefaults(): void {
-  if (AppStorage.get('isShowPlay') === undefined) {
-    AppStorage.setOrCreate('isShowPlay', false)
-  }
   if (AppStorage.get('playbackRuntimeReady') === undefined) {
     AppStorage.setOrCreate('playbackRuntimeReady', false)
   }
@@ -42,17 +39,12 @@ export function ensurePlaybackHostStorageDefaults(): void {
   }
 }
 
-export function showPlaybackPlayer(show: boolean): void {
-  AppStorage.setOrCreate('isShowPlay', show)
-}
-
 export function setPlaybackRuntimeReady(ready: boolean): void {
   AppStorage.setOrCreate('playbackRuntimeReady', ready)
 }
 
 export function requestPlaybackPlayerOpen(): void {
   const currentToken = AppStorage.get<number>('playbackHostOpenRequestToken') ?? 0
-  AppStorage.setOrCreate('isShowPlay', true)
   AppStorage.setOrCreate('playbackHostOpenRequestToken', currentToken + 1)
 }
 

+ 17 - 0
entry/src/main/ets/playback/PlaybackRestoreCoordinator.ets

@@ -26,4 +26,21 @@ export class PlaybackRestoreCoordinator {
     }
     return undefined
   }
+
+  public static resolvePendingMusicCardActivation(action: string, source: string,
+    hasRuntime: boolean): PlaybackActivationActionType | undefined {
+    if (hasRuntime) {
+      return undefined
+    }
+    if (source !== MusicCardActionConstants.ACTION_SOURCE_WIDGET) {
+      return undefined
+    }
+    const activationAction = PlaybackRestoreCoordinator.resolveMusicCardActivationAction(action)
+    if (activationAction === 'resume_play'
+      || activationAction === 'play_previous'
+      || activationAction === 'play_next') {
+      return activationAction
+    }
+    return undefined
+  }
 }

+ 289 - 126
entry/src/main/ets/view/LocalMusic.ets

@@ -135,12 +135,16 @@ import { PlaybackSnapshot as PlaybackStateBridgeSnapshot, PlaybackStateBridge }
 import { consumePendingPlaybackActivation } from '../playback/PlaybackActivationStore';
 import { peekPendingPlaybackActivation } from '../playback/PlaybackActivationStore';
 import { PlaybackSnapshotStore } from '../playback/PlaybackSnapshotStore'
-import { PlaybackSnapshot as HostPlaybackSnapshot } from '../playback/model/PlaybackSnapshot'
+import {
+  PlaybackSnapshot as HostPlaybackSnapshot,
+  PlaybackSnapshotSong as HostPlaybackSnapshotSong
+} from '../playback/model/PlaybackSnapshot'
 import { lyricService, SongData } from '../common/service/LyricService';
 import { shouldResolveRemoteLyricOnPlay, tryResolveRemotePlaybackLyric } from '../common/util/RemotePlaybackLyricUtil';
 import { resolvePlayerDismissMorphTarget } from '../common/util/PlayerDismissHelper';
 import { resolvePlayerLyricUpdateTargets } from '../common/util/PlayerLyricUpdateTargetsHelper';
 import { isBaiduStreamingPlaybackUrl, resolveSeekGuardDecision } from '../common/util/PlaybackSeekGuard';
+import { PlayerPage } from './player/PlayerPage';
 import {
   cloneVideoItem,
   preloadNextSongIfNeeded,
@@ -237,6 +241,7 @@ import { LyricParser } from '../lyric/parse/LyricParser';
 import { Lyric } from '../lyric/bean/Lyric';
 
 const TAG = 'LocalMusic';
+const KILLCARD_TRACE = '[KILLCARD_TRACE]';
 const HICAR_LOG_TAG = 'HiCarWindow';
 const PREF_PLAY_COVER_SCALE_GUIDE_SHOWN = 'play_cover_scale_guide_show';
 
@@ -672,6 +677,7 @@ export struct LocalMusic {
   private isHostSnapshotDisplayOnly: boolean = false
   private playQueueScope: 'view' | 'history' | 'fav' | 'playlist' | 'single' | 'find_paged_local' = 'view'
   private isHydratingPlayList: boolean = false
+  private suppressPlaybackCardStatePublish: boolean = false
   private preserveExistingQueueOnNextPlay: boolean = false
   private nextPlayQueueScope: 'view' | 'history' | 'fav' | 'playlist' | 'single' | 'find_paged_local' = 'view'
   private findPagedLocalQueuePageIndex: number = 0
@@ -838,7 +844,7 @@ export struct LocalMusic {
   // @Consume currentSong: VideoItem | undefined;
   @StorageLink('currentSong') @Watch('onHomeBgCurrentSongChange') currentSong: VideoItem | undefined = undefined;
   // @StorageLink('isPlay') isPlay: boolean = false;
-  @StorageLink('isShowPlay') isShowPlay: boolean = false;
+  @Consume isShowPlay: boolean;
   @State isShowCoverScaleGuide: boolean = false
   @StorageLink('songList') songList: Array<VideoItem> = [];
   @State translateY: number = 0;
@@ -1069,12 +1075,19 @@ export struct LocalMusic {
 
   private onPlaybackHostOpenRequestChange(): void {
     if (this.playbackHostOpenRequestToken === this.lastHandledPlaybackHostOpenRequestToken) {
+      Logger.info(TAG,
+        `${KILLCARD_TRACE} onPlaybackHostOpenRequestChange skip sameToken token=${this.playbackHostOpenRequestToken}`)
       return
     }
     this.lastHandledPlaybackHostOpenRequestToken = this.playbackHostOpenRequestToken
     Logger.info(TAG,
       `[MiniState] onPlaybackHostOpenRequestChange token=${this.playbackHostOpenRequestToken}, ` +
       `isShowPlay=${this.isShowPlay}, songListLength=${this.songList.length}, currentSong=${this.currentSong?.name ?? ''}`)
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} onPlaybackHostOpenRequestChange enter token=${this.playbackHostOpenRequestToken}, ` +
+      `isShowPlay=${this.isShowPlay}, songListLength=${this.songList.length}, ` +
+      `currentSong=${this.currentSong?.name ?? ''}, controlStatus=${this.CONTROL_PlayStatus}, ` +
+      `videoUrl=${this.videoUrl}, hostDisplayOnly=${this.isHostSnapshotDisplayOnly}`)
     if (!this.isShowPlay) {
       this.setShowPlayTrue()
     }
@@ -1112,9 +1125,15 @@ export struct LocalMusic {
   }
   // 组件生命周期
   aboutToAppear() {
+    this.suppressPlaybackCardStatePublish = false
     setPlaybackRuntimeReady(true)
     this.playbackSnapshotHydrated = this.hydrateFromPlaybackSnapshot()
     this.syncPlaybackUiStateFromControlStatus()
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} aboutToAppear hydrated=${this.playbackSnapshotHydrated}, ` +
+      `songListLength=${this.songList.length}, currentSong=${this.currentSong?.name ?? ''}, ` +
+      `controlStatus=${this.CONTROL_PlayStatus}, isShowPlay=${this.isShowPlay}, ` +
+      `hostDisplayOnly=${this.isHostSnapshotDisplayOnly}`)
     this.onPlaybackHostOpenRequestChange()
     this.onPlaybackHostPlaylistRequestChange()
 
@@ -1267,6 +1286,7 @@ export struct LocalMusic {
 
           // 同步更新 AppStorage
           AppStorage.setOrCreate('currentSong', this.currentSong);
+          this.syncDetachedPlayerState();
         } else {
           Logger.info(TAG, `LocalMusic: 跳过非当前播放歌曲的元数据更新: ${payload.filePath}, 当前: ${this.currentSong?.filePath}`);
         }
@@ -2337,6 +2357,11 @@ export struct LocalMusic {
   // 组件消失生命周期
   aboutToDisappear() {
     console.info('LifeCycleComponent aboutToDisappear');
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} aboutToDisappear enter controlStatus=${this.CONTROL_PlayStatus}, ` +
+      `isPlaying=${this.isPlaying}, songListLength=${this.songList.length}, ` +
+      `currentSong=${this.currentSong?.name ?? ''}, hostDisplayOnly=${this.isHostSnapshotDisplayOnly}, ` +
+      `suppressPublish=${this.suppressPlaybackCardStatePublish}`)
     setPlaybackRuntimeReady(false)
     this.playbackSnapshotHydrated = false
     this.pendingInternalLoopModeReport = -1
@@ -2389,7 +2414,17 @@ export struct LocalMusic {
     this.audioInterruptResumePending = false;
     this.mIjkMediaPlayer.setScreenOnWhilePlaying(false);
     if (this.CONTROL_PlayStatus != PlayStatus.INIT && !this.isHostSnapshotDisplayOnly) {
+      Logger.info(TAG,
+        `${KILLCARD_TRACE} aboutToDisappear persist-and-stop reason=control-active, ` +
+        `controlStatus=${this.CONTROL_PlayStatus}, currentSong=${this.currentSong?.name ?? ''}`)
+      this.persistLocalPlaybackRecoverySnapshot('aboutToDisappear')
+      this.notifyPlaybackCardStateChanged()
+      this.suppressPlaybackCardStatePublish = true
       this.stop();
+    } else {
+      Logger.info(TAG,
+        `${KILLCARD_TRACE} aboutToDisappear skip persist-stop controlStatus=${this.CONTROL_PlayStatus}, ` +
+        `hostDisplayOnly=${this.isHostSnapshotDisplayOnly}`)
     }
     this.isHostSnapshotDisplayOnly = false;
     this.mIjkMediaPlayer.off('audioInterrupt');
@@ -2887,21 +2922,36 @@ export struct LocalMusic {
 
   private hydrateFromPlaybackSnapshot(): boolean {
     if (this.playbackSnapshotHydrated) {
+      Logger.info(TAG, `${KILLCARD_TRACE} hydrateFromPlaybackSnapshot skip alreadyHydrated=true`)
       return true
     }
     const snapshot = this.playbackSnapshotStore.read()
     if (!snapshot) {
+      Logger.info(TAG, `${KILLCARD_TRACE} hydrateFromPlaybackSnapshot skip snapshot-empty`)
       return false
     }
     const pendingActivation = peekPendingPlaybackActivation()
-    const shouldHydrateAsPlaying = shouldHydrateSnapshotAsPlaying(snapshot.isPlaying, pendingActivation)
+    const hostDisplayPlaying = isPlaybackControlPlaying(
+      (AppStorage.get('CONTROL_PlayStatus') as number | undefined) ?? PlayStatus.INIT
+    )
+    const shouldHydrateAsPlaying = hostDisplayPlaying || shouldHydrateSnapshotAsPlaying(snapshot.isPlaying, pendingActivation)
     Logger.info(TAG,
       `[MiniState] hydrate snapshot song=${snapshot.currentSongName}, index=${snapshot.currentIndex}, ` +
       `isPlaying=${snapshot.isPlaying}, shouldResume=${snapshot.shouldResumeWhenActivated}, ` +
-      `pendingAction=${pendingActivation?.actionType ?? ''}, displayPlaying=${shouldHydrateAsPlaying}, ` +
+      `pendingAction=${pendingActivation?.actionType ?? ''}, hostDisplayPlaying=${hostDisplayPlaying}, ` +
+      `displayPlaying=${shouldHydrateAsPlaying}, ` +
       `position=${snapshot.positionMs}, duration=${snapshot.durationMs}`)
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} hydrateFromPlaybackSnapshot snapshot song=${snapshot.currentSongName}, ` +
+      `index=${snapshot.currentIndex}, isPlaying=${snapshot.isPlaying}, ` +
+      `shouldResume=${snapshot.shouldResumeWhenActivated}, pendingAction=${pendingActivation?.actionType ?? ''}, ` +
+      `hostDisplayPlaying=${hostDisplayPlaying}, displayPlaying=${shouldHydrateAsPlaying}, ` +
+      `queueLength=${snapshot.queue.length}, position=${snapshot.positionMs}, duration=${snapshot.durationMs}`)
     const recovered = this.playbackSnapshotStore.recoverQueueState(snapshot)
     if (ArrayUtil.isEmpty(recovered.queue) || recovered.currentSong === undefined) {
+      Logger.warn(TAG,
+        `${KILLCARD_TRACE} hydrateFromPlaybackSnapshot recover-failed queueLength=${recovered.queue.length}, ` +
+        `currentIndex=${recovered.currentIndex}, hasCurrentSong=${recovered.currentSong !== undefined}`)
       return false
     }
 
@@ -2947,11 +2997,19 @@ export struct LocalMusic {
     AppStorage.setOrCreate('musicPlayType', this.playType)
     this.playbackSnapshotHydrated = true
     this.isHostSnapshotDisplayOnly = true
+    if (pendingActivation) {
+      consumePendingPlaybackActivation()
+    }
     Logger.info(TAG,
       `从后台宿主快照恢复页面状态 song=${this.currentSong.name}, index=${this.curIndex}, ` +
       `isPlaying=${shouldHydrateAsPlaying}, rawSnapshotPlaying=${snapshot.isPlaying}, ` +
       `shouldResume=${snapshot.shouldResumeWhenActivated}, pendingAction=${pendingActivation?.actionType ?? ''}, ` +
       `position=${snapshot.positionMs}`)
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} hydrateFromPlaybackSnapshot applied song=${this.currentSong.name}, index=${this.curIndex}, ` +
+      `displayPlaying=${shouldHydrateAsPlaying}, rawSnapshotPlaying=${snapshot.isPlaying}, ` +
+      `controlStatus=${this.CONTROL_PlayStatus}, progress=${this.progressValue}, ` +
+      `isShowPlay=${this.isShowPlay}, hostDisplayOnly=${this.isHostSnapshotDisplayOnly}`)
     return true
   }
 
@@ -2976,6 +3034,11 @@ export struct LocalMusic {
       `[MiniState] refreshPlaybackBarFromSnapshot status=${this.CONTROL_PlayStatus}, isPlaying=${displayPlaying}, ` +
       `pendingAction=${pendingActivation?.actionType ?? ''}, progress=${this.progressValue}, ` +
       `song=${snapshot.currentSongName}, position=${snapshot.positionMs}, duration=${snapshot.durationMs}`)
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} refreshPlaybackBarFromSnapshot status=${this.CONTROL_PlayStatus}, ` +
+      `displayPlaying=${displayPlaying}, pendingAction=${pendingActivation?.actionType ?? ''}, ` +
+      `progress=${this.progressValue}, song=${snapshot.currentSongName}, ` +
+      `position=${snapshot.positionMs}, duration=${snapshot.durationMs}`)
   }
 
   private syncLegacyPageSessionMetadata(song: VideoItem | undefined, durationMs: number): void {
@@ -4079,12 +4142,12 @@ export struct LocalMusic {
             }
           })
       }
-      .bindContentCover($$this.isShowPlay, this.MusicPlayBuilder(), {
-        modalTransition: ModalTransition.DEFAULT,
-        onWillDisappear: () => {
-          this.setShowPlayFalse()
-        },
-      })
+      // .bindContentCover($$this.isShowPlay, this.MusicPlayBuilder(), {
+      //   modalTransition: ModalTransition.DEFAULT,
+      //   onWillDisappear: () => {
+      //     this.setShowPlayFalse()
+      //   },
+      // })
     }
     .alignContent(Alignment.Bottom)
     .width('100%')
@@ -9459,12 +9522,18 @@ export struct LocalMusic {
   }
 
   setShowPlayTrue(){
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} setShowPlayTrue before isShowPlay=${this.isShowPlay}, ` +
+      `songListLength=${this.songList.length}, currentSong=${this.currentSong?.name ?? ''}`)
     this.getUIContext()?.animateTo({
       duration: 500,
       curve: Curve.Friction
     }, () => {
       this.isShowPlay = true;
       this.showCoverScaleGuideIfNeeded()
+      Logger.info(TAG,
+        `${KILLCARD_TRACE} setShowPlayTrue applied isShowPlay=${this.isShowPlay}, ` +
+        `currentSong=${this.currentSong?.name ?? ''}, videoUrl=${this.videoUrl}`)
     });
   }
 
@@ -9482,13 +9551,21 @@ export struct LocalMusic {
     Logger.info(TAG,
       `[MiniState] showPlayerView songListLength=${this.songList.length}, currentSong=${this.currentSong?.name ?? ''}, ` +
       `videoUrl=${this.videoUrl}, isShowPlay=${this.isShowPlay}`)
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} showPlayerView enter songListLength=${this.songList.length}, ` +
+      `currentSong=${this.currentSong?.name ?? ''}, videoUrl=${this.videoUrl}, ` +
+      `isShowPlay=${this.isShowPlay}, controlStatus=${this.CONTROL_PlayStatus}`)
     console.info('onecold  showPlayerView  this.songList'+ JSON.stringify(this.songList))
     if (ArrayUtil.isNotEmpty(this.songList)) {
+      Logger.info(TAG,
+        `${KILLCARD_TRACE} showPlayerView ready queueLength=${this.songList.length}, ` +
+        `currentSong=${this.currentSong?.name ?? ''}`)
       // this.setShowPlayTrue()
       let lyricPath = this.videoUrl.substring(0, this.videoUrl.lastIndexOf('.')) + '.lrc';
       this.initLyric(lyricPath);
       this.startAutoHide();
     } else {
+      Logger.warn(TAG, `${KILLCARD_TRACE} showPlayerView blocked empty-queue`)
       ToastUtil.showToast('当前播放列表为空,请先导入音乐。');
     }
   }
@@ -12710,126 +12787,100 @@ export struct LocalMusic {
     })
   }
 
-  @Builder
-  MusicPlayBuilder() {
-    Column() {
-      this.IjkMusicPlayerView()
-    }
-    .transition(TransitionEffect.asymmetric(
-      TransitionEffect.opacity(1),
-      TransitionEffect.OPACITY
-    ))
-    .visualEffect(deviceInfo.sdkApiVersion>=20&&this.bgController?
-      new hdsEffect.HdsEffectBuilder()
-        .shaderEffect({
-          effectType: hdsEffect.EffectType.UV_BACKGROUND_FLOW_LIGHT,
-          animation: {
-            duration: 10000,
-            iterations: -1,
-            autoPlay: true,
-            onFinish: ()=> {
-              console.info('Succeeded in finishing');
-            }
-          },
-          controller: this.bgController,
-        })
-        .buildEffect():null)
-    .height('100%')
-    .width('100%')
-    .onDisAppear(() => {
-      this.translateY = 0;
+  private handlePlayerPagePanUpdate(event?: GestureEvent): void {
+    if (!event) {
+      return
+    }
+    const offsetY = event.offsetY ?? 0
+    const isSwipeUp = offsetY < 0
+
+    if (isSwipeUp) {
+      this.translateY = 0
+      this.scaleValueImage = 1
+      this.scaleValueText = 1
       this.playDragUiOpacity = 1
+      return
+    }
+    if (this.isGeKongPlayNext) {
+      return
+    }
+    this.translateY = offsetY
+
+    this.getUIContext().animateTo({
+      duration: 500,
+      curve: Curve.Sharp
+    }, () => {
+      this.scaleValueImage = Math.min(1, Math.max(0.5, 1 - this.translateY / 880))
+      console.info('onecold scaleValueImage:', this.scaleValueImage)
+      this.scaleValueText = Math.min(1, Math.max(0.88, 1 - this.translateY / 2000))
+      this.playDragUiOpacity = Math.min(1, Math.max(0, 1 - this.translateY / 300))
     })
-    .translate({ y: this.translateY })
-    .gesture(
-      PanGesture(this.panOption)
-        .onActionUpdate((event?: GestureEvent) => {
-          if (event) {
-            // 判断滑动方向
-            const offsetY = event.offsetY ?? 0;
-            const isSwipeUp = offsetY < 0;
-
-            if (isSwipeUp) {
-              // 向上滑动:不应用位移动画,保持 translateY = 0
-              this.translateY = 0;
-              this.scaleValueImage = 1
-              this.scaleValueText = 1
-              this.playDragUiOpacity = 1
-            } else {
-              if(this.isGeKongPlayNext){
-                return
-              }
-              // 向下滑动:应用位移动画
-              this.translateY = offsetY;
+  }
 
-              // 向下滑动时的缩放效果
-              this.getUIContext().animateTo({
-                duration: 500,
-                curve: Curve.Sharp
-              }, () => {
-                this.scaleValueImage = Math.min(1, Math.max(0.5, 1 - this.translateY / 880));
-                console.info('onecold scaleValueImage:', this.scaleValueImage)
-                this.scaleValueText = Math.min(1, Math.max(0.88, 1 - this.translateY / 2000));
-                this.playDragUiOpacity = Math.min(1, Math.max(0, 1 - this.translateY / 300));
-              })
-            }
-          }
-        })
-        .onActionEnd((event?: GestureEvent) => {
-          // 判断滑动方向
-          const offsetY = event?.offsetY ?? 0;
-          const isSwipeUp = offsetY < 0;
-          const minDistance = 300; // 最小滑动距离
-          const minVelocity = 500; // 最小滑动速度
-          const velocity = event?.velocityY ?? 0;
-
-          if (isSwipeUp) {
-            // 向上滑动:切换下一首
-            const shouldSwitch = this.isPlayPageSwipeNext &&
-              (Math.abs(offsetY) > minDistance || Math.abs(velocity) > minVelocity);
-
-            if (shouldSwitch) {
-              // 直接切换下一首,无需动画(因为界面没有移动)
-              this.playbackController.playNext().catch(() => {
-                Logger.error('heanup MusicPlayBuilder', '向上滑动切换下一首失败');
-              });
-            }
-          } else {
-            //这里加个变量,是否开启隔空手势播放下一首
-            if(this.isGeKongPlayNext){
-              void this.playbackController.playNext()
-              return
-            }
-            // 向下滑动:关闭播放页
-            const shouldClose = this.translateY > minDistance || velocity > minVelocity;
-
-            if (shouldClose) {
-              // 添加关闭动画
-              this.getUIContext().animateTo({
-                duration: 600,
-                curve: Curve.Friction
-              }, () => {
-                this.isShowPlay = false;
-                this.scaleValueImage = 1
-                this.scaleValueText = 1
-                this.playDragUiOpacity = 1
-                this.translateY = 0;
-              })
-            } else {
-              // 回弹动画
-              this.getUIContext().animateTo({
-                duration: 600,
-                curve: Curve.Friction
-              }, () => {
-                this.scaleValueImage = 1
-                this.scaleValueText = 1
-                this.playDragUiOpacity = 1
-                this.translateY = 0;
-              })
-            }
-          }
+  private handlePlayerPagePanEnd(event?: GestureEvent): void {
+    const offsetY = event?.offsetY ?? 0
+    const isSwipeUp = offsetY < 0
+    const minDistance = 300
+    const minVelocity = 500
+    const velocity = event?.velocityY ?? 0
+
+    if (isSwipeUp) {
+      const shouldSwitch = this.isPlayPageSwipeNext &&
+        (Math.abs(offsetY) > minDistance || Math.abs(velocity) > minVelocity)
+
+      if (shouldSwitch) {
+        this.playbackController.playNext().catch(() => {
+          Logger.error('heanup MusicPlayBuilder', '向上滑动切换下一首失败')
         })
-    )
+      }
+      return
+    }
+    if (this.isGeKongPlayNext) {
+      void this.playbackController.playNext()
+      return
+    }
+    const shouldClose = this.translateY > minDistance || velocity > minVelocity
+
+    this.getUIContext().animateTo({
+      duration: 600,
+      curve: Curve.Friction
+    }, () => {
+      if (shouldClose) {
+        this.isShowPlay = false
+      }
+      this.scaleValueImage = 1
+      this.scaleValueText = 1
+      this.playDragUiOpacity = 1
+      this.translateY = 0
+    })
+  }
+
+  private cleanupMusiPlayerViewState(): void {
+    this.translateY = 0
+    this.playDragUiOpacity = 1
+  }
+
+  @Builder
+  MusicPlayBuilder() {
+    PlayerPage({
+      translateY: this.translateY,
+      enableBackgroundEffect: true,
+      showBackgroundEffect: deviceInfo.sdkApiVersion >= 20,
+      bgController: this.bgController,
+      panOptions: this.panOption,
+      onPanUpdate: (event?: GestureEvent): void => {
+        this.handlePlayerPagePanUpdate(event)
+      },
+      onPanEnd: (event?: GestureEvent): void => {
+        this.handlePlayerPagePanEnd(event)
+      },
+      onDisappearCleanup: (): void => {
+        this.cleanupMusiPlayerViewState()
+      },
+      contentBuilder: () => {
+        this.IjkMusicPlayerView()
+      }
+    })
   }
 
   @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD; //一多界面适配
@@ -13805,11 +13856,29 @@ export struct LocalMusic {
     }
     this.animationState = isPlaying ? AnimationStatus.Running : AnimationStatus.Paused
     AppStorage.setOrCreate('animationState', this.animationState)
+    this.syncDetachedPlayerState()
     Logger.info(TAG,
       `[MiniState] syncPlaybackUiStateFromControlStatus status=${this.CONTROL_PlayStatus}, isPlaying=${this.isPlaying}, ` +
       `hostDisplayOnly=${this.isHostSnapshotDisplayOnly}, currentSong=${this.currentSong?.name ?? ''}`)
   }
 
+  private syncDetachedPlayerState(positionMs?: number, durationMs?: number): void {
+    const safePositionMs = positionMs !== undefined ? Math.max(0, positionMs) : Math.max(0, this.getActivePlaybackPositionMs())
+    const safeDurationMs = durationMs !== undefined ? Math.max(0, durationMs) : Math.max(0, this.getActiveDuration())
+    AppStorage.setOrCreate('songList', this.songList)
+    AppStorage.setOrCreate('currIndex', this.curIndex)
+    AppStorage.setOrCreate('currentSong', this.currentSong)
+    AppStorage.setOrCreate('CONTROL_PlayStatus', this.CONTROL_PlayStatus)
+    AppStorage.setOrCreate('progressValue', this.progressValue)
+    AppStorage.setOrCreate('playbackPositionMs', safePositionMs)
+    AppStorage.setOrCreate('playbackDurationMs', safeDurationMs)
+    AppStorage.setOrCreate('playbackIsFavorite', this.currentSong?.isFav === 1)
+    AppStorage.setOrCreate('cover', this.currentSong?.pixelMapPath ?? this.cover ?? '')
+    AppStorage.setOrCreate('musicPlayType', this.playType)
+    AppStorage.setOrCreate('currentSongName', this.currentSong?.name ?? '')
+    AppStorage.setOrCreate('currentSongArtist', this.currentSong?.artist ?? '')
+  }
+
   //封面旋转动画
   animationRoFun() {
     if (this.isPlaying && !this.isCoverRectangle && this.isPageVisible) {
@@ -17823,7 +17892,16 @@ export struct LocalMusic {
   }
 
   private notifyPlaybackCardStateChanged(): void {
+    if (this.suppressPlaybackCardStatePublish) {
+      Logger.info(TAG,
+        `${KILLCARD_TRACE} notifyPlaybackCardStateChanged skip suppressed=true, ` +
+        `currentSong=${this.currentSong?.name ?? ''}, controlStatus=${this.CONTROL_PlayStatus}`)
+      return
+    }
     if (!MusicCardManager.shouldPagePublishPlaybackState(this.isHostSnapshotDisplayOnly)) {
+      Logger.info(TAG,
+        `${KILLCARD_TRACE} notifyPlaybackCardStateChanged skip hostDisplayOnly=${this.isHostSnapshotDisplayOnly}, ` +
+        `currentSong=${this.currentSong?.name ?? ''}, controlStatus=${this.CONTROL_PlayStatus}`)
       return
     }
     try {
@@ -17836,10 +17914,15 @@ export struct LocalMusic {
       options.lyricText = this.lyricContent
       options.coverPath = this.currentSong?.pixelMapPath ?? this.cover ?? ''
       options.coverImageName = ''
+      Logger.info(TAG,
+        `${KILLCARD_TRACE} notifyPlaybackCardStateChanged publish song=${this.currentSong?.name ?? ''}, ` +
+        `status=${this.CONTROL_PlayStatus}, isPlaying=${options.isPlaying}, ` +
+        `position=${options.positionMs}, duration=${options.durationMs}, cover=${options.coverPath}`)
       const snapshot = MusicCardManager.getInstance().buildSnapshotFromPlaybackState(options)
       MusicCardManager.getInstance().notifyPlaybackStateChanged(context, snapshot)
     } catch (error) {
       Logger.warn(TAG, `notifyPlaybackCardStateChanged failed: ${error}`)
+      Logger.warn(TAG, `${KILLCARD_TRACE} notifyPlaybackCardStateChanged failed error=${error}`)
     }
   }
 
@@ -18018,6 +18101,7 @@ export struct LocalMusic {
     } catch (_error) {
       // 卡片刷新不能影响主播放流程
     }
+    this.syncDetachedPlayerState(position, duration)
   }
 
   private startProgressTask() {
@@ -18042,6 +18126,7 @@ export struct LocalMusic {
     this.currentTime = this.stringForTime(duration)
     this.progressValue = this.PROGRESS_MAX_VALUE
     this.slideEnable = false
+    this.syncDetachedPlayerState(duration, duration)
     this.stop()
   }
 
@@ -18114,6 +18199,7 @@ export struct LocalMusic {
     this.currentSong = this.songList[this.curIndex]
     this.applyCurrentSongMetadata(this.currentSong)
     this.playbackStateBridge.replaceQueue(this.songList, this.curIndex)
+    this.syncDetachedPlayerState(0, this.getSongDurationMs(this.currentSong))
   }
 
   private ensureSongQueued(song: VideoItem): number {
@@ -18136,6 +18222,7 @@ export struct LocalMusic {
     this.stop()
     this.songList[this.curIndex] = song
     this.currentSong = this.songList[this.curIndex]
+    this.syncDetachedPlayerState(0, this.getSongDurationMs(this.currentSong))
   }
 
   private applyCastTrackDurationState(durationMs: number): void {
@@ -18146,6 +18233,7 @@ export struct LocalMusic {
     this.durationTime = Math.floor(durationMs / 1000)
     this.durationStringTime = secondToTime(this.durationTime)
     this.totalTime = this.stringForTime(durationMs)
+    this.syncDetachedPlayerState(0, durationMs)
     this.syncLegacyPageSessionMetadata(this.currentSong, durationMs)
   }
 
@@ -19280,6 +19368,10 @@ export struct LocalMusic {
 
   public setIsPlaying(isPlayer: boolean) {
     this.isPlaying = isPlayer;
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} setIsPlaying isPlayer=${isPlayer}, controlStatus=${this.CONTROL_PlayStatus}, ` +
+      `currentSong=${this.currentSong?.name ?? ''}, currentIndex=${this.curIndex}, ` +
+      `hostDisplayOnly=${this.isHostSnapshotDisplayOnly}`)
 
     // 发送播放状态变化事件给歌单详情页面
     try {
@@ -19306,9 +19398,78 @@ export struct LocalMusic {
     } catch (error) {
       Logger.warn(TAG, `回写 PlaybackStateBridge 失败: ${error}`)
     }
+    this.syncDetachedPlayerState()
+    this.persistLocalPlaybackRecoverySnapshot('setIsPlaying')
     this.notifyPlaybackCardStateChanged()
   }
 
+  private persistLocalPlaybackRecoverySnapshot(reason: string): void {
+    if (this.isHostSnapshotDisplayOnly) {
+      Logger.info(TAG,
+        `${KILLCARD_TRACE} persistLocalPlaybackRecoverySnapshot skip hostDisplayOnly=true reason=${reason}`)
+      return
+    }
+    if (ArrayUtil.isEmpty(this.songList) || !this.currentSong) {
+      Logger.info(TAG,
+        `${KILLCARD_TRACE} persistLocalPlaybackRecoverySnapshot skip missing-queue reason=${reason}, ` +
+        `songListLength=${this.songList.length}, hasCurrentSong=${this.currentSong !== undefined}`)
+      return
+    }
+    try {
+      const snapshot = this.buildLocalPlaybackRecoverySnapshot()
+      this.playbackSnapshotStore.write(snapshot)
+      Logger.info(TAG,
+        `[MiniState] persistLocalPlaybackRecoverySnapshot reason=${reason}, song=${snapshot.currentSongName}, ` +
+        `index=${snapshot.currentIndex}, isPlaying=${snapshot.isPlaying}, position=${snapshot.positionMs}, ` +
+        `duration=${snapshot.durationMs}, queueLength=${snapshot.queue.length}`)
+      Logger.info(TAG,
+        `${KILLCARD_TRACE} persistLocalPlaybackRecoverySnapshot reason=${reason}, ` +
+        `song=${snapshot.currentSongName}, index=${snapshot.currentIndex}, isPlaying=${snapshot.isPlaying}, ` +
+        `position=${snapshot.positionMs}, duration=${snapshot.durationMs}, queueLength=${snapshot.queue.length}, ` +
+        `shouldResume=${snapshot.shouldResumeWhenActivated}`)
+    } catch (error) {
+      Logger.warn(TAG, `[MiniState] persistLocalPlaybackRecoverySnapshot failed reason=${reason}, error=${error}`)
+      Logger.warn(TAG, `${KILLCARD_TRACE} persistLocalPlaybackRecoverySnapshot failed reason=${reason}, error=${error}`)
+    }
+  }
+
+  private buildLocalPlaybackRecoverySnapshot(): HostPlaybackSnapshot {
+    const queue: HostPlaybackSnapshotSong[] = this.songList.map((item: VideoItem): HostPlaybackSnapshotSong => {
+      return {
+        filePath: item.filePath,
+        id: item.id,
+        type: item.type,
+        name: item.name,
+        isFav: item.isFav,
+        artist: item.artist,
+        album: item.album,
+        duration: item.duration,
+        pixelMapPath: item.pixelMapPath,
+        lyricContent: item.lyricContent,
+        remote_rel_path: item.remote_rel_path,
+        webdav_account_id: item.webdav_account_id
+      }
+    })
+    const safeIndex = this.curIndex >= 0 && this.curIndex < this.songList.length ? this.curIndex :
+      (this.songList.length > 0 ? 0 : -1)
+    const snapshotIsPlaying = this.CONTROL_PlayStatus === PlayStatus.PLAY
+    return {
+      queue,
+      currentIndex: safeIndex,
+      currentSongKey: this.currentSong?.filePath ?? '',
+      currentSongName: this.currentSong?.name ?? '',
+      currentArtist: this.currentSong?.artist,
+      isPlaying: snapshotIsPlaying,
+      positionMs: Math.max(0, this.getActivePlaybackPositionMs()),
+      durationMs: this.getActiveDuration(),
+      cover: this.currentSong?.pixelMapPath ?? this.cover ?? '',
+      playType: this.playType,
+      playlistContext: 'page_runtime',
+      updatedAt: Date.now(),
+      shouldResumeWhenActivated: this.CONTROL_PlayStatus !== PlayStatus.INIT
+    }
+  }
+
   private setPlaybackStateChangeListener(): void {
     // 注意: HarmonyOS 的投播控制器可能不返回 state 字段
     // 我们需要通过其他方式来判断播放状态
@@ -19730,6 +19891,7 @@ export struct LocalMusic {
     this.isCurrentTime = false;
     const lyricPosition = position + this.timeOffset * 1000;
     this.updateLyricPosition(lyricPosition);
+    this.syncDetachedPlayerState(position, duration)
   }
   /**
    * Gesture method onActionUpdate.
@@ -19839,6 +20001,7 @@ export struct LocalMusic {
       this.mDestroyPage = true;
       this.CONTROL_PlayStatus = PlayStatus.PAUSE;
       this.syncLegacyPageSessionState(false)
+      this.persistLocalPlaybackRecoverySnapshot('pause')
       this.playChange()
       if (this.pipController) {
         this.pipController.updatePiPControlStatus(PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE,

+ 687 - 0
entry/src/main/ets/view/player/MusiPlayerView.ets

@@ -0,0 +1,687 @@
+import { hdsEffect } from '@kit.UIDesignKit'
+import { PreferencesUtil, StrUtil } from '@pura/harmony-utils'
+import { PlayStatus } from '../../common/PlayStatus'
+import { CommonConstants } from '../../common/constants/CommonConstants'
+import { isPlaybackControlPlaying } from '../../common/player/PlaybackControlStateHelper'
+import { MusicPlaybackController } from '../../controller/MusicPlaybackController'
+import { LyricController } from '../../lyric/LyricController'
+import { Lyric } from '../../lyric/bean/Lyric'
+import { LyricParser } from '../../lyric/parse/LyricParser'
+import { LyricView2 } from '../../lyric/view/LyricView2'
+import { SettingPage } from '../../pages/SettingPage'
+import { PlaybackViewState, PlaybackViewStateCenter } from '../../playback/PlaybackViewStateCenter'
+import Logger from '../../common/util/Logger'
+import { VideoItem } from '../../viewmodel/VideoItem'
+import { resolvePlayerLyricContent, resolvePlayerTimeTexts } from './MusiPlayerViewStateHelper'
+
+@Extend(Button)
+function playerControlButtonStyle(width: number, height: number, enableShadow: boolean) {
+  .hitTestBehavior(HitTestMode.Transparent)
+  .height(height)
+  .width(width)
+  .stateEffect(false)
+  .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 })
+  .backgroundColor(Color.Transparent)
+  .type(ButtonType.Circle)
+  .shadow(enableShadow ? ShadowStyle.OUTER_DEFAULT_XS : undefined)
+}
+
+const TAG = 'MusiPlayerView'
+
+@Component
+export struct MusiPlayerView {
+  onClose: () => void = () => {}
+  @State isChangeBg: boolean = true
+  @State currentSong: VideoItem | undefined = undefined
+  @State controlPlayStatus: number = PlayStatus.INIT
+  @State progressValue: number = 0
+  @State playbackPositionMs: number = 0
+  @State playbackDurationMs: number = 0
+  @State cover: string | undefined = ''
+  @StorageLink('musicPlayType') playType: number = 0
+  @StorageLink('playbackIsFavorite') isFavorite: boolean = false
+  @StorageProp('themeColor') themeColor: string =
+    PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR)
+  @StorageProp('EnablePointLight') enablePointLight: boolean = true
+  @StorageProp('EnableShadow') enableShadow: boolean = true
+  @StorageProp('SdkApiVersion') sdkApiVersion: number = 17
+  @State isMusicBGCover: boolean = true
+  @State currentTime: string = '00:00'
+  @State totalTime: string = '00:00'
+  @State currentSwiperIndex: number = 0
+  @State sliderProgressValue: number = 0
+  @State activeButtonKey: string = ''
+  @State pointLightOptions: hdsEffect.PointLightOptions | undefined = undefined
+  private playbackController: MusicPlaybackController = MusicPlaybackController.getInstance()
+  private lyricController: LyricController = new LyricController()
+    .setEmptyHint('暂无歌词')
+    .setTextColor('#B3FFFFFF')
+    .setHighlightColor('#FFFFFFFF')
+    .setLightText(true)
+    .setAlignMode('center')
+    .setHightLightCenter(true)
+  private lyricParser: LyricParser = new LyricParser()
+  private appliedLyricContent: string = ''
+  private lyricSyncTimer: number = -1
+  private readonly buttonScale: number = 1.3
+  private readonly pointLightHeight: number = 100
+  private playbackViewStateCenter: PlaybackViewStateCenter = PlaybackViewStateCenter.getInstance()
+  private readonly playbackStateObserver: (state: PlaybackViewState) => void =
+    (state: PlaybackViewState): void => {
+      this.applyPlaybackViewState(state)
+    }
+
+  aboutToAppear(): void {
+    this.isMusicBGCover = PreferencesUtil.getBooleanSync(SettingPage.iS_MUSIC_BG_COVER, true)
+    Logger.info(TAG, '[PLAYER_DETACH] player aboutToAppear subscribe')
+    this.playbackViewStateCenter.subscribe(this.playbackStateObserver)
+    this.startLyricSyncTimer()
+  }
+
+  aboutToDisappear(): void {
+    Logger.info(TAG, '[PLAYER_DETACH] player aboutToDisappear unsubscribe')
+    this.playbackViewStateCenter.unsubscribe(this.playbackStateObserver)
+    this.stopLyricSyncTimer()
+  }
+
+  private syncTimeTexts(): void {
+    const timeTexts = resolvePlayerTimeTexts(this.playbackPositionMs, this.playbackDurationMs)
+    this.currentTime = timeTexts.currentTime
+    this.totalTime = timeTexts.totalTime
+  }
+
+  private applyPlaybackViewState(state: PlaybackViewState): void {
+    const previousSongPath = this.currentSong?.filePath ?? ''
+    const nextSongPath = state.currentSong?.filePath ?? ''
+    const songChanged = previousSongPath !== nextSongPath
+    const lyricChanged = this.currentSong?.lyricContent !== state.currentSong?.lyricContent
+    Logger.info(TAG,
+      `[PLAYER_DETACH] player apply song=${state.currentSong?.name ?? ''}, status=${state.controlPlayStatus}, ` +
+      `position=${state.playbackPositionMs}, duration=${state.playbackDurationMs}, progress=${state.progressValue}, ` +
+      `songChanged=${songChanged}, lyricChanged=${lyricChanged}`)
+    this.currentSong = state.currentSong
+    this.controlPlayStatus = state.controlPlayStatus
+    this.progressValue = state.progressValue
+    this.sliderProgressValue = state.progressValue
+    this.playbackPositionMs = state.playbackPositionMs
+    this.playbackDurationMs = state.playbackDurationMs
+    this.cover = state.cover
+    this.syncTimeTexts()
+    if (songChanged || lyricChanged) {
+      this.syncLyricContentIfNeeded(true)
+    } else {
+      this.syncLyricContentIfNeeded()
+    }
+    this.syncLyricPosition()
+  }
+
+  private startLyricSyncTimer(): void {
+    this.stopLyricSyncTimer()
+    this.lyricSyncTimer = setInterval(() => {
+      this.syncLyricContentIfNeeded()
+    }, 500)
+  }
+
+  private stopLyricSyncTimer(): void {
+    if (this.lyricSyncTimer >= 0) {
+      clearInterval(this.lyricSyncTimer)
+      this.lyricSyncTimer = -1
+    }
+  }
+
+  private syncLyricContentIfNeeded(force: boolean = false): void {
+    const lyricContent = resolvePlayerLyricContent(this.currentSong)
+    if (!force && lyricContent === this.appliedLyricContent) {
+      return
+    }
+
+    this.appliedLyricContent = lyricContent
+    if (lyricContent.length === 0) {
+      this.lyricController.setLyric(null)
+      return
+    }
+
+    const lines: string[] = lyricContent.split('\n').map((line: string) => line.trim())
+    const lyric: Lyric = this.lyricParser.parse(lines)
+    this.lyricController.setLyric(lyric)
+    this.syncLyricPosition()
+  }
+
+  private syncLyricPosition(): void {
+    this.lyricController.updatePosition(Math.max(0, this.playbackPositionMs))
+  }
+
+  @Builder
+  private CoverArtworkBuilder() {
+    Image(StrUtil.isNotEmpty(this.cover) ? this.cover as string : $r('app.media.alt'))
+      .width('88%')
+      .aspectRatio(1)
+      .objectFit(ImageFit.Contain)
+      .borderRadius(22)
+      .clip(true)
+      .shadow({
+        radius: 22,
+        type: ShadowType.BLUR,
+        color: 'on_primary'
+      })
+      .margin({ top: 12, left: 8, right: 8 })
+  }
+
+  @Builder
+  private SongInfoBuilder() {
+    Row() {
+      Column({ space: 8 }) {
+        Text(this.currentSong?.name ?? '暂无播放')
+          .fontSize(20)
+          .fontWeight(FontWeight.Bold)
+          .fontColor(Color.White)
+          .textAlign(TextAlign.Start)
+          .maxLines(1)
+          .width('99%')
+          .textOverflow({ overflow: TextOverflow.MARQUEE })
+
+        Text(this.buildArtistAlbumText())
+          .fontSize(15)
+          .fontColor(Color.White)
+          .textAlign(TextAlign.Start)
+          .maxLines(1)
+          .width('99%')
+          .textOverflow({ overflow: TextOverflow.MARQUEE })
+          .visibility(StrUtil.isEmpty(this.buildArtistAlbumText()) ? Visibility.None : Visibility.Visible)
+      }
+      .layoutWeight(1)
+      .alignItems(HorizontalAlign.Start)
+      .margin({ left: 15 })
+
+      Button() {
+        SymbolGlyph($r('sys.symbol.moon'))
+          .fontColor([Color.White])
+          .fontSize(22)
+      }
+      .playerControlButtonStyle(this.resolveButtonFrame(26), this.resolveButtonFrame(26), false)
+      .opacity(0.92)
+      .margin({ left: 8, right: 8 })
+    }
+    .width('90%')
+    .margin({ top: 14 })
+  }
+
+  @Builder
+  private CoverPageBuilder() {
+    Column() {
+      this.CoverArtworkBuilder()
+      this.SongInfoBuilder()
+    }
+    .width('100%')
+    .height('100%')
+    .justifyContent(FlexAlign.Start)
+    .alignItems(HorizontalAlign.Center)
+    .padding({ top: 6 })
+  }
+
+  private buildArtistAlbumText(): string {
+    const artist = this.currentSong?.artist ?? ''
+    const album = this.currentSong?.album ?? ''
+    if (StrUtil.isNotEmpty(artist) && StrUtil.isNotEmpty(album)) {
+      return `${artist}  ${album}`
+    }
+    if (StrUtil.isNotEmpty(artist)) {
+      return artist
+    }
+    if (StrUtil.isNotEmpty(album)) {
+      return album
+    }
+    return ''
+  }
+
+  @Builder
+  private LyricTopItemBuilder() {
+    Row() {
+      Stack({ alignContent: Alignment.Center }) {
+        Image(StrUtil.isNotEmpty(this.cover) ? this.cover as string : $r('app.media.alt'))
+          .height(58)
+          .width(58)
+          .clip(true)
+          .alt($r('app.media.alt'))
+          .borderRadius(8)
+          .shadow({
+            radius: 22,
+            type: ShadowType.BLUR,
+            color: 'on_primary'
+          })
+      }
+      .width(62)
+      .height('100%')
+
+      Column() {
+        Text(this.currentSong?.name ?? '暂无播放')
+          .fontSize(16)
+          .maxLines(1)
+          .fontWeight(FontWeight.Bolder)
+          .fontColor(Color.White)
+          .textOverflow({ overflow: TextOverflow.Ellipsis })
+          .margin({ left: 10 })
+
+        Text(this.currentSong?.artist ?? '')
+          .fontSize(13)
+          .fontWeight(FontWeight.Bolder)
+          .padding({ top: 8 })
+          .fontColor(Color.White)
+          .margin({ left: 10 })
+          .maxLines(1)
+          .textOverflow({ overflow: TextOverflow.Ellipsis })
+          .visibility(StrUtil.isEmpty(this.currentSong?.artist ?? '') ? Visibility.None : Visibility.Visible)
+      }
+      .height('100%')
+      .layoutWeight(1)
+      .justifyContent(FlexAlign.Center)
+      .alignItems(HorizontalAlign.Start)
+      .margin({ right: 8 })
+
+      Column() {
+        Button() {
+          Image($r('app.media.lyric'))
+            .width(23)
+            .aspectRatio(CommonConstants.ASPECT_RATIO)
+        }
+        .playerControlButtonStyle(this.resolveButtonFrame(23), this.resolveButtonFrame(23), false)
+      }
+      .width(44)
+      .height('100%')
+      .justifyContent(FlexAlign.Center)
+      .alignItems(HorizontalAlign.End)
+    }
+    .width('90%')
+    .padding({ left: 28, right: 28 })
+    .height(58)
+    .alignItems(VerticalAlign.Center)
+    .margin({ top: 38 })
+  }
+
+  @Builder
+  private LyricPageBuilder() {
+    Column() {
+      this.LyricTopItemBuilder()
+
+      LyricView2({
+        controller: this.lyricController,
+        enableSeek: false
+      })
+        .width('100%')
+        .layoutWeight(1)
+        .margin({ top: 2, bottom: 8 })
+    }
+    .width('100%')
+    .height('100%')
+    .justifyContent(FlexAlign.Start)
+    .alignItems(HorizontalAlign.Center)
+    .padding({ left: 4, right: 4 })
+  }
+
+  @Builder
+  private EmptySpectrumBuilder() {
+    Blank()
+      .height(0)
+  }
+
+  @Builder
+  private TopBarBuilder() {
+    Row() {
+      Button() {
+        SymbolGlyph($r('sys.symbol.chevron_down'))
+          .fontColor([Color.White])
+          .fontSize(22)
+      }
+      .playerControlButtonStyle(this.resolveButtonFrame(26), this.resolveButtonFrame(26), false)
+      .onClick(() => {
+        this.onClose()
+      })
+
+      Blank()
+    }
+    .width('100%')
+    .height(48)
+    .padding({ left: 18, right: 18, top: 18, bottom: 4 })
+  }
+
+  private onSeek(value: number): void {
+    const seekValue = MusicPlaybackController.resolveSeekValueFromPercent(value, this.playbackDurationMs)
+    void this.playbackController.seekTo(`${Math.floor(seekValue)}`, 'musi-player-view')
+  }
+
+  private hasCoverBackground(): boolean {
+    if (this.cover && this.cover.length > 0) {
+      return true
+    }
+    return false
+  }
+
+  private resolveButtonFrame(size: number): number {
+    return size * this.buttonScale
+  }
+
+  private handleButtonTouch(buttonKey: string, event: TouchEvent): void {
+    if (!this.enablePointLight || this.sdkApiVersion < 20) {
+      return
+    }
+    if (event.type === TouchType.Down) {
+      this.activeButtonKey = buttonKey
+      this.pointLightOptions = {
+        color: this.themeColor,
+        intensity: 1,
+        height: this.pointLightHeight
+      }
+      return
+    }
+    if ((event.type === TouchType.Up || event.type === TouchType.Cancel) && this.activeButtonKey === buttonKey) {
+      this.activeButtonKey = ''
+      this.pointLightOptions = undefined
+    }
+  }
+
+  private buildPointLightEffect(buttonKey: string) {
+    return this.enablePointLight && this.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+          .pointLight({
+            options: this.activeButtonKey === buttonKey ? this.pointLightOptions : undefined,
+            illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+          })
+          .buildEffect()
+      : undefined
+  }
+
+  @Builder
+  private MenuButtonContent() {
+    Image($r('app.media.menu'))
+      .width(24)
+      .aspectRatio(CommonConstants.ASPECT_RATIO)
+  }
+
+  @Builder
+  private PreviousButtonContent() {
+    Image($r('app.media.ic_previous'))
+      .width(32)
+      .aspectRatio(CommonConstants.ASPECT_RATIO)
+  }
+
+  @Builder
+  private NextButtonContent() {
+    Image($r('app.media.ic_next'))
+      .width(32)
+      .aspectRatio(CommonConstants.ASPECT_RATIO)
+  }
+
+  @Builder
+  private LoopModeButtonContent() {
+    if (this.playType === 0) {
+      Image($r('app.media.loop'))
+        .width(24)
+        .aspectRatio(CommonConstants.ASPECT_RATIO)
+    } else if (this.playType === 1) {
+      Image($r('app.media.single'))
+        .width(24)
+        .aspectRatio(CommonConstants.ASPECT_RATIO)
+    } else if (this.playType === 2) {
+      Image($r('app.media.normal_play'))
+        .width(24)
+        .aspectRatio(CommonConstants.ASPECT_RATIO)
+    } else if (this.playType === 3) {
+      Image($r('app.media.random'))
+        .width(24)
+        .aspectRatio(CommonConstants.ASPECT_RATIO)
+    } else {
+      Image($r('app.media.noloop'))
+        .width(24)
+        .aspectRatio(CommonConstants.ASPECT_RATIO)
+    }
+  }
+
+  @Builder
+  private PlayOrPauseButtonContent() {
+    Stack() {
+      Image(isPlaybackControlPlaying(this.controlPlayStatus)
+        ? $r('app.media.ic_public_play')
+        : $r('app.media.ic_public_pause'))
+        .width(40)
+        .fillColor(Color.White)
+        .aspectRatio(CommonConstants.ASPECT_RATIO)
+
+      if (false) {
+        Progress({ value: 0, total: 100, type: ProgressType.Ring })
+      }
+    }
+    .width(41)
+    .height(41)
+  }
+
+  @Builder
+  private ProgressControls() {
+    Column() {
+      Row() {
+        Blank()
+          .width(22)
+
+        Slider({
+          value: this.sliderProgressValue,
+          min: 0,
+          max: 100,
+          step: 1,
+          style: SliderStyle.InSet
+        })
+          .height(15)
+          .blockColor('rgba(255,255,255,1)')
+          .trackColor('rgba(255,255,255,0.3)')
+          .selectedColor(Color.White)
+          .trackThickness(4)
+          .showSteps(false)
+          .showTips(false)
+          .layoutWeight(1)
+          .enabled(true)
+          .onChange((value: number, mode: SliderChangeMode) => {
+            if (mode !== 2) {
+              return
+            }
+            this.onSeek(value)
+          })
+
+        Blank()
+          .width(22)
+      }
+      .justifyContent(FlexAlign.Center)
+      .padding({ left: 10, right: 10 })
+
+      Row() {
+        Text(this.currentTime)
+          .fontSize(10)
+          .fontColor(Color.White)
+          .margin({ left: 25 })
+        Blank()
+        Text(this.totalTime)
+          .fontSize(10)
+          .fontColor(Color.White)
+          .margin({ right: 25 })
+      }
+      .width('90%')
+      .height(10)
+    }
+    .width('100%')
+    .alignItems(HorizontalAlign.Center)
+    .margin({ top: 2 })
+  }
+
+  @Builder
+  private CenterControls() {
+    Row() {
+      Button() {
+        this.MenuButtonContent()
+      }
+      .playerControlButtonStyle(this.resolveButtonFrame(26), this.resolveButtonFrame(26), this.enableShadow)
+      .onTouch((event: TouchEvent) => {
+        this.handleButtonTouch('menu', event)
+      })
+      .visualEffect(this.buildPointLightEffect('menu'))
+
+      Button() {
+        this.PreviousButtonContent()
+      }
+      .playerControlButtonStyle(this.resolveButtonFrame(26), this.resolveButtonFrame(26), this.enableShadow)
+      .onTouch((event: TouchEvent) => {
+        this.handleButtonTouch('previous', event)
+      })
+      .visualEffect(this.buildPointLightEffect('previous'))
+      .onClick(() => {
+        void this.playbackController.playPrevious()
+      })
+
+      Button() {
+        this.PlayOrPauseButtonContent()
+      }
+      .playerControlButtonStyle(this.resolveButtonFrame(41), this.resolveButtonFrame(41), this.enableShadow)
+      .onTouch((event: TouchEvent) => {
+        this.handleButtonTouch('play', event)
+      })
+      .visualEffect(this.buildPointLightEffect('play'))
+      .onClick(() => {
+        void this.playbackController.playOrPause()
+      })
+
+      Button() {
+        this.NextButtonContent()
+      }
+      .playerControlButtonStyle(this.resolveButtonFrame(26), this.resolveButtonFrame(26), this.enableShadow)
+      .onTouch((event: TouchEvent) => {
+        this.handleButtonTouch('next', event)
+      })
+      .visualEffect(this.buildPointLightEffect('next'))
+      .onClick(() => {
+        void this.playbackController.playNext()
+      })
+
+      Button() {
+        this.LoopModeButtonContent()
+      }
+      .playerControlButtonStyle(this.resolveButtonFrame(26), this.resolveButtonFrame(26), this.enableShadow)
+      .onTouch((event: TouchEvent) => {
+        this.handleButtonTouch('mode', event)
+      })
+      .visualEffect(this.buildPointLightEffect('mode'))
+      .onClick(() => {
+        void this.playbackController.setLoopMode()
+      })
+    }
+    .width('95%')
+    .margin({ bottom: 14 })
+    .justifyContent(FlexAlign.SpaceEvenly)
+  }
+
+  @Builder
+  private BottomUtilityControls() {
+    Row() {
+      Button() {
+        SymbolGlyph($r('sys.symbol.rectangle_portrait_rotate'))
+          .fontColor([Color.White])
+          .fontSize(22)
+      }
+      .playerControlButtonStyle(this.resolveButtonFrame(26), this.resolveButtonFrame(26), false)
+
+      Button() {
+        SymbolGlyph($r('sys.symbol.rename'))
+          .fontColor([Color.White])
+          .fontSize(22)
+      }
+      .playerControlButtonStyle(this.resolveButtonFrame(26), this.resolveButtonFrame(26), false)
+
+      Button() {
+        SymbolGlyph($r('sys.symbol.slider_vertical_3'))
+          .fontColor([Color.White])
+          .fontSize(22)
+      }
+      .playerControlButtonStyle(this.resolveButtonFrame(26), this.resolveButtonFrame(26), false)
+
+      Button() {
+        SymbolGlyph(this.isFavorite ? $r('sys.symbol.heart_fill') : $r('sys.symbol.heart'))
+          .fontColor([Color.White])
+          .fontSize(22)
+      }
+      .playerControlButtonStyle(this.resolveButtonFrame(26), this.resolveButtonFrame(26), false)
+
+      Button() {
+        SymbolGlyph($r('sys.symbol.music_note_list'))
+          .fontColor([Color.White])
+          .fontSize(22)
+      }
+      .playerControlButtonStyle(this.resolveButtonFrame(26), this.resolveButtonFrame(26), false)
+      .onClick(() => {
+        void this.playbackController.openPlayList()
+      })
+    }
+    .width('95%')
+    .height(32)
+    .justifyContent(FlexAlign.SpaceAround)
+    .margin({ top: 2 })
+  }
+
+  @Builder
+  private PlayerControlsSection() {
+    Column() {
+      this.EmptySpectrumBuilder()
+      this.ProgressControls()
+      this.CenterControls()
+      this.BottomUtilityControls()
+    }
+    .width('100%')
+    .justifyContent(FlexAlign.Center)
+    .alignItems(HorizontalAlign.Center)
+    .padding({ bottom: 28 })
+  }
+
+  build() {
+    Stack({ alignContent: Alignment.Center }) {
+      if (this.hasCoverBackground() && this.isChangeBg) {
+        Column()
+          .backgroundImage(!this.isMusicBGCover || StrUtil.isEmpty(this.cover) ? null : this.cover)
+          .backgroundImageSize(!this.isMusicBGCover ? { width: '100%' } : { height: '150%', width: '100%' })
+          .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
+          .width('100%')
+          .height('100%')
+          .alignItems(HorizontalAlign.Center)
+      } else {
+        Column()
+          .width('100%')
+          .height('100%')
+          .linearGradient({ direction: GradientDirection.Right, colors: [
+            ['#00BC70', 0.0], ['#D81B60', 1.0]] })
+      }
+
+      Column() {
+        this.TopBarBuilder()
+
+        Swiper() {
+          this.CoverPageBuilder()
+          this.LyricPageBuilder()
+        }
+        .onChange((index: number) => {
+          this.currentSwiperIndex = index
+          if (index === 1) {
+            this.syncLyricContentIfNeeded()
+            this.syncLyricPosition()
+          }
+        })
+        .indicator(false)
+        .displayCount(1)
+        .loop(false)
+        .autoPlay(false)
+        .layoutWeight(1)
+        .width('100%')
+
+        this.PlayerControlsSection()
+      }
+      .width('100%')
+      .height('100%')
+      .justifyContent(FlexAlign.End)
+      .padding({ top: 0, bottom: 5 })
+    }
+    .backgroundBrightness({ rate: 0, lightUpDegree: -0.1 })
+    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
+  }
+}

+ 27 - 0
entry/src/main/ets/view/player/MusiPlayerViewStateHelper.ets

@@ -0,0 +1,27 @@
+import { stringForTime } from '../../common/util/CommUtils'
+import { VideoItem } from '../../viewmodel/VideoItem'
+
+export interface PlayerTimeTexts {
+  currentTime: string
+  totalTime: string
+}
+
+function normalizeMilliseconds(value: number): number {
+  if (!Number.isFinite(value) || value < 0) {
+    return 0
+  }
+
+  return value
+}
+
+export function resolvePlayerTimeTexts(positionMs: number, durationMs: number): PlayerTimeTexts {
+  return {
+    currentTime: stringForTime(normalizeMilliseconds(positionMs)),
+    totalTime: stringForTime(normalizeMilliseconds(durationMs))
+  }
+}
+
+export function resolvePlayerLyricContent(currentSong?: VideoItem): string {
+  const lyricContent = currentSong?.lyricContent ?? ''
+  return lyricContent.trim().length > 0 ? lyricContent : ''
+}

+ 0 - 368
entry/src/main/ets/view/player/PlayerControls.ets

@@ -1,368 +0,0 @@
-import { PlayStatus } from '../../common/PlayStatus'
-import { isPlaybackControlPlaying } from '../../common/player/PlaybackControlStateHelper'
-import { PointLightButton } from '../PointLight/PointLightButton'
-import { PointLightDefaultButton } from '../PointLight/PointLightDeFaultButton'
-
-@Component
-export struct PlayerControls {
-  // 播控支持紧凑模式,给横屏封面页复用同一套独立组件。
-  @Prop compactMode: boolean = false
-  // 整体透明度和底部偏移由外部页面控制,避免组件再持有布局状态。
-  @Prop controlOpacity: number = 1
-  @Prop bottomOffset: number = 70
-  // 是否展示底部功能操作区,HiCar 等场景沿用原有隐藏逻辑。
-  @Prop showBottomRow: boolean = true
-  // 播控区需要的播放态、进度态都作为纯数据输入。
-  @Prop isLandscape: boolean = false
-  @Prop progressValue: number = 0
-  @Prop progressMaxValue: number = 100
-  @Prop slideEnable: boolean = false
-  @Prop currentTime: string = '00:00'
-  @Prop totalTime: string = '00:00'
-  @Prop controlPlayStatus: number = PlayStatus.INIT
-  @Prop isCircleBtn: boolean = false
-  @Prop isPlayerLoading: boolean = false
-  @Prop playType: number = 0
-  @Prop fastForwardSeconds: string = '10'
-  @Prop isShowBackFast: boolean = false
-  // 编辑、收藏等按钮是否展示,由外部决定当前歌曲是否可操作。
-  @Prop showEditButton: boolean = false
-  @Prop isFavorite: boolean = false
-
-  // 频谱区域也改成可注入 builder,后续可以继续抽到独立组件而不动页面骨架。
-  @BuilderParam spectrumBuilder: () => void
-
-  // 播控内部只触发动作,不再持有具体业务逻辑。
-  onPlayPrevious: () => void = (): void => {}
-  onPlayOrPause: () => void = (): void => {}
-  onPlayNext: () => void = (): void => {}
-  onToggleMore: () => void = (): void => {}
-  onSetLoopMode: () => void = (): void => {}
-  onSeek: (value: number) => void = (_value: number): void => {}
-  onBackFast: () => void = (): void => {}
-  onForwardFast: () => void = (): void => {}
-  onRotate: () => void = (): void => {}
-  onToggleEdit: () => void = (): void => {}
-  onToggleEqualizer: () => void = (): void => {}
-  onToggleFavorite: () => void = (): void => {}
-  onOpenPlaylist: () => void = (): void => {}
-
-  private resolvePlayModeIcon(): Resource {
-    // 播放模式图标统一在独立组件里收口,减少 LocalMusic 内部判断分支。
-    if (this.playType === 0) {
-      return $r('app.media.loop')
-    }
-    if (this.playType === 1) {
-      return $r('app.media.single')
-    }
-    if (this.playType === 2) {
-      return $r('app.media.normal_play')
-    }
-    if (this.playType === 3) {
-      return $r('app.media.random')
-    }
-    return $r('app.media.noloop')
-  }
-
-  private resolveFastForwardText(): string {
-    // 统一把快进秒数当成字符串展示,避免上层状态类型变化时影响 UI 组件。
-    return this.fastForwardSeconds
-  }
-
-  @Builder
-  private MoreButtonContent() {
-    // 更多按钮沿用原有资源,保持现有视觉风格不变。
-    Image($r('app.media.menu'))
-      .width(24)
-  }
-
-  @Builder
-  private PreviousButtonContent() {
-    Image($r('app.media.ic_previous'))
-      .width(32)
-      .aspectRatio(1)
-  }
-
-  @Builder
-  private NextButtonContent() {
-    Image($r('app.media.ic_next'))
-      .width(32)
-      .aspectRatio(1)
-  }
-
-  @Builder
-  private PlayOrPauseButtonContent() {
-    Stack() {
-      // 播放暂停按钮单独保留加载圈,兼容远程歌曲缓冲场景。
-      Image(isPlaybackControlPlaying(this.controlPlayStatus)
-        ? (this.isCircleBtn ? $r('app.media.hm_pause') : $r('app.media.ic_public_play'))
-        : (this.isCircleBtn ? $r('app.media.hm_play2') : $r('app.media.ic_public_pause')))
-        .width(this.isLandscape ? 38 : 40)
-        .fillColor(Color.White)
-        .aspectRatio(1)
-
-      if (this.isPlayerLoading) {
-        Progress({ value: 0, total: 100, type: ProgressType.Ring })
-          .width(this.isLandscape ? 40 : 43)
-          .height(this.isLandscape ? 40 : 43)
-          .color(Color.White)
-          .style({ strokeWidth: 5, status: ProgressStatus.LOADING })
-      }
-    }
-    .width(this.isLandscape ? 38 : 41)
-    .height(this.isLandscape ? 38 : 41)
-  }
-
-  @Builder
-  private CenterControls() {
-    Row() {
-      PointLightButton({
-        builder: () => {
-          this.MoreButtonContent()
-        },
-        isPx: false,
-        builderHeight: 26,
-        builderWidth: 26,
-      })
-        .onClick(() => {
-          this.onToggleMore()
-        })
-
-      PointLightButton({
-        builder: () => {
-          this.PreviousButtonContent()
-        },
-        isPx: false,
-        builderHeight: 26,
-        builderWidth: 26,
-      })
-        .onClick(() => {
-          this.onPlayPrevious()
-        })
-
-      PointLightButton({
-        builder: () => {
-          this.PlayOrPauseButtonContent()
-        },
-        isPx: false,
-        builderHeight: this.isLandscape ? 38 : 41,
-        builderWidth: this.isLandscape ? 38 : 41,
-      })
-        .onClick(() => {
-          this.onPlayOrPause()
-        })
-
-      PointLightButton({
-        builder: () => {
-          this.NextButtonContent()
-        },
-        isPx: false,
-        builderHeight: 26,
-        builderWidth: 26,
-      })
-        .onClick(() => {
-          this.onPlayNext()
-        })
-
-      PointLightButton({
-        builder: () => {
-          Image(this.resolvePlayModeIcon())
-            .width(24)
-            .aspectRatio(1)
-        },
-        isPx: false,
-        builderHeight: 26,
-        builderWidth: 26,
-      })
-        .onClick(() => {
-          this.onSetLoopMode()
-        })
-    }
-    .width('95%')
-    .justifyContent(FlexAlign.SpaceEvenly)
-  }
-
-  @Builder
-  private ProgressControls() {
-    Column() {
-      Row() {
-        Stack() {
-          SymbolGlyph($r('sys.symbol.arrow_counterclockwise'))
-            .fontColor([Color.White])
-            .fontSize(21)
-            .effectStrategy(1)
-          Text(this.resolveFastForwardText())
-            .fontColor(Color.White)
-            .fontSize(10)
-            .fontWeight(FontWeight.Bold)
-        }
-        .padding({ left: 12, bottom: 6 })
-        .visibility(this.isShowBackFast ? Visibility.Visible : Visibility.None)
-        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
-        .onClick(() => {
-          this.onBackFast()
-        })
-
-        Slider({
-          value: this.progressValue,
-          min: 0,
-          max: this.progressMaxValue,
-          step: 1,
-          style: SliderStyle.InSet
-        })
-          .height(20)
-          .blockColor('rgba(255,255,255,1)')
-          .trackColor('rgba(255,255,255,0.3)')
-          .selectedColor(Color.White)
-          .trackThickness(4)
-          .showSteps(false)
-          .showTips(true)
-          .layoutWeight(1)
-          .enabled(this.slideEnable)
-          .onChange((value: number, mode: SliderChangeMode) => {
-            if (mode !== 2) {
-              return
-            }
-            this.onSeek(value)
-          })
-
-        Stack() {
-          SymbolGlyph($r('sys.symbol.arrow_clockwise'))
-            .fontColor([Color.White])
-            .fontSize(21)
-            .effectStrategy(1)
-          Text(this.resolveFastForwardText())
-            .fontColor(Color.White)
-            .fontSize(10)
-            .fontWeight(FontWeight.Bold)
-        }
-        .padding({ right: 10, bottom: 6 })
-        .visibility(this.isShowBackFast ? Visibility.Visible : Visibility.None)
-        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
-        .onClick(() => {
-          this.onForwardFast()
-        })
-      }
-      .justifyContent(FlexAlign.Center)
-      .padding({ left: 20, right: 20 })
-      .width(this.isLandscape ? '88%' : '95%')
-
-      Row() {
-        Text(this.currentTime)
-          .fontSize(10)
-          .fontColor(Color.White)
-          .margin({ left: 25 })
-        Blank()
-        Text(this.totalTime)
-          .fontSize(10)
-          .fontColor(Color.White)
-          .margin({ right: 25 })
-      }
-      .width(this.isLandscape ? '85%' : '90%')
-      .height(10)
-    }
-    .width('100%')
-    .alignItems(HorizontalAlign.Center)
-  }
-
-  @Builder
-  private BottomActionRow() {
-    Row() {
-      PointLightDefaultButton({
-        isSysBol: true,
-        pointColor: Color.White,
-        imageResource: $r('sys.symbol.rectangle_portrait_rotate'),
-        isPx: false,
-        builderHeight: 22,
-        builderWidth: 22,
-      })
-        .onClick(() => {
-          this.onRotate()
-        })
-
-      if (this.showEditButton) {
-        // 编辑标签只在当前有歌曲时展示,避免空态按钮触发无效动作。
-        PointLightDefaultButton({
-          isSysBol: true,
-          pointColor: Color.White,
-          imageResource: $r('sys.symbol.rename'),
-          isPx: false,
-          builderHeight: 22,
-          builderWidth: 22,
-        })
-          .onClick(() => {
-            this.onToggleEdit()
-          })
-      }
-
-      PointLightDefaultButton({
-        isSysBol: true,
-        pointColor: Color.White,
-        imageResource: $r('sys.symbol.slider_vertical_3'),
-        isPx: false,
-        builderHeight: 22,
-        builderWidth: 22,
-      })
-        .onClick(() => {
-          this.onToggleEqualizer()
-        })
-
-      PointLightDefaultButton({
-        isSysBol: true,
-        pointColor: Color.White,
-        imageResource: this.isFavorite ? $r('sys.symbol.heart_fill') : $r('sys.symbol.heart'),
-        isPx: false,
-        builderHeight: 22,
-        builderWidth: 22,
-      })
-        .onClick(() => {
-          this.onToggleFavorite()
-        })
-
-      PointLightDefaultButton({
-        isSysBol: true,
-        pointColor: Color.White,
-        imageResource: $r('sys.symbol.music_note_list'),
-        isPx: false,
-        builderHeight: 26,
-        builderWidth: 26,
-      })
-        .onClick(() => {
-          this.onOpenPlaylist()
-        })
-    }
-    .width(this.isLandscape ? '88%' : '95%')
-    .justifyContent(FlexAlign.SpaceAround)
-    .visibility(this.showBottomRow ? Visibility.Visible : Visibility.None)
-    .height(30)
-    .animation({
-      duration: 666,
-      curve: 'ease-in-out'
-    })
-  }
-
-  @Builder
-  private FullControls() {
-    Column() {
-      // 频谱显示是否启用由外部 builder 自己判断,组件本身不再持有该业务状态。
-      this.spectrumBuilder()
-      this.ProgressControls()
-      this.CenterControls()
-      this.BottomActionRow()
-    }
-    .justifyContent(FlexAlign.Center)
-    .alignItems(HorizontalAlign.Center)
-    .opacity(this.controlOpacity)
-    .position({ bottom: this.bottomOffset })
-  }
-
-  build() {
-    if (this.compactMode) {
-      // 横屏封面区域只复用紧凑播控,不再在 LocalMusic 里额外维护一套 UI。
-      this.CenterControls()
-    } else {
-      // 竖屏和完整播放页统一走独立播控组件。
-      this.FullControls()
-    }
-  }
-}

+ 19 - 4
entry/src/main/ets/view/player/PlayerPage.ets

@@ -1,5 +1,6 @@
 import { deviceInfo } from '@kit.BasicServicesKit'
 import { hdsEffect } from '@kit.UIDesignKit'
+import { StrUtil } from '@pura/harmony-utils'
 
 @Component
 export struct PlayerPage {
@@ -11,15 +12,21 @@ export struct PlayerPage {
   @Prop showBackgroundEffect: boolean = false
   // 背景流光控制器仍然由 LocalMusic 持有,独立页面只做承载。
   @Prop bgController: hdsEffect.ShaderEffectController | undefined = undefined
+  // 新页面壳层也支持承载封面背景,便于 NewIndex 先接入独立的播放页容器。
+  @Prop backgroundCover: string | undefined = ''
+  @Prop backgroundCoverEnabled: boolean = false
+  @Prop backgroundCoverFillMode: 'width' | 'height' | 'height-width' = 'width'
+  @Prop pageBackgroundBlurStyle: BlurStyle = BlurStyle.NONE
+  @Prop paddingTop: number = 0
+  @Prop alignItemsValue: HorizontalAlign = HorizontalAlign.Center
+  @Prop panOptions: PanGestureOptions = new PanGestureOptions()
+  @BuilderParam contentBuilder: () => void
 
   // 页面拖拽和销毁清理动作通过回调下沉给 LocalMusic,保证业务状态仍在一个地方维护。
   onPanUpdate: (event?: GestureEvent) => void = (_event?: GestureEvent): void => {}
   onPanEnd: (event?: GestureEvent) => void = (_event?: GestureEvent): void => {}
   onDisappearCleanup: () => void = (): void => {}
 
-  // 实际播放器内容由外部 builder 传入,这样播放页文件独立后还能平滑迁移剩余内容。
-  @BuilderParam contentBuilder: () => void
-
   build() {
     Column() {
       // 播放页只负责壳层结构,内部内容逐步从 LocalMusic 迁移。
@@ -48,6 +55,14 @@ export struct PlayerPage {
       : undefined)
     .height('100%')
     .width('100%')
+    .padding({ top: this.paddingTop })
+    .alignItems(this.alignItemsValue)
+    .backgroundImage(this.backgroundCoverEnabled && StrUtil.isNotEmpty(this.backgroundCover)
+      ? this.backgroundCover : null)
+    .backgroundImageSize(this.backgroundCoverFillMode === 'height-width'
+      ? { height: '150%', width: '100%' }
+      : (this.backgroundCoverFillMode === 'height' ? { height: '150%' } : { width: '100%' }))
+    .backgroundBlurStyle(this.pageBackgroundBlurStyle)
     .onDisAppear(() => {
       // 页面消失时统一回调外部清理拖拽中间态,避免残留到下一次打开。
       this.onDisappearCleanup()
@@ -55,7 +70,7 @@ export struct PlayerPage {
     .translate({ y: this.translateY })
     .gesture(
       // 播放页的关闭/切歌手势放在独立页面里承载,但具体业务判断仍交给 LocalMusic。
-      PanGesture()
+      PanGesture(this.panOptions)
         .onActionUpdate((event?: GestureEvent) => {
           this.onPanUpdate(event)
         })

+ 5 - 0
entry/src/main/ets/widget/pages/MusicPlayerWidgetCard.ets

@@ -10,6 +10,7 @@ import {
 const cardStorage: LocalStorage = new LocalStorage()
 const ENTRY_ABILITY_NAME = 'EntryAbility'
 const TAG = 'MusicPlayerWidgetCard'
+const KILLCARD_TRACE = '[KILLCARD_TRACE]'
 
 @Entry(cardStorage)
 @Component
@@ -38,6 +39,10 @@ struct MusicPlayerWidgetCard {
       `coverImageName=${this.coverImageName}, coverPath=${this.coverPath}, hasCoverImage=${this.hasCoverImage}, ` +
       `hasSong=${this.hasSong}, isPlaying=${this.isPlaying}, currentPositionMs=${this.currentPositionMs}, ` +
       `durationMs=${this.durationMs}`)
+    Logger.info(TAG,
+      `${KILLCARD_TRACE} widget aboutToAppear formId=${this.formId}, title=${this.title}, ` +
+      `isPlaying=${this.isPlaying}, position=${this.currentPositionMs}, duration=${this.durationMs}, ` +
+      `coverPath=${this.coverPath}, coverImageName=${this.coverImageName}, hasSong=${this.hasSong}`)
     this.syncSliderFromPlayback()
   }