Explorar o código

发现页优化

onecold hai 4 meses
pai
achega
ed8cfcd871

+ 21 - 13
entry/src/main/ets/view/ConfigTitle.ets

@@ -1,28 +1,36 @@
 import { SymbolGlyphFancyModifier } from "../common/util/AttributeModifierUtil"
 
-
 @Component
 export struct ConfigTitle {
-  @State @Require text: string
-  @State currentIndex: number = 0
+  @Require @Prop text: string
+  @Prop actionText: string = '换一换'
+  @Prop showAction: boolean = true
+  onActionClick: () => void = () => {}
 
   build() {
     Row() {
       Text(this.text)
-        .fontSize(18)
+        .fontSize(16)
         .fontWeight(FontWeight.Bold)
         .lineHeight(25)
         .fontColor('#E6000000')
-      Row({ space: 5 }) {
-        Text('换一换').fontSize(14).lineHeight(16).fontColor('#99000000')
-        SymbolGlyph($r('sys.symbol.arrow_clockwise'))
-          .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
-      }
-      .onClick(() => {
 
-      })
-    }.width('100%')
+      if (this.showAction) {
+        Row({ space: 5 }) {
+          Text(this.actionText)
+            .fontSize(14)
+            .lineHeight(16)
+            .fontColor('#99000000')
+          SymbolGlyph($r('sys.symbol.arrow_clockwise'))
+            .attributeModifier(new SymbolGlyphFancyModifier(18, '', ''))
+        }
+        .onClick(() => {
+          this.onActionClick()
+        })
+      }
+    }
+    .width('100%')
     .justifyContent(FlexAlign.SpaceBetween)
     .padding({ top: 22, bottom: 10 })
   }
-}
+}

+ 271 - 25
entry/src/main/ets/view/FindView.ets

@@ -8,7 +8,7 @@ import Logger from '../common/util/Logger'
 import { VideoItem } from '../viewmodel/VideoItem'
 import { ConfigTitle } from './ConfigTitle'
 import { PointLightActionButton } from './PointLight/PointLightActionButton'
