Browse Source

美化发现页的专辑详情页面

onecold 4 tháng trước cách đây
mục cha
commit
a81bd2e88b

+ 36 - 0
entry/src/main/ets/common/util/FindPlaylistStore.ets

@@ -0,0 +1,36 @@
+import { VideoItem } from '../../viewmodel/VideoItem';
+
+let findPlaylistId: string = '';
+let findPlaylistName: string = '';
+let findVideoItems: VideoItem[] = [];
+let findCurrentPlayIndex: number = 0;
+
+export function setFindPlaylist(playlistId: string, playlistName: string, items: VideoItem[], startIndex: number): void {
+  findPlaylistId = playlistId;
+  findPlaylistName = playlistName;
+  findVideoItems = items.slice();
+  findCurrentPlayIndex = startIndex;
+}
+
+export function getFindPlaylistId(): string {
+  return findPlaylistId;
+}
+
+export function getFindPlaylistName(): string {
+  return findPlaylistName;
+}
+
+export function getFindVideoItems(): VideoItem[] {
+  return findVideoItems;
+}
+
+export function getFindCurrentPlayIndex(): number {
+  return findCurrentPlayIndex;
+}
+
+export function clearFindPlaylist(): void {
+  findPlaylistId = '';
+  findPlaylistName = '';
+  findVideoItems = [];
+  findCurrentPlayIndex = 0;
+}

+ 5 - 1
entry/src/main/ets/pages/NewIndex.ets

@@ -98,6 +98,7 @@ struct NewIndex {
   @Provide isShowDrawer: boolean = false;
   /** 抽屉菜单 X 轴偏移量,用于手势滑动动画 */
   @Provide offsetX: number = 0;
+  @StorageProp('findCanBack') findCanBack: boolean = false
   /** 本地视频列表,页面参数传入 */
   @Provide videoLocalList: Array<VideoItem> = []
   /** Y 轴偏移量(预留) */
@@ -220,7 +221,10 @@ struct NewIndex {
       const eventData: emitter.EventData = {};
       emitter.emit({ eventId: EventConstants.EVENT_SWIPE_BACK_DISK }, eventData);
     } else if(this.mType === 8) {
-      if (this.isShowDrawer) {
+      if (this.findCanBack) {
+        const eventData: emitter.EventData = {}
+        emitter.emit({ eventId: EventConstants.EVENT_FIND_VIEW_BACK }, eventData)
+      } else if (this.isShowDrawer) {
         this.getUIContext()?.animateTo({ duration: 555 }, () => {
           this.isShowDrawer = false
         })

+ 418 - 0
entry/src/main/ets/view/FindAlbumDetail.ets

@@ -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 })
+  }
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 361 - 340
entry/src/main/ets/view/FindView.ets


+ 19 - 0
entry/src/main/ets/view/LocalMusic.ets

@@ -14,6 +14,7 @@ import {
   getNavidromeTotalCount,
   setNavidromeTotalCount
 } from '../common/util/NavidromePlaylistStore';
+import { getFindVideoItems, getFindCurrentPlayIndex } from '../common/util/FindPlaylistStore';
 import { triggerLoadMoreSongs, hasMoreData } from '../common/util/NavidromeRandomLoader';
 import {
   ImplOnBufferingUpdateListener,
@@ -20157,6 +20158,24 @@ export struct LocalMusic {
           return;
         }
       }
+
+      if (playlistId === 'find-album-playlist' || playlistId.indexOf('find-') === 0) {
+        Logger.info(`heanup 检测到发现页播放请求,从内存读取videoItems`)
+        const videoItems = getFindVideoItems();
+        const currentPlayIndex = getFindCurrentPlayIndex();
+        Logger.info(`heanup 发现页歌曲列表长度: ${videoItems.length}, 索引: ${currentPlayIndex}`)
+        if (videoItems && videoItems.length > 0) {
+          if (isJump) {
+            this.setShowPlayTrue()
+          }
+          this.finishLoadingPlaylist(videoItems, currentPlayIndex, playlistName, totalCount)
+          return
+        } else {
+          Logger.warn('heanup 发现页播放请求缺少歌曲数据')
+          return
+        }
+      }
+
       // 从数据库重新加载这些歌曲,使用索引记录位置以保持顺序
       const songMap: Map<number, VideoItem> = new Map()
       let completedQueries = 0

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác