import { VideoItem } from '../viewmodel/VideoItem'; import { LengthMetrics, SegmentButton, SegmentButtonItemTuple, SegmentButtonOptions, SymbolGlyphModifier } from '@kit.ArkUI'; import { PreferencesUtil, ToastUtil, StrUtil, ArrayUtil } from '@pura/harmony-utils'; import { ButtonFancyModifier, SymbolGlyphFancyModifier, ShadowModifier } from '../common/util/AttributeModifierUtil'; import { CommonConstants } from '../common/constants/CommonConstants'; import { WebDavAccount } from '../viewmodel/WebDavAccount'; import Logger from '../common/util/Logger'; import { RemoteDriveType } from '../common/enums/RemoteDriveType'; import { navidromeRestApi, NavidromeRestAlbum, NavidromeRestArtist, NavidromeRestSong } from '../common/network/NavidromeRestApi'; import { Utility } from '../common/util/Utility'; import { Constants } from '../Constants'; import { EventConstants } from '../common/constants/EventConstants'; import { emitter } from '@kit.BasicServicesKit'; import { setNavidromePlaylist } from '../common/util/NavidromePlaylistStore'; import { navidromeApi } from '../common/network/NavidromeApi'; const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist'; interface PlaylistEventData { playlistId: string; playlistName: string; songCount: number; startIndex: number; isJump: boolean; songFilePaths: string[]; } enum NavFilterType { None = 0, Artist = 1, Album = 2 } @Component export struct NavidromePage { @Link mType: number; @Link offsetX: number; @Link isShowDrawer: boolean; @State isNoJumpToHome: boolean = false //网盘播放不跳转首页 @Prop @Watch('onSwitchAccount') selectedAccount: WebDavAccount; @StorageProp('currentTheme') currentTheme: number = 0; @State selectedTab: number = 0; // 0: 全部, 1: 艺术家, 2: 专辑 @State allVideos: VideoItem[] = []; @State artists: NavidromeRestArtist[] = []; @State albums: NavidromeRestAlbum[] = []; @State loading: boolean = false; @StorageProp('isDarkMode') isDarkMode: boolean = false; @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined; @StorageProp('topRectHeight') topRectHeight: number = 0; @State @Watch('onTabSelectedIndexesChanged') tabSelectedIndexes: number[] = [0]; @StorageProp('themeColor') themeColor: string = PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR); private tabs: string[] = ['全部', '艺术家', '专辑']; private loadTicket: number = 0; @State filterType: NavFilterType = NavFilterType.None; @State filterLabel: string = ''; @State filterId: string = ''; private coverUrlCache: Map = new Map(); // 搜索和排序相关状态 @State isSearchMode: boolean = false; @State searchText: string = ''; // 用户输入内容 @State filteredList: Array = []; // 过滤后的歌曲结果 @State sortType: number = 0; // 排序类型 0:名称升序 1:名称降序 2:时间升序 3:时间降序 4:大小升序 5:大小降序 // SegmentButton选项 @State tabOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({ buttons: [{ text: '全部' }, { text: '艺术家' }, { text: '专辑' }] as SegmentButtonItemTuple, direction: Direction.Ltr, buttonPadding: { top: 12, bottom: 12 }, backgroundColor: $r('app.color.index_background'), selectedBackgroundColor: $r('app.color.start_window_background'), selectedFontColor: $r('app.color.text_color'), fontSize: 14, selectedFontSize: 15, localizedTextPadding: { end: LengthMetrics.vp(20), start: LengthMetrics.vp(20) } }); //当胶囊按钮的选择发生变化时调用此函数 onTabSelectedIndexesChanged() { this.selectedTab = this.tabSelectedIndexes[0]; console.info('heanup', `Selected tab: ${this.tabs[this.selectedTab]}`); // 从艺术家或专辑切换回全部时,清除筛选状态 if (this.selectedTab === 0 && this.filterType !== NavFilterType.None) { // 不清除筛选,保持筛选状态 } else if (this.selectedTab !== 0) { // 切换到艺术家或专辑标签页时,清除筛选和搜索状态 this.clearFilter(); this.isSearchMode = false; this.searchText = ''; this.filteredList = []; } } //切换不同的NavidromePage async onSwitchAccount() { await this.refreshNavidromeData(true); } aboutToAppear() { this.isNoJumpToHome = PreferencesUtil.getBooleanSync('isNoJumpToHome', true) this.sortType = PreferencesUtil.getNumberSync('navidromeSortType', 0); this.refreshNavidromeData(); } private async refreshNavidromeData(showToastWhenMissing: boolean = false): Promise { const account = this.resolveActiveAccount(); if (!account) { this.resetData(); if (showToastWhenMissing) { ToastUtil.showToast('请先选择 Navidrome 账号'); } return; } await this.loadNavidromeLibrary(account); this.doSortType(this.sortType) } private resolveActiveAccount(): WebDavAccount | undefined { if (!this.selectedAccount) { return undefined; } if (this.selectedAccount.webType !== RemoteDriveType.Navidrome) { return undefined; } if (!this.selectedAccount.host || this.selectedAccount.host.length === 0) { return undefined; } return this.selectedAccount; } private resetData(): void { this.allVideos = []; this.artists = []; this.albums = []; this.clearFilter(); this.coverUrlCache.clear(); } private async loadNavidromeLibrary(account: WebDavAccount): Promise { const ticket = ++this.loadTicket; this.loading = true; this.coverUrlCache.clear(); try { const requestTasks: Promise[] = [ navidromeRestApi.fetchAllSongs(account), navidromeRestApi.fetchArtists(account), navidromeRestApi.fetchAlbums(account) ]; const responses = await Promise.all(requestTasks); const songs = responses[0] as NavidromeRestSong[]; const artistList = responses[1] as NavidromeRestArtist[]; const albumList = responses[2] as NavidromeRestAlbum[]; if (ticket !== this.loadTicket) { return; } const albumCoverMap = await this.buildAlbumCoverMap(albumList, account); if (ticket !== this.loadTicket) { return; } const artistCoverMap = await this.buildArtistCoverMap(artistList, account); if (ticket !== this.loadTicket) { return; } const videos = await this.convertSongsToVideoItems(songs, account, albumCoverMap); if (ticket !== this.loadTicket) { return; } this.allVideos = videos; this.artists = artistList.map(artist => { artist.coverUrl = artistCoverMap.get(artist.id); return artist; }); this.albums = albumList.map(album => { album.coverUrl = albumCoverMap.get(album.id); return album; }); Logger.info('heanup', `Navidrome 已加载: 歌曲 ${songs.length} 首, 艺术家 ${artistList.length} 位, 专辑 ${albumList.length} 张`); } catch (error) { if (ticket === this.loadTicket) { Logger.error('heanup', `Navidrome 数据加载失败: ${(error as Error).message}`); ToastUtil.showToast((error as Error).message ?? 'Navidrome 数据加载失败'); } } finally { if (ticket === this.loadTicket) { this.loading = false; } } } private async buildAlbumCoverMap(albums: NavidromeRestAlbum[], account: WebDavAccount): Promise> { const map = new Map(); const tasks: Promise[] = []; for (let i = 0; i < albums.length; i++) { const album = albums[i]; tasks.push((async () => { try { const directUrl = this.resolveEmbedCover(account, album.embedArtPath ?? album.coverArtPath); if (directUrl) { map.set(album.id, directUrl); return; } const coverId = album.coverArt ?? album.coverArtId ?? (album.id ? `al-${album.id}` : undefined); const url = await this.buildCoverUrl(account, coverId); if (url) { map.set(album.id, url); } } catch (error) { Logger.warn('heanup', `Navidrome 专辑封面解析失败: ${(error as Error).message}`); } })()); } await Promise.all(tasks); return map; } private async buildArtistCoverMap(artists: NavidromeRestArtist[], account: WebDavAccount): Promise> { const map = new Map(); const tasks: Promise[] = []; for (let i = 0; i < artists.length; i++) { const artist = artists[i]; tasks.push((async () => { try { const directUrl = artist.mediumImageUrl ?? artist.largeImageUrl ?? this.resolveEmbedCover(account, artist.coverArtPath); if (directUrl) { map.set(artist.id, directUrl); return; } const coverId = artist.coverArt ?? artist.coverArtId ?? (artist.id ? `ar-${artist.id}` : undefined); const url = await this.buildCoverUrl(account, coverId, 256); if (url) { map.set(artist.id, url); } } catch (error) { Logger.warn('heanup', `Navidrome 艺术家封面解析失败: ${(error as Error).message}`); } })()); } await Promise.all(tasks); return map; } private async convertSongsToVideoItems(songs: NavidromeRestSong[], account: WebDavAccount, albumCoverMap: Map): Promise { const tasks: Promise[] = []; for (let i = 0; i < songs.length; i++) { const song = songs[i]; tasks.push((async () => { let coverUrl: string | undefined; try { coverUrl = await this.resolveSongCover(song, account, albumCoverMap); } catch (error) { Logger.warn('heanup', `Navidrome 单曲封面解析失败: ${(error as Error).message}`); } return this.convertSongToVideoItem(song, account, coverUrl); })()); } return Promise.all(tasks); } private async resolveSongCover(song: NavidromeRestSong, account: WebDavAccount, albumCoverMap: Map): Promise { if (song.albumId) { const albumCover = albumCoverMap.get(song.albumId); if (albumCover) { return albumCover; } } const directUrl = this.resolveEmbedCover(account, song.embedArtPath ?? song.coverArtPath); if (directUrl) { return directUrl; } const coverId = song.coverArt ?? song.coverArtId ?? song.id; return this.buildCoverUrl(account, coverId); } private async buildCoverUrl(account: WebDavAccount, coverId?: string, size: number = 300): Promise { if (!coverId || coverId.trim().length === 0) { return undefined; } const normalizedId = coverId.trim(); const cacheKey = `${normalizedId}_${size}`; const cached = this.coverUrlCache.get(cacheKey); if (cached) { return cached; } const url = await navidromeApi.buildCoverArtUrl(account, normalizedId, size); if (url) { this.coverUrlCache.set(cacheKey, url); } return url; } private resolveEmbedCover(account: WebDavAccount, path?: string): string | undefined { return navidromeRestApi.resolveResourceUrl(account, path); } private convertSongToVideoItem(song: NavidromeRestSong, account: WebDavAccount, coverUrl?: string): VideoItem { const title = song.title ?? Constants.UNKNOWN_TITLE; const videoItem = new VideoItem( title, song.id, `navidrome://${account.id ?? 0}/${song.id}`, CommonConstants.TYPE_NAVIDROME, song.size ?? 0, song.createdAt ?? '', Utility.formatFSize(song.size ?? 0), undefined, song.artist ?? Constants.UNKNOWN_ARTIST, song.album ?? '', `${title}${song.suffix ? '.' + song.suffix : ''}` ); const durationStr = this.formatSongDuration(song.duration); if (durationStr) { videoItem.duration = durationStr; } videoItem.size = Utility.formatFSize(song.size ?? 0); videoItem.bit_rate = song.bitRate ? song.bitRate.toString() : undefined; videoItem.genre = song.genre; videoItem.webdav_account_id = account.id?.toString(); videoItem.remote_rel_path = song.id; videoItem.navArtistId = song.artistId; videoItem.navAlbumId = song.albumId; videoItem.pixelMapPath = coverUrl; // 调试日志:检查歌曲的 albumId 和 artistId if (this.allVideos.length < 3) { Logger.info('heanup', `歌曲 ${title}: artistId=${song.artistId}, albumId=${song.albumId}, artist=${song.artist}, album=${song.album}`); } if (song.track !== undefined && song.track !== null) { videoItem.track = song.track.toString(); } if (song.year !== undefined && song.year !== null) { videoItem.year = song.year.toString(); } if (song.contentType) { videoItem.mimeType = song.contentType; } return videoItem; } private formatSongDuration(durationSeconds?: number): string | undefined { if (durationSeconds === undefined || durationSeconds === null || durationSeconds < 0) { return undefined; } const totalSeconds = Math.floor(durationSeconds); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; const pad = (value: number) => value.toString().padStart(2, '0'); return `${pad(minutes)}:${pad(seconds)}`; } @Builder topTitleBar() { Column() { Row({ space: 6 }) { // 标题或搜索框 if (!this.isSearchMode) { // 左侧返回按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.sort')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .animation({ duration: 300, curve: Curve.Ease }) .onClick(() => { this.getUIContext().animateTo({ duration: 555 }, () => { // 动画闭包内控制Image组件的出现和消失 this.isShowDrawer = !this.isShowDrawer this.offsetX = 0 }) }) .attributeModifier(new ShadowModifier()) .zIndex(0) Text(this.selectedAccount.name) .margin({ left: 3, right: 10 }) .fontColor($r('app.color.text_color')) .fontSize(18) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE }) .layoutWeight(1) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { // 可以添加标题点击事件 }) .animation({ duration: 300, curve: Curve.Ease }) } else { Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.chevron_left')) .attributeModifier(new SymbolGlyphFancyModifier(24, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .animation({ duration: 300, curve: Curve.Ease }) .onClick(() => { this.isSearchMode = false; this.onSearchInput(''); }) .attributeModifier(new ShadowModifier()) .zIndex(0) } // 搜索框 Search({ value: this.searchText, placeholder: '搜索标题、艺术家...' }) .searchButton('搜索', { fontColor: this.themeColor }) .searchIcon({ src: $r('sys.media.ohos_ic_public_search_filled') }) .cancelButton({ style: CancelButtonStyle.CONSTANT, icon: { src: $r('sys.media.ohos_ic_public_cancel_filled') } }) .layoutWeight(1) .height(35) .maxLength(20) .backgroundColor(this.isDarkMode?Color.Black:'#F5F5F5') .placeholderColor(Color.Grey) .placeholderFont({ size: 14, weight: 400 }) .textFont({ size: 14, weight: 400 }) .onSubmit((value: string) => { this.onSearchInput(value); }) .onChange((value: string) => { this.onSearchInput(value); }) .visibility(this.isSearchMode?Visibility.Visible:Visibility.None) .animation({ duration: 300, curve: Curve.Ease }) // 搜索/排序按钮 if (!this.isSearchMode) { // 搜索按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.magnifyingglass')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .animation({ duration: 300, curve: Curve.Ease }) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .attributeModifier(new ShadowModifier()) .zIndex(0) .visibility(this.selectedTab==0?Visibility.Visible:Visibility.None) .onClick(() => { this.isSearchMode = true; if(ArrayUtil.isEmpty(this.filteredList)){ this.filteredList = [...this.allVideos]; } }) // 排序按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.list_number')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .animation({ duration: 300, curve: Curve.Ease }) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .bindMenu(this.SortMenuBuilder) .attributeModifier(new ShadowModifier()) .zIndex(0) } } .width('100%') .height(55) .padding({ left: 15, right: 15 }) .justifyContent(FlexAlign.SpaceBetween) .alignItems(VerticalAlign.Center) // 分段按钮 SegmentButton({ options: this.tabOptions, selectedIndexes: $tabSelectedIndexes }) .width('100%') .padding({ left: 25, right: 25, top: 5, bottom: 5 }) } .width('100%') .padding({ top: this.topRectHeight + 5 }) .backgroundColor($r('app.color.start_window_background')) } @Builder SortMenuBuilder() { Menu() { MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')), content: $r('app.string.sort_by_name') }) .onClick(async () => { this.doSortType(0); PreferencesUtil.put("navidromeSortType", 0); }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')), content: '按名称降序' }) .onClick(async () => { this.doSortType(1); PreferencesUtil.put("navidromeSortType", 1); }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.clock')), content: '按艺术家升序' }) .onClick(async () => { this.doSortType(2); PreferencesUtil.put("navidromeSortType", 2); }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.timer')), content: '按艺术家降序' }) .onClick(async () => { this.doSortType(3); PreferencesUtil.put("navidromeSortType", 3); }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')), content: '按专辑升序' }) .onClick(async () => { this.doSortType(4); PreferencesUtil.put("navidromeSortType", 4); }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')), content: '按专辑降序' }) .onClick(async () => { this.doSortType(5); PreferencesUtil.put("navidromeSortType", 5); }) } } doSortType(index: number) { this.sortType = index; const songs = this.isSearchMode ? this.filteredList :this.allVideos; // 对歌曲列表进行排序 switch (index) { case 0: // 名称升序 Utility.doSortListAscending(songs,false) break; case 1: // 名称降序 Utility.doSortListDescending(songs,true) break; case 2: // 艺术家升序 songs.sort((a, b) => { // 处理艺术家可能为undefined的字符串比较 const artistA = a.artist?.trim() || ''; // 可选添加 trim() 处理空格 const artistB = b.artist?.trim() || ''; return artistA.localeCompare(artistB); }); break; case 3: // 艺术家降序 songs.sort((a, b) => { // 处理艺术家可能为undefined的字符串比较 const artistA = a.artist?.trim() || ''; // 可选添加 trim() 处理空格 const artistB = b.artist?.trim() || ''; return artistB.localeCompare(artistA); }); break; case 4: // 专辑升序 songs.sort((a, b) => { const albumA = a.album?.trim() || ''; // 可选添加 trim() 处理空格 const albumB = b.album?.trim() || ''; return albumA.localeCompare(albumB); }); break; case 5: // 专辑降序 songs.sort((a, b) => { const albumA = a.album?.trim() || ''; // 可选添加 trim() 处理空格 const albumB = b.album?.trim() || ''; return albumB.localeCompare(albumA); }); break; } // 更新显示列表 if (this.isSearchMode) { this.filteredList = [...songs]; } } // 实时搜索逻辑 private onSearchInput(value: string) { this.searchText = value.trim(); let mSearchList: Array = [...this.allVideos]; // 新增条件判断:空输入时显示所有数据 if (this.searchText === '') { this.filteredList = [...mSearchList]; // 创建新数组以触发状态更新 } else { this.filteredList = mSearchList.filter((item: VideoItem) => { // 支持模糊匹配和艺术家 专辑匹配 const regex = new RegExp(this.searchText.toLowerCase().replace(/\s+/g, '.*'), 'i'); return regex.test(item.name.toLowerCase()) || regex.test(item.fileName?.toLowerCase() ?? "") || regex.test(item.artist?.toLowerCase() ?? "") || regex.test(item.album?.toLowerCase() ?? "") }); } } private getCurrentCount(): number { switch (this.selectedTab) { case 1: return this.artists.length; case 2: return this.albums.length; default: return this.getVisibleSongs().length; } } private getEmptyTitle(): string { switch (this.selectedTab) { case 1: return '暂无艺术家'; case 2: return '暂无专辑'; default: return this.filterType === NavFilterType.None ? '暂无音乐' : '该筛选下暂无歌曲'; } } private getEmptySubtitle(): string { switch (this.selectedTab) { case 1: return '当前筛选没有找到艺术家'; case 2: return '当前筛选没有找到专辑'; default: return this.filterType === NavFilterType.None ? '当前分类下没有找到音乐文件' : '请尝试调整筛选条件'; } } @Builder buildSongItem(song: VideoItem, index: number) { Button({ type: ButtonType.Normal, stateEffect: false }) { Row({ space: 12 }) { // 歌曲封面 Image(song.pixelMapPath ? song.pixelMapPath : $r('app.media.music_red')) .width(48) .height(48) .borderRadius(10) .sourceSize({ width: 38, height: 38 }) .alt($r('app.media.music_red')) .fillColor(song.pixelMapPath ? undefined : this.themeColor) .objectFit(ImageFit.Cover) .margin({ left: 8 }) .onClick(() => { this.playSong(song, index, true); }) // 歌曲信息 Column({ space: 4 }) { Text(song.name) .fontSize(15) .fontColor(this.currentSong?.filePath == song.filePath ? this.themeColor : $r('app.color.index_tab_font_color')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row() { Text((song.artist ?? '') + " ") .fontSize(13) .fontColor(this.currentSong?.filePath == song.filePath ? this.themeColor : $r('app.color.index_tab_font_color')) .opacity(0.6) .maxLines(1) .visibility(song.artist ? Visibility.Visible : Visibility.None) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.buildSongMetaLine(song)) .fontSize(13) .fontColor(this.currentSong?.filePath == song.filePath ? this.themeColor : $r('app.color.index_tab_font_color')) .opacity(0.6) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width('90%') } .alignItems(HorizontalAlign.Start) .layoutWeight(1) .padding({ right: 20 }) Column() { ImageAnimator() .images(CommonConstants.IMAGE_FRAME_INFO)// 动画数组 .duration(1000)// 持续 .state(AnimationStatus.Running)// 动画状态 .fillMode(FillMode.Forwards) .visibility(this.currentSong?.filePath == song.filePath ? Visibility.Visible : Visibility.None) .width(18) .margin({ right: 12, top: 8, bottom: 8 }) .height(18) .iterations(-1) // 播放次数 } } } .width('100%') .padding(12) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 }) .backgroundColor(Color.Transparent) .onClick(() => { this.playSong(song, index); }) } @Builder buildArtistItem(artist: NavidromeRestArtist) { Button({ type: ButtonType.Normal, stateEffect: false }) { Row({ space: 12 }) { Image(artist.coverUrl ?? $r('app.media.music_red')) .width(48) .height(48) .borderRadius(10) .fillColor(artist.coverUrl ? undefined : this.themeColor) .objectFit(ImageFit.Cover) .margin({ left: 8 }) Column({ space: 4 }) { Text(artist.name ?? Constants.UNKNOWN_ARTIST) .fontSize(15) .fontColor($r('app.color.index_tab_font_color')) .maxLines(1) .textAlign(TextAlign.Start) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.buildArtistMetaLine(artist)) .fontSize(13) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) .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.onArtistSelected(artist); }) } @Builder buildAlbumItem(album: NavidromeRestAlbum) { Button({ type: ButtonType.Normal, stateEffect: false }) { Row({ space: 12 }) { Image(album.coverUrl ?? $r('app.media.music_red')) .width(48) .height(48) .borderRadius(10) .fillColor(album.coverUrl ? undefined : this.themeColor) .objectFit(ImageFit.Cover) .margin({ left: 8 }) Column({ space: 4 }) { Text(album.name ?? '未知专辑') .fontSize(15) .fontColor($r('app.color.index_tab_font_color')) .maxLines(1) .textAlign(TextAlign.Start) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row() { Text(album.artist ?? Constants.UNKNOWN_ARTIST) .fontSize(13) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.buildAlbumMetaLine(album)) .fontSize(13) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) .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.onAlbumSelected(album); }) } private buildSongMetaLine(song: VideoItem): string { const parts: string[] = []; if (song.duration) { parts.push(song.duration as string); } // if (song.size) { // parts.push(song.size as string); // } // if (parts.length === 0 && song.cTime) { // parts.push(song.cTime as string); // } return parts.join(' · '); } private buildArtistMetaLine(artist: NavidromeRestArtist): string { const albumCount = artist.albumCount ?? 0; const songCount = artist.songCount ?? 0; const playCount = artist.playCount ?? 0; return `专辑 ${albumCount} · 歌曲 ${songCount} `; } private buildAlbumMetaLine(album: NavidromeRestAlbum): string { const parts: string[] = []; if (album.duration !== undefined) { const duration = this.formatSongDuration(album.duration); if (duration) { parts.push(duration); } } if (album.minYear) { parts.push(`发行 ${album.minYear}`); } return parts.join(' · '); } private getVisibleSongs(): VideoItem[] { if(this.isSearchMode) return this.filteredList; if (this.filterType === NavFilterType.Artist && this.filterId.length > 0) { const filtered = this.allVideos.filter(item => item.navArtistId === this.filterId || item.artist === this.filterLabel); Logger.info('heanup', `艺术家筛选: filterId=${this.filterId}, filterLabel=${this.filterLabel}, 结果数=${filtered.length}`); return filtered; } if (this.filterType === NavFilterType.Album && this.filterId.length > 0) { const filtered = this.allVideos.filter(item => { const match = item.navAlbumId === this.filterId || item.album === this.filterLabel; if (!match && this.allVideos.indexOf(item) < 3) { // 只打印前3首歌的调试信息 Logger.info('heanup', `歌曲 ${item.name}: navAlbumId=${item.navAlbumId}, album=${item.album}, 期望albumId=${this.filterId}, 期望album=${this.filterLabel}`); } return match; }); Logger.info('heanup', `专辑筛选: filterId=${this.filterId}, filterLabel=${this.filterLabel}, 结果数=${filtered.length}`); return filtered; } return this.allVideos; } private onArtistSelected(artist: NavidromeRestArtist): void { if (!artist || !artist.id) { return; } this.applyFilter(NavFilterType.Artist, artist.id, artist.name ?? Constants.UNKNOWN_ARTIST); } private onAlbumSelected(album: NavidromeRestAlbum): void { if (!album || !album.id) { return; } this.applyFilter(NavFilterType.Album, album.id, album.name ?? '未知专辑'); } private applyFilter(type: NavFilterType, id: string, label: string): void { Logger.info('heanup', `应用筛选: type=${type}, id=${id}, label=${label}`); // 先更新状态 this.filterType = type; this.filterId = id; this.filterLabel = label; // 退出搜索模式,确保显示筛选结果 this.isSearchMode = false; this.searchText = ''; this.filteredList = []; // 调试日志:检查筛选结果 const visibleSongs = this.getVisibleSongs(); Logger.info('heanup', `筛选后歌曲数量: ${visibleSongs.length}, 总歌曲数: ${this.allVideos.length}`); if (visibleSongs.length > 0) { Logger.info('heanup', `第一首歌: ${visibleSongs[0].name}, albumId=${visibleSongs[0].navAlbumId}, album=${visibleSongs[0].album}`); } // 然后执行动画切换标签页 this.getUIContext().animateTo({ duration: 555 }, () => { this.selectedTab = 0; this.tabSelectedIndexes = [0]; }) } private clearFilter(): void { this.filterType = NavFilterType.None; this.filterId = ''; this.filterLabel = ''; } private playSong(song: VideoItem, index: number, isJump: boolean = false): void { try { if (!this.allVideos || this.allVideos.length === 0) { ToastUtil.showToast('暂无可播放的歌曲'); return; } const account = this.resolveActiveAccount(); if (!account || !account.id) { ToastUtil.showToast('Navidrome账号信息不完整,无法播放'); return; } Logger.info('heanup', `Navidrome 播放歌曲: ${song.name}, 索引: ${index}`); const targetIndex = this.allVideos.findIndex(item => item.id === song.id); const startIndex = targetIndex >= 0 ? targetIndex : index; setNavidromePlaylist(this.allVideos, startIndex); const playlistData: PlaylistEventData = { playlistId: NAVIDROME_PLAYLIST_ID, playlistName: `Navidrome - ${account.name ?? '未知账户'}`, songCount: this.allVideos.length, startIndex, isJump: isJump,//设置true会弹出播放页 songFilePaths: this.allVideos.map(item => item.filePath) }; const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }; emitter.emit(eventPlaylistPlay, { data: playlistData }); Logger.info('heanup', `Navidrome 发送播放事件,歌曲数: ${this.allVideos.length}, 起始: ${index}`); if(!this.isNoJumpToHome){ // 跳转到首页播放器 this.getUIContext()?.animateTo({ duration: 555 }, () => { this.mType = 0 }) } } catch (error) { const err = error as Error; Logger.error('heanup', '播放歌曲失败: ' + err.message); ToastUtil.showToast('播放失败'); } } @Builder buildContentView(){ // 主内容区域 if (this.loading) { Column() { LoadingProgress() .width(50) .height(50) .color($r('app.color.title_bar_bg')) Text('加载中...') .margin({ top: 10 }) .fontSize(14) .fontColor($r('app.color.index_tab_unselected_font_color')) } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) } else { if (this.selectedTab === 0 && this.filterType !== NavFilterType.None) { Row({ space: 8 }) { Text(`筛选:${this.filterLabel}`) .fontSize(13) .fontColor(this.themeColor) .layoutWeight(1) Button('清除筛选') .type(ButtonType.Capsule) .backgroundColor(this.themeColor) .fontSize(12) .onClick(() => this.clearFilter()) } .width('90%') .padding({ left: 16, right: 16, top: 6, bottom: 2 }) } List({ space: 8 }) { if (this.selectedTab === 0) { ForEach(this.getVisibleSongs(), (item: VideoItem, index: number) => { ListItem() { this.buildSongItem(item, index) } }, (item: VideoItem) => item.id) } else if (this.selectedTab === 1) { ForEach(this.artists, (artist: NavidromeRestArtist) => { ListItem() { this.buildArtistItem(artist) } }, (artist: NavidromeRestArtist) => artist.id) } else { ForEach(this.albums, (album: NavidromeRestAlbum) => { ListItem() { this.buildAlbumItem(album) } }, (album: NavidromeRestAlbum) => album.id) } } .width('100%') .layoutWeight(1) .padding({ top: 10, bottom: 10 }) .listDirection(Axis.Vertical) .scrollBar(BarState.Auto) .edgeEffect(EdgeEffect.Spring) // 空状态 if (this.getCurrentCount() === 0) { Column() { Image($r('app.media.music_red')) .width(80) .height(80) .opacity(0.6) Text(this.getEmptyTitle()) .margin({ top: 16 }) .fontSize(16) .fontColor($r('app.color.index_tab_unselected_font_color')) Text(this.getEmptySubtitle()) .margin({ top: 8 }) .fontSize(14) .fontColor($r('app.color.index_tab_unselected_font_color')) } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) } } } build() { Stack() { this.buildContentView(); Column() { this.topTitleBar() } } .alignContent(Alignment.Top) .width('100%') .height('100%') } }