import { PlayStatus } from '../../common/PlayStatus' import { PointLightButton } from '../PointLight/PointLightButton' import { PointLightDefaultButton } from '../PointLight/PointLightDeFaultButton' @Component export struct PlayerControls { // 播控支持紧凑模式,给横屏封面页复用同一套独立组件。 @Prop compactMode: boolean = false // 整体透明度和底部偏移由外部页面控制,避免组件再持有布局状态。 @Prop controlOpacity: number = 1 @Prop bottomOffset: number = 70 // 是否展示底部功能操作区,HiCar 等场景沿用原有隐藏逻辑。 @Prop showBottomRow: boolean = true // 播控区需要的播放态、进度态都作为纯数据输入。 @Prop isLandscape: boolean = false @Prop progressValue: number = 0 @Prop progressMaxValue: number = 100 @Prop slideEnable: boolean = false @Prop currentTime: string = '00:00' @Prop totalTime: string = '00:00' @Prop controlPlayStatus: number = PlayStatus.INIT @Prop isCircleBtn: boolean = false @Prop isPlayerLoading: boolean = false @Prop playType: number = 0 @Prop fastForwardSeconds: string = '10' @Prop isShowBackFast: boolean = false // 编辑、收藏等按钮是否展示,由外部决定当前歌曲是否可操作。 @Prop showEditButton: boolean = false @Prop isFavorite: boolean = false // 频谱区域也改成可注入 builder,后续可以继续抽到独立组件而不动页面骨架。 @BuilderParam spectrumBuilder: () => void // 播控内部只触发动作,不再持有具体业务逻辑。 onPlayPrevious: () => void = (): void => {} onPlayOrPause: () => void = (): void => {} onPlayNext: () => void = (): void => {} onToggleMore: () => void = (): void => {} onSetLoopMode: () => void = (): void => {} onSeek: (value: number) => void = (_value: number): void => {} onBackFast: () => void = (): void => {} onForwardFast: () => void = (): void => {} onRotate: () => void = (): void => {} onToggleEdit: () => void = (): void => {} onToggleEqualizer: () => void = (): void => {} onToggleFavorite: () => void = (): void => {} onOpenPlaylist: () => void = (): void => {} private resolvePlayModeIcon(): Resource { // 播放模式图标统一在独立组件里收口,减少 LocalMusic 内部判断分支。 if (this.playType === 0) { return $r('app.media.loop') } if (this.playType === 1) { return $r('app.media.single') } if (this.playType === 2) { return $r('app.media.normal_play') } if (this.playType === 3) { return $r('app.media.random') } return $r('app.media.noloop') } private resolveFastForwardText(): string { // 统一把快进秒数当成字符串展示,避免上层状态类型变化时影响 UI 组件。 return this.fastForwardSeconds } @Builder private MoreButtonContent() { // 更多按钮沿用原有资源,保持现有视觉风格不变。 Image($r('app.media.menu')) .width(24) } @Builder private PreviousButtonContent() { Image($r('app.media.ic_previous')) .width(32) .aspectRatio(1) } @Builder private NextButtonContent() { Image($r('app.media.ic_next')) .width(32) .aspectRatio(1) } @Builder private PlayOrPauseButtonContent() { Stack() { // 播放暂停按钮单独保留加载圈,兼容远程歌曲缓冲场景。 Image(this.controlPlayStatus === PlayStatus.PLAY ? (this.isCircleBtn ? $r('app.media.hm_pause') : $r('app.media.ic_public_play')) : (this.isCircleBtn ? $r('app.media.hm_play2') : $r('app.media.ic_public_pause'))) .width(this.isLandscape ? 38 : 40) .fillColor(Color.White) .aspectRatio(1) if (this.isPlayerLoading) { Progress({ value: 0, total: 100, type: ProgressType.Ring }) .width(this.isLandscape ? 40 : 43) .height(this.isLandscape ? 40 : 43) .color(Color.White) .style({ strokeWidth: 5, status: ProgressStatus.LOADING }) } } .width(this.isLandscape ? 38 : 41) .height(this.isLandscape ? 38 : 41) } @Builder private CenterControls() { Row() { PointLightButton({ builder: () => { this.MoreButtonContent() }, isPx: false, builderHeight: 26, builderWidth: 26, }) .onClick(() => { this.onToggleMore() }) PointLightButton({ builder: () => { this.PreviousButtonContent() }, isPx: false, builderHeight: 26, builderWidth: 26, }) .onClick(() => { this.onPlayPrevious() }) PointLightButton({ builder: () => { this.PlayOrPauseButtonContent() }, isPx: false, builderHeight: this.isLandscape ? 38 : 41, builderWidth: this.isLandscape ? 38 : 41, }) .onClick(() => { this.onPlayOrPause() }) PointLightButton({ builder: () => { this.NextButtonContent() }, isPx: false, builderHeight: 26, builderWidth: 26, }) .onClick(() => { this.onPlayNext() }) PointLightButton({ builder: () => { Image(this.resolvePlayModeIcon()) .width(24) .aspectRatio(1) }, isPx: false, builderHeight: 26, builderWidth: 26, }) .onClick(() => { this.onSetLoopMode() }) } .width('95%') .justifyContent(FlexAlign.SpaceEvenly) } @Builder private ProgressControls() { Column() { Row() { Stack() { SymbolGlyph($r('sys.symbol.arrow_counterclockwise')) .fontColor([Color.White]) .fontSize(21) .effectStrategy(1) Text(this.resolveFastForwardText()) .fontColor(Color.White) .fontSize(10) .fontWeight(FontWeight.Bold) } .padding({ left: 12, bottom: 6 }) .visibility(this.isShowBackFast ? Visibility.Visible : Visibility.None) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { this.onBackFast() }) Slider({ value: this.progressValue, min: 0, max: this.progressMaxValue, 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.slideEnable) .onChange((value: number, mode: SliderChangeMode) => { if (mode !== 2) { return } this.onSeek(value) }) Stack() { SymbolGlyph($r('sys.symbol.arrow_clockwise')) .fontColor([Color.White]) .fontSize(21) .effectStrategy(1) Text(this.resolveFastForwardText()) .fontColor(Color.White) .fontSize(10) .fontWeight(FontWeight.Bold) } .padding({ right: 10, bottom: 6 }) .visibility(this.isShowBackFast ? Visibility.Visible : Visibility.None) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { this.onForwardFast() }) } .justifyContent(FlexAlign.Center) .padding({ left: 20, right: 20 }) .width(this.isLandscape ? '88%' : '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(this.isLandscape ? '85%' : '90%') .height(10) } .width('100%') .alignItems(HorizontalAlign.Center) } @Builder private BottomActionRow() { Row() { PointLightDefaultButton({ isSysBol: true, pointColor: Color.White, imageResource: $r('sys.symbol.rectangle_portrait_rotate'), isPx: false, builderHeight: 22, builderWidth: 22, }) .onClick(() => { this.onRotate() }) if (this.showEditButton) { // 编辑标签只在当前有歌曲时展示,避免空态按钮触发无效动作。 PointLightDefaultButton({ isSysBol: true, pointColor: Color.White, imageResource: $r('sys.symbol.rename'), isPx: false, builderHeight: 22, builderWidth: 22, }) .onClick(() => { this.onToggleEdit() }) } PointLightDefaultButton({ isSysBol: true, pointColor: Color.White, imageResource: $r('sys.symbol.slider_vertical_3'), isPx: false, builderHeight: 22, builderWidth: 22, }) .onClick(() => { this.onToggleEqualizer() }) 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.onToggleFavorite() }) PointLightDefaultButton({ isSysBol: true, pointColor: Color.White, imageResource: $r('sys.symbol.music_note_list'), isPx: false, builderHeight: 26, builderWidth: 26, }) .onClick(() => { this.onOpenPlaylist() }) } .width(this.isLandscape ? '88%' : '95%') .justifyContent(FlexAlign.SpaceAround) .visibility(this.showBottomRow ? Visibility.Visible : Visibility.None) .height(30) .animation({ duration: 666, curve: 'ease-in-out' }) } @Builder private FullControls() { Column() { // 频谱显示是否启用由外部 builder 自己判断,组件本身不再持有该业务状态。 this.spectrumBuilder() this.ProgressControls() this.CenterControls() this.BottomActionRow() } .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .opacity(this.controlOpacity) .position({ bottom: this.bottomOffset }) } build() { if (this.compactMode) { // 横屏封面区域只复用紧凑播控,不再在 LocalMusic 里额外维护一套 UI。 this.CenterControls() } else { // 竖屏和完整播放页统一走独立播控组件。 this.FullControls() } } }