|
@@ -13,7 +13,7 @@ import { CommonConstants } from '../common/constants/CommonConstants';
|
|
|
import { WebDavAccount } from '../viewmodel/WebDavAccount';
|
|
import { WebDavAccount } from '../viewmodel/WebDavAccount';
|
|
|
import Logger from '../common/util/Logger';
|
|
import Logger from '../common/util/Logger';
|
|
|
import { RemoteDriveType } from '../common/enums/RemoteDriveType';
|
|
import { RemoteDriveType } from '../common/enums/RemoteDriveType';
|
|
|
-import { navidromeRestApi, NavidromeRestAlbum, NavidromeRestArtist, NavidromeRestSong } from '../common/network/NavidromeRestApi';
|
|
|
|
|
|
|
+import { navidromeRestApi, NavidromeRestAlbum, NavidromeRestArtist, NavidromeRestSong, NavidromeRestPlaylist } from '../common/network/NavidromeRestApi';
|
|
|
import { Utility } from '../common/util/Utility';
|
|
import { Utility } from '../common/util/Utility';
|
|
|
import { Constants } from '../Constants';
|
|
import { Constants } from '../Constants';
|
|
|
import { EventConstants } from '../common/constants/EventConstants';
|
|
import { EventConstants } from '../common/constants/EventConstants';
|
|
@@ -39,7 +39,8 @@ interface PlaylistEventData {
|
|
|
enum NavFilterType {
|
|
enum NavFilterType {
|
|
|
None = 0,
|
|
None = 0,
|
|
|
Artist = 1,
|
|
Artist = 1,
|
|
|
- Album = 2
|
|
|
|
|
|
|
+ Album = 2,
|
|
|
|
|
+ Playlist = 3
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
interface CachePreviewInfo {
|
|
interface CachePreviewInfo {
|
|
@@ -69,13 +70,16 @@ export struct NavidromePage {
|
|
|
@State allVideos: VideoItem[] = [];
|
|
@State allVideos: VideoItem[] = [];
|
|
|
@State artists: NavidromeRestArtist[] = [];
|
|
@State artists: NavidromeRestArtist[] = [];
|
|
|
@State albums: NavidromeRestAlbum[] = [];
|
|
@State albums: NavidromeRestAlbum[] = [];
|
|
|
|
|
+ @State playlists: NavidromeRestPlaylist[] = [];
|
|
|
@State loading: boolean = false;
|
|
@State loading: boolean = false;
|
|
|
@State songNextStart: number | null = 0;
|
|
@State songNextStart: number | null = 0;
|
|
|
@State artistNextStart: number | null = 0;
|
|
@State artistNextStart: number | null = 0;
|
|
|
@State albumNextStart: number | null = 0;
|
|
@State albumNextStart: number | null = 0;
|
|
|
|
|
+ @State playlistNextStart: number | null = 0;
|
|
|
@State isSongPageLoading: boolean = false;
|
|
@State isSongPageLoading: boolean = false;
|
|
|
@State isArtistPageLoading: boolean = false;
|
|
@State isArtistPageLoading: boolean = false;
|
|
|
@State isAlbumPageLoading: boolean = false;
|
|
@State isAlbumPageLoading: boolean = false;
|
|
|
|
|
+ @State isPlaylistPageLoading: boolean = false;
|
|
|
|
|
|
|
|
// 新增:详情视图状态
|
|
// 新增:详情视图状态
|
|
|
@State isDetailView: boolean = false; // 是否在艺术家/专辑详情视图
|
|
@State isDetailView: boolean = false; // 是否在艺术家/专辑详情视图
|
|
@@ -101,7 +105,8 @@ export struct NavidromePage {
|
|
|
const buttons = [
|
|
const buttons = [
|
|
|
{ text: '全部' },
|
|
{ text: '全部' },
|
|
|
{ text: '艺术家' },
|
|
{ text: '艺术家' },
|
|
|
- { text: '专辑' }
|
|
|
|
|
|
|
+ { text: '专辑' },
|
|
|
|
|
+ { text: '歌单' }
|
|
|
] as SegmentButtonItemTuple;
|
|
] as SegmentButtonItemTuple;
|
|
|
return SegmentButtonOptions.capsule({
|
|
return SegmentButtonOptions.capsule({
|
|
|
buttons,
|
|
buttons,
|
|
@@ -238,15 +243,18 @@ export struct NavidromePage {
|
|
|
this.allVideos = [];
|
|
this.allVideos = [];
|
|
|
this.artists = [];
|
|
this.artists = [];
|
|
|
this.albums = [];
|
|
this.albums = [];
|
|
|
|
|
+ this.playlists = [];
|
|
|
this.clearFilter();
|
|
this.clearFilter();
|
|
|
this.coverUrlCache.clear();
|
|
this.coverUrlCache.clear();
|
|
|
this.albumCoverLookup.clear();
|
|
this.albumCoverLookup.clear();
|
|
|
this.songNextStart = 0;
|
|
this.songNextStart = 0;
|
|
|
this.artistNextStart = 0;
|
|
this.artistNextStart = 0;
|
|
|
this.albumNextStart = 0;
|
|
this.albumNextStart = 0;
|
|
|
|
|
+ this.playlistNextStart = 0;
|
|
|
this.isSongPageLoading = false;
|
|
this.isSongPageLoading = false;
|
|
|
this.isArtistPageLoading = false;
|
|
this.isArtistPageLoading = false;
|
|
|
this.isAlbumPageLoading = false;
|
|
this.isAlbumPageLoading = false;
|
|
|
|
|
+ this.isPlaylistPageLoading = false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private async loadNavidromeLibrary(account: WebDavAccount): Promise<void> {
|
|
private async loadNavidromeLibrary(account: WebDavAccount): Promise<void> {
|
|
@@ -262,12 +270,13 @@ export struct NavidromePage {
|
|
|
await Promise.all([
|
|
await Promise.all([
|
|
|
this.loadNextSongPage(account, ticket),
|
|
this.loadNextSongPage(account, ticket),
|
|
|
this.loadNextArtistPage(account, ticket),
|
|
this.loadNextArtistPage(account, ticket),
|
|
|
- this.loadNextAlbumPage(account, ticket)
|
|
|
|
|
|
|
+ this.loadNextAlbumPage(account, ticket),
|
|
|
|
|
+ this.loadNextPlaylistPage(account, ticket)
|
|
|
]);
|
|
]);
|
|
|
if (ticket !== this.loadTicket) {
|
|
if (ticket !== this.loadTicket) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- void ServerLogUtil.info('NavidromeLoad', `首次加载完成: 歌曲 ${this.allVideos.length} / 艺术家 ${this.artists.length} / 专辑 ${this.albums.length}`);
|
|
|
|
|
|
|
+ void ServerLogUtil.info('NavidromeLoad', `首次加载完成: 歌曲 ${this.allVideos.length} / 艺术家 ${this.artists.length} / 专辑 ${this.albums.length} / 歌单 ${this.playlists.length}`);
|
|
|
await this.logCacheStatistics(account);
|
|
await this.logCacheStatistics(account);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
if (ticket === this.loadTicket) {
|
|
if (ticket === this.loadTicket) {
|
|
@@ -394,6 +403,30 @@ export struct NavidromePage {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private async loadNextPlaylistPage(account: WebDavAccount, ticket?: number): Promise<void> {
|
|
|
|
|
+ if (this.playlistNextStart === null || this.isPlaylistPageLoading) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const currentTicket = ticket ?? this.loadTicket;
|
|
|
|
|
+ this.isPlaylistPageLoading = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await navidromeRestApi.fetchPlaylistPage(account, this.playlistNextStart);
|
|
|
|
|
+ if (currentTicket !== this.loadTicket) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const chunk = response.data ?? [];
|
|
|
|
|
+ if (chunk.length === 0) {
|
|
|
|
|
+ this.playlistNextStart = null;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.playlists = [...this.playlists, ...chunk];
|
|
|
|
|
+ this.playlistNextStart = response.nextStart;
|
|
|
|
|
+ void ServerLogUtil.info('NavidromeLoad', `歌单列表追加: 本次 ${chunk.length} 个, 总数 ${this.playlists.length}`);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.isPlaylistPageLoading = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private async handleReachEnd(): Promise<void> {
|
|
private async handleReachEnd(): Promise<void> {
|
|
|
if (this.loading) {
|
|
if (this.loading) {
|
|
|
return;
|
|
return;
|
|
@@ -415,6 +448,9 @@ export struct NavidromePage {
|
|
|
case 2:
|
|
case 2:
|
|
|
await this.loadNextAlbumPage(account);
|
|
await this.loadNextAlbumPage(account);
|
|
|
break;
|
|
break;
|
|
|
|
|
+ case 3:
|
|
|
|
|
+ await this.loadNextPlaylistPage(account);
|
|
|
|
|
+ break;
|
|
|
default:
|
|
default:
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
@@ -1084,6 +1120,8 @@ export struct NavidromePage {
|
|
|
return this.artists.length;
|
|
return this.artists.length;
|
|
|
case 2:
|
|
case 2:
|
|
|
return this.albums.length;
|
|
return this.albums.length;
|
|
|
|
|
+ case 3:
|
|
|
|
|
+ return this.playlists.length;
|
|
|
default:
|
|
default:
|
|
|
return this.getVisibleSongs().length;
|
|
return this.getVisibleSongs().length;
|
|
|
}
|
|
}
|
|
@@ -1095,6 +1133,8 @@ export struct NavidromePage {
|
|
|
return '暂无艺术家';
|
|
return '暂无艺术家';
|
|
|
case 2:
|
|
case 2:
|
|
|
return '暂无专辑';
|
|
return '暂无专辑';
|
|
|
|
|
+ case 3:
|
|
|
|
|
+ return '暂无歌单';
|
|
|
default:
|
|
default:
|
|
|
if (this.isSearchMode && this.searchText.length > 0) {
|
|
if (this.isSearchMode && this.searchText.length > 0) {
|
|
|
return this.isSearchLoading ? '正在搜索远程歌曲' : '没有匹配的远程歌曲';
|
|
return this.isSearchLoading ? '正在搜索远程歌曲' : '没有匹配的远程歌曲';
|
|
@@ -1112,6 +1152,8 @@ export struct NavidromePage {
|
|
|
return '当前筛选没有找到艺术家';
|
|
return '当前筛选没有找到艺术家';
|
|
|
case 2:
|
|
case 2:
|
|
|
return '当前筛选没有找到专辑';
|
|
return '当前筛选没有找到专辑';
|
|
|
|
|
+ case 3:
|
|
|
|
|
+ return '当前筛选没有找到歌单';
|
|
|
default:
|
|
default:
|
|
|
if (this.isSearchMode && this.searchText.length > 0) {
|
|
if (this.isSearchMode && this.searchText.length > 0) {
|
|
|
return this.isSearchLoading ? '请稍候,正在通过 API 搜索' : '换个关键词再试试吧';
|
|
return this.isSearchLoading ? '请稍候,正在通过 API 搜索' : '换个关键词再试试吧';
|
|
@@ -1309,6 +1351,59 @@ export struct NavidromePage {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ buildPlaylistItem(playlist: NavidromeRestPlaylist) {
|
|
|
|
|
+ Button({ type: ButtonType.Capsule, stateEffect: true }) {
|
|
|
|
|
+ Row({ space: 12 }) {
|
|
|
|
|
+ Image($r('app.media.alt'))
|
|
|
|
|
+ .width(48)
|
|
|
|
|
+ .height(48)
|
|
|
|
|
+ .borderRadius(10)
|
|
|
|
|
+ .alt($r('app.media.alt'))
|
|
|
|
|
+ .draggable(false)
|
|
|
|
|
+ .sourceSize({ width: 38, height: 38 })
|
|
|
|
|
+ .fillColor(this.themeColor)
|
|
|
|
|
+ .objectFit(ImageFit.Cover)
|
|
|
|
|
+ .margin({ left: 20 })
|
|
|
|
|
+
|
|
|
|
|
+ Column({ space: 4 }) {
|
|
|
|
|
+ Text(playlist.name ?? '未知歌单')
|
|
|
|
|
+ .fontSize(15)
|
|
|
|
|
+ .fontColor($r('app.color.index_tab_font_color'))
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textAlign(TextAlign.Start)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+
|
|
|
|
|
+ Text(this.buildPlaylistMetaLine(playlist))
|
|
|
|
|
+ .fontSize(13)
|
|
|
|
|
+ .fontColor($r('app.color.index_tab_font_color'))
|
|
|
|
|
+ .opacity(0.6)
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+
|
|
|
|
|
+ if (playlist.ownerName) {
|
|
|
|
|
+ Text(playlist.ownerName)
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor($r('app.color.index_tab_font_color'))
|
|
|
|
|
+ .opacity(0.5)
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+ .padding({ right: 20 })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding(12)
|
|
|
|
|
+ .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.85 })
|
|
|
|
|
+ .backgroundColor(Color.Transparent)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.onPlaylistSelected(playlist);
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private buildSongMetaLine(song: VideoItem): string {
|
|
private buildSongMetaLine(song: VideoItem): string {
|
|
|
const parts: string[] = [];
|
|
const parts: string[] = [];
|
|
|
if (song.duration) {
|
|
if (song.duration) {
|
|
@@ -1344,6 +1439,20 @@ export struct NavidromePage {
|
|
|
return parts.join(' · ');
|
|
return parts.join(' · ');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private buildPlaylistMetaLine(playlist: NavidromeRestPlaylist): string {
|
|
|
|
|
+ const parts: string[] = [];
|
|
|
|
|
+ if (playlist.songCount !== undefined) {
|
|
|
|
|
+ parts.push(`${playlist.songCount} 首歌`);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (playlist.duration !== undefined) {
|
|
|
|
|
+ const duration = this.formatSongDuration(playlist.duration);
|
|
|
|
|
+ if (duration) {
|
|
|
|
|
+ parts.push(duration);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return parts.join(' · ');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private getVisibleSongs(): VideoItem[] {
|
|
private getVisibleSongs(): VideoItem[] {
|
|
|
if (this.isSearchMode) {
|
|
if (this.isSearchMode) {
|
|
|
return this.searchText.length > 0 ? this.filteredList : this.allVideos;
|
|
return this.searchText.length > 0 ? this.filteredList : this.allVideos;
|
|
@@ -1378,11 +1487,24 @@ export struct NavidromePage {
|
|
|
void this.applyFilter(NavFilterType.Album, album.id, album.name ?? '未知专辑');
|
|
void this.applyFilter(NavFilterType.Album, album.id, album.name ?? '未知专辑');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private onPlaylistSelected(playlist: NavidromeRestPlaylist): void {
|
|
|
|
|
+ if (!playlist || !playlist.id) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.isShowTitleBar = true;
|
|
|
|
|
+ // 保存当前标签页,进入详情视图
|
|
|
|
|
+ this.previousTab = this.selectedTab;
|
|
|
|
|
+ this.isDetailView = true;
|
|
|
|
|
+ // 应用筛选但不切换标签页
|
|
|
|
|
+ void this.applyFilter(NavFilterType.Playlist, playlist.id, playlist.name ?? '未知歌单');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private async applyFilter(type: NavFilterType, id: string, label: string): Promise<void> {
|
|
private async applyFilter(type: NavFilterType, id: string, label: string): Promise<void> {
|
|
|
|
|
|
|
|
// 记录筛选操作
|
|
// 记录筛选操作
|
|
|
void ServerLogUtil.info('NavidromeFilter', `应用筛选:`);
|
|
void ServerLogUtil.info('NavidromeFilter', `应用筛选:`);
|
|
|
- void ServerLogUtil.info('NavidromeFilter', `- 类型: ${type} (${type === NavFilterType.Artist ? '艺术家' : type === NavFilterType.Album ? '专辑' : '无'})`);
|
|
|
|
|
|
|
+ const typeText = type === NavFilterType.Artist ? '艺术家' : type === NavFilterType.Album ? '专辑' : type === NavFilterType.Playlist ? '歌单' : '无';
|
|
|
|
|
+ void ServerLogUtil.info('NavidromeFilter', `- 类型: ${type} (${typeText})`);
|
|
|
void ServerLogUtil.info('NavidromeFilter', `- ID: ${id}`);
|
|
void ServerLogUtil.info('NavidromeFilter', `- ID: ${id}`);
|
|
|
void ServerLogUtil.info('NavidromeFilter', `- 标签: ${label}`);
|
|
void ServerLogUtil.info('NavidromeFilter', `- 标签: ${label}`);
|
|
|
void ServerLogUtil.info('NavidromeFilter', `- 筛选前总歌曲数: ${this.allVideos.length}`);
|
|
void ServerLogUtil.info('NavidromeFilter', `- 筛选前总歌曲数: ${this.allVideos.length}`);
|
|
@@ -1429,6 +1551,8 @@ export struct NavidromePage {
|
|
|
songs = await navidromeRestApi.fetchSongsByArtist(account, this.filterId,label);
|
|
songs = await navidromeRestApi.fetchSongsByArtist(account, this.filterId,label);
|
|
|
} else if (this.filterType === NavFilterType.Album) {
|
|
} else if (this.filterType === NavFilterType.Album) {
|
|
|
songs = await navidromeRestApi.fetchSongsByAlbum(account, this.filterId,label);
|
|
songs = await navidromeRestApi.fetchSongsByAlbum(account, this.filterId,label);
|
|
|
|
|
+ } else if (this.filterType === NavFilterType.Playlist) {
|
|
|
|
|
+ songs = await navidromeRestApi.fetchSongsByPlaylist(account, this.filterId);
|
|
|
}
|
|
}
|
|
|
const videoItems = await this.convertSongsToVideoItems(songs, account);
|
|
const videoItems = await this.convertSongsToVideoItems(songs, account);
|
|
|
if (expectedFilterId !== this.filterId || expectedFilterType !== this.filterType) {
|
|
if (expectedFilterId !== this.filterId || expectedFilterType !== this.filterType) {
|
|
@@ -1586,12 +1710,18 @@ export struct NavidromePage {
|
|
|
this.buildArtistItem(artist)
|
|
this.buildArtistItem(artist)
|
|
|
}
|
|
}
|
|
|
}, (artist: NavidromeRestArtist) => artist.id)
|
|
}, (artist: NavidromeRestArtist) => artist.id)
|
|
|
- } else {
|
|
|
|
|
|
|
+ } else if (this.selectedTab === 2) {
|
|
|
ForEach(this.albums, (album: NavidromeRestAlbum) => {
|
|
ForEach(this.albums, (album: NavidromeRestAlbum) => {
|
|
|
ListItem() {
|
|
ListItem() {
|
|
|
this.buildAlbumItem(album)
|
|
this.buildAlbumItem(album)
|
|
|
}
|
|
}
|
|
|
}, (album: NavidromeRestAlbum) => album.id)
|
|
}, (album: NavidromeRestAlbum) => album.id)
|
|
|
|
|
+ } else if (this.selectedTab === 3) {
|
|
|
|
|
+ ForEach(this.playlists, (playlist: NavidromeRestPlaylist) => {
|
|
|
|
|
+ ListItem() {
|
|
|
|
|
+ this.buildPlaylistItem(playlist)
|
|
|
|
|
+ }
|
|
|
|
|
+ }, (playlist: NavidromeRestPlaylist) => playlist.id)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
.onScrollFrameBegin((offset: number) => {
|
|
.onScrollFrameBegin((offset: number) => {
|