Sfoglia il codice sorgente

MusicPlayerView继续改

onecold 4 mesi fa
parent
commit
202b94e054

+ 2 - 2
entry/src/main/ets/pages/NewIndex.ets

@@ -63,7 +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 { MusicPlayerView } from '../view/player/MusicPlayerView';
 import {
   resolveMiniPlayerMorphTarget,
 } from '../common/util/PlayerDismissHelper';
@@ -1309,7 +1309,7 @@ struct NewIndex {
   //新的播放页,这里引入外部的单独播放页组件
   @Builder
   MusicPlayBuilder() {
-    MusiPlayerView({
+    MusicPlayerView({
       onClose: (): void => {
         this.isShowMusicPlayView = false
       }

+ 118 - 0
entry/src/main/ets/playback/PlaybackViewStateCenter.ets

@@ -0,0 +1,118 @@
+import { PlayStatus } from '../common/PlayStatus'
+import Logger from '../common/util/Logger'
+import { VideoItem } from '../viewmodel/VideoItem'
+
+const TAG = 'PlaybackViewStateCenter'
+
+export class PlaybackViewState {
+  currentSong: VideoItem | undefined = undefined
+  controlPlayStatus: number = PlayStatus.INIT
+  progressValue: number = 0
+  playbackPositionMs: number = 0
+  playbackDurationMs: number = 0
+  cover: string | undefined = ''
+}
+
+function clonePlaybackViewState(source: PlaybackViewState): PlaybackViewState {
+  const target = new PlaybackViewState()
+  target.currentSong = source.currentSong
+  target.controlPlayStatus = source.controlPlayStatus
+  target.progressValue = source.progressValue
+  target.playbackPositionMs = source.playbackPositionMs
+  target.playbackDurationMs = source.playbackDurationMs
+  target.cover = source.cover
+  return target
+}
+
+export class PlaybackViewStateCenter {
+  private static instance?: PlaybackViewStateCenter
+  private latestState: PlaybackViewState = PlaybackViewStateCenter.buildStateFromStorage()
+  private observers: Array<(state: PlaybackViewState) => void> = []
+  private lastLoggedSongKey: string = ''
+  private lastLoggedStatus: number = PlayStatus.INIT
+  private lastLoggedSecond: number = -1
+
+  public static getInstance(): PlaybackViewStateCenter {
+    if (!PlaybackViewStateCenter.instance) {
+      PlaybackViewStateCenter.instance = new PlaybackViewStateCenter()
+    }
+    return PlaybackViewStateCenter.instance
+  }
+
+  private static buildStateFromStorage(): PlaybackViewState {
+    const state = new PlaybackViewState()
+    state.currentSong = AppStorage.get<VideoItem>('currentSong')
+    state.controlPlayStatus = AppStorage.get<number>('CONTROL_PlayStatus') ?? PlayStatus.INIT
+    state.progressValue = AppStorage.get<number>('progressValue') ?? 0
+    state.playbackPositionMs = AppStorage.get<number>('playbackPositionMs') ?? 0
+    state.playbackDurationMs = AppStorage.get<number>('playbackDurationMs') ?? 0
+    state.cover = AppStorage.get<string>('cover') ?? ''
+    return state
+  }
+
+  private ensureLatestStateFromStorage(): void {
+    if (this.latestState.currentSong !== undefined) {
+      return
+    }
+    const storageSong = AppStorage.get<VideoItem>('currentSong')
+    if (storageSong === undefined) {
+      return
+    }
+    this.latestState = PlaybackViewStateCenter.buildStateFromStorage()
+  }
+
+  public subscribe(callback: (state: PlaybackViewState) => void): void {
+    this.ensureLatestStateFromStorage()
+    this.observers.push(callback)
+    Logger.info(TAG,
+      `[PLAYER_DETACH] center subscribe observers=${this.observers.length}, ` +
+      `song=${this.latestState.currentSong?.name ?? ''}, status=${this.latestState.controlPlayStatus}, ` +
+      `position=${this.latestState.playbackPositionMs}, duration=${this.latestState.playbackDurationMs}`)
+    callback(clonePlaybackViewState(this.latestState))
+  }
+
+  public unsubscribe(callback: (state: PlaybackViewState) => void): void {
+    this.observers = this.observers.filter((observer: (state: PlaybackViewState) => void): boolean => observer !== callback)
+    Logger.info(TAG, `[PLAYER_DETACH] center unsubscribe observers=${this.observers.length}`)
+  }
+
+  public publish(source: string, state: PlaybackViewState): void {
+    this.latestState = clonePlaybackViewState(state)
+    this.logIfNeeded(source, this.latestState)
+    for (let i = 0; i < this.observers.length; i++) {
+      this.observers[i](clonePlaybackViewState(this.latestState))
+    }
+  }
+
+  public publishValues(source: string, currentSong: VideoItem | undefined, controlPlayStatus: number,
+    progressValue: number, playbackPositionMs: number, playbackDurationMs: number, cover: string | undefined): void {
+    const state = new PlaybackViewState()
+    state.currentSong = currentSong
+    state.controlPlayStatus = controlPlayStatus
+    state.progressValue = progressValue
+    state.playbackPositionMs = playbackPositionMs
+    state.playbackDurationMs = playbackDurationMs
+    state.cover = cover
+    this.publish(source, state)
+  }
+
+  private logIfNeeded(source: string, state: PlaybackViewState): void {
+    const songKey = state.currentSong?.filePath ?? ''
+    const currentSecond = Math.floor(Math.max(0, state.playbackPositionMs) / 1000)
+    if (songKey === this.lastLoggedSongKey &&
+      state.controlPlayStatus === this.lastLoggedStatus &&
+      currentSecond === this.lastLoggedSecond) {
+      return
+    }
+    this.lastLoggedSongKey = songKey
+    this.lastLoggedStatus = state.controlPlayStatus
+    this.lastLoggedSecond = currentSecond
+    Logger.info(TAG,
+      `[PLAYER_DETACH] center publish source=${source}, song=${state.currentSong?.name ?? ''}, ` +
+      `status=${state.controlPlayStatus}, position=${state.playbackPositionMs}, ` +
+      `duration=${state.playbackDurationMs}, progress=${state.progressValue}, observers=${this.observers.length}`)
+    Logger.info(TAG,
+      `[publish] source=${source}, song=${state.currentSong?.name ?? ''}, status=${state.controlPlayStatus}, ` +
+      `position=${state.playbackPositionMs}, duration=${state.playbackDurationMs}, progress=${state.progressValue}`)
+  }
+}

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

@@ -1,687 +0,0 @@
-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])
-  }
-}

+ 771 - 0
entry/src/main/ets/view/player/MusicPlayerView.ets

@@ -0,0 +1,771 @@
+import { PreferencesUtil, StrUtil, ToastUtil } 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'
+import { PointLightButton } from '../PointLight/PointLightButton'
+import { PointLightDefaultButton } from '../PointLight/PointLightDeFaultButton'
+
+const TAG = 'MusicPlayerView'
+
+@Component
+export struct MusicPlayerView {
+  @StorageProp('isLandscape')   isLandscape: boolean = false;
+  @StorageProp('topSafeHeight') topSafeHeight: number = 0;
+  @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0;
+  @StorageProp('windowWidth') windowWidth: number = 0;
+  @StorageProp('windowHeight') windowHeight: number = 0;
+  @StorageProp('isHiCarStatus') isHiCarStatus: boolean = false;
+  @StorageProp('curDisplayIsHiCar') curDisplayIsHiCar: boolean = false;
+  onClose: () => void = () => {}
+  @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)
+  @State isMusicBGCover: boolean = true
+  @State currentTime: string = '00:00'
+  @State totalTime: string = '00:00'
+  @State currentSwiperIndex: number = 0
+  @State sliderProgressValue: number = 0
+  @State showSimple: boolean = false
+  @State rotateAngle: number = 0
+  @State rotateAngle2: number = -45
+  @State isRotationRunning: boolean = false
+  @State isPlayerLoading: boolean = false
+  private rotationTimer: number = -1
+  private playbackController: MusicPlaybackController = MusicPlaybackController.getInstance()
+  private lyricController: LyricController = new LyricController()
+    .setEmptyHint('暂无歌词')
+    .setTextColor('#DDDDDD')
+    .setHighlightColor('#FFFFFF')
+    .setLightText(true)
+    .setAlignMode('center')
+    .setHightLightCenter(true)
+  private lyricParser: LyricParser = new LyricParser()
+  private appliedLyricContent: string = ''
+  private lyricSyncTimer: number = -1
+  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)
+    this.showSimple = PreferencesUtil.getBooleanSync('showSimple', false)
+    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()
+    this.stopRotation()
+  }
+
+  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()
+    this.syncRotationState()
+  }
+
+  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))
+  }
+
+  private syncRotationState(): void {
+    const isPlaying = isPlaybackControlPlaying(this.controlPlayStatus)
+    if (isPlaying) {
+      this.startRotation()
+      return
+    }
+    this.stopRotation()
+  }
+
+  private startRotation(): void {
+    if (!this.isRotationRunning) {
+      this.isRotationRunning = true
+      this.rotationTimer = setInterval(() => {
+        if (this.isRotationRunning) {
+          this.rotateAngle += 36
+        }
+      }, 1000)
+    }
+    animateTo({
+      duration: 777,
+      iterations: 1,
+      curve: Curve.Linear
+    }, () => {
+      this.rotateAngle2 = -9
+    })
+  }
+
+  private stopRotation(): void {
+    this.isRotationRunning = false
+    if (this.rotationTimer >= 0) {
+      clearInterval(this.rotationTimer)
+      this.rotationTimer = -1
+    }
+    animateTo({
+      duration: 1000,
+      curve: Curve.Linear,
+      iterations: 1,
+    }, () => {
+      this.rotateAngle2 = -45
+    })
+  }
+
+  private onSeek(value: number): void {
+    const seekValue = MusicPlaybackController.resolveSeekValueFromPercent(value, this.playbackDurationMs)
+    void this.playbackController.seekTo(`${Math.floor(seekValue)}`, 'musi-player-view')
+  }
+
+  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 ''
+  }
+
+  private resolveSongName(): string {
+    return this.currentSong?.name ?? '暂无播放'
+  }
+
+  private resolveSongArtist(): string {
+    return this.currentSong?.artist ?? ''
+  }
+
+  private toggleSimpleMode(): void {
+    this.showSimple = !this.showSimple
+    PreferencesUtil.put('showSimple', this.showSimple)
+  }
+
+  private notifyFeaturePending(featureName: string): void {
+    ToastUtil.showToast(`${featureName}功能待迁移`)
+  }
+
+  @Builder
+  private PlayTitle() {
+    Row() {
+      Row() {
+        PointLightDefaultButton({
+          isSysBol: true,
+          pointColor: Color.White,
+          imageResource: $r('sys.symbol.chevron_down'),
+          isPx: false,
+          builderHeight: 20,
+          builderWidth: 20,
+        })
+          .onClick(() => {
+            this.onClose()
+          })
+      }
+      .width('100%')
+    }
+    .width('100%')
+    .padding({ left: 25, top: this.topSafeHeight })
+  }
+
+  @Builder
+  private RectangleCoverView() {
+    Stack({ alignContent: Alignment.Center }) {
+      Image(StrUtil.isNotEmpty(this.cover) ? this.cover as string : $r('app.media.alt'))
+        .height(px2vp(this.windowHeight)*0.4)
+        .width('auto')
+        .aspectRatio(1)
+        .objectFit(ImageFit.Contain)
+        .alt($r('app.media.alt'))
+        .margin({ right: 8, left: 8, top: 12 })
+        .borderRadius(20)
+        .clip(true)
+        .shadow({
+          radius: 22,
+          type: ShadowType.BLUR,
+          color: 'on_primary'
+        })
+        .onClick(() => {
+          this.isMusicBGCover = !this.isMusicBGCover
+        })
+    }
+  }
+
+  @Builder
+  private musicNameInfo() {
+    Row() {
+      Column({ space: 8 }) {
+        Text(this.resolveSongName())
+          .fontSize(this.resolveSongName().length >= 28 ? 18 : 20)
+          .fontWeight(FontWeight.Bold)
+          .textOverflow({ overflow: TextOverflow.MARQUEE })
+          .maxLines(1)
+          .width('99%')
+          .textAlign(TextAlign.Start)
+          .fontColor(Color.White)
+
+        Text(this.buildArtistAlbumText())
+          .fontSize(15)
+          .textOverflow({ overflow: TextOverflow.MARQUEE })
+          .fontColor(Color.White)
+          .textAlign(TextAlign.Start)
+          .visibility(StrUtil.isEmpty(this.buildArtistAlbumText()) ? Visibility.None : Visibility.Visible)
+          .width('99%')
+          .maxLines(1)
+      }
+      .layoutWeight(1)
+      .margin({ left: 15 })
+
+      Blank()
+
+      Row() {
+        PointLightDefaultButton({
+          isSysBol: true,
+          pointColor: Color.White,
+          imageResource: this.showSimple ? $r('sys.symbol.sun_max') : $r('sys.symbol.moon'),
+          isPx: false,
+          builderHeight: 26,
+          builderWidth: 26,
+        })
+          .onClick(() => {
+            this.toggleSimpleMode()
+          })
+      }
+      .margin({ left: 8, right: 8 })
+    }
+    .width('90%')
+    .margin({ top: 15 })
+    .visibility(this.currentSwiperIndex === 0 ? Visibility.Visible : Visibility.None)
+  }
+
+  @Builder
+  private LyricsTopItem() {
+    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() {
+        Column() {
+          Text(this.resolveSongName())
+            .fontSize(16)
+            .maxLines(1)
+            .fontWeight(FontWeight.Bolder)
+            .fontColor(Color.White)
+            .textOverflow({ overflow: TextOverflow.Ellipsis })
+            .margin({ left: 10 })
+
+          Text(this.resolveSongArtist())
+            .fontSize(13)
+            .fontWeight(FontWeight.Bolder)
+            .padding({ top: 8 })
+            .fontColor(Color.White)
+            .margin({ left: 10 })
+            .maxLines(1)
+            .textOverflow({ overflow: TextOverflow.Ellipsis })
+            .visibility(StrUtil.isEmpty(this.resolveSongArtist()) ? Visibility.None : Visibility.Visible)
+        }
+        .height('100%')
+        .width('100%')
+        .justifyContent(FlexAlign.Center)
+        .alignItems(HorizontalAlign.Start)
+      }
+      .height('100%')
+      .layoutWeight(1)
+      .margin({ right: 8 })
+
+      Column() {
+        PointLightDefaultButton({
+          isSysBol: false,
+          pointColor: Color.White,
+          imageResource: $r('app.media.lyric'),
+          isPx: false,
+          builderHeight: 23,
+          builderWidth: 23,
+        })
+          .onClick(() => {
+            this.notifyFeaturePending('歌词设置')
+          })
+      }
+      .width(44)
+      .height('100%')
+      .justifyContent(FlexAlign.Center)
+      .alignItems(HorizontalAlign.End)
+    }
+    .width('90%')
+    .padding({ left: 28, right: 28 })
+    .height(58)
+    .alignItems(VerticalAlign.Center)
+  }
+
+  @Builder
+  private playMenuBuilder() {
+    Image($r('app.media.menu'))
+      .width(24)
+      .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 })
+  }
+
+  @Builder
+  private playPreviousBuilder() {
+    Image($r('app.media.ic_previous'))
+      .width(32)
+      .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 })
+      .aspectRatio(CommonConstants.ASPECT_RATIO)
+  }
+
+  @Builder
+  private playNextBuilder() {
+    Image($r('app.media.ic_next'))
+      .width(32)
+      .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 })
+      .aspectRatio(CommonConstants.ASPECT_RATIO)
+  }
+
+  @Builder
+  private PlayOrPauseButton() {
+    Stack() {
+      Column() {
+        Image(this.controlPlayStatus === PlayStatus.PLAY ? $r('app.media.hm_pause') : $r('app.media.hm_play2'))
+          .width(40)
+          .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 })
+          .fillColor(Color.White)
+          .aspectRatio(CommonConstants.ASPECT_RATIO)
+      }
+
+      if (this.isPlayerLoading) {
+        Progress({ value: 0, total: 100, type: ProgressType.Ring })
+          .width(43)
+          .height(43)
+          .color(Color.White)
+          .style({ strokeWidth: 5, status: ProgressStatus.LOADING })
+      }
+    }
+    .width(41)
+    .height(41)
+  }
+
+  @Builder
+  private playModeBuilder() {
+    Column() {
+      if (this.playType === 0) {
+        Image($r('app.media.loop'))
+          .width(24)
+          .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 })
+          .aspectRatio(CommonConstants.ASPECT_RATIO)
+      } else if (this.playType === 1) {
+        Image($r('app.media.single'))
+          .width(24)
+          .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 })
+          .aspectRatio(CommonConstants.ASPECT_RATIO)
+      } else if (this.playType === 2) {
+        Image($r('app.media.normal_play'))
+          .width(24)
+          .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 })
+          .aspectRatio(CommonConstants.ASPECT_RATIO)
+      } else if (this.playType === 3) {
+        Image($r('app.media.random'))
+          .width(24)
+          .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 })
+          .aspectRatio(CommonConstants.ASPECT_RATIO)
+      } else {
+        Image($r('app.media.noloop'))
+          .width(24)
+          .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 })
+          .aspectRatio(CommonConstants.ASPECT_RATIO)
+      }
+    }
+  }
+
+  @Builder
+  private playModeButton() {
+    PointLightButton({
+      builder: () => {
+        this.playModeBuilder()
+      },
+      isPx: false,
+      builderHeight: 26,
+      builderWidth: 26,
+    })
+      .onClick(async () => {
+        await this.playbackController.setLoopMode()
+      })
+  }
+
+  @Builder
+  private TopPlayProgressView() {
+    Column() {
+      Row() {
+        Slider({
+          value: this.sliderProgressValue,
+          min: 0,
+          max: 100,
+          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.playbackDurationMs > 0)
+          .onChange((value: number, mode: SliderChangeMode) => {
+            this.sliderProgressValue = value
+            if (mode !== 2) {
+              return
+            }
+            this.onSeek(value)
+          })
+      }
+      .justifyContent(FlexAlign.Center)
+      .padding({ left: 20, right: 20 })
+      .width('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('90%')
+      .height(10)
+    }
+    .width('100%')
+    .alignItems(HorizontalAlign.Center)
+  }
+
+  @Builder
+  private playCenterView() {
+    Row() {
+      PointLightButton({
+        builder: () => {
+          this.playMenuBuilder()
+        },
+        isPx: false,
+        builderHeight: 26,
+        builderWidth: 26,
+      })
+        .onClick(() => {
+          this.notifyFeaturePending('更多')
+        })
+
+      PointLightButton({
+        builder: () => {
+          this.playPreviousBuilder()
+        },
+        isPx: false,
+        builderHeight: 26,
+        builderWidth: 26,
+      })
+        .onClick(async () => {
+          await this.playbackController.playPrevious()
+        })
+
+      PointLightButton({
+        builder: () => {
+          this.PlayOrPauseButton()
+        },
+        isPx: false,
+        builderHeight: 41,
+        builderWidth: 41,
+      })
+        .onClick(() => {
+          void this.playbackController.playOrPause()
+        })
+
+      PointLightButton({
+        builder: () => {
+          this.playNextBuilder()
+        },
+        isPx: false,
+        builderHeight: 26,
+        builderWidth: 26,
+      })
+        .onClick(async () => {
+          await this.playbackController.playNext()
+        })
+
+      this.playModeButton()
+    }
+    .width('95%')
+    .margin({ bottom: 15 })
+    .justifyContent(FlexAlign.SpaceEvenly)
+  }
+
+  @Builder
+  private playListBuilder() {
+    PointLightDefaultButton({
+      isSysBol: true,
+      pointColor: Color.White,
+      imageResource: $r('sys.symbol.music_note_list'),
+      isPx: false,
+      builderHeight: 26,
+      builderWidth: 26,
+    })
+      .onClick(() => {
+        void this.playbackController.openPlayList()
+      })
+  }
+
+  @Builder
+  private BottomView() {
+    Row() {
+      PointLightDefaultButton({
+        isSysBol: true,
+        pointColor: Color.White,
+        imageResource: $r('sys.symbol.rectangle_portrait_rotate'),
+        isPx: false,
+        builderHeight: 22,
+        builderWidth: 22,
+      })
+        .onClick(() => {
+          this.notifyFeaturePending('旋转')
+        })
+
+      PointLightDefaultButton({
+        isSysBol: true,
+        pointColor: Color.White,
+        imageResource: $r('sys.symbol.rename'),
+        isPx: false,
+        builderHeight: 22,
+        builderWidth: 22,
+      })
+        .onClick(() => {
+          this.notifyFeaturePending('编辑标签')
+        })
+
+      PointLightDefaultButton({
+        isSysBol: true,
+        pointColor: Color.White,
+        imageResource: $r('sys.symbol.slider_vertical_3'),
+        isPx: false,
+        builderHeight: 22,
+        builderWidth: 22,
+      })
+        .onClick(() => {
+          this.notifyFeaturePending('均衡器')
+        })
+
+      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.notifyFeaturePending('收藏')
+        })
+
+      this.playListBuilder()
+    }
+    .width('95%')
+    .justifyContent(FlexAlign.SpaceAround)
+    .visibility(this.showSimple ? Visibility.None : Visibility.Visible)
+    .height(30)
+    .animation({
+      duration: 666,
+      curve: 'ease-in-out' // 可选动画曲线
+    })
+  }
+
+  @Builder
+  private BottomControl() {
+    Column() {
+      this.TopPlayProgressView()
+      this.playCenterView()
+      this.BottomView()
+    }
+    .justifyContent(FlexAlign.Center)
+    .alignItems(HorizontalAlign.Center)
+    .margin({ top: 10 })
+    .position({ bottom: 55}) // 将  固定在底部
+  }
+
+  @Builder
+  private CoverPageBuilder() {
+    Column() {
+      this.RectangleCoverView()
+      this.musicNameInfo()
+      this.BottomControl()
+    }
+    .width('100%')
+    .height('100%')
+    .justifyContent(FlexAlign.Start)
+    .alignItems(HorizontalAlign.Center)
+    .padding({ top: 20 })
+  }
+
+  @Builder
+  private LyricPageBuilder() {
+    Column() {
+      this.LyricsTopItem()
+
+      LyricView2({
+        controller: this.lyricController,
+        enableSeek: true,
+        seekUIColor: '#ff0000',
+        seekLineColor: '#80ffffff',
+        seekUIStyle: 'listItem',
+        onSeekAction: (position: number) => {
+          void this.playbackController.seekTo(`${Math.max(0, Math.floor(position))}`, 'musi-player-lyric-seek')
+          return true
+        }
+      })
+        .width('100%')
+        .layoutWeight(1)
+        .margin({ top: 2, bottom: 10, left: 4, right: 4 })
+    }
+    .width('100%')
+    .height('100%')
+    .justifyContent(FlexAlign.Start)
+    .alignItems(HorizontalAlign.Center)
+    .padding({ left: 4, right: 4, top: 38 })
+  }
+
+  build() {
+    Stack({ alignContent: Alignment.Center }) {
+      Column()
+        .width('100%')
+        .height('100%')
+        .linearGradient({
+          direction: GradientDirection.Right,
+          colors: [['#00BC70', 0.0], ['#D81B60', 1.0]]
+        })
+        .visibility(this.isMusicBGCover && StrUtil.isNotEmpty(this.cover) ? Visibility.None : Visibility.Visible)
+
+      Column() {
+        this.PlayTitle()
+
+        Swiper() {
+          this.CoverPageBuilder()
+          this.LyricPageBuilder()
+        }
+        .onChange((index: number) => {
+          this.currentSwiperIndex = index
+          if (index === 1) {
+            this.syncLyricContentIfNeeded()
+            this.syncLyricPosition()
+          }
+        })
+        .position({ x: 0, y:this.isLandscape?42: 35 })
+        .indicator(false)
+        .displayCount(1)
+        .loop(false)
+        .autoPlay(false)
+        .layoutWeight(1)
+        .width('100%')
+      }
+      .width('100%')
+      .height('100%')
+      .justifyContent(FlexAlign.Start)
+      .padding({ bottom: 5 })
+    }
+    .backgroundImage(!this.isMusicBGCover || StrUtil.isEmpty(this.cover) ? null : this.cover)
+    .backgroundImageSize(!this.isMusicBGCover ? { width: '100%' } : { height: '150%', width: '100%' })
+    .backgroundBlurStyle(!this.isMusicBGCover ? BlurStyle.NONE : BlurStyle.BACKGROUND_ULTRA_THICK)
+    .backgroundBrightness({ rate: 0, lightUpDegree: -0.1 })
+    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
+  }
+}