Эх сурвалжийг харах

Merge branch 'master' of https://git.ss5.xyz/onecold/TTMusic

chendeben 7 сар өмнө
parent
commit
0dd019c924

+ 2 - 2
AppScope/app.json5

@@ -2,8 +2,8 @@
   "app": {
     "bundleName": "com.xgplayer.ttmusic.hm",
     "vendor": "example",
-    "versionCode": 20251205,
-    "versionName": "1.7.6",
+    "versionCode": 20251224,
+    "versionName": "1.8.0",
     "icon": "$media:app_icon",
     "label": "$string:app_name",
     "multiAppMode": {

+ 128 - 8
entry/src/main/ets/common/network/NavidromeRestApi.ets

@@ -1,6 +1,7 @@
 import { http } from '@kit.NetworkKit';
 import { WebDavAccount } from '../../viewmodel/WebDavAccount';
 import { ServerLogUtil } from '../util/ServerLogUtil';
+import { navidromeApi } from './NavidromeApi';
 
 const TAG = 'heanup NavidromeRestApi';
 
@@ -82,6 +83,24 @@ export interface NavidromeRestAlbum {
   coverUrl?: string;
 }
 
+export interface NavidromeRestPlaylist {
+  id: string;
+  name?: string;
+  comment?: string;
+  duration?: number;
+  size?: number;
+  songCount?: number;
+  ownerName?: string;
+  ownerId?: string;
+  public?: boolean;
+  path?: string;
+  sync?: boolean;
+  createdAt?: string;
+  updatedAt?: string;
+  rules?: object;
+  evaluatedAt?: string;
+}
+
 export interface NavidromePagedResponse<T> {
   data: T[];
   nextStart: number | null;
@@ -96,23 +115,118 @@ export class NavidromeRestApi {
   }
 
   async fetchArtistPage(account: WebDavAccount, start: number): Promise<NavidromePagedResponse<NavidromeRestArtist>> {
-    return this.fetchPage<NavidromeRestArtist>(account, '/api/artist', start, 'name', 'ASC');
+    return this.fetchPage<NavidromeRestArtist>(account, '/api/artist', start, 'name', 'ASC','albumartist');
   }
 
   async fetchAlbumPage(account: WebDavAccount, start: number): Promise<NavidromePagedResponse<NavidromeRestAlbum>> {
     return this.fetchPage<NavidromeRestAlbum>(account, '/api/album', start, 'name', 'ASC');
   }
 
-  async fetchSongsByArtist(account: WebDavAccount, artistId: string): Promise<NavidromeRestSong[]> {
-    return this.fetchSongsWithFilter(account, [new QueryParam('artist_id', artistId)]);
+  async fetchPlaylistPage(account: WebDavAccount, start: number): Promise<NavidromePagedResponse<NavidromeRestPlaylist>> {
+    return this.fetchPage<NavidromeRestPlaylist>(account, '/api/playlist', start, 'name', 'ASC');
+  }
+
+  async fetchSongsByArtist(account: WebDavAccount, artistId: string, label?: string): Promise<NavidromeRestSong[]> {
+    // 这里判断fetchSongsWithFilter如果返回空数据,则用搜索的接口去搜索艺术家 label就是艺术家名称
+    const songs = await this.fetchSongsWithFilter(account, [new QueryParam('artist_id', artistId)]);
+
+    // 如果通过 artist_id 查询结果为空,且有艺术家名称,则使用搜索接口
+    if ((!songs || songs.length === 0) && label && label.trim().length > 0) {
+      void ServerLogUtil.warn(TAG, `通过 artist_id 查询为空,尝试搜索艺术家: "${label}"`);
+      try {
+        const searchResults = await navidromeApi.searchSongs(account, label.trim(), 500);
+
+        // 将 NavidromeSong 转换为 NavidromeRestSong
+        if (searchResults && searchResults.length > 0) {
+          void ServerLogUtil.info(TAG, `搜索接口返回 ${searchResults.length} 首歌曲`);
+          return this.convertToRestSongs(searchResults);
+        }
+      } catch (error) {
+        const err = error as Error;
+        void ServerLogUtil.error(TAG, `搜索艺术家歌曲失败: ${err.message}`);
+      }
+    }
+
+    return songs ?? [];
+  }
+
+  async fetchSongsByAlbum(account: WebDavAccount, albumId: string, label?: string): Promise<NavidromeRestSong[]> {
+    // 这里判断fetchSongsWithFilter如果返回空数据,则用搜索的接口去搜索专辑 label就是专辑名称
+    const songs = await this.fetchSongsWithFilter(account, [new QueryParam('album_id', albumId)]);
+
+    // 如果通过 album_id 查询结果为空,且有专辑名称,则使用搜索接口
+    if ((!songs || songs.length === 0) && label && label.trim().length > 0) {
+      void ServerLogUtil.warn(TAG, `通过 album_id 查询为空,尝试搜索专辑: "${label}"`);
+      try {
+
+        const searchResults = await navidromeApi.searchSongs(account, label.trim(), 500);
+
+        // 将 NavidromeSong 转换为 NavidromeRestSong
+        if (searchResults && searchResults.length > 0) {
+          void ServerLogUtil.info(TAG, `搜索接口返回 ${searchResults.length} 首歌曲`);
+          return this.convertToRestSongs(searchResults);
+        }
+      } catch (error) {
+        const err = error as Error;
+        void ServerLogUtil.error(TAG, `搜索专辑歌曲失败: ${err.message}`);
+      }
+    }
+
+    return songs ?? [];
+  }
+
+  async fetchSongsByPlaylist(account: WebDavAccount, playlistId: string): Promise<NavidromeRestSong[]> {
+    const results: NavidromeRestSong[] = [];
+    let start = 0;
+    while (true) {
+      const end = start + this.PAGE_SIZE;
+      const params = [
+        new QueryParam('_start', `${start}`),
+        new QueryParam('_end', `${end}`),
+        new QueryParam('_sort', 'createdAt'),
+        new QueryParam('_order', 'DESC')
+      ];
+      const path = `/api/playlist/${playlistId}/tracks`;
+      const chunk = await this.get<NavidromeRestSong[]>(account, path, params);
+      if (!chunk || chunk.length === 0) {
+        break;
+      }
+      results.push(...chunk);
+      if (chunk.length < this.PAGE_SIZE) {
+        break;
+      }
+      start = end;
+    }
+    return results;
   }
 
-  async fetchSongsByAlbum(account: WebDavAccount, albumId: string): Promise<NavidromeRestSong[]> {
-    return this.fetchSongsWithFilter(account, [new QueryParam('album_id', albumId)]);
+  // 将 NavidromeSong 转换为 NavidromeRestSong
+  private convertToRestSongs(songs: import('./NavidromeApi').NavidromeSong[]): NavidromeRestSong[] {
+    return songs.map((song): NavidromeRestSong => ({
+      id: song.id,
+      title: song.title,
+      artist: song.artist,
+      artistId: song.artistId,
+      album: song.album,
+      albumId: song.albumId,
+      duration: song.duration,
+      bitRate: song.bitRate,
+      suffix: song.suffix,
+      size: song.size,
+      createdAt: song.created,
+      genre: song.genre,
+      track: song.track,
+      year: song.year,
+      contentType: song.contentType,
+      coverArt: song.coverArt,
+      coverArtId: song.coverArt,
+      coverArtPath: undefined,
+      embedArtPath: undefined
+    }));
   }
 
   private async fetchPage<T extends object>(account: WebDavAccount, path: string,
-    start: number, sortField: string, order: 'ASC' | 'DESC'): Promise<NavidromePagedResponse<T>> {
+    start: number, sortField: string, order: 'ASC' | 'DESC',role?:string): Promise<NavidromePagedResponse<T>> {
     const end = start + this.PAGE_SIZE;
     const params: Array<QueryParam> = [
       new QueryParam('_start', `${start}`),
@@ -120,6 +234,9 @@ export class NavidromeRestApi {
       new QueryParam('_sort', sortField),
       new QueryParam('_order', order)
     ];
+    if(role){
+      params.push(new QueryParam('_role', role));
+    }
     void ServerLogUtil.debug(TAG, `${path} 分页请求: start=${start}, end=${end}`);
     const chunk = await this.get<T[]>(account, path, params);
     const nextStart = chunk && chunk.length === this.PAGE_SIZE ? end : null;
@@ -221,7 +338,9 @@ export class NavidromeRestApi {
         readTimeout: 15000,
         expectDataType: http.HttpDataType.STRING,
         header: {
-          'Content-Type': 'application/json'
+          'Content-Type': 'application/json; charset=utf-8',
+          'Accept': 'application/json; charset=utf-8',
+          'Accept-Charset': 'utf-8'
         },
         extraData: JSON.stringify(this.buildLoginBody(username, password))
       });
@@ -252,7 +371,8 @@ export class NavidromeRestApi {
     return {
       'x-nd-authorization': `Bearer ${auth.token}`,
       'x-nd-client-unique-id': auth.clientId,
-      'Accept': 'application/json'
+      'Accept': 'application/json; charset=utf-8',
+      'Accept-Charset': 'utf-8'
     };
   }
 

+ 0 - 2
entry/src/main/ets/entryability/EntryAbility.ets

@@ -24,7 +24,6 @@ import { systemShare } from '@kit.ShareKit';
 import Logger from '../common/util/Logger';
 import statusBarManager from '@hms.pcService.statusBarManager';
 import StatusBarViewExtensionAbility from '@hms.pcService.StatusBarViewExtensionAbility';
-import { SpiderMan } from '@simplepeng/spider-man';
 import { smartMobilityCommon } from '@kit.CarKit';
 import { url } from '@kit.ArkTS';
 import { display } from '@kit.ArkUI';
@@ -238,7 +237,6 @@ export default class EntryAbility extends UIAbility {
     onWindowStageCreate(windowStage: window.WindowStage) {
         // Main window is created, set main page for this ability
         hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
-        SpiderMan.init();
         AppUtil.init(this.context);
         //1.获取应用主窗口。
         let windowClass: window.Window | null = null;

+ 6 - 48
entry/src/main/ets/view/LocalMusic.ets

@@ -2339,7 +2339,7 @@ export struct LocalMusic {
 
 
     for (let i = 0; i < this.fileList.length; i++) {
-      console.info(`The name of file: ${this.fileList[i]}`);
+      // console.info(`The name of file: ${this.fileList[i]}`);
       let path = curPath + '/' + this.fileList[i]
       if (FileUtil.isDirectory(path)) {
         let item: VideoItem =
@@ -6618,30 +6618,15 @@ export struct LocalMusic {
               || item.type === CommonConstants.TYPE_IS_ARTIST || item.type === CommonConstants.TYPE_IS_ALBUM) {
               this.DirItem(item, index)
 
-            } else if (item.type === CommonConstants.TYPE_IS_CSJAD) { //如果是穿山甲广告
-
             } else {
               this.MusicItem(item, index)
 
             }
           }
 
-          .shadow(this.scaleItem === index ? {
-            radius: 70,
-            color: '#15000000',
-            offsetX: 0,
-            offsetY: 0
-          } :
-            {
-              radius: 0,
-              color: '#15000000',
-              offsetX: 0,
-              offsetY: 0
-            })
           .animation({ curve: Curve.Sharp, duration: 300 })
 
         }
-        // .transition(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }))
         .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 0.9, y: 0.9 })
           .animation({ duration: 500 }),
           TransitionEffect.scale({ x: 0, y: 0 })))
@@ -6717,23 +6702,7 @@ export struct LocalMusic {
                     this.itemMove(indexA, indexA - 1);
                   });
                 }
-                let curListOffset = this.listScroller.currentOffset()
-                // 获取手指信息
-                let fingerInfo = event.fingerList[0]
-                let clickPercentY =
-                  (fingerInfo.globalY - Number(this.listArea.globalPosition.y)) / Number(this.listArea.height)
-                if (clickPercentY > 0.8 && !this.listScroller.isAtEnd()) {
-                  let scrollVelocity = clickPercentY > 0.9 ? 4 : 2
-                  if (this.listMaxScrollOffsetY - curListOffset.yOffset > scrollVelocity + 5) {
-                    this.listScroller.scrollTo({ xOffset: 0, yOffset: curListOffset.yOffset += scrollVelocity })
 
-                  }
-                } else if (clickPercentY < 0.2 && curListOffset.yOffset >= 0) {
-                  let scrollVelocity = clickPercentY < 0.1 ? 4 : 2
-                  if (curListOffset.yOffset > scrollVelocity + 5) {
-                    this.listScroller.scrollTo({ xOffset: 0, yOffset: curListOffset.yOffset -= scrollVelocity })
-                  }
-                }
 
               })
               .onActionEnd((event: GestureEvent) => {
@@ -6848,19 +6817,6 @@ export struct LocalMusic {
       }
 
 
-      if (this.currentWidthBreakpoint !== WidthBreakpoint.WIDTH_SM ||
-        (this.currentHeightBreakpoint !== HeightBreakpoint.HEIGHT_MD &&
-          this.currentHeightBreakpoint !== HeightBreakpoint.HEIGHT_SM)) {
-        if (this.isScrollHide) {
-          this.setBarHeightHide2(offset)
-
-        } else {
-          this.setBarHeightNormal()
-        }
-
-      } else {
-        this.setBarHeightHide(offset)
-      }
 
       return { offsetRemain: offset };
     })
@@ -10304,6 +10260,8 @@ export struct LocalMusic {
         Image(this.cover)
           .height(58)
           .width(58)
+          .clip(true)
+          .sourceSize({width:38, height:38})
           .alt($r('app.media.alt'))
           .borderRadius(8)
           .clickEffect({ level: ClickEffectLevel.HEAVY })
@@ -10323,14 +10281,14 @@ export struct LocalMusic {
             .fontSize(16)
             .maxLines(1)
             .fontWeight(FontWeight.Bolder)
-            .fontColor(this.currentLyricColor)
+            .fontColor(Color.White)
             .margin({ left: this.currentLyricAlignMode === 0 ? 24 : 0 })
           Row() {
             Text(this.currentSong?.artist !== undefined ? this.currentSong?.artist : '')
               .fontSize(13)
-              .fontWeight(FontWeight.Bold)
+              .fontWeight(FontWeight.Bolder)
               .padding({ top: 8 })
-              .fontColor(this.currentLyricColor)
+              .fontColor(Color.White)
               .margin({ left: this.currentLyricAlignMode === 0 ? 24 : 0 })
             Blank()
 

+ 251 - 64
entry/src/main/ets/view/NavidromePage.ets

@@ -13,7 +13,7 @@ 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 { navidromeRestApi, NavidromeRestAlbum, NavidromeRestArtist, NavidromeRestSong, NavidromeRestPlaylist } from '../common/network/NavidromeRestApi';
 import { Utility } from '../common/util/Utility';
 import { Constants } from '../Constants';
 import { EventConstants } from '../common/constants/EventConstants';
@@ -42,7 +42,8 @@ interface PlaylistEventData {
 enum NavFilterType {
   None = 0,
   Artist = 1,
-  Album = 2
+  Album = 2,
+  Playlist = 3
 }
 
 interface CachePreviewInfo {
@@ -77,20 +78,27 @@ export struct NavidromePage {
   @State allVideos: VideoItem[] = [];
   @State artists: NavidromeRestArtist[] = [];
   @State albums: NavidromeRestAlbum[] = [];
+  @State playlists: NavidromeRestPlaylist[] = [];
   @State loading: boolean = false;
   @State songNextStart: number | null = 0;
   @State artistNextStart: number | null = 0;
   @State albumNextStart: number | null = 0;
+  @State playlistNextStart: number | null = 0;
   @State isSongPageLoading: boolean = false;
   @State isArtistPageLoading: boolean = false;
   @State isAlbumPageLoading: boolean = false;
+  @State isPlaylistPageLoading: boolean = false;
+
+  // 新增:详情视图状态
+  @State isDetailView: boolean = false; // 是否在艺术家/专辑详情视图
+  @State previousTab: number = 0; // 进入详情视图前的标签页索引
   @StorageProp('isDarkMode') isDarkMode: boolean = false;
   @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined;
   @StorageProp('topSafeHeight') topSafeHeight: number = 0;
   @State @Watch('onTabSelectedIndexesChanged') tabSelectedIndexes: number[] = [0];
   @StorageProp('themeColor') themeColor: string =
     PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR);
-  private tabs: string[] = ['全部', '艺术家', '专辑'];
+  private tabs: string[] = ['全部', '艺术家', '专辑', '歌单'];
   private loadTicket: number = 0;
   @State filterType: NavFilterType = NavFilterType.None;
   @State filterLabel: string = '';
@@ -118,7 +126,8 @@ export struct NavidromePage {
     const buttons = [
       { text: '全部' },
       { text: '艺术家' },
-      { text: '专辑' }
+      { text: '专辑' },
+      { text: '歌单' }
     ] as SegmentButtonItemTuple;
     return SegmentButtonOptions.capsule({
       buttons,
@@ -171,7 +180,13 @@ export struct NavidromePage {
   onTabSelectedIndexesChanged() {
     this.selectedTab = this.tabSelectedIndexes[0];
     console.info('heanup', `Selected tab: ${this.tabs[this.selectedTab]}`);
-    
+
+    // 如果在详情视图模式下切换标签页,退出详情视图并清除筛选
+    if (this.isDetailView) {
+      this.isDetailView = false;
+      this.clearFilter();
+    }
+
     // 从艺术家或专辑切换回全部时,清除筛选状态
     if (this.selectedTab === 0 && this.filterType !== NavFilterType.None) {
       // 不清除筛选,保持筛选状态
@@ -252,15 +267,18 @@ export struct NavidromePage {
     this.allVideos = [];
     this.artists = [];
     this.albums = [];
+    this.playlists = [];
     this.clearFilter();
     this.coverUrlCache.clear();
     this.albumCoverLookup.clear();
     this.songNextStart = 0;
     this.artistNextStart = 0;
     this.albumNextStart = 0;
+    this.playlistNextStart = 0;
     this.isSongPageLoading = false;
     this.isArtistPageLoading = false;
     this.isAlbumPageLoading = false;
+    this.isPlaylistPageLoading = false;
   }
 
   private async loadMediaLibrary(account: WebDavAccount): Promise<void> {
@@ -290,12 +308,13 @@ export struct NavidromePage {
       await Promise.all([
         this.loadNextSongPage(account, ticket),
         this.loadNextArtistPage(account, ticket),
-        this.loadNextAlbumPage(account, ticket)
+        this.loadNextAlbumPage(account, ticket),
+        this.loadNextPlaylistPage(account, ticket)
       ]);
       if (ticket !== this.loadTicket) {
         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);
     } catch (error) {
       if (ticket === this.loadTicket) {
@@ -424,6 +443,20 @@ export struct NavidromePage {
         return artist;
       });
       this.artists = [...this.artists, ...processed];
+      console.info('onecold  帮我打印这个artists的前20条数据');
+      // 打印前20条艺术家数据
+      const previewCount = Math.min(20, this.artists.length);
+      console.info(`onecold artists总数: ${this.artists.length}, 打印前${previewCount}条:`);
+      for (let i = 0; i < previewCount; i++) {
+        const artist = this.artists[i];
+        console.info(`onecold artist[${i}]:`, JSON.stringify({
+          id: artist.id,
+          name: artist.name,
+          albumCount: artist.albumCount,
+          songCount: artist.songCount,
+          coverUrl: artist.coverUrl
+        }));
+      }
       this.artistNextStart = response.nextStart;
       void ServerLogUtil.info('ArtistCover', `艺术家列表追加: 本次 ${processed.length} 位, 总数 ${this.artists.length}`);
     } finally {
@@ -608,6 +641,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> {
     if (this.loading) {
       return;
@@ -630,6 +687,9 @@ export struct NavidromePage {
         case 2:
           await this.loadNextAlbumPage(account);
           break;
+        case 3:
+          await this.loadNextPlaylistPage(account);
+          break;
         default:
           break;
       }
@@ -1160,35 +1220,63 @@ export struct NavidromePage {
       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)
+          // 详情视图模式下显示返回按钮
+          if (this.isDetailView) {
+            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.isDetailView = false;
+              this.clearFilter();
             })
+            .attributeModifier(new ShadowModifier())
+            .zIndex(0)
+
+            Text(this.filterLabel)
+              .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 })
+              .animation({ duration: 300, curve: Curve.Ease })
+          } else {
+            // 正常模式:侧边栏按钮和账号名
+            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'))
@@ -1493,6 +1581,8 @@ export struct NavidromePage {
         return this.artists.length;
       case 2:
         return this.albums.length;
+      case 3:
+        return this.playlists.length;
       default:
         return this.getVisibleSongs().length;
     }
@@ -1504,6 +1594,8 @@ export struct NavidromePage {
         return '暂无艺术家';
       case 2:
         return '暂无专辑';
+      case 3:
+        return '暂无歌单';
       default:
         if (this.isSearchMode && this.searchText.length > 0) {
           return this.isSearchLoading ? '正在搜索远程歌曲' : '没有匹配的远程歌曲';
@@ -1521,6 +1613,8 @@ export struct NavidromePage {
         return '当前筛选没有找到艺术家';
       case 2:
         return '当前筛选没有找到专辑';
+      case 3:
+        return '当前筛选没有找到歌单';
       default:
         if (this.isSearchMode && this.searchText.length > 0) {
           return this.isSearchLoading ? '请稍候,正在通过 API 搜索' : '换个关键词再试试吧';
@@ -1549,6 +1643,10 @@ export struct NavidromePage {
           .fillColor(song.pixelMapPath ? undefined : this.themeColor)
           .objectFit(ImageFit.Cover)
           .margin({ left: 20 })
+          .animation({
+            duration: 500,
+            curve: Curve.Friction  // 可选动画曲线
+          })
           .shadow({
             radius: StrUtil.isEmpty(song.pixelMapPath) ?6:14,
             type: ShadowType.BLUR,
@@ -1620,9 +1718,10 @@ export struct NavidromePage {
           .height(48)
           .borderRadius(10)
           .draggable(false)
-          .interpolation(ImageInterpolation.High)// 用于重采样后的抗锯齿
-          .autoResize(true) // 重采样,可减少内存占用
-          // .sourceSize({ width: 38, height: 38 })
+          .alt($r('app.media.alt'))
+          // .interpolation(ImageInterpolation.High)// 用于重采样后的抗锯齿
+          // .autoResize(true) // 重采样,可减少内存占用
+          .sourceSize({ width: 38, height: 38 })
           .fillColor(artist.coverUrl ? undefined : this.themeColor)
           .objectFit(ImageFit.Cover)
           .margin({ left: 20 })
@@ -1666,6 +1765,7 @@ export struct NavidromePage {
           .height(48)
           .borderRadius(10)
           .clip(true)
+          .alt($r('app.media.alt'))
           .draggable(false)
           // .interpolation(ImageInterpolation.High)// 用于重采样后的抗锯齿
           // .autoResize(true) // 重采样,可减少内存占用
@@ -1712,6 +1812,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 {
     const parts: string[] = [];
     if (song.duration) {
@@ -1747,6 +1900,20 @@ export struct NavidromePage {
     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[] {
     if (this.isSearchMode) {
       return this.searchText.length > 0 ? this.filteredList : this.allVideos;
@@ -1761,6 +1928,11 @@ export struct NavidromePage {
     if (!artist || !artist.id) {
       return;
     }
+    this.isShowTitleBar = true;
+    // 保存当前标签页,进入详情视图
+    this.previousTab = this.selectedTab;
+    this.isDetailView = true;
+    // 应用筛选但不切换标签页
     void this.applyFilter(NavFilterType.Artist, artist.id, artist.name ?? Constants.UNKNOWN_ARTIST);
   }
 
@@ -1768,14 +1940,32 @@ export struct NavidromePage {
     if (!album || !album.id) {
       return;
     }
+    this.isShowTitleBar = true;
+    // 保存当前标签页,进入详情视图
+    this.previousTab = this.selectedTab;
+    this.isDetailView = true;
+    // 应用筛选但不切换标签页
     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> {
 
     // 记录筛选操作
     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', `- 标签: ${label}`);
     void ServerLogUtil.info('NavidromeFilter', `- 筛选前总歌曲数: ${this.allVideos.length}`);
@@ -1794,19 +1984,15 @@ export struct NavidromePage {
     this.isFilterLoading = true;
 
     // 调试日志:检查筛选结果
-    void this.loadSongsForFilter().catch((error: Error) => {
+    void this.loadSongsForFilter(label).catch((error: Error) => {
       void ServerLogUtil.error('NavidromeFilter', `筛选歌曲加载失败: ${error.message}`);
       ToastUtil.showToast(error.message || '筛选数据加载失败');
     });
 
-    // 然后执行动画切换标签页
-    this.getUIContext().animateTo({ duration: 555 }, () => {
-      this.selectedTab = 0;
-      this.tabSelectedIndexes = [0];
-    })
+    // 不再自动切换标签页,保持在详情视图模式
   }
 
-  private async loadSongsForFilter(): Promise<void> {
+  private async loadSongsForFilter(label:string): Promise<void> {
     if (this.filterType === NavFilterType.None || !this.filterId) {
       this.filterSongs = [];
       this.isFilterLoading = false;
@@ -1847,9 +2033,11 @@ export struct NavidromePage {
     try {
       let songs: NavidromeRestSong[] = [];
       if (this.filterType === NavFilterType.Artist) {
-        songs = await navidromeRestApi.fetchSongsByArtist(account, this.filterId);
+        songs = await navidromeRestApi.fetchSongsByArtist(account, this.filterId,label);
       } else if (this.filterType === NavFilterType.Album) {
-        songs = await navidromeRestApi.fetchSongsByAlbum(account, this.filterId);
+        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);
       if (expectedFilterId !== this.filterId || expectedFilterType !== this.filterType) {
@@ -1872,6 +2060,8 @@ export struct NavidromePage {
     this.filterLabel = '';
     this.filterSongs = [];
     this.isFilterLoading = false;
+    // 退出详情视图
+    this.isDetailView = false;
   }
 
   private playSong(song: VideoItem, index: number, isJump: boolean = false): void {
@@ -1956,21 +2146,7 @@ export struct NavidromePage {
       .padding({top:this.topSafeHeight+98})
       .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(11)
-            .onClick(() => this.clearFilter())
-        }
-        .width('90%')
-        .padding({ left: 16, right: 16, top: this.topSafeHeight+118, bottom: 2 })
-      }
+
 
       if (this.isSearchMode && this.selectedTab === 0 && this.searchText.length > 0 && this.isSearchLoading) {
         Column() {
@@ -2000,7 +2176,14 @@ export struct NavidromePage {
         .layoutWeight(1)
       } else {
         List({scroller:this.scroller, space: 8 }) {
-          if (this.selectedTab === 0) {
+          // 详情视图模式下显示筛选后的歌曲,否则根据标签页显示对应内容
+          if (this.isDetailView) {
+            ForEach(this.getVisibleSongs(), (item: VideoItem, index: number) => {
+              ListItem() {
+                this.buildSongItem(item, index)
+              }
+            }, (item: VideoItem) => item.id)
+          } else if (this.selectedTab === 0) {
             ForEach(this.getVisibleSongs(), (item: VideoItem, index: number) => {
               ListItem() {
                 this.buildSongItem(item, index)
@@ -2012,12 +2195,18 @@ export struct NavidromePage {
                 this.buildArtistItem(artist)
               }
             }, (artist: NavidromeRestArtist) => artist.id)
-          } else {
+          } else if (this.selectedTab === 2) {
             ForEach(this.albums, (album: NavidromeRestAlbum) => {
               ListItem() {
                 this.buildAlbumItem(album)
               }
             }, (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) => {
@@ -2026,10 +2215,8 @@ export struct NavidromePage {
             const currentOffsetY = this.scroller.currentOffset().yOffset;
             // 判断滚动方向
             if (currentOffsetY > this.prevOffsetY) {
-              console.log("onecold 向上滚动");
               this.isShowTitleBar = false
             } else if (currentOffsetY < this.prevOffsetY) {
-              console.log("onecold 向下滚动");
               this.isShowTitleBar = true
             }
             // 更新前一次偏移量

+ 2 - 21
lib/src/main/ets/view/LyricView2.ets

@@ -170,16 +170,7 @@ export struct LyricView2 {
     }
 
     @State blurDegree: number = 3
-    private calculateBlurFactor(index: number, currentIndex: number): number {
-        const distance = Math.abs(index - currentIndex);
-        return Math.min(this.blurDegree, distance * 1.5);// Adjust the blur factor based on distance
-    }
 
-    private calculateOpacityFactor(index: number, currentIndex: number): number {
-        const distance = Math.abs(index - currentIndex);
-        let maxOp =  Math.max(0.3,1-this.blurDegree*0.2)
-        return Math.max(maxOp, 1 - distance * 0.08); // 透明度随着距离增加而减小
-    }
 
 
     // 优化建议代码示例:增加滚动节流
@@ -215,9 +206,7 @@ export struct LyricView2 {
                 .padding(8)
                 .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }),
                     TransitionEffect.scale({ x: 0, y: 0 })  ))
-                // .transition(TransitionEffect.asymmetric(TransitionEffect.move(TransitionEdge.BOTTOM).animation({ duration: 500,curve: Curve.Ease, delay: 100*index  }),
-                //     TransitionEffect.scale({ x: 0, y: 0 })  ))
-                .border({ radius: 4 })
+                .border({ radius: 12 })
                 .backgroundColor(this.seekUIStyle == 'listItem' && index == this.seekIndex && item.text.length > 0
                     && this.enableSeek && this.isUserTouching ? '#80a9a9a9' : '#00000000')
                 .onClick(() => {
@@ -235,8 +224,8 @@ export struct LyricView2 {
         .width('100%')
         .height('100%')
         .scrollBar(BarState.Off)
+        .fadingEdge(true,{fadingEdgeLength:LengthMetrics.percent(20)})
         .cachedCount(this.cacheSize)
-        .fadingEdge(true,{fadingEdgeLength:LengthMetrics.percent(40)})
         .transition(TransitionEffect.asymmetric(
              this.isSingleLine? TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }):
              TransitionEffect.move(TransitionEdge.BOTTOM).animation({ duration: 500 }),
@@ -286,8 +275,6 @@ export struct LyricView2 {
         Column(){
             Text(item.text)
                 .fontSize(this.textSize)
-                .opacity(this.calculateOpacityFactor(index, this.currentIndex))
-                .blur(this.calculateBlurFactor(index, this.currentIndex))
                 .textAlign(this.alignMode == 'center' ? TextAlign.Center : TextAlign.Start)
                 .scale({
                     x: index == this.currentIndex ? this.controller.getHighlightScale() : 1,
@@ -311,8 +298,6 @@ export struct LyricView2 {
                             index == this.currentIndex ? this.textHighlightColor : this.textColor : this.textColor)
                         .fontWeight(index == this.currentIndex && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
                         .margin({ top: 4 })
-                        .opacity(this.calculateOpacityFactor(index, this.currentIndex))
-                        .blur(this.calculateBlurFactor(index, this.currentIndex))
                 }
                 .justifyContent(this.alignMode === 'center' ? FlexAlign.Center : FlexAlign.Start)
                 .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex ?'95%': '85%')
@@ -335,8 +320,6 @@ export struct LyricView2 {
                         .fontWeight(this.currentMediaPosition >= word.startTime && this.isHighlightBold ?
                             index == this.currentIndex? FontWeight.Bold : this.textWeight: this.textWeight)
                         .margin(isEnglish(word.word) ?{ right:4 }:{})
-                        .opacity(this.calculateOpacityFactor(index, this.currentIndex))
-                        .blur(this.calculateBlurFactor(index, this.currentIndex))
                         .visibility(this.isSingleLine?
                             (index == this.currentIndex ?Visibility.Visible:Visibility.None)
                             :Visibility.Visible)
@@ -362,8 +345,6 @@ export struct LyricView2 {
                             index == this.currentIndex ? this.textHighlightColor : this.textColor : this.textColor)
                         .fontWeight(index == this.currentIndex && this.isHighlightBold ? FontWeight.Bold : this.textWeight)
                         .margin({ top: 4 })
-                        .opacity(this.calculateOpacityFactor(index, this.currentIndex))
-                        .blur(this.calculateBlurFactor(index, this.currentIndex))
                 }
                 .justifyContent(this.alignMode === 'center' ? FlexAlign.Center : FlexAlign.Start)
                 .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex ?'95%': '85%')