|
@@ -0,0 +1,418 @@
|
|
|
|
|
+import { StrUtil } from '@pura/harmony-utils'
|
|
|
|
|
+import { CommonConstants } from '../common/constants/CommonConstants'
|
|
|
|
|
+import { VideoItem } from '../viewmodel/VideoItem'
|
|
|
|
|
+
|
|
|
|
|
+function getFindAlbumSongKey(item: VideoItem, index: number): string {
|
|
|
|
|
+ if (StrUtil.isNotEmpty(item.filePath)) {
|
|
|
|
|
+ return item.filePath
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StrUtil.isNotEmpty(item.name)) {
|
|
|
|
|
+ return `${item.name}_${index}`
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StrUtil.isNotEmpty(item.fileName)) {
|
|
|
|
|
+ return `${item.fileName}_${index}`
|
|
|
|
|
+ }
|
|
|
|
|
+ return `find_album_song_${index}`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@Component
|
|
|
|
|
+export struct FindAlbumDetail {
|
|
|
|
|
+ @Require @Prop albumTitle: string
|
|
|
|
|
+ @Require @Prop albumArtist: string
|
|
|
|
|
+ @Require @Prop albumCoverPath: string
|
|
|
|
|
+ @Require @Prop albumSourceLabel: string
|
|
|
|
|
+ @Prop songs: VideoItem[] = []
|
|
|
|
|
+ @Prop topSafeHeight: number = 0
|
|
|
|
|
+ @Prop bottomSafeHeight: number = 0
|
|
|
|
|
+ @Prop themeColor: string = CommonConstants.DEFAULT_THEME_COLOR
|
|
|
|
|
+ onPlayAll: () => void = () => {}
|
|
|
|
|
+ onRandomPlay: () => void = () => {}
|
|
|
|
|
+ onBack: () => void = () => {}
|
|
|
|
|
+ onSongTap: (index: number) => void = (_index: number) => {}
|
|
|
|
|
+
|
|
|
|
|
+ private getThemeAccent(): ResourceColor {
|
|
|
|
|
+ return this.themeColor
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getSongTitle(item: VideoItem): string {
|
|
|
|
|
+ if (StrUtil.isNotEmpty(item.name)) {
|
|
|
|
|
+ return item.name
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StrUtil.isNotEmpty(item.fileName)) {
|
|
|
|
|
+ return item.fileName as string
|
|
|
|
|
+ }
|
|
|
|
|
+ return '未知歌曲'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getSongSubtitle(item: VideoItem): string {
|
|
|
|
|
+ if (StrUtil.isNotEmpty(item.artist) && StrUtil.isNotEmpty(item.fileName) && item.artist !== item.fileName) {
|
|
|
|
|
+ return `${item.artist} · ${item.fileName}`
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StrUtil.isNotEmpty(item.artist)) {
|
|
|
|
|
+ return item.artist as string
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StrUtil.isNotEmpty(item.fileName)) {
|
|
|
|
|
+ return item.fileName as string
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StrUtil.isNotEmpty(item.album)) {
|
|
|
|
|
+ return item.album as string
|
|
|
|
|
+ }
|
|
|
|
|
+ return '本地音乐'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getSongCover(item: VideoItem): string | Resource {
|
|
|
|
|
+ return StrUtil.isNotEmpty(item.pixelMapPath) ? item.pixelMapPath as string : $r('app.media.alt')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getAlbumCover(): string | Resource {
|
|
|
|
|
+ return StrUtil.isNotEmpty(this.albumCoverPath) ? this.albumCoverPath : $r('app.media.alt')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getPrimaryTextColor(): ResourceColor {
|
|
|
|
|
+ return $r('app.color.text_color')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getSecondaryTextColor(): ResourceColor {
|
|
|
|
|
+ return $r('app.color.find_secondary_text')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getCardBackgroundColor(): ResourceColor {
|
|
|
|
|
+ return $r('app.color.find_card_background')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getAlbumArtistText(): string {
|
|
|
|
|
+ if (StrUtil.isNotEmpty(this.albumArtist)) {
|
|
|
|
|
+ return this.albumArtist
|
|
|
|
|
+ }
|
|
|
|
|
+ return '未知歌手'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getAlbumSourceText(): string {
|
|
|
|
|
+ if (StrUtil.isNotEmpty(this.albumSourceLabel)) {
|
|
|
|
|
+ return this.albumSourceLabel
|
|
|
|
|
+ }
|
|
|
|
|
+ return '专辑'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getSongIndexText(item: VideoItem, index: number): string {
|
|
|
|
|
+ if (StrUtil.isNotEmpty(item.track)) {
|
|
|
|
|
+ const parsed = Number.parseInt(item.track as string, 10)
|
|
|
|
|
+ if (!Number.isNaN(parsed) && parsed > 0) {
|
|
|
|
|
+ return String(parsed)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return String(index + 1)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ private buildSourceTag(text: string, isStrong: boolean = false) {
|
|
|
|
|
+ Text(text)
|
|
|
|
|
+ .fontSize(11)
|
|
|
|
|
+ .fontWeight(isStrong ? FontWeight.Medium : FontWeight.Regular)
|
|
|
|
|
+ .fontColor(isStrong ? $r('app.color.white') : this.getPrimaryTextColor())
|
|
|
|
|
+ .padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
|
|
|
|
+ .backgroundColor(isStrong ? this.getThemeAccent() : '#F1EDF7')
|
|
|
|
|
+ .borderRadius(999)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ private buildHeroTag(text: string) {
|
|
|
|
|
+ Text(text)
|
|
|
|
|
+ .fontSize(11)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor('#F8FFFFFF')
|
|
|
|
|
+ .padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
|
|
|
|
+ .backgroundColor('#24FFFFFF')
|
|
|
|
|
+ .borderRadius(999)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ private buildHeaderActions() {
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ Button({ type: ButtonType.Capsule, stateEffect: false }) {
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ SymbolGlyph($r('sys.symbol.play_fill'))
|
|
|
|
|
+ .fontSize(18)
|
|
|
|
|
+ .fontColor([this.getThemeAccent()])
|
|
|
|
|
+ .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
|
|
|
|
|
+
|
|
|
|
|
+ Text('播放全部')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.getPrimaryTextColor())
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
|
|
+ .alignItems(VerticalAlign.Center)
|
|
|
|
|
+ }
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .height(44)
|
|
|
|
|
+ .backgroundColor('#FFFFFFFF')
|
|
|
|
|
+ .borderRadius(22)
|
|
|
|
|
+ .border({ width: 1, color: '#10FFFFFF', radius: 22 })
|
|
|
|
|
+ .shadow({ radius: 14, color: '#22000000', offsetY: 8 })
|
|
|
|
|
+ .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.97 })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.onPlayAll()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ Button({ type: ButtonType.Capsule, stateEffect: false }) {
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ SymbolGlyph($r('sys.symbol.arrow_clockwise'))
|
|
|
|
|
+ .fontSize(18)
|
|
|
|
|
+ .fontColor([Color.White])
|
|
|
|
|
+ .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
|
|
|
|
|
+
|
|
|
|
|
+ Text('随机播放')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor('#FFFFFFFF')
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
|
|
+ .alignItems(VerticalAlign.Center)
|
|
|
|
|
+ }
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .height(44)
|
|
|
|
|
+ .backgroundColor('#1FFFFFFF')
|
|
|
|
|
+ .borderRadius(22)
|
|
|
|
|
+ .border({ width: 1, color: '#30FFFFFF', radius: 22 })
|
|
|
|
|
+ .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.97 })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.onRandomPlay()
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ private buildHeroHeader() {
|
|
|
|
|
+ Stack({ alignContent: Alignment.TopStart }) {
|
|
|
|
|
+ Image(this.getAlbumCover())
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(410)
|
|
|
|
|
+ .objectFit(ImageFit.Cover)
|
|
|
|
|
+
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ Blank()
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(410)
|
|
|
|
|
+ .linearGradient({
|
|
|
|
|
+ direction: GradientDirection.Bottom,
|
|
|
|
|
+ colors: [['#05070D12', 0], ['#0A0E1F66', 0.4], ['#11141FE8', 1]]
|
|
|
|
|
+ })
|
|
|
|
|
+ .opacity(1)
|
|
|
|
|
+
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ Row() {
|
|
|
|
|
+ Button({ type: ButtonType.Circle, stateEffect: false }) {
|
|
|
|
|
+ SymbolGlyph($r('sys.symbol.chevron_left'))
|
|
|
|
|
+ .fontSize(20)
|
|
|
|
|
+ .fontColor([Color.White])
|
|
|
|
|
+ }
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor('#24000000')
|
|
|
|
|
+ .border({ width: 1, color: '#26FFFFFF', radius: 20 })
|
|
|
|
|
+ .shadow(ShadowStyle.OUTER_DEFAULT_XS)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.onBack()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ Blank()
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .alignItems(VerticalAlign.Center)
|
|
|
|
|
+
|
|
|
|
|
+ Blank()
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 14 }) {
|
|
|
|
|
+ Column({ space: 8 }) {
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ this.buildHeroTag(this.getAlbumSourceText())
|
|
|
|
|
+ this.buildHeroTag(this.songs.length + ' 首歌曲')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Text(this.albumTitle)
|
|
|
|
|
+ .fontSize(26)
|
|
|
|
|
+ .fontWeight(FontWeight.Bold)
|
|
|
|
|
+ .textAlign(TextAlign.Start)
|
|
|
|
|
+ .fontColor('#FFFFFFFF')
|
|
|
|
|
+ .maxLines(2)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+
|
|
|
|
|
+ Text(this.getAlbumArtistText())
|
|
|
|
|
+ .fontSize(15)
|
|
|
|
|
+ .lineHeight(21)
|
|
|
|
|
+ .textAlign(TextAlign.Start)
|
|
|
|
|
+ .fontColor('#D8FFFFFF')
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+
|
|
|
|
|
+ this.buildHeaderActions()
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(410)
|
|
|
|
|
+ .padding({ left: 18, right: 18, top: this.topSafeHeight + 8, bottom: 26 })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height(410)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ private buildSongSectionHeader() {
|
|
|
|
|
+ Row() {
|
|
|
|
|
+ Text('曲目列表')
|
|
|
|
|
+ .fontSize(18)
|
|
|
|
|
+ .fontWeight(FontWeight.Bold)
|
|
|
|
|
+ .fontColor(this.getPrimaryTextColor())
|
|
|
|
|
+
|
|
|
|
|
+ Blank()
|
|
|
|
|
+
|
|
|
|
|
+ this.buildSourceTag(this.songs.length + ' 首')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding({ left: 16, right: 16, top: 2, bottom: 4 })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ private buildSongCard(item: VideoItem, index: number) {
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ Stack({ alignContent: Alignment.Center }) {
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ Blank()
|
|
|
|
|
+ }
|
|
|
|
|
+ .width(34)
|
|
|
|
|
+ .height(34)
|
|
|
|
|
+ .backgroundColor('#F1ECF8')
|
|
|
|
|
+ .borderRadius(17)
|
|
|
|
|
+
|
|
|
|
|
+ Text(this.getSongIndexText(item, index))
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.getThemeAccent())
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Image(this.getSongCover(item))
|
|
|
|
|
+ .width(58)
|
|
|
|
|
+ .height(58)
|
|
|
|
|
+ .borderRadius(18)
|
|
|
|
|
+ .objectFit(ImageFit.Cover)
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 6 }) {
|
|
|
|
|
+ Text(this.getSongTitle(item))
|
|
|
|
|
+ .fontSize(15)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.getPrimaryTextColor())
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+
|
|
|
|
|
+ Text(this.getSongSubtitle(item))
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .lineHeight(17)
|
|
|
|
|
+ .fontColor(this.getSecondaryTextColor())
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ }
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 8 }) {
|
|
|
|
|
+ Text(item.duration ? item.duration : '--:--')
|
|
|
|
|
+ .fontSize(11)
|
|
|
|
|
+ .fontColor(this.getSecondaryTextColor())
|
|
|
|
|
+
|
|
|
|
|
+ SymbolGlyph($r('sys.symbol.play_fill'))
|
|
|
|
|
+ .fontSize(13)
|
|
|
|
|
+ .fontColor([this.getThemeAccent()])
|
|
|
|
|
+ .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignItems(HorizontalAlign.End)
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding({ left: 14, right: 14, top: 14, bottom: 14 })
|
|
|
|
|
+ .backgroundColor('#FBFAFD')
|
|
|
|
|
+ .borderRadius(24)
|
|
|
|
|
+ .border({ width: 1, color: '#0F000000', radius: 24 })
|
|
|
|
|
+ .shadow({ radius: 12, color: '#12000000', offsetY: 6 })
|
|
|
|
|
+ .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.985 })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.onSongTap(index)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ private buildEmptyState() {
|
|
|
|
|
+ Column({ space: 10 }) {
|
|
|
|
|
+ SymbolGlyph($r('sys.symbol.music_note_list'))
|
|
|
|
|
+ .fontSize(44)
|
|
|
|
|
+ .fontColor([this.getThemeAccent()])
|
|
|
|
|
+ .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
|
|
|
|
|
+
|
|
|
|
|
+ Text('专辑里还没有歌曲')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
|
|
+ .fontColor(this.getPrimaryTextColor())
|
|
|
|
|
+
|
|
|
|
|
+ Text('等歌曲扫描完成后,这里会自动列出完整曲目')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .textAlign(TextAlign.Center)
|
|
|
|
|
+ .fontColor(this.getSecondaryTextColor())
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding({ left: 24, right: 24, top: 28, bottom: 28 })
|
|
|
|
|
+ .backgroundColor(this.getCardBackgroundColor())
|
|
|
|
|
+ .borderRadius(24)
|
|
|
|
|
+ .alignItems(HorizontalAlign.Center)
|
|
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ build() {
|
|
|
|
|
+ List({ space: 12 }) {
|
|
|
|
|
+ ListItem() {
|
|
|
|
|
+ this.buildHeroHeader()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (this.songs.length === 0) {
|
|
|
|
|
+ ListItem() {
|
|
|
|
|
+ this.buildSongSectionHeader()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ListItem() {
|
|
|
|
|
+ this.buildEmptyState()
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding({ left: 16, right: 16 })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ListItem() {
|
|
|
|
|
+ this.buildSongSectionHeader()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ForEach(this.songs, (item: VideoItem, index: number) => {
|
|
|
|
|
+ ListItem() {
|
|
|
|
|
+ this.buildSongCard(item, index)
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding({ left: 16, right: 16 })
|
|
|
|
|
+ }, getFindAlbumSongKey)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .scrollBar(BarState.Off)
|
|
|
|
|
+ .edgeEffect(EdgeEffect.Spring)
|
|
|
|
|
+ .backgroundColor('#F7F5FB')
|
|
|
|
|
+ .padding({ bottom: this.bottomSafeHeight + 18 })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|