Просмотр исходного кода

feat(navidrome): 实现分页加载和缓存预览功能

- 添加分页响应接口和页面大小常量
- 实现歌曲、艺术家、专辑的分页获取方法
- 替换原有的全量数据获取逻辑
- 添加分页状态管理和加载控制
- 实现滚动到底部时自动加载下一页
- 更新缓存路径预览逻辑,使用 RemoteDriveManager
- 移除旧的搜索和排序相关状态
- 优化数据加载和转换流程
- 添加详细的日志记录和错误处理
chendeben 8 месяцев назад
Родитель
Сommit
77214b21df

+ 28 - 22
entry/src/main/ets/common/network/NavidromeRestApi.ets

@@ -82,37 +82,43 @@ export interface NavidromeRestAlbum {
   coverUrl?: string;
 }
 
+export interface NavidromePagedResponse<T> {
+  data: T[];
+  nextStart: number | null;
+}
+
 export class NavidromeRestApi {
   private authCache: Map<string, NavidromeAuthContext> = new Map();
+  private readonly PAGE_SIZE: number = 500;
 
-  async fetchAllSongs(account: WebDavAccount): Promise<NavidromeRestSong[]> {
-    const params: Array<QueryParam> = [
-      new QueryParam('_start', '0'),
-      new QueryParam('_end', '0'),
-      new QueryParam('_sort', 'createdAt'),
-      new QueryParam('_order', 'DESC')
-    ];
-    return this.get<NavidromeRestSong[]>(account, '/api/song', params);
+  async fetchSongPage(account: WebDavAccount, start: number): Promise<NavidromePagedResponse<NavidromeRestSong>> {
+    return this.fetchPage<NavidromeRestSong>(account, '/api/song', start, 'createdAt', 'DESC');
   }
 
-  async fetchArtists(account: WebDavAccount): Promise<NavidromeRestArtist[]> {
-    const params: Array<QueryParam> = [
-      new QueryParam('_start', '0'),
-      new QueryParam('_end', '0'),
-      new QueryParam('_sort', 'name'),
-      new QueryParam('_order', 'ASC')
-    ];
-    return this.get<NavidromeRestArtist[]>(account, '/api/artist', params);
+  async fetchArtistPage(account: WebDavAccount, start: number): Promise<NavidromePagedResponse<NavidromeRestArtist>> {
+    return this.fetchPage<NavidromeRestArtist>(account, '/api/artist', start, 'name', 'ASC');
+  }
+
+  async fetchAlbumPage(account: WebDavAccount, start: number): Promise<NavidromePagedResponse<NavidromeRestAlbum>> {
+    return this.fetchPage<NavidromeRestAlbum>(account, '/api/album', start, 'name', 'ASC');
   }
 
-  async fetchAlbums(account: WebDavAccount): Promise<NavidromeRestAlbum[]> {
+  private async fetchPage<T extends object>(account: WebDavAccount, path: string,
+    start: number, sortField: string, order: 'ASC' | 'DESC'): Promise<NavidromePagedResponse<T>> {
+    const end = start + this.PAGE_SIZE;
     const params: Array<QueryParam> = [
-      new QueryParam('_start', '0'),
-      new QueryParam('_end', '0'),
-      new QueryParam('_sort', 'name'),
-      new QueryParam('_order', 'ASC')
+      new QueryParam('_start', `${start}`),
+      new QueryParam('_end', `${end}`),
+      new QueryParam('_sort', sortField),
+      new QueryParam('_order', order)
     ];
-    return this.get<NavidromeRestAlbum[]>(account, '/api/album', params);
+    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;
+    return {
+      data: chunk ?? [],
+      nextStart
+    };
   }
 
   private async get<T>(account: WebDavAccount, path: string, params?: Array<QueryParam>, retry: boolean = true): Promise<T> {

+ 207 - 89
entry/src/main/ets/view/NavidromePage.ets

@@ -21,7 +21,7 @@ import { emitter } from '@kit.BasicServicesKit';
 import { setNavidromePlaylist } from '../common/util/NavidromePlaylistStore';
 import { navidromeApi } from '../common/network/NavidromeApi';
 import { ServerLogUtil } from '../common/util/ServerLogUtil';
-import { RemoteCacheType, resolveCacheFilePath } from '../common/network/RemoteSongCache';
+import { RemoteDriveManager } from '../common/util/RemoteDriveManager';
 const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist';
 
 interface PlaylistEventData {
@@ -39,6 +39,11 @@ enum NavFilterType {
   Album = 2
 }
 
+interface CachePreviewInfo {
+  cacheRoot: string;
+  examplePath: string;
+}
+
 @Component
 export struct NavidromePage {
   @Link mType: number;
@@ -52,6 +57,12 @@ export struct NavidromePage {
   @State artists: NavidromeRestArtist[] = [];
   @State albums: NavidromeRestAlbum[] = [];
   @State loading: boolean = false;
+  @State songNextStart: number | null = 0;
+  @State artistNextStart: number | null = 0;
+  @State albumNextStart: number | null = 0;
+  @State isSongPageLoading: boolean = false;
+  @State isArtistPageLoading: boolean = false;
+  @State isAlbumPageLoading: boolean = false;
   @StorageProp('isDarkMode') isDarkMode: boolean = false;
   @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined;
   @StorageProp('topRectHeight') topRectHeight: number = 0;
@@ -64,6 +75,49 @@ export struct NavidromePage {
   @State filterLabel: string = '';
   @State filterId: string = '';
   private coverUrlCache: Map<string, string> = new Map();
+  private albumCoverLookup: Map<string, string> = new Map();
+
+  private createTabOptions(): SegmentButtonOptions {
+    const buttons = [
+      { text: '全部' },
+      { text: '艺术家' },
+      { text: '专辑' }
+    ] as SegmentButtonItemTuple;
+    return SegmentButtonOptions.capsule({
+      buttons,
+      direction: Direction.Ltr,
+      buttonPadding: { top: 10, bottom: 10 },
+      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)
+      }
+    });
+  }
+  private getCachePreviewInfo(account: WebDavAccount): CachePreviewInfo | null {
+    try {
+      const manager = RemoteDriveManager.getInstance();
+      const context = manager.context;
+      if (!context) {
+        return null;
+      }
+      const baseDir = context.filesDir ?? context.cacheDir;
+      if (!baseDir) {
+        return null;
+      }
+      const accountId = account.id?.toString() || 'default';
+      const cacheRoot = `${baseDir}/remote_cache/webdav/${accountId}`;
+      const examplePath = `${cacheRoot}/navidrome_preview.cache`;
+      return { cacheRoot, examplePath };
+    } catch (error) {
+      Logger.error('NavidromePage', `获取缓存预览路径失败: ${(error as Error).message}`);
+      return null;
+    }
+  }
 
   // 搜索和排序相关状态
   @State isSearchMode: boolean = false;
@@ -72,20 +126,7 @@ export struct NavidromePage {
   @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: 10, bottom: 10 },
-    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)
-    }
-  });
+  @State tabOptions: SegmentButtonOptions = this.createTabOptions();
 
   //当胶囊按钮的选择发生变化时调用此函数
   onTabSelectedIndexesChanged() {
@@ -154,90 +195,169 @@ export struct NavidromePage {
     this.albums = [];
     this.clearFilter();
     this.coverUrlCache.clear();
+    this.albumCoverLookup.clear();
+    this.songNextStart = 0;
+    this.artistNextStart = 0;
+    this.albumNextStart = 0;
+    this.isSongPageLoading = false;
+    this.isArtistPageLoading = false;
+    this.isAlbumPageLoading = false;
   }
 
   private async loadNavidromeLibrary(account: WebDavAccount): Promise<void> {
     const ticket = ++this.loadTicket;
     this.loading = true;
-    this.coverUrlCache.clear();
-
-    // 记录开始加载的信息
+    this.resetData();
     void ServerLogUtil.info('NavidromeLoad', '开始加载 Navidrome 数据库');
     void ServerLogUtil.info('NavidromeLoad', `服务器: ${ServerLogUtil.sanitizeAccount(account)}`);
-    void ServerLogUtil.info('NavidromeLoad', `封面URL缓存已清,当前缓存数量: ${this.coverUrlCache.size}`);
+    void ServerLogUtil.info('NavidromeLoad', `封面URL缓存已清,当前缓存数量: ${this.coverUrlCache.size}`);
 
     try {
-      const requestTasks: Promise<object>[] = [
-        navidromeRestApi.fetchAllSongs(account),
-        navidromeRestApi.fetchArtists(account),
-        navidromeRestApi.fetchAlbums(account)
-      ];
-
-      void ServerLogUtil.debug('NavidromeLoad', '并发请求歌曲、艺术家、专辑数据');
-      const responses = await Promise.all(requestTasks);
-      const songs = responses[0] as NavidromeRestSong[];
-      const artistList = responses[1] as NavidromeRestArtist[];
-      const albumList = responses[2] as NavidromeRestAlbum[];
-
+      await this.logAccountCacheInfo(account);
+      await Promise.all([
+        this.loadNextSongPage(account, ticket),
+        this.loadNextArtistPage(account, ticket),
+        this.loadNextAlbumPage(account, ticket)
+      ]);
       if (ticket !== this.loadTicket) {
         return;
       }
+      void ServerLogUtil.info('NavidromeLoad', `首次加载完成: 歌曲 ${this.allVideos.length} / 艺术家 ${this.artists.length} / 专辑 ${this.albums.length}`);
+      await this.logCacheStatistics(account);
+    } catch (error) {
+      if (ticket === this.loadTicket) {
+        void ServerLogUtil.error('NavidromeLoad', `数据加载失败: ${(error as Error).message}`);
+        void ServerLogUtil.error('NavidromeLoad', `失败时服务器信息: ${ServerLogUtil.sanitizeAccount(account)}`);
+        ToastUtil.showToast((error as Error).message ?? 'Navidrome 数据加载失败');
+      }
+    } finally {
+      if (ticket === this.loadTicket) {
+        this.loading = false;
+        void ServerLogUtil.info('NavidromeLoad', '加载流程结束');
+      }
+    }
+  }
 
-      // 记录原始数据加载结果
-      void ServerLogUtil.info('NavidromeLoad', `原始数据加载完成: 歌曲 ${songs.length} 首, 艺术家 ${artistList.length} 位, 专辑 ${albumList.length} 张`);
-
-      // 处理专辑封面
-      void ServerLogUtil.debug('NavidromeLoad', '开始构建专辑封面映射');
-      const albumCoverMap = await this.buildAlbumCoverMap(albumList, account);
-      if (ticket !== this.loadTicket) {
+  private async loadNextSongPage(account: WebDavAccount, ticket?: number): Promise<void> {
+    if (this.songNextStart === null || this.isSongPageLoading) {
+      return;
+    }
+    const currentTicket = ticket ?? this.loadTicket;
+    this.isSongPageLoading = true;
+    try {
+      const response = await navidromeRestApi.fetchSongPage(account, this.songNextStart);
+      if (currentTicket !== this.loadTicket) {
         return;
       }
-      void ServerLogUtil.info('NavidromeLoad', `专辑封面映射完成: ${albumCoverMap.size}/${albumList.length} 张专辑有封面`);
-
-      // 处理艺术家封面
-      void ServerLogUtil.debug('NavidromeLoad', '开始构建艺术家封面映射');
-      const artistCoverMap = await this.buildArtistCoverMap(artistList, account);
-      if (ticket !== this.loadTicket) {
+      const chunk = response.data ?? [];
+      if (chunk.length === 0) {
+        this.songNextStart = null;
         return;
       }
-      void ServerLogUtil.info('NavidromeLoad', `艺术家封面映射完成: ${artistCoverMap.size}/${artistList.length} 位艺术家有封面`);
-
-      // 转换歌曲数据
-      void ServerLogUtil.debug('NavidromeLoad', '开始转换歌曲为VideoItem对象');
-      const videos = await this.convertSongsToVideoItems(songs, account, albumCoverMap);
-      if (ticket !== this.loadTicket) {
+      const videos = await this.convertSongsToVideoItems(chunk, account);
+      if (currentTicket !== this.loadTicket) {
         return;
       }
+      this.allVideos = [...this.allVideos, ...videos];
+      if (this.isSearchMode && this.searchText.length > 0) {
+        this.onSearchInput(this.searchText);
+      }
+      this.songNextStart = response.nextStart;
+      void ServerLogUtil.info('NavidromeLoad', `歌曲列表追加: 本次 ${videos.length} 首, 总数 ${this.allVideos.length}`);
+    } finally {
+      this.isSongPageLoading = false;
+    }
+  }
 
-      this.allVideos = videos;
-      this.artists = artistList.map(artist => {
-        artist.coverUrl = artistCoverMap.get(artist.id);
+  private async loadNextArtistPage(account: WebDavAccount, ticket?: number): Promise<void> {
+    if (this.artistNextStart === null || this.isArtistPageLoading) {
+      return;
+    }
+    const currentTicket = ticket ?? this.loadTicket;
+    this.isArtistPageLoading = true;
+    try {
+      const response = await navidromeRestApi.fetchArtistPage(account, this.artistNextStart);
+      if (currentTicket !== this.loadTicket) {
+        return;
+      }
+      const chunk = response.data ?? [];
+      if (chunk.length === 0) {
+        this.artistNextStart = null;
+        return;
+      }
+      const coverMap = await this.buildArtistCoverMap(chunk, account);
+      if (currentTicket !== this.loadTicket) {
+        return;
+      }
+      const processed = chunk.map(artist => {
+        artist.coverUrl = coverMap.get(artist.id);
         return artist;
       });
-      this.albums = albumList.map(album => {
-        album.coverUrl = albumCoverMap.get(album.id);
-        return album;
-      });
-
-      // 记录最终加载结果
-      void ServerLogUtil.info('NavidromeLoad', `数据转换完成: 歌曲 ${songs.length} -> VideoItem ${videos.length}`);
-      void ServerLogUtil.info('NavidromePage', `加载完成: 歌曲 ${songs.length} / 艺术家 ${artistList.length} / 专辑 ${albumList.length}`);
-      void ServerLogUtil.debug('NavidromePage', `歌曲ID列表: ${songs.slice(0, 50).map(s => s.id).join(',')}${songs.length > 50 ? '...' : ''}`);
-
-      // 记录缓存统计信息
-      await this.logCacheStatistics(account);
+      this.artists = [...this.artists, ...processed];
+      this.artistNextStart = response.nextStart;
+      void ServerLogUtil.info('ArtistCover', `艺术家列表追加: 本次 ${processed.length} 位, 总数 ${this.artists.length}`);
+    } finally {
+      this.isArtistPageLoading = false;
+    }
+  }
 
-    } catch (error) {
-      if (ticket === this.loadTicket) {
-        void ServerLogUtil.error('NavidromeLoad', `数据加载失败: ${(error as Error).message}`);
-        void ServerLogUtil.error('NavidromeLoad', `失败时服务器信息: ${ServerLogUtil.sanitizeAccount(account)}`);
-        ToastUtil.showToast((error as Error).message ?? 'Navidrome 数据加载失败');
+  private async loadNextAlbumPage(account: WebDavAccount, ticket?: number): Promise<void> {
+    if (this.albumNextStart === null || this.isAlbumPageLoading) {
+      return;
+    }
+    const currentTicket = ticket ?? this.loadTicket;
+    this.isAlbumPageLoading = true;
+    try {
+      const response = await navidromeRestApi.fetchAlbumPage(account, this.albumNextStart);
+      if (currentTicket !== this.loadTicket) {
+        return;
       }
-    } finally {
-      if (ticket === this.loadTicket) {
-        this.loading = false;
-        void ServerLogUtil.info('NavidromeLoad', '加载流程结束');
+      const chunk = response.data ?? [];
+      if (chunk.length === 0) {
+        this.albumNextStart = null;
+        return;
+      }
+      const coverMap = await this.buildAlbumCoverMap(chunk, account);
+      if (currentTicket !== this.loadTicket) {
+        return;
       }
+      coverMap.forEach((value, key) => {
+        if (value) {
+          this.albumCoverLookup.set(key, value);
+        }
+      });
+      const processed = chunk.map(album => {
+        album.coverUrl = coverMap.get(album.id);
+        return album;
+      });
+      this.albums = [...this.albums, ...processed];
+      this.albumNextStart = response.nextStart;
+      void ServerLogUtil.info('AlbumCover', `专辑列表追加: 本次 ${processed.length} 张, 总数 ${this.albums.length}`);
+    } finally {
+      this.isAlbumPageLoading = false;
+    }
+  }
+
+  private async handleReachEnd(): Promise<void> {
+    if (this.loading) {
+      return;
+    }
+    const account = this.selectedAccount;
+    if (!account) {
+      return;
+    }
+    switch (this.selectedTab) {
+      case 0:
+        await this.loadNextSongPage(account);
+        break;
+      case 1:
+        await this.loadNextArtistPage(account);
+        break;
+      case 2:
+        await this.loadNextAlbumPage(account);
+        break;
+      default:
+        break;
     }
   }
 
@@ -333,8 +453,7 @@ export struct NavidromePage {
     return map;
   }
 
-  private async convertSongsToVideoItems(songs: NavidromeRestSong[], account: WebDavAccount,
-    albumCoverMap: Map<string, string>): Promise<VideoItem[]> {
+  private async convertSongsToVideoItems(songs: NavidromeRestSong[], account: WebDavAccount): Promise<VideoItem[]> {
     const tasks: Promise<VideoItem>[] = [];
     let successCount = 0;
     let failedCount = 0;
@@ -347,7 +466,7 @@ export struct NavidromePage {
         try {
           let coverUrl: string | undefined;
           try {
-            coverUrl = await this.resolveSongCover(song, account, albumCoverMap);
+            coverUrl = await this.resolveSongCover(song, account);
           } catch (error) {
             void ServerLogUtil.warn('SongConvert', `歌曲 ${song.title} 封面解析失败: ${(error as Error).message}`);
           }
@@ -374,10 +493,9 @@ export struct NavidromePage {
     return results;
   }
 
-  private async resolveSongCover(song: NavidromeRestSong, account: WebDavAccount,
-    albumCoverMap: Map<string, string>): Promise<string | undefined> {
+  private async resolveSongCover(song: NavidromeRestSong, account: WebDavAccount): Promise<string | undefined> {
     if (song.albumId) {
-      const albumCover = albumCoverMap.get(song.albumId);
+      const albumCover = song.albumId ? this.albumCoverLookup.get(song.albumId) : undefined;
       if (albumCover) {
         return albumCover;
       }
@@ -421,16 +539,13 @@ export struct NavidromePage {
       void ServerLogUtil.info('AccountInfo', `账号信息: ${ServerLogUtil.sanitizeAccount(account)}`);
 
       // 记录缓存路径信息
-      const accountId = account.id?.toString() || 'unknown';
-
-      // 尝试获取Navidrome缓存路径信息
-      try {
-        const cachePathInfo = await resolveCacheFilePath(RemoteCacheType.WEBDAV, accountId, 'navidrome_test_path');
+      const cachePreview = this.getCachePreviewInfo(account);
+      if (cachePreview) {
         void ServerLogUtil.info('AccountInfo', `Navidrome缓存目录结构:`);
-        void ServerLogUtil.info('AccountInfo', `- 缓存根目录: ${cachePathInfo.cacheDir}`);
-        void ServerLogUtil.info('AccountInfo', `- 示例缓存路径: ${cachePathInfo.cachePath}`);
-      } catch (error) {
-        void ServerLogUtil.warn('AccountInfo', `缓存路径解析失败: ${(error as Error).message}`);
+        void ServerLogUtil.info('AccountInfo', `- 缓存根目录: ${cachePreview.cacheRoot}`);
+        void ServerLogUtil.info('AccountInfo', `- 示例缓存路径: ${cachePreview.examplePath}`);
+      } else {
+        void ServerLogUtil.warn('AccountInfo', '无法预览缓存目录:上下文或根目录不可用');
       }
 
       // 记录封面缓存状态
@@ -1269,6 +1384,9 @@ export struct NavidromePage {
       .listDirection(Axis.Vertical)
       .scrollBar(BarState.Auto)
       .edgeEffect(EdgeEffect.Spring)
+      .onReachEnd(() => {
+        void this.handleReachEnd();
+      })
 
       // 空状态
       if (this.getCurrentCount() === 0) {