|
|
@@ -0,0 +1,755 @@
|
|
|
+import { StrUtil, ToastUtil } from '@pura/harmony-utils'
|
|
|
+import { BusinessError, emitter } from '@kit.BasicServicesKit'
|
|
|
+import { CommonConstants } from '../common/constants/CommonConstants'
|
|
|
+import { EventConstants } from '../common/constants/EventConstants'
|
|
|
+import MediaTable from '../common/util/MediaTable'
|
|
|
+import { BreakpointTypeEnum } from '../common/util/BreakpointSystem'
|
|
|
+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()
|
|
|
+}
|
|
|
+
|
|
|
+const TAG = 'FindView'
|
|
|
+const SWIPER_MAX_COUNT = 5
|
|
|
+const HOT_SECTION_COUNT = 10
|
|
|
+const CLOUD_SECTION_COUNT = 4
|
|
|
+const RECENT_SECTION_COUNT = 6
|
|
|
+const TOP_PLAYED_COUNT = 30
|
|
|
+
|
|
|
+interface FindPlaylistEventData {
|
|
|
+ playlistId: string
|
|
|
+ playlistName: string
|
|
|
+ songCount: number
|
|
|
+ startIndex: number
|
|
|
+ isJump: boolean
|
|
|
+ songFilePaths: string[]
|
|
|
+}
|
|
|
+
|
|
|
+@Component
|
|
|
+export struct FindView {
|
|
|
+ @Consume mType: number
|
|
|
+
|
|
|
+ @State private swiperSongs: VideoItem[] = []
|
|
|
+ @State private hotSongs: VideoItem[] = []
|
|
|
+ @State private remoteSongs: VideoItem[] = []
|
|
|
+ @State private recentSongs: VideoItem[] = []
|
|
|
+ @State private isRefreshing: boolean = false
|
|
|
+ @State private isPageLoading: boolean = true
|
|
|
+ @State private refreshText: string = '加载中...'
|
|
|
+ @State private swiperIndex: number = 0
|
|
|
+ @State private refreshPullRatio: number = 1
|
|
|
+ @State private maxRefreshingHeight: number = 100
|
|
|
+
|
|
|
+ @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD
|
|
|
+ @StorageProp('windowWidth') windowWidth: number = 0
|
|
|
+ @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR
|
|
|
+
|
|
|
+ private mediaTable?: MediaTable
|
|
|
+ private localSongsPool: VideoItem[] = []
|
|
|
+ private topPlayedSongs: VideoItem[] = []
|
|
|
+
|
|
|
+ aboutToAppear(): void {
|
|
|
+ this.ensureMediaTable()
|
|
|
+ }
|
|
|
+
|
|
|
+ private ensureMediaTable(): void {
|
|
|
+ if (this.mediaTable) {
|
|
|
+ void this.loadDiscoveryContent(false)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ this.mediaTable = new MediaTable(getContext(this), () => {
|
|
|
+ void this.loadDiscoveryContent(false)
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ const message = this.toErrorMessage(error)
|
|
|
+ Logger.error(TAG, `初始化媒体数据库失败: ${message}`)
|
|
|
+ this.refreshText = '推荐加载失败,请下拉重试'
|
|
|
+ this.isPageLoading = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async loadDiscoveryContent(triggeredByRefresh: boolean): Promise<void> {
|
|
|
+ if (!this.mediaTable) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (triggeredByRefresh) {
|
|
|
+ this.isRefreshing = true
|
|
|
+ this.refreshText = '正在刷新推荐...'
|
|
|
+ } else if (this.isPageLoading) {
|
|
|
+ this.refreshText = '加载中...'
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const allSongs = await this.mediaTable.queryAllVideos()
|
|
|
+ const uniqueLocalSongs = this.filterUniqueSongs(allSongs)
|
|
|
+ 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)
|
|
|
+ ])
|
|
|
+ const remoteSongs = requestResults[0] as VideoItem[]
|
|
|
+ const recentSongs = requestResults[1] as VideoItem[]
|
|
|
+ const topPlayedSongs = requestResults[2] as VideoItem[]
|
|
|
+
|
|
|
+ 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.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}`
|
|
|
+ )
|
|
|
+ } catch (error) {
|
|
|
+ const message = this.toErrorMessage(error)
|
|
|
+ Logger.error(TAG, `发现页加载失败: ${message}`)
|
|
|
+ this.swiperSongs = []
|
|
|
+ this.hotSongs = []
|
|
|
+ this.remoteSongs = []
|
|
|
+ this.recentSongs = []
|
|
|
+ this.topPlayedSongs = []
|
|
|
+ this.localSongsPool = []
|
|
|
+ this.refreshText = '推荐加载失败,请下拉重试'
|
|
|
+ } finally {
|
|
|
+ this.isRefreshing = false
|
|
|
+ this.isPageLoading = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private queryTopPlayedSongs(limitCount: number): Promise<VideoItem[]> {
|
|
|
+ return new Promise<VideoItem[]>((resolve) => {
|
|
|
+ if (!this.mediaTable) {
|
|
|
+ resolve([])
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.mediaTable.queryByPlayCountDesc(limitCount, (result: VideoItem[]) => {
|
|
|
+ resolve(result)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ private filterSongsWithCover(items: VideoItem[]): VideoItem[] {
|
|
|
+ const result: VideoItem[] = []
|
|
|
+ const seen: Set<string> = new Set<string>()
|
|
|
+ for (let i = 0; i < items.length; i++) {
|
|
|
+ const item = items[i]
|
|
|
+ const filePath = item.filePath || ''
|
|
|
+ if (StrUtil.isEmpty(filePath) || seen.has(filePath)) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if (StrUtil.isEmpty(item.pixelMapPath)) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ seen.add(filePath)
|
|
|
+ result.push(item)
|
|
|
+ }
|
|
|
+ return result
|
|
|
+ }
|
|
|
+
|
|
|
+ private filterUniqueSongs(items: VideoItem[]): VideoItem[] {
|
|
|
+ const result: VideoItem[] = []
|
|
|
+ const seen: Set<string> = new Set<string>()
|
|
|
+ for (let i = 0; i < items.length; i++) {
|
|
|
+ const item = items[i]
|
|
|
+ const filePath = item.filePath || ''
|
|
|
+ if (StrUtil.isEmpty(filePath) || seen.has(filePath)) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ seen.add(filePath)
|
|
|
+ result.push(item)
|
|
|
+ }
|
|
|
+ return result
|
|
|
+ }
|
|
|
+
|
|
|
+ private pickRandomSongs(items: VideoItem[], count: number): VideoItem[] {
|
|
|
+ const copy: VideoItem[] = items.slice()
|
|
|
+ for (let i = copy.length - 1; i > 0; i--) {
|
|
|
+ const randomIndex = Math.floor(Math.random() * (i + 1))
|
|
|
+ const current = copy[i]
|
|
|
+ copy[i] = copy[randomIndex]
|
|
|
+ copy[randomIndex] = current
|
|
|
+ }
|
|
|
+ return copy.slice(0, Math.min(count, copy.length))
|
|
|
+ }
|
|
|
+
|
|
|
+ private emitPlaylistPlay(playlistId: string, playlistName: string, songs: VideoItem[], startIndex: number): void {
|
|
|
+ if (songs.length === 0) {
|
|
|
+ ToastUtil.showToast('暂无可播放歌曲')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const safeIndex = Math.min(Math.max(startIndex, 0), songs.length - 1)
|
|
|
+ const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }
|
|
|
+ const playlistData: FindPlaylistEventData = {
|
|
|
+ playlistId,
|
|
|
+ playlistName,
|
|
|
+ songCount: songs.length,
|
|
|
+ startIndex: safeIndex,
|
|
|
+ isJump: true,
|
|
|
+ songFilePaths: songs.map((item: VideoItem): string => item.filePath)
|
|
|
+ }
|
|
|
+ emitter.emit(eventPlaylistPlay, { data: playlistData })
|
|
|
+ }
|
|
|
+
|
|
|
+ private handleActionButtonTap(type: string): void {
|
|
|
+ if (type === 'top') {
|
|
|
+ this.emitPlaylistPlay('find-top-played', '最近爱听', this.topPlayedSongs, 0)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (type === 'home') {
|
|
|
+ this.mType = 0
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (this.localSongsPool.length === 0) {
|
|
|
+ ToastUtil.showToast('本地歌曲为空')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const startIndex = Math.floor(Math.random() * this.localSongsPool.length)
|
|
|
+ this.emitPlaylistPlay('find-random-local', '随机静听', this.localSongsPool, startIndex)
|
|
|
+ }
|
|
|
+
|
|
|
+ private handleSwiperTap(index: number): void {
|
|
|
+ this.emitPlaylistPlay('find-swiper', '发现推荐', this.swiperSongs, index)
|
|
|
+ }
|
|
|
+
|
|
|
+ private handleRemoteSongTap(index: number): void {
|
|
|
+ this.emitPlaylistPlay('find-cloud', '漫步云端', this.remoteSongs, index)
|
|
|
+ }
|
|
|
+
|
|
|
+ private handleHotSongTap(index: number): void {
|
|
|
+ this.emitPlaylistPlay('find-local-random', '本地随机', this.hotSongs, index)
|
|
|
+ }
|
|
|
+
|
|
|
+ private handleRecentSongTap(index: number): void {
|
|
|
+ this.emitPlaylistPlay('find-recent', '最近播放', this.recentSongs, index)
|
|
|
+ }
|
|
|
+
|
|
|
+ private getSwiperAspectRatio(): number {
|
|
|
+ return this.currentBreakpoint === BreakpointTypeEnum.SM ? 1.06 : 1.66
|
|
|
+ }
|
|
|
+
|
|
|
+ private getSwiperTextSize(smallSize: number, largeSize: number): number {
|
|
|
+ return this.currentBreakpoint === BreakpointTypeEnum.SM ? smallSize : largeSize
|
|
|
+ }
|
|
|
+
|
|
|
+ private getIndicatorItemWidth(): number {
|
|
|
+ const widthVp = this.windowWidth > 0 ? px2vp(this.windowWidth) : 360
|
|
|
+ const count = Math.max(1, this.swiperSongs.length)
|
|
|
+ const gap = 8
|
|
|
+ const horizontalPadding = this.currentBreakpoint === BreakpointTypeEnum.SM ? 64 : 120
|
|
|
+ const usableWidth = Math.max(80, widthVp - horizontalPadding - gap * (count - 1))
|
|
|
+ return usableWidth / count
|
|
|
+ }
|
|
|
+
|
|
|
+ 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.album)) {
|
|
|
+ return `${item.artist} · ${item.album}`
|
|
|
+ }
|
|
|
+ if (StrUtil.isNotEmpty(item.artist)) {
|
|
|
+ return item.artist 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 getSongKey(item: VideoItem, index: number): string {
|
|
|
+ if (StrUtil.isNotEmpty(item.filePath)) {
|
|
|
+ return item.filePath
|
|
|
+ }
|
|
|
+ return `${this.getSongTitle(item)}_${index}`
|
|
|
+ }
|
|
|
+
|
|
|
+ private getCloudTypeLabel(item: VideoItem): string {
|
|
|
+ switch (item.type) {
|
|
|
+ case CommonConstants.TYPE_WEBDAV:
|
|
|
+ return 'WebDAV'
|
|
|
+ case CommonConstants.TYPE_SMB:
|
|
|
+ return 'SMB'
|
|
|
+ case CommonConstants.TYPE_NAVIDROME:
|
|
|
+ return 'Navidrome'
|
|
|
+ case CommonConstants.TYPE_FTP:
|
|
|
+ return 'FTP'
|
|
|
+ case CommonConstants.TYPE_BAIDU:
|
|
|
+ return '百度网盘'
|
|
|
+ case CommonConstants.TYPE_JELLYFIN:
|
|
|
+ return 'Jellyfin'
|
|
|
+ case CommonConstants.TYPE_EMBY:
|
|
|
+ return 'Emby'
|
|
|
+ case CommonConstants.TYPE_AUDIOSTATION:
|
|
|
+ return 'AudioStation'
|
|
|
+ case CommonConstants.TYPE_PLEX:
|
|
|
+ return 'Plex'
|
|
|
+ case CommonConstants.TYPE_DAOLIYU:
|
|
|
+ return '道理鱼'
|
|
|
+ default:
|
|
|
+ return '云端'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private getRecentSongSubtitle(item: VideoItem): string {
|
|
|
+ if (StrUtil.isNotEmpty(item.lastPlayedStr)) {
|
|
|
+ return item.lastPlayedStr as string
|
|
|
+ }
|
|
|
+ return this.getSongSubtitle(item)
|
|
|
+ }
|
|
|
+
|
|
|
+ private toErrorMessage(error: Object): string {
|
|
|
+ const businessError = error as BusinessError
|
|
|
+ if (businessError && businessError.message) {
|
|
|
+ return businessError.message
|
|
|
+ }
|
|
|
+ const rawError = error as Error
|
|
|
+ if (rawError && StrUtil.isNotEmpty(rawError.message)) {
|
|
|
+ return rawError.message
|
|
|
+ }
|
|
|
+ return `${error}`
|
|
|
+ }
|
|
|
+
|
|
|
+ @Builder
|
|
|
+ private buildLoadingState() {
|
|
|
+ Column({ space: 14 }) {
|
|
|
+ LoadingProgress()
|
|
|
+ .width(46)
|
|
|
+ .height(46)
|
|
|
+ .color(this.themeColor)
|
|
|
+
|
|
|
+ Text(this.refreshText)
|
|
|
+ .fontSize(15)
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
+ .fontColor('#99000000')
|
|
|
+ }
|
|
|
+ .width('100%')
|
|
|
+ .layoutWeight(1)
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
+ .alignItems(HorizontalAlign.Center)
|
|
|
+ }
|
|
|
+
|
|
|
+ @Builder
|
|
|
+ private buildTopErrorBanner() {
|
|
|
+ if (StrUtil.isNotEmpty(this.refreshText)) {
|
|
|
+ Row({ space: 6 }) {
|
|
|
+ SymbolGlyph($r('sys.symbol.exclamationmark_circle'))
|
|
|
+ .fontSize(15)
|
|
|
+ .fontColor(['#805A3D00'])
|
|
|
+ Text(this.refreshText)
|
|
|
+ .layoutWeight(1)
|
|
|
+ .fontSize(12)
|
|
|
+ .fontColor('#805A3D00')
|
|
|
+ .maxLines(1)
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
+ }
|
|
|
+ .width('100%')
|
|
|
+ .padding({ left: 12, right: 12, top: 10, bottom: 10 })
|
|
|
+ .backgroundColor('#FFF6E5')
|
|
|
+ .borderRadius(16)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Builder
|
|
|
+ private buildSwiperSection() {
|
|
|
+ if (this.swiperSongs.length > 0) {
|
|
|
+ Swiper() {
|
|
|
+ ForEach(this.swiperSongs, (item: VideoItem, index: number) => {
|
|
|
+ Stack({ alignContent: Alignment.BottomStart }) {
|
|
|
+ Image(this.getSongCover(item))
|
|
|
+ .width('100%')
|
|
|
+ .aspectRatio(this.getSwiperAspectRatio())
|
|
|
+ .objectFit(ImageFit.Cover)
|
|
|
+
|
|
|
+ Column({ space: 10 }) {
|
|
|
+
|
|
|
+ Column({ space: 4 }) {
|
|
|
+ Text(this.getSongTitle(item))
|
|
|
+ .fontColor(Color.White)
|
|
|
+ .fontSize(this.getSwiperTextSize(18, 24))
|
|
|
+ .fontWeight(FontWeight.Bold)
|
|
|
+ .maxLines(1)
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
+
|
|
|
+ Text(this.getSongSubtitle(item))
|
|
|
+ .fontColor('#E6FFFFFF')
|
|
|
+ .fontSize(this.getSwiperTextSize(12, 15))
|
|
|
+ .maxLines(1)
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
+ }
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
+ }
|
|
|
+ .width('100%')
|
|
|
+ .padding({
|
|
|
+ left: this.currentBreakpoint === BreakpointTypeEnum.SM ? 16 : 22,
|
|
|
+ right: this.currentBreakpoint === BreakpointTypeEnum.SM ? 16 : 22,
|
|
|
+ bottom: this.currentBreakpoint === BreakpointTypeEnum.SM ? 16 : 22
|
|
|
+ })
|
|
|
+ .linearGradient({
|
|
|
+ direction: GradientDirection.Top,
|
|
|
+ colors: [['#D9000000', 0], ['#7A000000', 0.48], ['#00000000', 1]]
|
|
|
+ })
|
|
|
+ .borderRadius(24)
|
|
|
+ }
|
|
|
+ .onClick(() => {
|
|
|
+ this.handleSwiperTap(index)
|
|
|
+ })
|
|
|
+ }, (item: VideoItem, index: number) => this.getSongKey(item, index))
|
|
|
+ }
|
|
|
+ .width('100%')
|
|
|
+ .clip(true)
|
|
|
+ .autoPlay(true)
|
|
|
+ .interval(3200)
|
|
|
+ .loop(true)
|
|
|
+ .onChange((index: number) => {
|
|
|
+ this.swiperIndex = index
|
|
|
+ })
|
|
|
+ .indicator(new DotIndicator()
|
|
|
+ .itemWidth(this.getIndicatorItemWidth())
|
|
|
+ .itemHeight(2)
|
|
|
+ .selectedItemWidth(this.getIndicatorItemWidth())
|
|
|
+ .selectedItemHeight(4))
|
|
|
+ } else {
|
|
|
+ this.buildSectionEmptyState('还没有可展示的封面音乐', '给本地歌曲补上封面后,这里会自动随机推荐', true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Builder
|
|
|
+ private buildActionButtons() {
|
|
|
+ Row({ space: 10 }) {
|
|
|
+ PointLightActionButton({
|
|
|
+ text: '最近爱听',
|
|
|
+ iconResource: $r('sys.symbol.heart'),
|
|
|
+ pointColor: this.themeColor,
|
|
|
+ textColor: $r('app.color.text_color'),
|
|
|
+ buttonColor: '#F6F7FB'
|
|
|
+ })
|
|
|
+ .layoutWeight(1)
|
|
|
+ .onClick(() => {
|
|
|
+ this.handleActionButtonTap('top')
|
|
|
+ })
|
|
|
+
|
|
|
+ PointLightActionButton({
|
|
|
+ text: '天天静听',
|
|
|
+ iconResource: $r('sys.symbol.music_note_list'),
|
|
|
+ pointColor: this.themeColor,
|
|
|
+ textColor: $r('app.color.text_color'),
|
|
|
+ buttonColor: '#F6F7FB'
|
|
|
+ })
|
|
|
+ .layoutWeight(1)
|
|
|
+ .onClick(() => {
|
|
|
+ this.handleActionButtonTap('home')
|
|
|
+ })
|
|
|
+
|
|
|
+ PointLightActionButton({
|
|
|
+ text: '随机静听',
|
|
|
+ iconResource: $r('sys.symbol.arrow_clockwise'),
|
|
|
+ pointColor: this.themeColor,
|
|
|
+ textColor: $r('app.color.text_color'),
|
|
|
+ buttonColor: '#F6F7FB'
|
|
|
+ })
|
|
|
+ .layoutWeight(1)
|
|
|
+ .onClick(() => {
|
|
|
+ this.handleActionButtonTap('random')
|
|
|
+ })
|
|
|
+ }
|
|
|
+ .width('100%')
|
|
|
+ }
|
|
|
+
|
|
|
+ @Builder
|
|
|
+ private buildCloudSection() {
|
|
|
+ ConfigTitle({ text: '漫步云端' })
|
|
|
+ if (this.remoteSongs.length === 0) {
|
|
|
+ this.buildSectionEmptyState('还没有网盘歌曲', '先去网盘页或远程音乐页加载歌曲,这里会自动展示', false)
|
|
|
+ } else {
|
|
|
+ GridRow({
|
|
|
+ columns: this.currentBreakpoint === BreakpointTypeEnum.SM ? 2 : 4,
|
|
|
+ gutter: 8
|
|
|
+ }) {
|
|
|
+ ForEach(this.remoteSongs, (item: VideoItem, index: number) => {
|
|
|
+ GridCol() {
|
|
|
+ Column({ space: 6 }) {
|
|
|
+ Stack({ alignContent: Alignment.Bottom }) {
|
|
|
+ Image(this.getSongCover(item))
|
|
|
+ .width('100%')
|
|
|
+ .aspectRatio(16 / 9)
|
|
|
+ .borderRadius(16)
|
|
|
+ .objectFit(ImageFit.Cover)
|
|
|
+
|
|
|
+ Row() {
|
|
|
+ Text(this.getCloudTypeLabel(item))
|
|
|
+ .fontColor(Color.White)
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
+ .padding({ left: 6, right: 6, top: 3, bottom: 3 })
|
|
|
+ .fontSize(9)
|
|
|
+ .backgroundColor('#7A000000')
|
|
|
+ .borderRadius(999)
|
|
|
+ }
|
|
|
+ .width('100%')
|
|
|
+ .padding({ left: 8, right: 8, bottom: 8 })
|
|
|
+ .justifyContent(FlexAlign.Start)
|
|
|
+ }
|
|
|
+
|
|
|
+ Text(this.getSongTitle(item))
|
|
|
+ .fontSize(14)
|
|
|
+ .fontWeight(FontWeight.Bold)
|
|
|
+ .lineHeight(18)
|
|
|
+ .fontColor('#E6000000')
|
|
|
+ .maxLines(1)
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
+ .width('100%')
|
|
|
+
|
|
|
+ Text(this.getSongSubtitle(item))
|
|
|
+ .fontSize(11)
|
|
|
+ .lineHeight(14)
|
|
|
+ .fontColor('#80000000')
|
|
|
+ .maxLines(1)
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
+ .width('100%')
|
|
|
+ }
|
|
|
+ .onClick(() => {
|
|
|
+ this.handleRemoteSongTap(index)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }, (item: VideoItem, index: number) => this.getSongKey(item, index))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Builder
|
|
|
+ private buildLocalRandomSection() {
|
|
|
+ ConfigTitle({ text: '本地随机' })
|
|
|
+ if (this.hotSongs.length === 0) {
|
|
|
+ this.buildSectionEmptyState('本地随机还没有内容', '本地曲库加载完成后,这里会随机展示歌曲', false)
|
|
|
+ } else {
|
|
|
+ List({ space: 8 }) {
|
|
|
+ ForEach(this.hotSongs, (item: VideoItem, index: number) => {
|
|
|
+ ListItem() {
|
|
|
+ Column({ space: 5 }) {
|
|
|
+ Stack({ alignContent: Alignment.Bottom }) {
|
|
|
+ Image(this.getSongCover(item))
|
|
|
+ .aspectRatio(3 / 4)
|
|
|
+ .borderRadius(16)
|
|
|
+ .width(160)
|
|
|
+ .objectFit(ImageFit.Cover)
|
|
|
+
|
|
|
+ Row() {
|
|
|
+ Text(this.getSongSubtitle(item))
|
|
|
+ .fontColor('#FFFFFFFF')
|
|
|
+ .fontSize(9)
|
|
|
+ .lineHeight(11)
|
|
|
+ .maxLines(1)
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
+ }
|
|
|
+ .justifyContent(FlexAlign.End)
|
|
|
+ .padding({ left: 8, right: 8, bottom: 9 })
|
|
|
+ .width(160)
|
|
|
+ }
|
|
|
+
|
|
|
+ Text(this.getSongTitle(item))
|
|
|
+ .lineHeight(16)
|
|
|
+ .maxLines(1)
|
|
|
+ .width(160)
|
|
|
+ .fontColor('#E6000000')
|
|
|
+ .fontSize(14)
|
|
|
+ .fontWeight(FontWeight.Bold)
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
+
|
|
|
+ Text(this.getRecentSongSubtitle(item))
|
|
|
+ .fontColor('#99000000')
|
|
|
+ .fontSize(10)
|
|
|
+ .maxLines(1)
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
+ .lineHeight(11)
|
|
|
+ .width(160)
|
|
|
+ .textAlign(TextAlign.Start)
|
|
|
+ }
|
|
|
+ .onClick(() => {
|
|
|
+ this.handleHotSongTap(index)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ .margin({
|
|
|
+ left: index === 0 ? 2 : 0,
|
|
|
+ right: index === this.hotSongs.length - 1 ? 2 : 0
|
|
|
+ })
|
|
|
+ }, (item: VideoItem, index: number) => this.getSongKey(item, index))
|
|
|
+ }
|
|
|
+ .listDirection(Axis.Horizontal)
|
|
|
+ .scrollBar(BarState.Off)
|
|
|
+ .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Builder
|
|
|
+ private buildRecentSection() {
|
|
|
+ ConfigTitle({ text: '最近播放' })
|
|
|
+ if (this.recentSongs.length === 0) {
|
|
|
+ this.buildSectionEmptyState('最近播放还是空的', '播放过的歌曲会自动出现在这里', false)
|
|
|
+ } else {
|
|
|
+ GridRow({
|
|
|
+ columns: this.currentBreakpoint === BreakpointTypeEnum.SM ? 3 : 5,
|
|
|
+ gutter: 8
|
|
|
+ }) {
|
|
|
+ ForEach(this.recentSongs, (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.getRecentSongSubtitle(item))
|
|
|
+ .fontSize(10)
|
|
|
+ .lineHeight(13)
|
|
|
+ .fontColor('#80000000')
|
|
|
+ .maxLines(1)
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
+ .width('100%')
|
|
|
+ }
|
|
|
+ .onClick(() => {
|
|
|
+ this.handleRecentSongTap(index)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }, (item: VideoItem, index: number) => this.getSongKey(item, index))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Builder
|
|
|
+ private buildSectionEmptyState(title: string, subtitle: string, useLargeCard: boolean) {
|
|
|
+ Column({ space: 8 }) {
|
|
|
+ Image($r('app.media.alt'))
|
|
|
+ .width(useLargeCard ? 70 : 56)
|
|
|
+ .height(useLargeCard ? 70 : 56)
|
|
|
+ .opacity(0.72)
|
|
|
+
|
|
|
+ Text(title)
|
|
|
+ .fontSize(15)
|
|
|
+ .fontWeight(FontWeight.Medium)
|
|
|
+ .fontColor('#CC000000')
|
|
|
+
|
|
|
+ Text(subtitle)
|
|
|
+ .fontSize(12)
|
|
|
+ .fontColor('#80000000')
|
|
|
+ .textAlign(TextAlign.Center)
|
|
|
+ }
|
|
|
+ .width('100%')
|
|
|
+ .padding({ top: useLargeCard ? 42 : 26, bottom: useLargeCard ? 42 : 26, left: 12, right: 12 })
|
|
|
+ .backgroundColor('#F7F8FB')
|
|
|
+ .borderRadius(20)
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
+ .alignItems(HorizontalAlign.Center)
|
|
|
+ }
|
|
|
+
|
|
|
+ @Builder
|
|
|
+ private buildDiscoveryContent() {
|
|
|
+ Scroll() {
|
|
|
+ Column(){
|
|
|
+ this.buildTopErrorBanner()
|
|
|
+ this.buildSwiperSection()
|
|
|
+ Column({ space: 14 }) {
|
|
|
+ this.buildActionButtons()
|
|
|
+ this.buildLocalRandomSection()
|
|
|
+ this.buildCloudSection()
|
|
|
+ this.buildRecentSection()
|
|
|
+ }
|
|
|
+ .width('100%')
|
|
|
+ .padding({
|
|
|
+ left: this.currentBreakpoint === BreakpointTypeEnum.SM ? 12 : 20,
|
|
|
+ right: this.currentBreakpoint === BreakpointTypeEnum.SM ? 12 : 20,
|
|
|
+ top: 16,
|
|
|
+ bottom: 24
|
|
|
+ })
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ .scrollBar(BarState.Off)
|
|
|
+ .edgeEffect(EdgeEffect.Spring)
|
|
|
+ .width('100%')
|
|
|
+ .layoutWeight(1)
|
|
|
+ }
|
|
|
+
|
|
|
+ build() {
|
|
|
+ Column() {
|
|
|
+ Refresh({ refreshing: $$this.isRefreshing }) {
|
|
|
+ Column() {
|
|
|
+ if (this.isPageLoading) {
|
|
|
+ this.buildLoadingState()
|
|
|
+ } else {
|
|
|
+ this.buildDiscoveryContent()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .width('100%')
|
|
|
+ .height('100%')
|
|
|
+ }
|
|
|
+ .pullDownRatio(this.refreshPullRatio)
|
|
|
+ .pullToRefresh(true)
|
|
|
+ .refreshOffset(0)
|
|
|
+ .onOffsetChange((offset: number) => {
|
|
|
+ this.refreshPullRatio = 1 - Math.pow((offset / this.maxRefreshingHeight), 3)
|
|
|
+ })
|
|
|
+ .onRefreshing(() => {
|
|
|
+ void this.loadDiscoveryContent(true)
|
|
|
+ })
|
|
|
+ .width('100%')
|
|
|
+ .height('100%')
|
|
|
+ }
|
|
|
+ .width('100%')
|
|
|
+ .height('100%')
|
|
|
+ .backgroundColor($r('sys.color.background_primary'))
|
|
|
+ }
|
|
|
+}
|