-
+        
 @Builder
 export function FindViewBuilder() {
   FindView()
@@ -17,8 +17,12 @@ export function FindViewBuilder() {
 const TAG = 'FindView'
 const SWIPER_MAX_COUNT = 5
 const HOT_SECTION_COUNT = 10
-const CLOUD_SECTION_COUNT = 4
+const REMOTE_POOL_COUNT = 32
+const CLOUD_SECTION_COUNT = 10
+const RECENT_POOL_COUNT = 24
 const RECENT_SECTION_COUNT = 6
+const POPULAR_SECTION_COUNT = 8
+const FAVORITE_SECTION_COUNT = 6
 const TOP_PLAYED_COUNT = 30
 
 interface FindPlaylistEventData {
@@ -38,6 +42,8 @@ export struct FindView {
   @State private hotSongs: VideoItem[] = []
   @State private remoteSongs: VideoItem[] = []
   @State private recentSongs: VideoItem[] = []
+  @State private popularSongs: VideoItem[] = []
+  @State private favoriteSongs: VideoItem[] = []
   @State private isRefreshing: boolean = false
   @State private isPageLoading: boolean = true
   @State private refreshText: string = '加载中...'
@@ -51,7 +57,11 @@ export struct FindView {
 
   private mediaTable?: MediaTable
   private localSongsPool: VideoItem[] = []
-  private topPlayedSongs: VideoItem[] = []
+  private coveredSongsPool: VideoItem[] = []
+  private remoteSongsPool: VideoItem[] = []
+  private recentSongsPool: VideoItem[] = []
+  private topPlayedSongsPool: VideoItem[] = []
+  private favoriteSongsPool: VideoItem[] = []
 
   aboutToAppear(): void {
     this.ensureMediaTable()
@@ -92,26 +102,38 @@ export struct FindView {
       const coveredSongs = this.filterSongsWithCover(uniqueLocalSongs)
 
       const requestResults = await Promise.all([
-        this.mediaTable.queryRemoteSongsAsync(16),
-        this.mediaTable.queryRecentPlayedRecordsAsync(RECENT_SECTION_COUNT),
-        this.queryTopPlayedSongs(TOP_PLAYED_COUNT)
+        this.mediaTable.queryRemoteSongsAsync(REMOTE_POOL_COUNT),
+        this.mediaTable.queryRecentPlayedRecordsAsync(RECENT_POOL_COUNT),
+        this.queryTopPlayedSongs(TOP_PLAYED_COUNT),
+        this.queryFavoriteSongs()
       ])
       const remoteSongs = requestResults[0] as VideoItem[]
       const recentSongs = requestResults[1] as VideoItem[]
       const topPlayedSongs = requestResults[2] as VideoItem[]
+      const favoriteSongs = requestResults[3] as VideoItem[]
+      const uniqueRemoteSongs = this.filterUniqueSongs(remoteSongs)
+      const uniqueRecentSongs = this.filterUniqueSongs(recentSongs)
+      const uniqueTopPlayedSongs = this.filterUniqueSongs(topPlayedSongs)
+      const uniqueFavoriteSongs = this.filterUniqueSongs(favoriteSongs)
 
       this.localSongsPool = uniqueLocalSongs
-      this.topPlayedSongs = this.filterUniqueSongs(topPlayedSongs)
-      this.swiperSongs = this.pickRandomSongs(coveredSongs, SWIPER_MAX_COUNT)
-      this.hotSongs = this.pickRandomSongs(uniqueLocalSongs, HOT_SECTION_COUNT)
-      this.remoteSongs = this.pickRandomSongs(this.filterUniqueSongs(remoteSongs), CLOUD_SECTION_COUNT)
-      this.recentSongs = this.filterUniqueSongs(recentSongs).slice(0, RECENT_SECTION_COUNT)
+      this.coveredSongsPool = coveredSongs
+      this.remoteSongsPool = uniqueRemoteSongs
+      this.recentSongsPool = uniqueRecentSongs
+      this.topPlayedSongsPool = uniqueTopPlayedSongs
+      this.favoriteSongsPool = uniqueFavoriteSongs
+      this.swiperSongs = this.pickRandomSongs(this.coveredSongsPool, SWIPER_MAX_COUNT)
+      this.hotSongs = this.pickPreferredSongs(this.localSongsPool, HOT_SECTION_COUNT)
+      this.remoteSongs = this.pickPreferredSongs(this.remoteSongsPool, CLOUD_SECTION_COUNT)
+      this.recentSongs = this.pickPreferredSongs(this.recentSongsPool, RECENT_SECTION_COUNT)
+      this.popularSongs = this.pickPreferredSongs(this.topPlayedSongsPool, POPULAR_SECTION_COUNT)
+      this.favoriteSongs = this.pickPreferredSongs(this.favoriteSongsPool, FAVORITE_SECTION_COUNT)
       this.swiperIndex = 0
       this.refreshText = ''
 
       Logger.info(
         TAG,
-        `发现页加载完成: local=${this.localSongsPool.length}, swiper=${this.swiperSongs.length}, remote=${this.remoteSongs.length}, recent=${this.recentSongs.length}, top=${this.topPlayedSongs.length}`
+        `发现页加载完成: local=${this.localSongsPool.length}, swiper=${this.swiperSongs.length}, remote=${this.remoteSongs.length}, recent=${this.recentSongs.length}, top=${this.topPlayedSongsPool.length}`
       )
     } catch (error) {
       const message = this.toErrorMessage(error)
@@ -120,8 +142,14 @@ export struct FindView {
       this.hotSongs = []
       this.remoteSongs = []
       this.recentSongs = []
-      this.topPlayedSongs = []
+      this.popularSongs = []
+      this.favoriteSongs = []
       this.localSongsPool = []
+      this.coveredSongsPool = []
+      this.remoteSongsPool = []
+      this.recentSongsPool = []
+      this.topPlayedSongsPool = []
+      this.favoriteSongsPool = []
       this.refreshText = '推荐加载失败,请下拉重试'
     } finally {
       this.isRefreshing = false
@@ -141,6 +169,18 @@ export struct FindView {
     })
   }
 
+  private queryFavoriteSongs(): Promise<VideoItem[]> {
+    return new Promise<VideoItem[]>((resolve) => {
+      if (!this.mediaTable) {
+        resolve([])
+        return
+      }
+      this.mediaTable.queryByisFav(1, (result: VideoItem[]) => {
+        resolve(result)
+      })
+    })
+  }
+
   private filterSongsWithCover(items: VideoItem[]): VideoItem[] {
     const result: VideoItem[] = []
     const seen: Set<string> = new Set<string>()
@@ -185,6 +225,30 @@ export struct FindView {
     return copy.slice(0, Math.min(count, copy.length))
   }
 
+  private pickPreferredSongs(items: VideoItem[], count: number): VideoItem[] {
+    const songsWithCover = this.filterSongsWithCover(items)
+    const result = this.pickRandomSongs(songsWithCover, count)
+    if (result.length >= count) {
+      return result
+    }
+
+    const seen: Set<string> = new Set<string>()
+    for (let i = 0; i < result.length; i++) {
+      seen.add(result[i].filePath)
+    }
+
+    const allSongs = this.pickRandomSongs(items, items.length)
+    for (let i = 0; i < allSongs.length && result.length < count; i++) {
+      const item = allSongs[i]
+      if (seen.has(item.filePath)) {
+        continue
+      }
+      seen.add(item.filePath)
+      result.push(item)
+    }
+    return result
+  }
+
   private emitPlaylistPlay(playlistId: string, playlistName: string, songs: VideoItem[], startIndex: number): void {
     if (songs.length === 0) {
       ToastUtil.showToast('暂无可播放歌曲')
@@ -197,7 +261,7 @@ export struct FindView {
       playlistName,
       songCount: songs.length,
       startIndex: safeIndex,
-      isJump: true,
+      isJump: false,
       songFilePaths: songs.map((item: VideoItem): string => item.filePath)
     }
     emitter.emit(eventPlaylistPlay, { data: playlistData })
@@ -205,7 +269,7 @@ export struct FindView {
 
   private handleActionButtonTap(type: string): void {
     if (type === 'top') {
-      this.emitPlaylistPlay('find-top-played', '最近爱听', this.topPlayedSongs, 0)
+      this.emitPlaylistPlay('find-top-played', '最近爱听', this.topPlayedSongsPool, 0)
       return
     }
     if (type === 'home') {
@@ -236,6 +300,30 @@ export struct FindView {
     this.emitPlaylistPlay('find-recent', '最近播放', this.recentSongs, index)
   }
 
+  private handlePopularSongTap(index: number): void {
+    this.emitPlaylistPlay('find-popular', '热门歌曲', this.popularSongs, index)
+  }
+
+  private handleFavoriteSongTap(index: number): void {
+    this.emitPlaylistPlay('find-favorite', '我的收藏', this.favoriteSongs, index)
+  }
+
+  private refreshLocalRandomSongs(): void {
+    this.hotSongs = this.pickPreferredSongs(this.localSongsPool, HOT_SECTION_COUNT)
+  }
+
+  private refreshCloudSongs(): void {
+    this.remoteSongs = this.pickPreferredSongs(this.remoteSongsPool, CLOUD_SECTION_COUNT)
+  }
+
+  private refreshRecentSongs(): void {
+    this.recentSongs = this.pickPreferredSongs(this.recentSongsPool, RECENT_SECTION_COUNT)
+  }
+
+  private refreshPopularSongs(): void {
+    this.popularSongs = this.pickPreferredSongs(this.topPlayedSongsPool, POPULAR_SECTION_COUNT)
+  }
+
   private getSwiperAspectRatio(): number {
     return this.currentBreakpoint === BreakpointTypeEnum.SM ? 1.06 : 1.66
   }
@@ -453,8 +541,8 @@ export struct FindView {
         })
 
       PointLightActionButton({
-        text: '天天静听',
-        iconResource: $r('sys.symbol.music_note_list'),
+        text: '本机随机',
+        iconResource: $r('sys.symbol.shuffle'),
         pointColor: this.themeColor,
         textColor: $r('app.color.text_color'),
         buttonColor: '#F6F7FB'
@@ -465,8 +553,8 @@ export struct FindView {
         })
 
       PointLightActionButton({
-        text: '随机静听',
-        iconResource: $r('sys.symbol.arrow_clockwise'),
+        text: '云端随机',
+        iconResource: $r('sys.symbol.reverse_order'),
         pointColor: this.themeColor,
         textColor: $r('app.color.text_color'),
         buttonColor: '#F6F7FB'
@@ -481,12 +569,17 @@ export struct FindView {
 
   @Builder
   private buildCloudSection() {
-    ConfigTitle({ text: '漫步云端' })
+    ConfigTitle({
+      text: '漫步云端',
+      onActionClick: () => {
+        this.refreshCloudSongs()
+      }
+    })
     if (this.remoteSongs.length === 0) {
       this.buildSectionEmptyState('还没有网盘歌曲', '先去网盘页或远程音乐页加载歌曲,这里会自动展示', false)
     } else {
       GridRow({
-        columns: this.currentBreakpoint === BreakpointTypeEnum.SM ? 2 : 4,
+        columns: this.currentBreakpoint === BreakpointTypeEnum.SM ? 2 : 5,
         gutter: 8
       }) {
         ForEach(this.remoteSongs, (item: VideoItem, index: number) => {
@@ -495,7 +588,7 @@ export struct FindView {
               Stack({ alignContent: Alignment.Bottom }) {
                 Image(this.getSongCover(item))
                   .width('100%')
-                  .aspectRatio(16 / 9)
+                  .aspectRatio(1)
                   .borderRadius(16)
                   .objectFit(ImageFit.Cover)
 
@@ -541,7 +634,12 @@ export struct FindView {
 
   @Builder
   private buildLocalRandomSection() {
-    ConfigTitle({ text: '本地随机' })
+    ConfigTitle({
+      text: '本地随机',
+      onActionClick: () => {
+        this.refreshLocalRandomSongs()
+      }
+    })
     if (this.hotSongs.length === 0) {
       this.buildSectionEmptyState('本地随机还没有内容', '本地曲库加载完成后,这里会随机展示歌曲', false)
     } else {
@@ -605,7 +703,12 @@ export struct FindView {
 
   @Builder
   private buildRecentSection() {
-    ConfigTitle({ text: '最近播放' })
+    ConfigTitle({
+      text: '最近播放',
+      onActionClick: () => {
+        this.refreshRecentSongs()
+      }
+    })
     if (this.recentSongs.length === 0) {
       this.buildSectionEmptyState('最近播放还是空的', '播放过的歌曲会自动出现在这里', false)
     } else {
@@ -624,7 +727,7 @@ export struct FindView {
                   .objectFit(ImageFit.Cover)
 
                 Row() {
-                  Text('最近播放')
+                  Text(item?.duration)
                     .fontColor(Color.White)
                     .fontWeight(FontWeight.Medium)
                     .padding({ left: 6, right: 6, top: 3, bottom: 3 })
@@ -668,6 +771,147 @@ export struct FindView {
     }
   }
 
+  @Builder
+  private buildPopularSection() {
+    ConfigTitle({
+      text: '热门歌曲',
+      onActionClick: () => {
+        this.refreshPopularSongs()
+      }
+    })
+    if (this.popularSongs.length === 0) {
+      this.buildSectionEmptyState('热门歌曲还没生成', '播放次数高的歌曲会优先展示在这里', false)
+    } else {
+      GridRow({
+        columns: this.currentBreakpoint === BreakpointTypeEnum.SM ? 2 : 4,
+        gutter: 8
+      }) {
+        ForEach(this.popularSongs, (item: VideoItem, index: number) => {
+          GridCol() {
+            Column({ space: 6 }) {
+              Stack({ alignContent: Alignment.Bottom }) {
+                Image(this.getSongCover(item))
+                  .aspectRatio(0.8)
+                  .objectFit(ImageFit.Cover)
+                  .width('100%')
+                  .borderRadius(16)
+
+                Row() {
+                  Text('热门')
+                    .fontColor('#FFFFFFFF')
+                    .fontWeight(FontWeight.Medium)
+                    .padding({ left: 6, right: 6, top: 3, bottom: 3 })
+                    .fontSize(9)
+                    .lineHeight(11)
+                    .backgroundColor('#7A000000')
+                    .borderRadius(999)
+
+                  Text(item.playCount ? `播放 ${item.playCount} 次` : '')
+                    .fontColor('#FFFFFFFF')
+                    .fontSize(9)
+                    .lineHeight(11)
+                    .visibility(item.playCount ? Visibility.Visible : Visibility.None)
+                }
+                .width('100%')
+                .justifyContent(FlexAlign.SpaceBetween)
+                .padding({ left: 8, right: 8, bottom: 8 })
+              }
+
+              Text(this.getSongTitle(item))
+                .fontColor('#E6000000')
+                .maxLines(1)
+                .fontSize(14)
+                .lineHeight(16)
+                .fontWeight(FontWeight.Bold)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+                .width('100%')
+
+              Text(this.getSongSubtitle(item))
+                .fontColor('#99000000')
+                .textAlign(TextAlign.Start)
+                .maxLines(1)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+                .fontSize(10)
+                .lineHeight(11)
+                .width('100%')
+            }
+            .onClick(() => {
+              this.handlePopularSongTap(index)
+            })
+          }
+        }, (item: VideoItem, index: number) => this.getSongKey(item, index))
+      }
+    }
+  }
+
+  @Builder
+  private buildFavoriteSection() {
+    ConfigTitle({
+      text: '我的收藏',
+      showAction: false
+    })
+    if (this.favoriteSongs.length === 0) {
+      this.buildSectionEmptyState('我的收藏还是空的', '收藏过的歌曲会自动显示在这里', false)
+    } else {
+      GridRow({
+        columns: this.currentBreakpoint === BreakpointTypeEnum.SM ? 3 : 5,
+        gutter: 8
+      }) {
+        ForEach(this.favoriteSongs, (item: VideoItem, index: number) => {
+          GridCol() {
+            Column({ space: 6 }) {
+              Stack({ alignContent: Alignment.Bottom }) {
+                Image(this.getSongCover(item))
+                  .width('100%')
+                  .aspectRatio(3 / 4)
+                  .borderRadius(16)
+                  .objectFit(ImageFit.Cover)
+
+                Row() {
+                  Text('我的收藏')
+                    .fontColor(Color.White)
+                    .fontWeight(FontWeight.Medium)
+                    .padding({ left: 6, right: 6, top: 3, bottom: 3 })
+                    .fontSize(9)
+                    .backgroundColor('#7A000000')
+                    .borderRadius(999)
+
+                  Text(item.playCount ? '播放 ' + item.playCount + ' 次' : '')
+                    .fontColor(Color.White)
+                    .fontSize(9)
+                    .visibility(item.playCount ? Visibility.Visible : Visibility.None)
+                }
+                .width('100%')
+                .padding({ left: 8, right: 8, bottom: 8 })
+                .justifyContent(FlexAlign.SpaceBetween)
+              }
+
+              Text(this.getSongTitle(item))
+                .fontSize(13)
+                .fontWeight(FontWeight.Bold)
+                .lineHeight(17)
+                .fontColor('#E6000000')
+                .maxLines(1)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+                .width('100%')
+
+              Text(this.getSongSubtitle(item))
+                .fontSize(10)
+                .lineHeight(13)
+                .fontColor('#80000000')
+                .maxLines(1)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+                .width('100%')
+            }
+            .onClick(() => {
+              this.handleFavoriteSongTap(index)
+            })
+          }
+        }, (item: VideoItem, index: number) => this.getSongKey(item, index))
+      }
+    }
+  }
+
   @Builder
   private buildSectionEmptyState(title: string, subtitle: string, useLargeCard: boolean) {
     Column({ space: 8 }) {
@@ -705,6 +949,8 @@ export struct FindView {
           this.buildLocalRandomSection()
           this.buildCloudSection()
           this.buildRecentSection()
+          this.buildPopularSection()
+          this.buildFavoriteSection()
         }
         .width('100%')
         .padding({

+ 3 - 4
entry/src/main/ets/view/PointLight/PointLightActionButton.ets

@@ -8,7 +8,7 @@ export struct PointLightActionButton {
   @Prop pointColor: ResourceColor = Color.White
   @Prop textColor: ResourceColor = $r('app.color.text_color')
   @Prop buttonColor: ResourceColor = $r('app.color.start_window_background_blur')
-  @Prop buttonHeight: Length = 58
+  @Prop buttonHeight: Length = 42
 
   public canShadow: boolean = true
   public canPointLight: boolean = true
@@ -25,8 +25,8 @@ export struct PointLightActionButton {
   }
 
   build() {
-    Button() {
-      Row({ space: 10 }) {
+    Button({ type: ButtonType.Capsule }) {
+      Row({ space: 8 }) {
         SymbolGlyph(this.iconResource)
           .fontSize(18)
           .fontColor([this.pointColor])
@@ -47,7 +47,6 @@ export struct PointLightActionButton {
     .width('100%')
     .height(this.buttonHeight)
     .stateEffect(false)
-    .type(ButtonType.Normal)
     .backgroundColor(this.buttonColor)
     .borderRadius(18)
     .border({ width: 1, color: '#1AFFFFFF', radius: 20 })