import { MusicCardActionConstants } from '../../common/player/MusicCardActionConstants' import Logger from '../../common/util/Logger' import { clampMusicCardWidgetProgress, formatMusicCardWidgetTime, resolveMusicCardWidgetCoverSource } from '../common/MusicPlayerWidgetHelper' const wideCardStorage: LocalStorage = new LocalStorage() const ENTRY_ABILITY_NAME = 'EntryAbility' const SLIDER_CHANGE_MODE_END = 2 const TAG = 'MusicPlayerWidgetWideCard' @Entry(wideCardStorage) @Component struct MusicPlayerWidgetWideCard { @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('currentTimeText') currentTimeText: string = '00:00' @LocalStorageProp('durationTimeText') durationTimeText: string = '00:00' @State isDragging: boolean = false @State dragProgressMs: number = 0 @State sliderProgressMs: number = 0 aboutToAppear(): void { Logger.info(TAG, `musicCard wide 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}`) this.syncSliderFromPlayback() } private syncSliderFromPlayback(): void { if (this.isDragging) { return } this.sliderProgressMs = clampMusicCardWidgetProgress(this.currentPositionMs, this.durationMs) } private postControlAction(action: string): void { Logger.info(TAG, `musicCard wide postControlAction formId=${this.formId}, action=${action}`) 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: action, ttmusic_music_card_source: MusicCardActionConstants.ACTION_SOURCE_WIDGET } }) } private postSeekAction(positionMs: number): void { Logger.info(TAG, `musicCard wide 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 wide 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 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 wide slider dragging formId=${this.formId}, value=${nextValue}, mode=${mode}`) this.isDragging = true this.dragProgressMs = nextValue return } Logger.info(TAG, `musicCard wide 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() { if (this.hasCoverBackground()) { 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',0.5],['#D81B60',1.0]]}) } Column({ space: 16 }) { Row({ space: 16 }) { Image(this.resolveCoverSource()) .width(120) .height(120) .borderRadius(10) .objectFit(ImageFit.Cover) Column({ space: 12 }) { Column({ space: 4 }) { Text(this.title) .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor(Color.White) .maxLines(2) .textAlign(TextAlign.Center) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.artist) .fontSize(15) .fontWeight(FontWeight.Bold) .fontColor('#E6FFFFFF') .maxLines(1) .margin({top:4}) .textAlign(TextAlign.Center) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width('100%') .alignItems(HorizontalAlign.Center) Column() { 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('#50FFFFFF') .selectedColor('#FFF6E8') .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(9) .fontColor('#CFFBFFFF') Text(this.durationTimeText) .fontSize(9) .fontColor('#CFFBFFFF') } .width('90%') .justifyContent(FlexAlign.SpaceBetween) .alignItems(VerticalAlign.Center) } .width('100%') .height(8) .justifyContent(FlexAlign.Center) .padding({top:5,left:2,right:2}) Row({space:2}) { this.ControlButton($r('sys.symbol.backward_end_fill'), MusicCardActionConstants.ACTION_PREVIOUS, 30, 28) Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph(this.isPlaying ? $r('sys.symbol.pause_fill') : $r('sys.symbol.play_fill')) .fontSize(34) .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_NEXT, 30, 28) } .justifyContent(FlexAlign.SpaceBetween) .width('100%') .alignItems(VerticalAlign.Center) } .width('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .layoutWeight(1) } } .width('100%') .height('100%') .padding({ left: 18, right: 18, top: 18, bottom: 18 }) } .width('100%') .height('100%') .clip(true) .onClick(() => this.postRouterAction(MusicCardActionConstants.ACTION_OPEN_PLAYER)) } }