import { MusicCardActionConstants } from '../../common/player/MusicCardActionConstants' import Logger from '../../common/util/Logger' import { buildMusicCardWidgetActionParams, clampMusicCardWidgetProgress, resolveMusicCardWidgetCoverSource, shouldDispatchMusicCardActionByCall } from '../common/MusicPlayerWidgetHelper' const cardStorage: LocalStorage = new LocalStorage() const ENTRY_ABILITY_NAME = 'EntryAbility' const TAG = 'MusicPlayerWidgetCard' @Entry(cardStorage) @Component struct MusicPlayerWidgetCard { @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 isChangeBg: boolean = true @State sliderProgressMs: number = 0 /** * 组件挂载时记录当前卡片状态,并同步一次进度条显示。 */ aboutToAppear(): void { Logger.info(TAG, `musicCard small 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 { this.sliderProgressMs = clampMusicCardWidgetProgress(this.currentPositionMs, this.durationMs) } /** * 向卡片宿主发送播放控制动作,按动作类型选择 call 或 router 分发。 */ private postControlAction(action: string): void { Logger.info(TAG, `musicCard small postControlAction formId=${this.formId}, action=${action}`) const useCallDispatch = shouldDispatchMusicCardActionByCall(action) const params = buildMusicCardWidgetActionParams(this.formId, action) postCardAction(this, { action: useCallDispatch ? 'call' : 'router', abilityName: ENTRY_ABILITY_NAME, params }) } /** * 通过 router 方式跳转到主界面,并携带卡片来源与动作参数。 */ private postRouterAction(action: string): void { Logger.info(TAG, `musicCard small 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 resolveProgressValue(): number { if (this.durationMs <= 0) { return 0 } return Math.min(100, Math.max(0, Math.floor(this.sliderProgressMs * 100 / this.durationMs))) } /** * 拼接标题与歌手文本,缺省时返回占位文案。 */ 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 '未在播放' } /** * 根据当前播放状态返回播放或暂停图标。 */ private resolvePlayPauseIcon(): Resource { return this.isPlaying ? $r('sys.symbol.pause_fill') : $r('sys.symbol.play_fill') } /** * 构建上一首、下一首等通用控制按钮。 */ @Builder private ControlButton(imageResource: Resource, action: string, buttonSize: number, symbolSize: number) { Button() { SymbolGlyph(imageResource) .fontSize(symbolSize) .fontColor([Color.White]) .effectStrategy(SymbolEffectStrategy.HIERARCHICAL) } .width(buttonSize * 1.35) .height(buttonSize * 1.35) .backgroundColor(Color.Transparent) .type(ButtonType.Circle) .stateEffect(true) .onClick(() => this.postControlAction(action)) } /** * 构建带环形进度指示的播放/暂停按钮。 */ @Builder private PlayProgressButton() { Stack({ alignContent: Alignment.Center }) { Progress({ value: this.resolveProgressValue(), total: 100, type: ProgressType.Ring }) .width(34) .height(34) .color(Color.White) .style({ strokeWidth: 2 }) Button() { SymbolGlyph(this.resolvePlayPauseIcon()) .fontSize(19) .fontColor([Color.White]) .effectStrategy(SymbolEffectStrategy.HIERARCHICAL) } .width(34) .height(34) .type(ButtonType.Circle) .backgroundColor('#26FFFFFF') .stateEffect(false) .onClick(() => this.postControlAction(MusicCardActionConstants.ACTION_PLAY_PAUSE)) } .width(34) .height(34) } /** * 判断当前是否具备可用于背景展示的封面资源。 */ private hasCoverBackground(): boolean { if (this.hasCoverImage && this.coverImageName.length > 0) { return true; } return this.coverPath.length > 0; } /** * 构建音乐播放卡片主体界面,并绑定卡片交互事件。 */ build() { Stack() { 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],['#D81B60',1.0]]}) } Column({ space: 12 }) { Image(this.resolveCoverSource()) .width(60) .height(60) .borderRadius(6) .objectFit(ImageFit.Cover) .onClick(()=>{ this.isChangeBg = !this.isChangeBg }) Text(this.resolveTitleArtistText()) .fontSize(13) .fontWeight(FontWeight.Bold) .fontColor(Color.White) .maxLines(1) .textAlign(TextAlign.Center) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row({ space: 16 }) { this.ControlButton($r('sys.symbol.backward_end_fill'), MusicCardActionConstants.ACTION_PREVIOUS, 18, 18) this.PlayProgressButton() this.ControlButton($r('sys.symbol.forward_end_fill'), MusicCardActionConstants.ACTION_NEXT, 18, 18) } .justifyContent(FlexAlign.Center) .alignItems(VerticalAlign.Center) } .width('100%') .height('100%') .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) .padding({ left: 16, right: 16, top: 16, bottom: 14 }) } .width('100%') .height('100%') .clip(true) .onClick(() => this.postRouterAction(MusicCardActionConstants.ACTION_OPEN_PLAYER)) } }