|
|
@@ -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])
|
|
|
+ }
|
|
|
+}
|