|
|
@@ -25,6 +25,12 @@ import { setFindPlaylist } from '../common/util/FindPlaylistStore'
|
|
|
import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore'
|
|
|
import PlaylistTable from '../common/util/PlaylistTable'
|
|
|
import { convertPlaylistSongsToVideoItems } from '../pages/PlaylistDetailPage'
|
|
|
+import {
|
|
|
+ buildPreferredRemotePlaybackPool,
|
|
|
+ buildSortedDiscoverySongs,
|
|
|
+ FindCollectionSortType,
|
|
|
+ resolveQueueStartIndex
|
|
|
+} from '../common/util/FindDiscoveryHelper'
|
|
|
|
|
|
@Builder
|
|
|
export function FindViewBuilder() {
|
|
|
@@ -143,6 +149,8 @@ export struct FindView {
|
|
|
@State private currentAlbumCoverPath: string = ''
|
|
|
@State private currentAlbumSourceLabel: string = ''
|
|
|
@State private currentAlbumSongs: VideoItem[] = []
|
|
|
+ @State private currentAlbumDefaultSortType: number = FindCollectionSortType.TRACK_ASC
|
|
|
+ @State private currentAlbumSupportRecentPlayedSort: boolean = false
|
|
|
@State private searchText: string = ''
|
|
|
@State private searchResults: VideoItem[] = []
|
|
|
@State private searchHistoryItems: string[] = []
|
|
|
@@ -183,6 +191,7 @@ export struct FindView {
|
|
|
private featuredAlbumsPool: FindAlbumGroup[] = []
|
|
|
private cloudAlbumsPool: FindAlbumGroup[] = []
|
|
|
private searchRemoteSongsPool: VideoItem[] = []
|
|
|
+ private remotePlaybackPool: VideoItem[] = []
|
|
|
private readonly searchHistoryScope: string = 'find_music'
|
|
|
private searchTicket: number = 0
|
|
|
private cloudSectionPageVersion: number = 0
|
|
|
@@ -284,22 +293,25 @@ export struct FindView {
|
|
|
const uniqueRecentSongs = this.filterUniqueSongs(recentSongs)
|
|
|
const uniqueTopPlayedSongs = this.filterUniqueSongs(topPlayedSongs)
|
|
|
const uniqueFavoriteSongs = this.filterUniqueSongs(favoriteSongs)
|
|
|
+ const sortedRecentSongs = buildSortedDiscoverySongs(uniqueRecentSongs, FindCollectionSortType.RECENT_DESC)
|
|
|
+ const sortedFavoriteSongs = buildSortedDiscoverySongs(uniqueFavoriteSongs, FindCollectionSortType.NAME_ASC)
|
|
|
|
|
|
this.playlistSongsCache.clear()
|
|
|
this.localSongsPool = uniqueLocalSongs
|
|
|
this.coveredSongsPool = coveredSongs
|
|
|
+ this.remotePlaybackPool = []
|
|
|
this.applyRemoteDiscoverySongs(uniqueRemoteSongs)
|
|
|
- this.recentSongsPool = uniqueRecentSongs
|
|
|
+ this.recentSongsPool = sortedRecentSongs
|
|
|
this.topPlayedSongsPool = uniqueTopPlayedSongs
|
|
|
- this.favoriteSongsPool = uniqueFavoriteSongs
|
|
|
+ this.favoriteSongsPool = sortedFavoriteSongs
|
|
|
this.heartPlaylists = playlists
|
|
|
this.featuredAlbumsPool = this.buildAlbumGroups(this.localSongsPool, false)
|
|
|
this.searchRemoteSongsPool = []
|
|
|
this.swiperSongs = this.pickRandomSongs(this.coveredSongsPool, SWIPER_MAX_COUNT)
|
|
|
this.hotSongs = this.pickPreferredSongs(this.localSongsPool, HOT_SECTION_COUNT)
|
|
|
- this.recentSongs = this.pickPreferredSongs(this.recentSongsPool, RECENT_SECTION_COUNT)
|
|
|
+ this.recentSongs = this.recentSongsPool.slice(0, Math.min(RECENT_SECTION_COUNT, this.recentSongsPool.length))
|
|
|
this.popularSongs = this.pickPreferredSongs(this.topPlayedSongsPool, POPULAR_SECTION_COUNT)
|
|
|
- this.favoriteSongs = this.pickPreferredSongs(this.favoriteSongsPool, FAVORITE_SECTION_COUNT)
|
|
|
+ this.favoriteSongs = this.favoriteSongsPool.slice(0, Math.min(FAVORITE_SECTION_COUNT, this.favoriteSongsPool.length))
|
|
|
this.featuredAlbums = this.pickAlbumGroups(this.featuredAlbumsPool, FEATURED_ALBUM_COUNT)
|
|
|
this.swiperIndex = 0
|
|
|
this.resetPagedSectionIndices()
|
|
|
@@ -337,6 +349,7 @@ export struct FindView {
|
|
|
this.featuredAlbumsPool = []
|
|
|
this.cloudAlbumsPool = []
|
|
|
this.searchRemoteSongsPool = []
|
|
|
+ this.remotePlaybackPool = []
|
|
|
this.featuredAlbums = []
|
|
|
this.featuredAlbumPages = []
|
|
|
this.cloudAlbums = []
|
|
|
@@ -363,7 +376,6 @@ export struct FindView {
|
|
|
|
|
|
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)
|
|
|
@@ -380,7 +392,7 @@ export struct FindView {
|
|
|
if (!this.mediaTable) {
|
|
|
return
|
|
|
}
|
|
|
- if (this.remoteSongsPool.length > 0 || this.cloudAlbumsPool.length > 0 || this.searchRemoteSongsPool.length > 0) {
|
|
|
+ if (this.remoteSongsPool.length > 0 || this.cloudAlbumsPool.length > 0) {
|
|
|
return
|
|
|
}
|
|
|
const remoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync(REMOTE_POOL_COUNT))
|
|
|
@@ -391,11 +403,7 @@ export struct FindView {
|
|
|
await this.bootstrapRemoteDiscoverySongsIfNeeded()
|
|
|
}
|
|
|
|
|
|
- private async bootstrapRemoteDiscoverySongsIfNeeded(): Promise<void> {
|
|
|
- if (this.isRemoteBootstrapLoading || !this.mediaTable || this.remoteSongsPool.length > 0) {
|
|
|
- return
|
|
|
- }
|
|
|
- this.isRemoteBootstrapLoading = true
|
|
|
+ private async prepareActiveRemoteAccount(): Promise<WebDavAccount | undefined> {
|
|
|
try {
|
|
|
const context = getContext(this) as common.Context
|
|
|
this.remoteDriveManager.setContext(context)
|
|
|
@@ -403,9 +411,39 @@ export struct FindView {
|
|
|
await this.remoteDriveManager.queryWebDavAccountsFromDB()
|
|
|
const accounts: WebDavAccount[] = this.remoteDriveManager.getAllWebDavAccounts()
|
|
|
if (accounts.length === 0) {
|
|
|
- return
|
|
|
+ return undefined
|
|
|
}
|
|
|
- const account = this.remoteDriveManager.getActivatedWebDavAccount() || accounts[0]
|
|
|
+ return this.remoteDriveManager.getActivatedWebDavAccount() || accounts[0]
|
|
|
+ } catch (error) {
|
|
|
+ Logger.warn(TAG, `发现页加载远程账号失败: ${this.toErrorMessage(error)}`)
|
|
|
+ return undefined
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async queryIndexedRemotePlaybackSongs(): Promise<VideoItem[]> {
|
|
|
+ const account = await this.prepareActiveRemoteAccount()
|
|
|
+ if (!account) {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+ const supportsGlobalIndex = this.remoteDriveManager.supportsGlobalSearchIndexForAccount(account)
|
|
|
+ const isIndexReady = supportsGlobalIndex && this.remoteDriveManager.isGlobalSearchIndexReady(account)
|
|
|
+ if (supportsGlobalIndex && !isIndexReady) {
|
|
|
+ ToastUtil.showToast('首次云端漫游正在建立全量歌曲索引,请稍候')
|
|
|
+ }
|
|
|
+ const indexedSongs = await this.remoteDriveManager.getGlobalSearchIndexSongs(account)
|
|
|
+ const clonedSongs = indexedSongs.map((item: VideoItem) => cloneVideoItem(item))
|
|
|
+ Logger.info(TAG,
|
|
|
+ `发现页全量云端索引池加载完成: account=${account.name}, type=${account.webType}, indexed=${clonedSongs.length}`)
|
|
|
+ return this.filterUniqueSongs(clonedSongs)
|
|
|
+ }
|
|
|
+
|
|
|
+ private async bootstrapRemoteDiscoverySongsIfNeeded(): Promise<void> {
|
|
|
+ if (this.isRemoteBootstrapLoading || !this.mediaTable || this.remoteSongsPool.length > 0) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.isRemoteBootstrapLoading = true
|
|
|
+ try {
|
|
|
+ const account = await this.prepareActiveRemoteAccount()
|
|
|
if (!account) {
|
|
|
return
|
|
|
}
|
|
|
@@ -809,28 +847,8 @@ export struct FindView {
|
|
|
return result
|
|
|
}
|
|
|
|
|
|
- private parseTrackNumber(track?: string): number {
|
|
|
- if (!track) {
|
|
|
- return 999999
|
|
|
- }
|
|
|
- const parsed = Number.parseInt(track, 10)
|
|
|
- if (Number.isNaN(parsed) || parsed <= 0) {
|
|
|
- return 999999
|
|
|
- }
|
|
|
- return parsed
|
|
|
- }
|
|
|
-
|
|
|
private sortAlbumSongs(items: VideoItem[]): VideoItem[] {
|
|
|
- const copy = items.slice()
|
|
|
- copy.sort((left: VideoItem, right: VideoItem) => {
|
|
|
- const leftTrack = this.parseTrackNumber(left.track)
|
|
|
- const rightTrack = this.parseTrackNumber(right.track)
|
|
|
- if (leftTrack !== rightTrack) {
|
|
|
- return leftTrack - rightTrack
|
|
|
- }
|
|
|
- return this.getSongTitle(left).localeCompare(this.getSongTitle(right))
|
|
|
- })
|
|
|
- return copy
|
|
|
+ return buildSortedDiscoverySongs(items, FindCollectionSortType.TRACK_ASC)
|
|
|
}
|
|
|
|
|
|
private pickAlbumCoverSong(items: VideoItem[]): VideoItem | undefined {
|
|
|
@@ -905,16 +923,67 @@ export struct FindView {
|
|
|
}
|
|
|
|
|
|
private async getFullRemoteSongsPool(): Promise<VideoItem[]> {
|
|
|
- if (this.searchRemoteSongsPool.length > 0) {
|
|
|
- return this.searchRemoteSongsPool
|
|
|
+ if (this.remotePlaybackPool.length > 0) {
|
|
|
+ return this.remotePlaybackPool
|
|
|
}
|
|
|
if (!this.mediaTable) {
|
|
|
return []
|
|
|
}
|
|
|
- await this.ensureRemoteDiscoverySongsAvailable()
|
|
|
- const remoteSongs = await this.mediaTable.queryRemoteSongsAsync()
|
|
|
- this.searchRemoteSongsPool = this.filterUniqueSongs(remoteSongs)
|
|
|
- return this.searchRemoteSongsPool
|
|
|
+ const indexedSongs = await this.queryIndexedRemotePlaybackSongs()
|
|
|
+ let remoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync())
|
|
|
+ if (indexedSongs.length === 0 && remoteSongs.length === 0) {
|
|
|
+ await this.ensureRemoteDiscoverySongsAvailable()
|
|
|
+ remoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync())
|
|
|
+ }
|
|
|
+ this.remotePlaybackPool = buildPreferredRemotePlaybackPool(indexedSongs, remoteSongs)
|
|
|
+ Logger.info(TAG, `发现页全量云端播放池已就绪: indexed=${indexedSongs.length}, db=${remoteSongs.length}, selected=${this.remotePlaybackPool.length}`)
|
|
|
+ return this.remotePlaybackPool
|
|
|
+ }
|
|
|
+
|
|
|
+ private async playFromFullRemoteQueue(playlistId: string, playlistName: string, fallbackSongs: VideoItem[],
|
|
|
+ targetSong?: VideoItem, randomStart: boolean = false): Promise<void> {
|
|
|
+ const fullQueue = await this.getFullRemoteSongsPool()
|
|
|
+ if (randomStart) {
|
|
|
+ const queue = fullQueue.length > 0 ? fullQueue : fallbackSongs
|
|
|
+ if (queue.length === 0) {
|
|
|
+ ToastUtil.showToast('需要先配置网盘并加载歌曲后才能播放')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const startIndex = Math.floor(Math.random() * queue.length)
|
|
|
+ this.emitPlaylistPlay(playlistId, playlistName, queue, startIndex, 3)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!targetSong) {
|
|
|
+ const queue = fullQueue.length > 0 ? fullQueue : fallbackSongs
|
|
|
+ if (queue.length === 0) {
|
|
|
+ ToastUtil.showToast('暂无可播放歌曲')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.emitPlaylistPlay(playlistId, playlistName, queue, 0)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const targetFilePath = targetSong.filePath || ''
|
|
|
+ const fullIndex = resolveQueueStartIndex(fullQueue, targetFilePath)
|
|
|
+ if (fullIndex >= 0) {
|
|
|
+ this.emitPlaylistPlay(playlistId, playlistName, fullQueue, fullIndex)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const fallbackIndex = resolveQueueStartIndex(fallbackSongs, targetFilePath)
|
|
|
+ if (fallbackIndex >= 0) {
|
|
|
+ Logger.warn(TAG, `全量云端队列未命中当前歌曲,回退到当前卡片队列: ${targetFilePath}`)
|
|
|
+ this.emitPlaylistPlay(playlistId, playlistName, fallbackSongs, fallbackIndex)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const queue = fullQueue.length > 0 ? fullQueue : fallbackSongs
|
|
|
+ if (queue.length === 0) {
|
|
|
+ ToastUtil.showToast('暂无可播放歌曲')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.emitPlaylistPlay(playlistId, playlistName, queue, 0)
|
|
|
}
|
|
|
|
|
|
private async handleActionButtonTap(type: string): Promise<void> {
|
|
|
@@ -937,8 +1006,7 @@ export struct FindView {
|
|
|
ToastUtil.showToast('需要先配置网盘并加载歌曲后才能播放')
|
|
|
return
|
|
|
}
|
|
|
- const startIndex = Math.floor(Math.random() * allRemoteSongs.length)
|
|
|
- this.emitPlaylistPlay('find-random-cloud', '云端漫游', allRemoteSongs, startIndex, 3)
|
|
|
+ await this.playFromFullRemoteQueue('find-random-cloud', '云端漫游', this.remoteSongsPool, undefined, true)
|
|
|
}
|
|
|
|
|
|
private handleSwiperTap(index: number): void {
|
|
|
@@ -946,7 +1014,8 @@ export struct FindView {
|
|
|
}
|
|
|
|
|
|
private handleRemoteSongTap(index: number): void {
|
|
|
- this.emitPlaylistPlay('find-cloud', '漫步云端', this.remoteSongs, index)
|
|
|
+ const targetSong = this.remoteSongs[index]
|
|
|
+ void this.playFromFullRemoteQueue('find-cloud', '漫步云端', this.remoteSongs, targetSong)
|
|
|
}
|
|
|
|
|
|
private handleHotSongTap(index: number): void {
|
|
|
@@ -954,7 +1023,8 @@ export struct FindView {
|
|
|
}
|
|
|
|
|
|
private handleCloudMoodSongTap(index: number): void {
|
|
|
- this.emitPlaylistPlay('find-cloud-mood', '云卷云舒', this.cloudMoodSongs, index)
|
|
|
+ const targetSong = this.cloudMoodSongs[index]
|
|
|
+ void this.playFromFullRemoteQueue('find-cloud-mood', '云卷云舒', this.cloudMoodSongs, targetSong)
|
|
|
}
|
|
|
|
|
|
private handleRecentSongTap(index: number): void {
|
|
|
@@ -991,7 +1061,7 @@ export struct FindView {
|
|
|
}
|
|
|
|
|
|
private refreshRecentSongs(): void {
|
|
|
- this.recentSongs = this.pickDifferentPreferredSongs(this.recentSongsPool, RECENT_SECTION_COUNT, this.recentSongs)
|
|
|
+ this.recentSongs = this.recentSongsPool.slice(0, Math.min(RECENT_SECTION_COUNT, this.recentSongsPool.length))
|
|
|
this.recentSectionPageIndex = 0
|
|
|
}
|
|
|
|
|
|
@@ -1036,10 +1106,14 @@ export struct FindView {
|
|
|
|
|
|
private openLocalSpecialList(target: string): void {
|
|
|
if (target === 'recent') {
|
|
|
+ this.currentAlbumDefaultSortType = FindCollectionSortType.RECENT_DESC
|
|
|
+ this.currentAlbumSupportRecentPlayedSort = true
|
|
|
this.openSongCollectionDetail('find-recent-collection', '最近播放', '', this.recentSongsPool)
|
|
|
return
|
|
|
}
|
|
|
if (target === 'favorite') {
|
|
|
+ this.currentAlbumDefaultSortType = FindCollectionSortType.NAME_ASC
|
|
|
+ this.currentAlbumSupportRecentPlayedSort = false
|
|
|
this.openSongCollectionDetail('find-favorite-collection', '我的收藏', '', this.favoriteSongsPool)
|
|
|
return
|
|
|
}
|
|
|
@@ -1065,6 +1139,8 @@ export struct FindView {
|
|
|
}
|
|
|
|
|
|
private openAlbumDetail(album: FindAlbumGroup): void {
|
|
|
+ this.currentAlbumDefaultSortType = FindCollectionSortType.TRACK_ASC
|
|
|
+ this.currentAlbumSupportRecentPlayedSort = false
|
|
|
this.currentAlbumId = album.id
|
|
|
this.currentAlbumTitle = album.title
|
|
|
this.currentAlbumArtist = album.artist
|
|
|
@@ -1099,6 +1175,8 @@ export struct FindView {
|
|
|
ToastUtil.showToast('歌单里还没有可展示歌曲')
|
|
|
return
|
|
|
}
|
|
|
+ this.currentAlbumDefaultSortType = FindCollectionSortType.TRACK_ASC
|
|
|
+ this.currentAlbumSupportRecentPlayedSort = false
|
|
|
const coverSong = this.pickAlbumCoverSong(songs)
|
|
|
this.currentAlbumId = `find-playlist-${playlist.id}`
|
|
|
this.currentAlbumTitle = playlist.name
|
|
|
@@ -1194,6 +1272,8 @@ export struct FindView {
|
|
|
this.currentAlbumCoverPath = ''
|
|
|
this.currentAlbumSourceLabel = ''
|
|
|
this.currentAlbumSongs = []
|
|
|
+ this.currentAlbumDefaultSortType = FindCollectionSortType.TRACK_ASC
|
|
|
+ this.currentAlbumSupportRecentPlayedSort = false
|
|
|
}
|
|
|
|
|
|
private canDeleteCurrentAlbumSongs(): boolean {
|
|
|
@@ -3113,6 +3193,8 @@ export struct FindView {
|
|
|
albumCoverPath: this.currentAlbumCoverPath,
|
|
|
albumSourceLabel: this.currentAlbumSourceLabel,
|
|
|
songs: this.currentAlbumSongs,
|
|
|
+ defaultSortType: this.currentAlbumDefaultSortType,
|
|
|
+ supportRecentPlayedSort: this.currentAlbumSupportRecentPlayedSort,
|
|
|
allowDelete: this.canDeleteCurrentAlbumSongs(),
|
|
|
onBack: () => {
|
|
|
this.exitAlbumMode()
|