MusicPlayerWidgetWideCard.ets 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import { MusicCardActionConstants } from '../../common/player/MusicCardActionConstants'
  2. import Logger from '../../common/util/Logger'
  3. import {
  4. clampMusicCardWidgetProgress,
  5. formatMusicCardWidgetTime,
  6. resolveMusicCardWidgetCoverSource
  7. } from '../common/MusicPlayerWidgetHelper'
  8. const wideCardStorage: LocalStorage = new LocalStorage()
  9. const ENTRY_ABILITY_NAME = 'EntryAbility'
  10. const SLIDER_CHANGE_MODE_END = 2
  11. const TAG = 'MusicPlayerWidgetWideCard'
  12. @Entry(wideCardStorage)
  13. @Component
  14. struct MusicPlayerWidgetWideCard {
  15. @LocalStorageProp('formId') formId: string = ''
  16. @LocalStorageProp('title') title: string = '未在播放'
  17. @LocalStorageProp('artist') artist: string = '点击打开播放器'
  18. @LocalStorageProp('coverPath') coverPath: string = ''
  19. @LocalStorageProp('coverImageName') coverImageName: string = ''
  20. @LocalStorageProp('hasCoverImage') hasCoverImage: boolean = false
  21. @LocalStorageProp('hasSong') hasSong: boolean = false
  22. @LocalStorageProp('isPlaying') isPlaying: boolean = false
  23. @LocalStorageProp('currentPositionMs') @Watch('syncSliderFromPlayback') currentPositionMs: number = 0
  24. @LocalStorageProp('durationMs') @Watch('syncSliderFromPlayback') durationMs: number = 0
  25. @LocalStorageProp('currentTimeText') currentTimeText: string = '00:00'
  26. @LocalStorageProp('durationTimeText') durationTimeText: string = '00:00'
  27. @State isDragging: boolean = false
  28. @State dragProgressMs: number = 0
  29. @State sliderProgressMs: number = 0
  30. aboutToAppear(): void {
  31. Logger.info(TAG,
  32. `musicCard wide aboutToAppear formId=${this.formId}, title=${this.title}, artist=${this.artist}, ` +
  33. `coverImageName=${this.coverImageName}, coverPath=${this.coverPath}, hasCoverImage=${this.hasCoverImage}, ` +
  34. `hasSong=${this.hasSong}, isPlaying=${this.isPlaying}, currentPositionMs=${this.currentPositionMs}, ` +
  35. `durationMs=${this.durationMs}`)
  36. this.syncSliderFromPlayback()
  37. }
  38. private syncSliderFromPlayback(): void {
  39. if (this.isDragging) {
  40. return
  41. }
  42. this.sliderProgressMs = clampMusicCardWidgetProgress(this.currentPositionMs, this.durationMs)
  43. }
  44. private postControlAction(action: string): void {
  45. Logger.info(TAG, `musicCard wide postControlAction formId=${this.formId}, action=${action}`)
  46. postCardAction(this, {
  47. action: 'call',
  48. abilityName: ENTRY_ABILITY_NAME,
  49. params: {
  50. method: MusicCardActionConstants.CALL_METHOD_HANDLE_ACTION,
  51. ttmusic_music_card_form_id: this.formId,
  52. ttmusic_music_card_action: action,
  53. ttmusic_music_card_source: MusicCardActionConstants.ACTION_SOURCE_WIDGET
  54. }
  55. })
  56. }
  57. private postSeekAction(positionMs: number): void {
  58. Logger.info(TAG, `musicCard wide postSeekAction formId=${this.formId}, positionMs=${positionMs}`)
  59. postCardAction(this, {
  60. action: 'call',
  61. abilityName: ENTRY_ABILITY_NAME,
  62. params: {
  63. method: MusicCardActionConstants.CALL_METHOD_HANDLE_ACTION,
  64. ttmusic_music_card_form_id: this.formId,
  65. ttmusic_music_card_action: MusicCardActionConstants.ACTION_SEEK_TO,
  66. ttmusic_music_card_seek_position_ms: `${Math.max(0, Math.floor(positionMs))}`,
  67. ttmusic_music_card_source: MusicCardActionConstants.ACTION_SOURCE_WIDGET
  68. }
  69. })
  70. }
  71. private postRouterAction(action: string): void {
  72. Logger.info(TAG, `musicCard wide postRouterAction formId=${this.formId}, action=${action}`)
  73. postCardAction(this, {
  74. action: 'router',
  75. abilityName: ENTRY_ABILITY_NAME,
  76. params: {
  77. ttmusic_music_card_form_id: this.formId,
  78. ttmusic_music_card_action: action,
  79. ttmusic_music_card_source: MusicCardActionConstants.ACTION_SOURCE_WIDGET
  80. }
  81. })
  82. }
  83. private resolveCoverSource(): string | Resource {
  84. return resolveMusicCardWidgetCoverSource(this.coverImageName, this.coverPath, this.hasCoverImage)
  85. }
  86. private resolveSliderValue(): number {
  87. if (this.isDragging) {
  88. return this.dragProgressMs
  89. }
  90. return this.sliderProgressMs
  91. }
  92. private resolveCurrentTimeText(): string {
  93. if (this.isDragging) {
  94. return formatMusicCardWidgetTime(this.dragProgressMs)
  95. }
  96. return this.currentTimeText
  97. }
  98. private handleSliderChange(value: number, mode: SliderChangeMode): void {
  99. const nextValue = clampMusicCardWidgetProgress(value, this.durationMs)
  100. if (mode !== SLIDER_CHANGE_MODE_END) {
  101. Logger.info(TAG, `musicCard wide slider dragging formId=${this.formId}, value=${nextValue}, mode=${mode}`)
  102. this.isDragging = true
  103. this.dragProgressMs = nextValue
  104. return
  105. }
  106. Logger.info(TAG, `musicCard wide slider end formId=${this.formId}, value=${nextValue}, mode=${mode}`)
  107. this.dragProgressMs = nextValue
  108. this.sliderProgressMs = nextValue
  109. this.isDragging = false
  110. this.postSeekAction(nextValue)
  111. }
  112. @Builder
  113. private ControlButton(imageResource: Resource, action: string, buttonSize: number, symbolSize: number) {
  114. Button({ type: ButtonType.Circle, stateEffect: true }) {
  115. SymbolGlyph(imageResource)
  116. .fontSize(symbolSize)
  117. .fontColor([Color.White])
  118. .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
  119. }
  120. .width(buttonSize * 1.35)
  121. .height(buttonSize * 1.35)
  122. .backgroundColor(Color.Transparent)
  123. .onClick(() => this.postControlAction(action))
  124. }
  125. private hasCoverBackground(): boolean {
  126. if (this.hasCoverImage && this.coverImageName.length > 0) {
  127. return true;
  128. }
  129. return this.coverPath.length > 0;
  130. }
  131. private resolveTitleArtistText(): string {
  132. const safeTitle = this.title?.trim() ?? ''
  133. const safeArtist = this.artist?.trim() ?? ''
  134. if (safeTitle !== '' && safeArtist !== '') {
  135. return `${safeTitle} - ${safeArtist}`
  136. }
  137. if (safeTitle !== '') {
  138. return safeTitle
  139. }
  140. if (safeArtist !== '') {
  141. return safeArtist
  142. }
  143. return '未在播放'
  144. }
  145. build() {
  146. Stack() {
  147. if (this.hasCoverBackground()) {
  148. Column()
  149. .width('100%')
  150. .height('100%')
  151. .backgroundImage(this.resolveCoverSource())
  152. .backgroundImageSize({ width: '100%' })
  153. .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
  154. } else {
  155. Column()
  156. .width('100%')
  157. .height('100%')
  158. .linearGradient( { direction: GradientDirection.Right, colors:[['#00BC70',0.0],['#ff37a0fc',0.5],['#D81B60',1.0]]})
  159. }
  160. Column({ space: 16 }) {
  161. Row({ space: 16 }) {
  162. Image(this.resolveCoverSource())
  163. .width(120)
  164. .height(120)
  165. .borderRadius(10)
  166. .objectFit(ImageFit.Cover)
  167. Column({ space: 12 }) {
  168. Column({ space: 4 }) {
  169. Text(this.title)
  170. .fontSize(18)
  171. .fontWeight(FontWeight.Bold)
  172. .fontColor(Color.White)
  173. .maxLines(2)
  174. .textAlign(TextAlign.Center)
  175. .textOverflow({ overflow: TextOverflow.Ellipsis })
  176. Text(this.artist)
  177. .fontSize(15)
  178. .fontWeight(FontWeight.Bold)
  179. .fontColor('#E6FFFFFF')
  180. .maxLines(1)
  181. .margin({top:4})
  182. .textAlign(TextAlign.Center)
  183. .textOverflow({ overflow: TextOverflow.Ellipsis })
  184. }
  185. .width('100%')
  186. .alignItems(HorizontalAlign.Center)
  187. Column() {
  188. Slider({
  189. value: this.resolveSliderValue(),
  190. min: 0,
  191. max: Math.max(this.durationMs, 1),
  192. step: 1000,
  193. style: SliderStyle.InSet
  194. })
  195. .width('100%')
  196. .height(6)
  197. .blockColor(Color.White)
  198. .trackColor('#50FFFFFF')
  199. .selectedColor('#FFF6E8')
  200. .trackThickness(3)
  201. .showSteps(false)
  202. .showTips(false)
  203. .enabled(this.hasSong && this.durationMs > 0)
  204. .onChange((value: number, mode: SliderChangeMode) => {
  205. this.handleSliderChange(value, mode)
  206. })
  207. Row() {
  208. Text(this.resolveCurrentTimeText())
  209. .fontSize(9)
  210. .fontColor('#CFFBFFFF')
  211. Text(this.durationTimeText)
  212. .fontSize(9)
  213. .fontColor('#CFFBFFFF')
  214. }
  215. .width('90%')
  216. .justifyContent(FlexAlign.SpaceBetween)
  217. .alignItems(VerticalAlign.Center)
  218. }
  219. .width('100%')
  220. .height(8)
  221. .justifyContent(FlexAlign.Center)
  222. .padding({top:5,left:2,right:2})
  223. Row({space:2}) {
  224. this.ControlButton($r('sys.symbol.backward_end_fill'),
  225. MusicCardActionConstants.ACTION_PREVIOUS, 30, 28)
  226. Button({ type: ButtonType.Circle, stateEffect: true }) {
  227. SymbolGlyph(this.isPlaying ? $r('sys.symbol.pause_fill') : $r('sys.symbol.play_fill'))
  228. .fontSize(34)
  229. .fontColor([Color.White])
  230. .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
  231. }
  232. .width(36)
  233. .height(36)
  234. .backgroundColor(Color.Transparent)
  235. .onClick(() => this.postControlAction(MusicCardActionConstants.ACTION_PLAY_PAUSE))
  236. this.ControlButton($r('sys.symbol.forward_end_fill'),
  237. MusicCardActionConstants.ACTION_NEXT, 30, 28)
  238. }
  239. .justifyContent(FlexAlign.SpaceBetween)
  240. .width('100%')
  241. .alignItems(VerticalAlign.Center)
  242. }
  243. .width('100%')
  244. .justifyContent(FlexAlign.Center)
  245. .alignItems(HorizontalAlign.Center)
  246. .layoutWeight(1)
  247. }
  248. }
  249. .width('100%')
  250. .height('100%')
  251. .padding({ left: 18, right: 18, top: 18, bottom: 18 })
  252. }
  253. .width('100%')
  254. .height('100%')
  255. .clip(true)
  256. .onClick(() => this.postRouterAction(MusicCardActionConstants.ACTION_OPEN_PLAYER))
  257. }
  258. }