|
@@ -0,0 +1,367 @@
|
|
|
|
|
+import { MusicCardActionConstants } from '../../common/player/MusicCardActionConstants'
|
|
|
|
|
+import Logger from '../../common/util/Logger'
|
|
|
|
|
+import {
|
|
|
|
|
+ clampMusicCardWidgetProgress,
|
|
|
|
|
+ formatMusicCardWidgetTime,
|
|
|
|
|
+ resolveMusicCardWidgetCoverSource
|
|
|
|
|
+} from '../common/MusicPlayerWidgetHelper'
|
|
|
|
|
+
|
|
|
|
|
+const dynamicLyricCardStorage: LocalStorage = new LocalStorage()
|
|
|
|
|
+const ENTRY_ABILITY_NAME = 'EntryAbility'
|
|
|
|
|
+const SLIDER_CHANGE_MODE_END = 2
|
|
|
|
|
+const TAG = 'MusicPlayerWidgetDynamicLyricCard'
|
|
|
|
|
+
|
|
|
|
|
+@Entry(dynamicLyricCardStorage)
|
|
|
|
|
+@Component
|
|
|
|
|
+struct MusicPlayerWidgetDynamicLyricCard {
|
|
|
|
|
+ @LocalStorageProp('formId') formId: string = ''
|
|
|
|
|
+ @LocalStorageProp('title') title: string = '未在播放'
|
|
|
|
|
+ @LocalStorageProp('artist') artist: string = '点击打开播放器'
|
|
|
|
|
+ @LocalStorageProp('coverPath') coverPath: string = ''
|
|
|
|
|
+ @LocalStorageProp('coverImageName') coverImageName: string = ''
|
|
|
|
|
+ @LocalStorageProp('hasCoverImage') hasCoverImage: boolean = false
|
|
|
|
|
+ @LocalStorageProp('hasSong') hasSong: boolean = false
|
|
|
|
|
+ @LocalStorageProp('isPlaying') isPlaying: boolean = false
|
|
|
|
|
+ @LocalStorageProp('currentPositionMs') @Watch('syncSliderFromPlayback') currentPositionMs: number = 0
|
|
|
|
|
+ @LocalStorageProp('durationMs') @Watch('syncSliderFromPlayback') durationMs: number = 0
|
|
|
|
|
+ @LocalStorageProp('updatedAtMs') @Watch('syncLyricDisplayFromSnapshot') updatedAtMs: number = 0
|
|
|
|
|
+ @LocalStorageProp('currentTimeText') currentTimeText: string = '00:00'
|
|
|
|
|
+ @LocalStorageProp('durationTimeText') durationTimeText: string = '00:00'
|
|
|
|
|
+ @LocalStorageProp('lyricLine0') lyricLine0: string = ''
|
|
|
|
|
+ @LocalStorageProp('lyricLine1') lyricLine1: string = '未在播放'
|
|
|
|
|
+ @LocalStorageProp('lyricLine2') lyricLine2: string = '点击打开播放器'
|
|
|
|
|
+ @LocalStorageProp('hasLyric') hasLyric: boolean = false
|
|
|
|
|
+
|
|
|
|
|
+ @State isDragging: boolean = false
|
|
|
|
|
+ @State dragProgressMs: number = 0
|
|
|
|
|
+ @State sliderProgressMs: number = 0
|
|
|
|
|
+ @State isChangeBg: boolean = true
|
|
|
|
|
+ @State displayLyricLine0: string = ''
|
|
|
|
|
+ @State displayLyricLine1: string = '未在播放'
|
|
|
|
|
+ @State displayLyricLine2: string = '点击打开播放器'
|
|
|
|
|
+ @State lyricTranslateY: number = 0
|
|
|
|
|
+ @State lyricOpacity: number = 1
|
|
|
|
|
+
|
|
|
|
|
+ aboutToAppear(): void {
|
|
|
|
|
+ Logger.info(TAG,
|
|
|
|
|
+ `musicCard dynamic lyric aboutToAppear formId=${this.formId}, title=${this.title}, artist=${this.artist}, ` +
|
|
|
|
|
+ `coverImageName=${this.coverImageName}, coverPath=${this.coverPath}, hasCoverImage=${this.hasCoverImage}, ` +
|
|
|
|
|
+ `hasSong=${this.hasSong}, isPlaying=${this.isPlaying}, currentPositionMs=${this.currentPositionMs}, ` +
|
|
|
|
|
+ `durationMs=${this.durationMs}, lyricLine0=${this.lyricLine0}, lyricLine1=${this.lyricLine1}, lyricLine2=${this.lyricLine2}, hasLyric=${this.hasLyric}`)
|
|
|
|
|
+ this.syncSliderFromPlayback()
|
|
|
|
|
+ this.syncLyricDisplay(false)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private syncSliderFromPlayback(): void {
|
|
|
|
|
+ if (this.isDragging) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.sliderProgressMs = clampMusicCardWidgetProgress(this.currentPositionMs, this.durationMs)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private postControlAction(action: string): void {
|
|
|
|
|
+ Logger.info(TAG, `musicCard dynamic lyric postControlAction formId=${this.formId}, action=${action}`)
|
|
|
|
|
+ postCardAction(this, {
|
|
|
|
|
+ action: 'router',
|
|
|
|
|
+ abilityName: ENTRY_ABILITY_NAME,
|
|
|
|
|
+ params: {
|
|
|
|
|
+ ttmusic_music_card_form_id: this.formId,
|
|
|
|
|
+ ttmusic_music_card_action: action,
|
|
|
|
|
+ ttmusic_music_card_source: MusicCardActionConstants.ACTION_SOURCE_WIDGET
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private postSeekAction(positionMs: number): void {
|
|
|
|
|
+ Logger.info(TAG, `musicCard dynamic lyric postSeekAction formId=${this.formId}, positionMs=${positionMs}`)
|
|
|
|
|
+ postCardAction(this, {
|
|
|
|
|
+ action: 'call',
|
|
|
|
|
+ abilityName: ENTRY_ABILITY_NAME,
|
|
|
|
|
+ params: {
|
|
|
|
|
+ method: MusicCardActionConstants.CALL_METHOD_HANDLE_ACTION,
|
|
|
|
|
+ ttmusic_music_card_form_id: this.formId,
|
|
|
|
|
+ ttmusic_music_card_action: MusicCardActionConstants.ACTION_SEEK_TO,
|
|
|
|
|
+ ttmusic_music_card_seek_position_ms: `${Math.max(0, Math.floor(positionMs))}`,
|
|
|
|
|
+ ttmusic_music_card_source: MusicCardActionConstants.ACTION_SOURCE_WIDGET
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private postRouterAction(action: string): void {
|
|
|
|
|
+ Logger.info(TAG, `musicCard dynamic lyric postRouterAction formId=${this.formId}, action=${action}`)
|
|
|
|
|
+ postCardAction(this, {
|
|
|
|
|
+ action: 'router',
|
|
|
|
|
+ abilityName: ENTRY_ABILITY_NAME,
|
|
|
|
|
+ params: {
|
|
|
|
|
+ ttmusic_music_card_form_id: this.formId,
|
|
|
|
|
+ ttmusic_music_card_action: action,
|
|
|
|
|
+ ttmusic_music_card_source: MusicCardActionConstants.ACTION_SOURCE_WIDGET
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private resolveCoverSource(): string | Resource {
|
|
|
|
|
+ return resolveMusicCardWidgetCoverSource(this.coverImageName, this.coverPath, this.hasCoverImage)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private resolveSliderValue(): number {
|
|
|
|
|
+ if (this.isDragging) {
|
|
|
|
|
+ return this.dragProgressMs
|
|
|
|
|
+ }
|
|
|
|
|
+ return this.sliderProgressMs
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private resolveDisplayLyricLine0(): string {
|
|
|
|
|
+ return this.lyricLine0?.trim() ?? ''
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private resolveDisplayLyricLine1(): string {
|
|
|
|
|
+ const safeLine = this.lyricLine1?.trim() ?? ''
|
|
|
|
|
+ if (safeLine !== '') {
|
|
|
|
|
+ return safeLine
|
|
|
|
|
+ }
|
|
|
|
|
+ return this.hasSong ? this.title : '未在播放'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private resolveDisplayLyricLine2(): string {
|
|
|
|
|
+ return this.lyricLine2?.trim() ?? ''
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private syncLyricDisplay(shouldAnimate: boolean): void {
|
|
|
|
|
+ const nextLine0 = this.resolveDisplayLyricLine0()
|
|
|
|
|
+ const nextLine1 = this.resolveDisplayLyricLine1()
|
|
|
|
|
+ const nextLine2 = this.resolveDisplayLyricLine2()
|
|
|
|
|
+ const changed = this.displayLyricLine0 !== nextLine0 ||
|
|
|
|
|
+ this.displayLyricLine1 !== nextLine1 ||
|
|
|
|
|
+ this.displayLyricLine2 !== nextLine2
|
|
|
|
|
+
|
|
|
|
|
+ if (!changed) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.displayLyricLine0 = nextLine0
|
|
|
|
|
+ this.displayLyricLine1 = nextLine1
|
|
|
|
|
+ this.displayLyricLine2 = nextLine2
|
|
|
|
|
+
|
|
|
|
|
+ if (!shouldAnimate) {
|
|
|
|
|
+ this.lyricTranslateY = 0
|
|
|
|
|
+ this.lyricOpacity = 1
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.lyricTranslateY = 14
|
|
|
|
|
+ this.lyricOpacity = 0.35
|
|
|
|
|
+ animateTo({ duration: 260, curve: Curve.EaseOut }, () => {
|
|
|
|
|
+ this.lyricTranslateY = 0
|
|
|
|
|
+ this.lyricOpacity = 1
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private syncLyricDisplayFromSnapshot(): void {
|
|
|
|
|
+ this.syncLyricDisplay(true)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private resolveCurrentTimeText(): string {
|
|
|
|
|
+ if (this.isDragging) {
|
|
|
|
|
+ return formatMusicCardWidgetTime(this.dragProgressMs)
|
|
|
|
|
+ }
|
|
|
|
|
+ return this.currentTimeText
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private handleSliderChange(value: number, mode: SliderChangeMode): void {
|
|
|
|
|
+ const nextValue = clampMusicCardWidgetProgress(value, this.durationMs)
|
|
|
|
|
+ if (mode !== SLIDER_CHANGE_MODE_END) {
|
|
|
|
|
+ Logger.info(TAG, `musicCard dynamic lyric slider dragging formId=${this.formId}, value=${nextValue}, mode=${mode}`)
|
|
|
|
|
+ this.isDragging = true
|
|
|
|
|
+ this.dragProgressMs = nextValue
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ Logger.info(TAG, `musicCard dynamic lyric slider end formId=${this.formId}, value=${nextValue}, mode=${mode}`)
|
|
|
|
|
+ this.dragProgressMs = nextValue
|
|
|
|
|
+ this.sliderProgressMs = nextValue
|
|
|
|
|
+ this.isDragging = false
|
|
|
|
|
+ this.postSeekAction(nextValue)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ private ControlButton(imageResource: Resource, action: string, buttonSize: number, symbolSize: number) {
|
|
|
|
|
+ Button({ type: ButtonType.Circle, stateEffect: true }) {
|
|
|
|
|
+ SymbolGlyph(imageResource)
|
|
|
|
|
+ .fontSize(symbolSize)
|
|
|
|
|
+ .fontColor([Color.White])
|
|
|
|
|
+ .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width(buttonSize * 1.35)
|
|
|
|
|
+ .height(buttonSize * 1.35)
|
|
|
|
|
+ .backgroundColor(Color.Transparent)
|
|
|
|
|
+ .onClick(() => this.postControlAction(action))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private hasCoverBackground(): boolean {
|
|
|
|
|
+ if (this.hasCoverImage && this.coverImageName.length > 0) {
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ return this.coverPath.length > 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private resolveTitleArtistText(): string {
|
|
|
|
|
+ const safeTitle = this.title?.trim() ?? ''
|
|
|
|
|
+ const safeArtist = this.artist?.trim() ?? ''
|
|
|
|
|
+ if (safeTitle !== '' && safeArtist !== '') {
|
|
|
|
|
+ return `${safeTitle} - ${safeArtist}`
|
|
|
|
|
+ }
|
|
|
|
|
+ if (safeTitle !== '') {
|
|
|
|
|
+ return safeTitle
|
|
|
|
|
+ }
|
|
|
|
|
+ if (safeArtist !== '') {
|
|
|
|
|
+ return safeArtist
|
|
|
|
|
+ }
|
|
|
|
|
+ return '未在播放'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ build() {
|
|
|
|
|
+ Stack({ alignContent: Alignment.Center }) {
|
|
|
|
|
+ if (this.hasCoverBackground() && this.isChangeBg) {
|
|
|
|
|
+ Column()
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .backgroundImage(this.resolveCoverSource())
|
|
|
|
|
+ .backgroundImageSize({ width: '100%' })
|
|
|
|
|
+ .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Column()
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .linearGradient({ direction: GradientDirection.Right, colors: [
|
|
|
|
|
+ ['#00BC70', 0.0], ['#ff37a0fc', 1.0]] })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 14 }) {
|
|
|
|
|
+ Image(this.resolveCoverSource())
|
|
|
|
|
+ .width(110)
|
|
|
|
|
+ .height(110)
|
|
|
|
|
+ .borderRadius(8)
|
|
|
|
|
+ .objectFit(ImageFit.Cover)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.isChangeBg = !this.isChangeBg
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ Text(this.resolveTitleArtistText())
|
|
|
|
|
+ .fontSize(18)
|
|
|
|
|
+ .fontWeight(FontWeight.Bold)
|
|
|
|
|
+ .fontColor(Color.White)
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textAlign(TextAlign.Center)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 6 }) {
|
|
|
|
|
+ Text(this.displayLyricLine0)
|
|
|
|
|
+ .fontSize(15)
|
|
|
|
|
+ .opacity(0.9)
|
|
|
|
|
+ .fontColor(Color.White)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textAlign(TextAlign.Center)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ .opacity(this.displayLyricLine0 === '' ? 0 : this.lyricOpacity * 0.78)
|
|
|
|
|
+ .translate({ x: 0, y: this.lyricTranslateY - 8 })
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+
|
|
|
|
|
+ Text(this.displayLyricLine1)
|
|
|
|
|
+ .fontSize(17)
|
|
|
|
|
+ .fontColor(Color.White)
|
|
|
|
|
+ .fontWeight(FontWeight.Bold)
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textAlign(TextAlign.Center)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ .opacity(this.lyricOpacity)
|
|
|
|
|
+ .translate({ x: 0, y: this.lyricTranslateY })
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+
|
|
|
|
|
+ Text(this.displayLyricLine2)
|
|
|
|
|
+ .fontSize(15)
|
|
|
|
|
+ .fontColor(Color.White)
|
|
|
|
|
+ .opacity(0.9)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textAlign(TextAlign.Center)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ .opacity(this.displayLyricLine2 === '' ? 0 : this.lyricOpacity * 0.78)
|
|
|
|
|
+ .translate({ x: 0, y: this.lyricTranslateY + 8 })
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(82)
|
|
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 4 }) {
|
|
|
|
|
+ Slider({
|
|
|
|
|
+ value: this.resolveSliderValue(),
|
|
|
|
|
+ min: 0,
|
|
|
|
|
+ max: Math.max(this.durationMs, 1),
|
|
|
|
|
+ step: 1000,
|
|
|
|
|
+ style: SliderStyle.InSet
|
|
|
|
|
+ })
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(6)
|
|
|
|
|
+ .blockColor(Color.White)
|
|
|
|
|
+ .trackColor('#58FFFFFF')
|
|
|
|
|
+ .selectedColor('#FFF6EC')
|
|
|
|
|
+ .trackThickness(3)
|
|
|
|
|
+ .showSteps(false)
|
|
|
|
|
+ .showTips(false)
|
|
|
|
|
+ .enabled(this.hasSong && this.durationMs > 0)
|
|
|
|
|
+ .onChange((value: number, mode: SliderChangeMode) => {
|
|
|
|
|
+ this.handleSliderChange(value, mode)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ Row() {
|
|
|
|
|
+ Text(this.resolveCurrentTimeText())
|
|
|
|
|
+ .fontSize(10)
|
|
|
|
|
+ .fontColor('#D8FFFFFF')
|
|
|
|
|
+ Text(this.durationTimeText)
|
|
|
|
|
+ .fontSize(10)
|
|
|
|
|
+ .fontColor('#D8FFFFFF')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .justifyContent(FlexAlign.SpaceBetween)
|
|
|
|
|
+ .alignItems(VerticalAlign.Center)
|
|
|
|
|
+ }
|
|
|
|
|
+ .height(10)
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+
|
|
|
|
|
+ Row({ space: 2 }) {
|
|
|
|
|
+ this.ControlButton($r('sys.symbol.backward_end_fill'),
|
|
|
|
|
+ MusicCardActionConstants.ACTION_PREVIOUS, 32, 28)
|
|
|
|
|
+
|
|
|
|
|
+ Button({ type: ButtonType.Circle, stateEffect: true }) {
|
|
|
|
|
+ SymbolGlyph(this.isPlaying ? $r('sys.symbol.pause_fill') : $r('sys.symbol.play_fill'))
|
|
|
|
|
+ .fontSize(30)
|
|
|
|
|
+ .fontColor([Color.White])
|
|
|
|
|
+ .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width(36)
|
|
|
|
|
+ .height(36)
|
|
|
|
|
+ .backgroundColor(Color.Transparent)
|
|
|
|
|
+ .onClick(() => this.postControlAction(MusicCardActionConstants.ACTION_PLAY_PAUSE))
|
|
|
|
|
+
|
|
|
|
|
+ this.ControlButton($r('sys.symbol.forward_end_fill'),
|
|
|
|
|
+ MusicCardActionConstants.ACTION_PREVIOUS, 32, 28)
|
|
|
|
|
+ }
|
|
|
|
|
+ .justifyContent(FlexAlign.SpaceBetween)
|
|
|
|
|
+ .alignItems(VerticalAlign.Center)
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
|
|
+ .alignItems(HorizontalAlign.Center)
|
|
|
|
|
+ .padding({ left: 24, right: 24, top: 20, bottom: 20 })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .clip(true)
|
|
|
|
|
+ .onClick(() => this.postRouterAction(MusicCardActionConstants.ACTION_OPEN_PLAYER))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|