|
@@ -1,13 +1,18 @@
|
|
|
import { PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils'
|
|
import { PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils'
|
|
|
import { BusinessError, emitter } from '@kit.BasicServicesKit'
|
|
import { BusinessError, emitter } from '@kit.BasicServicesKit'
|
|
|
|
|
+import { common } from '@kit.AbilityKit'
|
|
|
import { CommonConstants } from '../common/constants/CommonConstants'
|
|
import { CommonConstants } from '../common/constants/CommonConstants'
|
|
|
import { EventConstants } from '../common/constants/EventConstants'
|
|
import { EventConstants } from '../common/constants/EventConstants'
|
|
|
import MediaTable from '../common/util/MediaTable'
|
|
import MediaTable from '../common/util/MediaTable'
|
|
|
import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil'
|
|
import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil'
|
|
|
import { BreakpointType, BreakpointTypeEnum } from '../common/util/BreakpointSystem'
|
|
import { BreakpointType, BreakpointTypeEnum } from '../common/util/BreakpointSystem'
|
|
|
import Logger from '../common/util/Logger'
|
|
import Logger from '../common/util/Logger'
|
|
|
|
|
+import { RemoteDriveManager } from '../common/util/RemoteDriveManager'
|
|
|
|
|
+import { cloneVideoItem } from '../common/util/RemotePlayerUtil'
|
|
|
import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil'
|
|
import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil'
|
|
|
import { VideoItem } from '../viewmodel/VideoItem'
|
|
import { VideoItem } from '../viewmodel/VideoItem'
|
|
|
|
|
+import { WebDavAccount } from '../viewmodel/WebDavAccount'
|
|
|
|
|
+import { Playlist } from '../viewmodel/Playlist'
|
|
|
import { ConfigTitle } from './ConfigTitle'
|
|
import { ConfigTitle } from './ConfigTitle'
|
|
|
import { PointLightActionButton } from './PointLight/PointLightActionButton'
|
|
import { PointLightActionButton } from './PointLight/PointLightActionButton'
|
|
|
import { PointLightContentButton } from './PointLight/PointLightContentButton'
|
|
import { PointLightContentButton } from './PointLight/PointLightContentButton'
|
|
@@ -15,6 +20,9 @@ import { SettingPage } from '../pages/SettingPage'
|
|
|
import { FindAlbumDetail } from './FindAlbumDetail'
|
|
import { FindAlbumDetail } from './FindAlbumDetail'
|
|
|
import { PlayingIndicator } from './PlayingIndicator'
|
|
import { PlayingIndicator } from './PlayingIndicator'
|
|
|
import { setFindPlaylist } from '../common/util/FindPlaylistStore'
|
|
import { setFindPlaylist } from '../common/util/FindPlaylistStore'
|
|
|
|
|
+import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore'
|
|
|
|
|
+import PlaylistTable from '../common/util/PlaylistTable'
|
|
|
|
|
+import { convertPlaylistSongsToVideoItems } from '../pages/PlaylistDetailPage'
|
|
|
|
|
|
|
|
@Builder
|
|
@Builder
|
|
|
export function FindViewBuilder() {
|
|
export function FindViewBuilder() {
|
|
@@ -48,16 +56,6 @@ function getFindSongKey(item: VideoItem, index: number): string {
|
|
|
return `find_song_${index}`
|
|
return `find_song_${index}`
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-interface FindPlaylistEventData {
|
|
|
|
|
- playlistId: string
|
|
|
|
|
- playlistName: string
|
|
|
|
|
- songCount: number
|
|
|
|
|
- startIndex: number
|
|
|
|
|
- isJump: boolean
|
|
|
|
|
- songFilePaths: string[]
|
|
|
|
|
- playType?: number
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
interface FindAlbumGroup {
|
|
interface FindAlbumGroup {
|
|
|
id: string
|
|
id: string
|
|
|
title: string
|
|
title: string
|
|
@@ -93,6 +91,13 @@ function getFindSongPageKey(item: FindSongPage, index: number): string {
|
|
|
return 'find_song_page_' + index
|
|
return 'find_song_page_' + index
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function getFindPlaylistKey(item: Playlist, index: number): string {
|
|
|
|
|
+ if (StrUtil.isNotEmpty(item.id)) {
|
|
|
|
|
+ return item.id
|
|
|
|
|
+ }
|
|
|
|
|
+ return 'find_playlist_' + index
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function getFindAlbumPageKey(item: FindAlbumPage, index: number): string {
|
|
function getFindAlbumPageKey(item: FindAlbumPage, index: number): string {
|
|
|
if (StrUtil.isNotEmpty(item.id)) {
|
|
if (StrUtil.isNotEmpty(item.id)) {
|
|
|
return item.id
|
|
return item.id
|
|
@@ -123,6 +128,7 @@ export struct FindView {
|
|
|
@State private popularSectionPages: FindSongPage[] = []
|
|
@State private popularSectionPages: FindSongPage[] = []
|
|
|
@State private favoriteSongs: VideoItem[] = []
|
|
@State private favoriteSongs: VideoItem[] = []
|
|
|
@State private cloudMoodSongs: VideoItem[] = []
|
|
@State private cloudMoodSongs: VideoItem[] = []
|
|
|
|
|
+ @State private heartPlaylists: Playlist[] = []
|
|
|
@State private featuredAlbums: FindAlbumGroup[] = []
|
|
@State private featuredAlbums: FindAlbumGroup[] = []
|
|
|
@State private featuredAlbumPages: FindAlbumPage[] = []
|
|
@State private featuredAlbumPages: FindAlbumPage[] = []
|
|
|
@State private cloudAlbums: FindAlbumGroup[] = []
|
|
@State private cloudAlbums: FindAlbumGroup[] = []
|
|
@@ -163,6 +169,8 @@ export struct FindView {
|
|
|
@StorageLink('isPlaying') isPlaying: boolean = false
|
|
@StorageLink('isPlaying') isPlaying: boolean = false
|
|
|
|
|
|
|
|
private mediaTable?: MediaTable
|
|
private mediaTable?: MediaTable
|
|
|
|
|
+ private playlistTable?: PlaylistTable
|
|
|
|
|
+ private remoteDriveManager: RemoteDriveManager = RemoteDriveManager.getInstance()
|
|
|
private localSongsPool: VideoItem[] = []
|
|
private localSongsPool: VideoItem[] = []
|
|
|
private coveredSongsPool: VideoItem[] = []
|
|
private coveredSongsPool: VideoItem[] = []
|
|
|
private remoteSongsPool: VideoItem[] = []
|
|
private remoteSongsPool: VideoItem[] = []
|
|
@@ -178,6 +186,9 @@ export struct FindView {
|
|
|
private featuredAlbumPageVersion: number = 0
|
|
private featuredAlbumPageVersion: number = 0
|
|
|
private cloudAlbumPageVersion: number = 0
|
|
private cloudAlbumPageVersion: number = 0
|
|
|
private popularSectionPageVersion: number = 0
|
|
private popularSectionPageVersion: number = 0
|
|
|
|
|
+ private isRemoteBootstrapLoading: boolean = false
|
|
|
|
|
+ private playlistSongsCache: Map<string, VideoItem[]> = new Map<string, VideoItem[]>()
|
|
|
|
|
+ private heartPlaylistCoverTicket: number = 0
|
|
|
|
|
|
|
|
aboutToAppear(): void {
|
|
aboutToAppear(): void {
|
|
|
this.initSetting()
|
|
this.initSetting()
|
|
@@ -213,6 +224,13 @@ export struct FindView {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private ensureMediaTable(): void {
|
|
private ensureMediaTable(): void {
|
|
|
|
|
+ if (!this.playlistTable) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.playlistTable = new PlaylistTable(getContext(this) as common.Context)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Logger.warn(TAG, `初始化歌单数据库失败: ${this.toErrorMessage(error as Object)}`)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
if (this.mediaTable) {
|
|
if (this.mediaTable) {
|
|
|
void this.loadDiscoveryContent(false)
|
|
void this.loadDiscoveryContent(false)
|
|
|
return
|
|
return
|
|
@@ -250,39 +268,44 @@ export struct FindView {
|
|
|
this.mediaTable.queryRemoteSongsAsync(REMOTE_POOL_COUNT),
|
|
this.mediaTable.queryRemoteSongsAsync(REMOTE_POOL_COUNT),
|
|
|
this.mediaTable.queryRecentPlayedRecordsAsync(RECENT_POOL_COUNT),
|
|
this.mediaTable.queryRecentPlayedRecordsAsync(RECENT_POOL_COUNT),
|
|
|
this.queryTopPlayedSongs(TOP_PLAYED_COUNT),
|
|
this.queryTopPlayedSongs(TOP_PLAYED_COUNT),
|
|
|
- this.queryFavoriteSongs()
|
|
|
|
|
|
|
+ this.queryFavoriteSongs(),
|
|
|
|
|
+ this.queryPlaylists()
|
|
|
])
|
|
])
|
|
|
const remoteSongs = requestResults[0] as VideoItem[]
|
|
const remoteSongs = requestResults[0] as VideoItem[]
|
|
|
const recentSongs = requestResults[1] as VideoItem[]
|
|
const recentSongs = requestResults[1] as VideoItem[]
|
|
|
const topPlayedSongs = requestResults[2] as VideoItem[]
|
|
const topPlayedSongs = requestResults[2] as VideoItem[]
|
|
|
const favoriteSongs = requestResults[3] as VideoItem[]
|
|
const favoriteSongs = requestResults[3] as VideoItem[]
|
|
|
|
|
+ const playlists = requestResults[4] as Playlist[]
|
|
|
const uniqueRemoteSongs = this.filterUniqueSongs(remoteSongs)
|
|
const uniqueRemoteSongs = this.filterUniqueSongs(remoteSongs)
|
|
|
const uniqueRecentSongs = this.filterUniqueSongs(recentSongs)
|
|
const uniqueRecentSongs = this.filterUniqueSongs(recentSongs)
|
|
|
const uniqueTopPlayedSongs = this.filterUniqueSongs(topPlayedSongs)
|
|
const uniqueTopPlayedSongs = this.filterUniqueSongs(topPlayedSongs)
|
|
|
const uniqueFavoriteSongs = this.filterUniqueSongs(favoriteSongs)
|
|
const uniqueFavoriteSongs = this.filterUniqueSongs(favoriteSongs)
|
|
|
|
|
|
|
|
|
|
+ this.playlistSongsCache.clear()
|
|
|
this.localSongsPool = uniqueLocalSongs
|
|
this.localSongsPool = uniqueLocalSongs
|
|
|
this.coveredSongsPool = coveredSongs
|
|
this.coveredSongsPool = coveredSongs
|
|
|
- this.remoteSongsPool = uniqueRemoteSongs
|
|
|
|
|
|
|
+ this.applyRemoteDiscoverySongs(uniqueRemoteSongs)
|
|
|
this.recentSongsPool = uniqueRecentSongs
|
|
this.recentSongsPool = uniqueRecentSongs
|
|
|
this.topPlayedSongsPool = uniqueTopPlayedSongs
|
|
this.topPlayedSongsPool = uniqueTopPlayedSongs
|
|
|
this.favoriteSongsPool = uniqueFavoriteSongs
|
|
this.favoriteSongsPool = uniqueFavoriteSongs
|
|
|
|
|
+ this.heartPlaylists = playlists
|
|
|
this.featuredAlbumsPool = this.buildAlbumGroups(this.localSongsPool, false)
|
|
this.featuredAlbumsPool = this.buildAlbumGroups(this.localSongsPool, false)
|
|
|
- this.cloudAlbumsPool = this.buildAlbumGroups(this.remoteSongsPool, true)
|
|
|
|
|
this.searchRemoteSongsPool = []
|
|
this.searchRemoteSongsPool = []
|
|
|
this.swiperSongs = this.pickRandomSongs(this.coveredSongsPool, SWIPER_MAX_COUNT)
|
|
this.swiperSongs = this.pickRandomSongs(this.coveredSongsPool, SWIPER_MAX_COUNT)
|
|
|
this.hotSongs = this.pickPreferredSongs(this.localSongsPool, HOT_SECTION_COUNT)
|
|
this.hotSongs = this.pickPreferredSongs(this.localSongsPool, HOT_SECTION_COUNT)
|
|
|
- this.cloudMoodSongs = this.pickPreferredSongs(this.remoteSongsPool, HOT_SECTION_COUNT)
|
|
|
|
|
- this.remoteSongs = this.pickPreferredSongs(this.remoteSongsPool, CLOUD_SECTION_COUNT)
|
|
|
|
|
this.recentSongs = this.pickPreferredSongs(this.recentSongsPool, RECENT_SECTION_COUNT)
|
|
this.recentSongs = this.pickPreferredSongs(this.recentSongsPool, RECENT_SECTION_COUNT)
|
|
|
this.popularSongs = this.pickPreferredSongs(this.topPlayedSongsPool, POPULAR_SECTION_COUNT)
|
|
this.popularSongs = this.pickPreferredSongs(this.topPlayedSongsPool, POPULAR_SECTION_COUNT)
|
|
|
this.favoriteSongs = this.pickPreferredSongs(this.favoriteSongsPool, FAVORITE_SECTION_COUNT)
|
|
this.favoriteSongs = this.pickPreferredSongs(this.favoriteSongsPool, FAVORITE_SECTION_COUNT)
|
|
|
this.featuredAlbums = this.pickAlbumGroups(this.featuredAlbumsPool, FEATURED_ALBUM_COUNT)
|
|
this.featuredAlbums = this.pickAlbumGroups(this.featuredAlbumsPool, FEATURED_ALBUM_COUNT)
|
|
|
- this.cloudAlbums = this.pickAlbumGroups(this.cloudAlbumsPool, CLOUD_ALBUM_COUNT)
|
|
|
|
|
this.swiperIndex = 0
|
|
this.swiperIndex = 0
|
|
|
this.resetPagedSectionIndices()
|
|
this.resetPagedSectionIndices()
|
|
|
this.rebuildPagedSectionSources()
|
|
this.rebuildPagedSectionSources()
|
|
|
this.refreshText = ''
|
|
this.refreshText = ''
|
|
|
|
|
+ this.heartPlaylistCoverTicket += 1
|
|
|
|
|
+ void this.resolveHeartPlaylistCovers(this.heartPlaylistCoverTicket, playlists)
|
|
|
|
|
+ if (uniqueRemoteSongs.length === 0) {
|
|
|
|
|
+ void this.bootstrapRemoteDiscoverySongsIfNeeded()
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
Logger.info(
|
|
Logger.info(
|
|
|
TAG,
|
|
TAG,
|
|
@@ -300,6 +323,7 @@ export struct FindView {
|
|
|
this.popularSongs = []
|
|
this.popularSongs = []
|
|
|
this.popularSectionPages = []
|
|
this.popularSectionPages = []
|
|
|
this.favoriteSongs = []
|
|
this.favoriteSongs = []
|
|
|
|
|
+ this.heartPlaylists = []
|
|
|
this.localSongsPool = []
|
|
this.localSongsPool = []
|
|
|
this.coveredSongsPool = []
|
|
this.coveredSongsPool = []
|
|
|
this.remoteSongsPool = []
|
|
this.remoteSongsPool = []
|
|
@@ -320,6 +344,103 @@ export struct FindView {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private async queryPlaylists(): Promise<Playlist[]> {
|
|
|
|
|
+ if (!this.playlistTable) {
|
|
|
|
|
+ return []
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const playlists = await this.playlistTable.queryAllPlaylists()
|
|
|
|
|
+ return playlists.filter((item: Playlist) => StrUtil.isNotEmpty(item.id) && StrUtil.isNotEmpty(item.name))
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Logger.warn(TAG, `发现页查询歌单失败: ${this.toErrorMessage(error as Object)}`)
|
|
|
|
|
+ return []
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private applyRemoteDiscoverySongs(items: VideoItem[], refreshSections: boolean = false): void {
|
|
|
|
|
+ this.remoteSongsPool = items
|
|
|
|
|
+ this.searchRemoteSongsPool = items.slice()
|
|
|
|
|
+ this.cloudAlbumsPool = this.buildAlbumGroups(this.remoteSongsPool, true)
|
|
|
|
|
+ this.cloudMoodSongs = this.pickPreferredSongs(this.remoteSongsPool, HOT_SECTION_COUNT)
|
|
|
|
|
+ this.remoteSongs = this.pickPreferredSongs(this.remoteSongsPool, CLOUD_SECTION_COUNT)
|
|
|
|
|
+ this.cloudAlbums = this.pickAlbumGroups(this.cloudAlbumsPool, CLOUD_ALBUM_COUNT)
|
|
|
|
|
+ if (refreshSections) {
|
|
|
|
|
+ this.cloudSectionPageIndex = 0
|
|
|
|
|
+ this.cloudAlbumPageIndex = 0
|
|
|
|
|
+ this.updateCloudSectionPages()
|
|
|
|
|
+ this.updateCloudAlbumPages()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private async ensureRemoteDiscoverySongsAvailable(): Promise<void> {
|
|
|
|
|
+ if (!this.mediaTable) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.remoteSongsPool.length > 0 || this.cloudAlbumsPool.length > 0 || this.searchRemoteSongsPool.length > 0) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const remoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync(REMOTE_POOL_COUNT))
|
|
|
|
|
+ if (remoteSongs.length > 0) {
|
|
|
|
|
+ this.applyRemoteDiscoverySongs(remoteSongs, true)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ await this.bootstrapRemoteDiscoverySongsIfNeeded()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private async bootstrapRemoteDiscoverySongsIfNeeded(): Promise<void> {
|
|
|
|
|
+ if (this.isRemoteBootstrapLoading || !this.mediaTable || this.remoteSongsPool.length > 0) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.isRemoteBootstrapLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const context = getContext(this) as common.Context
|
|
|
|
|
+ this.remoteDriveManager.setContext(context)
|
|
|
|
|
+ await this.remoteDriveManager.createWebDavTableInDB()
|
|
|
|
|
+ await this.remoteDriveManager.queryWebDavAccountsFromDB()
|
|
|
|
|
+ const accounts: WebDavAccount[] = this.remoteDriveManager.getAllWebDavAccounts()
|
|
|
|
|
+ if (accounts.length === 0) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const account = this.remoteDriveManager.getActivatedWebDavAccount() || accounts[0]
|
|
|
|
|
+ if (!account) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ Logger.info(TAG, `发现页开始预热远程歌曲: account=${account.name}, type=${account.webType}`)
|
|
|
|
|
+ const seedSongs = await this.remoteDriveManager.getDiscoverySeedSongs(account, REMOTE_POOL_COUNT)
|
|
|
|
|
+ if (seedSongs.length === 0) {
|
|
|
|
|
+ Logger.info(TAG, `发现页远程预热未拿到歌曲: account=${account.name}`)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ let savedCount = 0
|
|
|
|
|
+ for (let index = 0; index < seedSongs.length; index += 1) {
|
|
|
|
|
+ const seedSong = cloneVideoItem(seedSongs[index])
|
|
|
|
|
+ if (!seedSong.webdav_account_id && account.id) {
|
|
|
|
|
+ seedSong.webdav_account_id = account.id.toString()
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StrUtil.isEmpty(seedSong.id)) {
|
|
|
|
|
+ seedSong.id = seedSong.remote_rel_path || seedSong.filePath
|
|
|
|
|
+ }
|
|
|
|
|
+ const saved = await this.mediaTable.saveOrUpdateWebDavItem(seedSong)
|
|
|
|
|
+ if (saved) {
|
|
|
|
|
+ savedCount += 1
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (savedCount <= 0) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const refreshedRemoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync(REMOTE_POOL_COUNT))
|
|
|
|
|
+ if (refreshedRemoteSongs.length === 0) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.applyRemoteDiscoverySongs(refreshedRemoteSongs, true)
|
|
|
|
|
+ Logger.info(TAG, `发现页远程预热完成: saved=${savedCount}, remote=${refreshedRemoteSongs.length}`)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Logger.warn(TAG, `发现页远程预热失败: ${this.toErrorMessage(error)}`)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.isRemoteBootstrapLoading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private queryTopPlayedSongs(limitCount: number): Promise<VideoItem[]> {
|
|
private queryTopPlayedSongs(limitCount: number): Promise<VideoItem[]> {
|
|
|
return new Promise<VideoItem[]>((resolve) => {
|
|
return new Promise<VideoItem[]>((resolve) => {
|
|
|
if (!this.mediaTable) {
|
|
if (!this.mediaTable) {
|
|
@@ -754,15 +875,16 @@ export struct FindView {
|
|
|
setFindPlaylist(playlistId, playlistName, songs, safeIndex)
|
|
setFindPlaylist(playlistId, playlistName, songs, safeIndex)
|
|
|
}
|
|
}
|
|
|
const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }
|
|
const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }
|
|
|
- const playlistData: FindPlaylistEventData = {
|
|
|
|
|
|
|
+ const playlistData = new PlaylistPlayRequest(
|
|
|
playlistId,
|
|
playlistId,
|
|
|
playlistName,
|
|
playlistName,
|
|
|
- songCount: songs.length,
|
|
|
|
|
- startIndex: safeIndex,
|
|
|
|
|
- isJump: false,
|
|
|
|
|
- songFilePaths: songs.map((item: VideoItem): string => item.filePath),
|
|
|
|
|
|
|
+ songs.length,
|
|
|
|
|
+ safeIndex,
|
|
|
|
|
+ songs.map((item: VideoItem): string => item.filePath),
|
|
|
|
|
+ false,
|
|
|
playType
|
|
playType
|
|
|
- }
|
|
|
|
|
|
|
+ )
|
|
|
|
|
+ savePendingPlaylistPlay(playlistData)
|
|
|
emitter.emit(eventPlaylistPlay, { data: playlistData })
|
|
emitter.emit(eventPlaylistPlay, { data: playlistData })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -785,6 +907,7 @@ export struct FindView {
|
|
|
if (!this.mediaTable) {
|
|
if (!this.mediaTable) {
|
|
|
return []
|
|
return []
|
|
|
}
|
|
}
|
|
|
|
|
+ await this.ensureRemoteDiscoverySongsAvailable()
|
|
|
const remoteSongs = await this.mediaTable.queryRemoteSongsAsync()
|
|
const remoteSongs = await this.mediaTable.queryRemoteSongsAsync()
|
|
|
this.searchRemoteSongsPool = this.filterUniqueSongs(remoteSongs)
|
|
this.searchRemoteSongsPool = this.filterUniqueSongs(remoteSongs)
|
|
|
return this.searchRemoteSongsPool
|
|
return this.searchRemoteSongsPool
|
|
@@ -948,6 +1071,115 @@ export struct FindView {
|
|
|
this.isSearchMode = false
|
|
this.isSearchMode = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private getPlaylistCover(playlist: Playlist): string | Resource {
|
|
|
|
|
+ if (StrUtil.isNotEmpty(playlist.coverPath)) {
|
|
|
|
|
+ return playlist.coverPath as string
|
|
|
|
|
+ }
|
|
|
|
|
+ const cachedSongs = this.playlistSongsCache.get(playlist.id)
|
|
|
|
|
+ const coverSong = cachedSongs ? this.pickAlbumCoverSong(cachedSongs) : undefined
|
|
|
|
|
+ if (coverSong && StrUtil.isNotEmpty(coverSong.pixelMapPath)) {
|
|
|
|
|
+ return coverSong.pixelMapPath as string
|
|
|
|
|
+ }
|
|
|
|
|
+ return $r('app.media.alt')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getPlaylistSubtitle(playlist: Playlist): string {
|
|
|
|
|
+ if (StrUtil.isNotEmpty(playlist.description)) {
|
|
|
|
|
+ return playlist.description as string
|
|
|
|
|
+ }
|
|
|
|
|
+ return `${playlist.songCount} 首歌曲`
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private presentPlaylistDetail(playlist: Playlist, songs: VideoItem[]): void {
|
|
|
|
|
+ if (songs.length === 0) {
|
|
|
|
|
+ ToastUtil.showToast('歌单里还没有可展示歌曲')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const coverSong = this.pickAlbumCoverSong(songs)
|
|
|
|
|
+ this.currentAlbumId = `find-playlist-${playlist.id}`
|
|
|
|
|
+ this.currentAlbumTitle = playlist.name
|
|
|
|
|
+ this.currentAlbumArtist = '心动歌单'
|
|
|
|
|
+ this.currentAlbumCoverPath = StrUtil.isNotEmpty(playlist.coverPath) ? playlist.coverPath as string :
|
|
|
|
|
+ (coverSong?.pixelMapPath ?? '')
|
|
|
|
|
+ this.currentAlbumSourceLabel = '心动歌单'
|
|
|
|
|
+ this.currentAlbumSongs = songs.slice()
|
|
|
|
|
+ this.isAlbumMode = true
|
|
|
|
|
+ this.isSearchMode = false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private async openPlaylistDetail(playlist: Playlist): Promise<void> {
|
|
|
|
|
+ if (!this.playlistTable) {
|
|
|
|
|
+ ToastUtil.showToast('歌单加载失败')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (playlist.songCount <= 0) {
|
|
|
|
|
+ ToastUtil.showToast('歌单里还没有歌曲')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const cachedSongs = this.playlistSongsCache.get(playlist.id)
|
|
|
|
|
+ if (cachedSongs && cachedSongs.length > 0) {
|
|
|
|
|
+ this.presentPlaylistDetail(playlist, cachedSongs)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const playlistSongs = await this.playlistTable.queryPlaylistSongs(playlist.id)
|
|
|
|
|
+ if (playlistSongs.length === 0) {
|
|
|
|
|
+ ToastUtil.showToast('歌单里还没有歌曲')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const songs = await convertPlaylistSongsToVideoItems(getContext(this) as common.Context, playlistSongs)
|
|
|
|
|
+ if (songs.length === 0) {
|
|
|
|
|
+ ToastUtil.showToast('歌单里还没有可播放歌曲')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.playlistSongsCache.set(playlist.id, songs)
|
|
|
|
|
+ this.presentPlaylistDetail(playlist, songs)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Logger.error(TAG, `发现页打开歌单失败: ${this.toErrorMessage(error as Object)}`)
|
|
|
|
|
+ ToastUtil.showToast('歌单加载失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private async resolveHeartPlaylistCovers(ticket: number, playlists: Playlist[]): Promise<void> {
|
|
|
|
|
+ if (!this.playlistTable || playlists.length === 0) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const context = getContext(this) as common.Context
|
|
|
|
|
+ for (let index = 0; index < playlists.length; index += 1) {
|
|
|
|
|
+ if (ticket !== this.heartPlaylistCoverTicket) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const playlist = playlists[index]
|
|
|
|
|
+ if (StrUtil.isNotEmpty(playlist.coverPath) || playlist.songCount <= 0) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ let songs = this.playlistSongsCache.get(playlist.id)
|
|
|
|
|
+ if (!songs || songs.length === 0) {
|
|
|
|
|
+ const playlistSongs = await this.playlistTable.queryPlaylistSongs(playlist.id)
|
|
|
|
|
+ if (playlistSongs.length === 0) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ songs = await convertPlaylistSongsToVideoItems(context, playlistSongs)
|
|
|
|
|
+ if (songs.length > 0) {
|
|
|
|
|
+ this.playlistSongsCache.set(playlist.id, songs)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!songs || songs.length === 0) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ const coverSong = this.pickAlbumCoverSong(songs)
|
|
|
|
|
+ if (!coverSong || StrUtil.isEmpty(coverSong.pixelMapPath)) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ playlist.coverPath = coverSong.pixelMapPath as string
|
|
|
|
|
+ this.heartPlaylists = this.heartPlaylists.slice()
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Logger.warn(TAG, `发现页补全歌单封面失败: ${playlist.name}, ${this.toErrorMessage(error as Object)}`)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private exitAlbumMode(): void {
|
|
private exitAlbumMode(): void {
|
|
|
this.getUIContext()?.animateTo({ duration: 500 }, () => {
|
|
this.getUIContext()?.animateTo({ duration: 500 }, () => {
|
|
|
this.isAlbumMode = false
|
|
this.isAlbumMode = false
|
|
@@ -2204,6 +2436,83 @@ export struct FindView {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ private buildHeartPlaylistCardContent(playlist: Playlist) {
|
|
|
|
|
+ Column({ space: 6 }) {
|
|
|
|
|
+ Stack({ alignContent: Alignment.Bottom }) {
|
|
|
|
|
+ Image(this.getPlaylistCover(playlist))
|
|
|
|
|
+ .aspectRatio(3 / 4)
|
|
|
|
|
+ .borderRadius(16)
|
|
|
|
|
+ .width(160)
|
|
|
|
|
+ .objectFit(ImageFit.Cover)
|
|
|
|
|
+
|
|
|
|
|
+ Row() {
|
|
|
|
|
+ Text('心动歌单')
|
|
|
|
|
+ .fontColor($r('app.color.white'))
|
|
|
|
|
+ .fontSize(9)
|
|
|
|
|
+ .padding({ left: 6, right: 6, top: 3, bottom: 3 })
|
|
|
|
|
+ .backgroundColor('#7A000000')
|
|
|
|
|
+ .borderRadius(999)
|
|
|
|
|
+
|
|
|
|
|
+ Text(`${playlist.songCount} 首`)
|
|
|
|
|
+ .fontColor($r('app.color.white'))
|
|
|
|
|
+ .fontSize(9)
|
|
|
|
|
+ .padding({ left: 6, right: 6, top: 3, bottom: 3 })
|
|
|
|
|
+ .backgroundColor('#5C000000')
|
|
|
|
|
+ .borderRadius(999)
|
|
|
|
|
+ }
|
|
|
|
|
+ .justifyContent(FlexAlign.SpaceBetween)
|
|
|
|
|
+ .padding({ left: 8, right: 8, bottom: 8 })
|
|
|
|
|
+ .width(160)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Text(playlist.name)
|
|
|
|
|
+ .lineHeight(16)
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .width(160)
|
|
|
|
|
+ .fontColor(this.getPrimaryTextColor())
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontWeight(FontWeight.Bold)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+
|
|
|
|
|
+ Text(this.getPlaylistSubtitle(playlist))
|
|
|
|
|
+ .fontColor(this.getSecondaryTextColor())
|
|
|
|
|
+ .fontSize(10)
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ .lineHeight(11)
|
|
|
|
|
+ .width(160)
|
|
|
|
|
+ .textAlign(TextAlign.Start)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ private buildHeartPlaylistSection() {
|
|
|
|
|
+ ConfigTitle({
|
|
|
|
|
+ text: '心动歌单',
|
|
|
|
|
+ showAction: false
|
|
|
|
|
+ })
|
|
|
|
|
+ List({ space: 8 }) {
|
|
|
|
|
+ ForEach(this.heartPlaylists, (playlist: Playlist) => {
|
|
|
|
|
+ ListItem() {
|
|
|
|
|
+ PointLightContentButton({
|
|
|
|
|
+ pointColor: this.themeColor,
|
|
|
|
|
+ buttonRadius: 16,
|
|
|
|
|
+ builder: () => {
|
|
|
|
|
+ this.buildHeartPlaylistCardContent(playlist)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ void this.openPlaylistDetail(playlist)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }, getFindPlaylistKey)
|
|
|
|
|
+ }
|
|
|
|
|
+ .listDirection(Axis.Horizontal)
|
|
|
|
|
+ .scrollBar(BarState.Off)
|
|
|
|
|
+ .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@Builder
|
|
@Builder
|
|
|
private buildCloudMoodSection() {
|
|
private buildCloudMoodSection() {
|
|
|
ConfigTitle({
|
|
ConfigTitle({
|
|
@@ -2541,11 +2850,16 @@ export struct FindView {
|
|
|
if (this.favoriteSongsPool.length > 0 || this.favoriteSongs.length > 0) {
|
|
if (this.favoriteSongsPool.length > 0 || this.favoriteSongs.length > 0) {
|
|
|
this.buildFavoriteSection()
|
|
this.buildFavoriteSection()
|
|
|
}
|
|
}
|
|
|
|
|
+ if (this.heartPlaylists.length > 0) {
|
|
|
|
|
+ this.buildHeartPlaylistSection()
|
|
|
|
|
+ }
|
|
|
if (this.remoteSongsPool.length > 0 || this.remoteSongs.length > 0 || this.cloudMoodSongs.length > 0) {
|
|
if (this.remoteSongsPool.length > 0 || this.remoteSongs.length > 0 || this.cloudMoodSongs.length > 0) {
|
|
|
- this.buildCloudMoodSection()
|
|
|
|
|
this.buildCloudSection()
|
|
this.buildCloudSection()
|
|
|
}
|
|
}
|
|
|
this.buildFeaturedAlbumSection()
|
|
this.buildFeaturedAlbumSection()
|
|
|
|
|
+ if (this.remoteSongsPool.length > 0 || this.remoteSongs.length > 0 || this.cloudMoodSongs.length > 0) {
|
|
|
|
|
+ this.buildCloudMoodSection()
|
|
|
|
|
+ }
|
|
|
if (this.remoteSongsPool.length > 0 || this.cloudAlbumsPool.length > 0 || this.cloudAlbums.length > 0) {
|
|
if (this.remoteSongsPool.length > 0 || this.cloudAlbumsPool.length > 0 || this.cloudAlbums.length > 0) {
|
|
|
this.buildCloudAlbumSection()
|
|
this.buildCloudAlbumSection()
|
|
|
}
|
|
}
|