|
|
@@ -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({
|