소스 검색

WebDavMainPage的搜素功能可以搜索 全部目录的文件和文件夹

onecold 5 달 전
부모
커밋
e9978b6c4d

+ 4 - 0
entry/src/main/ets/common/enums/RemoteDriveManagerStates.ets

@@ -20,6 +20,10 @@ export enum RemoteDriveManagerStates{
   SetWebSongs = "SetWebSongs",
   LoadWebDavAccountSongs = "LoadWebDavAccountSongs",
   RenameWebDavSong = "RenameWebDavSong",
+  GlobalSearchIndexBuildStart = "GlobalSearchIndexBuildStart",
+  GlobalSearchIndexUpdated = "GlobalSearchIndexUpdated",
+  GlobalSearchIndexReady = "GlobalSearchIndexReady",
+  GlobalSearchIndexFailed = "GlobalSearchIndexFailed",
 
   // 下载队列
   DownloadQueueChanged = "DownloadQueueChanged",

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 883 - 87
entry/src/main/ets/common/util/RemoteDriveManager.ets


+ 189 - 58
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -1,4 +1,4 @@
-import { BreadcrumbItem, RemoteDriveManager } from '../common/util/RemoteDriveManager';
+import { BreadcrumbItem, RemoteDriveGlobalSearchResult, RemoteDriveManager } from '../common/util/RemoteDriveManager';
 import { WebDavAccount } from '../viewmodel/WebDavAccount';
 import { Song } from '../viewmodel/Song';
 import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates';
@@ -111,6 +111,7 @@ export struct WebDavMainPage {
   private listScroller: ListScroller = new ListScroller()
   @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0;
   searchController: SearchController = new SearchController()
+  private searchTicket: number = 0;
   @StorageProp('animationState') animationState: AnimationStatus= AnimationStatus.Running
   @State isNoJumpToHome: boolean = false //网盘播放不跳转首页
   @State isSearchMode: boolean = false
@@ -145,6 +146,7 @@ export struct WebDavMainPage {
   @State rootPath: string = ''
   @State isShowDownloadCenter: boolean = false
   @State downloadCenterTabIndex: number[] = [0]
+  @State isSearchLoading: boolean = false
   private readonly downloadFolderName: string = '下载'
   private readonly metadataUpdateEvent: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED };
   private thumbnailTaskToken: number = 0;
@@ -165,6 +167,12 @@ export struct WebDavMainPage {
     console.log('heanup 切换账户:', this.selectedAccount.name);
     this.thumbnailTaskToken += 1;
     this.thumbnailRunningKeys.clear();
+    this.searchTicket++;
+    this.searchText = '';
+    this.filteredList = [];
+    this.filteredFolderList = [];
+    this.isSearchMode = false;
+    this.isSearchLoading = false;
     this.songs = [];
     this.visibleFoldersState = [];
     this.updateListData(this.songs)
@@ -187,7 +195,12 @@ export struct WebDavMainPage {
     if (!noSort) {
       this.sortType = PreferencesUtil.getNumberSync('webDavSortType', 0)
       this.doSortType(this.sortType)
+      return
     }
+    this.refreshDisplaySongs(mList)
+  }
+
+  private refreshDisplaySongs(mList: Array<VideoItem>): void {
     this.dataSource.pushArrayData(mList)
     if(mList.length > 0){
       setTimeout(() => {
@@ -1523,6 +1536,11 @@ export struct WebDavMainPage {
         this.breadcrumbs = this.webdavManager.getBreadcrumbs();
         this.syncSelectionAfterRefresh();
         this.scheduleRemoteThumbPrefetch();
+        if (this.isSearchMode && this.searchText.length > 0) {
+          void this.applyGlobalSearch(this.searchText, ++this.searchTicket);
+        } else {
+          this.restoreCurrentDirectorySearchView();
+        }
 
         // promptAction.showToast({
         //   message: '加载成功: ' + this.webDavFiles.filter(f => f.isDirectory).length + '个文件夹, ' + this.songs.length + '首歌曲'
@@ -1537,6 +1555,16 @@ export struct WebDavMainPage {
       case RemoteDriveManagerStates.RemoveAccountSucceed:
         this.loadAccounts();
         break;
+      case RemoteDriveManagerStates.GlobalSearchIndexBuildStart:
+      case RemoteDriveManagerStates.GlobalSearchIndexUpdated:
+      case RemoteDriveManagerStates.GlobalSearchIndexReady:
+        if (this.isSearchMode && this.searchText.length > 0) {
+          void this.applyGlobalSearch(this.searchText, ++this.searchTicket);
+        }
+        break;
+      case RemoteDriveManagerStates.GlobalSearchIndexFailed:
+        this.isSearchLoading = false;
+        break;
     }
   }
 
@@ -2092,53 +2120,78 @@ export struct WebDavMainPage {
     }.attributeModifier(new MenuModifier())
   }
 
-
-  doSortType(index: number) {
-    this.sortType = index;
-    // 对歌曲列表进行排序
+  private sortSongsForType(target: Array<VideoItem>, index: number): Array<VideoItem> {
     switch (index) {
       case 0:
-        Utility.doSortListAscending(this.songs,this.isShowFileName)
-        this.visibleFoldersState.sort((a, b) => {
-          return a.fileName.localeCompare(b.fileName);
-        });
+        Utility.doSortListAscending(target, this.isShowFileName)
         break;
       case 1:
-        Utility.doSortListDescending(this.songs,this.isShowFileName)
-
-        this.visibleFoldersState.sort((a, b) => {
-          return b.fileName.localeCompare(a.fileName);
-        });
+        Utility.doSortListDescending(target, this.isShowFileName)
         break;
       case 2:
-        this.songs.sort((a, b) => {
+        target.sort((a, b) => {
           return a.cTime.localeCompare(b.cTime);
         });
-        this.visibleFoldersState.sort((a, b) => {
-          return a.time-b.time;
-        });
         break;
       case 3:
-        this.songs.sort((a, b) => {
+        target.sort((a, b) => {
           return b.cTime.localeCompare(a.cTime);
         });
-        this.visibleFoldersState.sort((a, b) => {
-          return b.time-a.time;
-        });
         break;
       case 4:
-        this.songs.sort((a, b) => {
+        target.sort((a, b) => {
           return a.videoSize - b.videoSize;
         });
         break;
       case 5:
-        this.songs.sort((a, b) => {
+        target.sort((a, b) => {
           return b.videoSize - a.videoSize;
         });
         break;
     }
+    return target;
+  }
 
-    
+  private sortFoldersForType(target: Array<FileInfo>, index: number): Array<FileInfo> {
+    switch (index) {
+      case 0:
+        target.sort((a, b) => {
+          return a.fileName.localeCompare(b.fileName);
+        });
+        break;
+      case 1:
+        target.sort((a, b) => {
+          return b.fileName.localeCompare(a.fileName);
+        });
+        break;
+      case 2:
+        target.sort((a, b) => {
+          return a.time - b.time;
+        });
+        break;
+      case 3:
+        target.sort((a, b) => {
+          return b.time - a.time;
+        });
+        break;
+    }
+    return target;
+  }
+
+  doSortType(index: number) {
+    this.sortType = index;
+    this.sortSongsForType(this.songs, index)
+    this.sortFoldersForType(this.visibleFoldersState, index)
+    if (this.filteredList.length > 0) {
+      this.filteredList = this.sortSongsForType([...this.filteredList], index)
+    }
+    if (this.filteredFolderList.length > 0) {
+      this.filteredFolderList = this.sortFoldersForType([...this.filteredFolderList], index)
+    }
+    if (this.isSearchMode && this.searchText.length > 0) {
+      this.refreshDisplaySongs(this.filteredList)
+      return
+    }
     this.updateListData(this.songs,true)
   }
 
@@ -2191,6 +2244,7 @@ export struct WebDavMainPage {
           .onClick(() => {
             if(this.isSearchMode){
               this.isSearchMode = false
+              this.searchController.stopEditing()
               this.onSearchInput('')
             }else{
               if(this.webdavManager.canGoBack()){
@@ -2216,7 +2270,7 @@ export struct WebDavMainPage {
         }
 
         //搜索框
-        Search({ controller: this.searchController,value: this.searchText, placeholder: '搜索标题、艺术家...' })
+        Search({ controller: this.searchController,value: this.searchText, placeholder: '输入名称...' })
           .searchButton('搜索',{fontColor:this.themeColor})
           .searchIcon({
             src: $r('sys.media.ohos_ic_public_search_filled')
@@ -2237,7 +2291,7 @@ export struct WebDavMainPage {
           .onSubmit((value: string) => {
             console.log('onecold onSubmit ='+value)
             this.searchController.stopEditing()
-            this.onSearchInput(this.searchText);
+            this.onSearchInput(value);
 
           })
           .onChange((value: string) => {
@@ -2260,8 +2314,9 @@ export struct WebDavMainPage {
           .attributeModifier(new ShadowModifier())
           .zIndex(0)
           .onClick(()=>{
-            this.filteredFolderList = [...this.visibleFoldersState]; // 显示所有文件夹
             this.isSearchMode = true
+            this.restoreCurrentDirectorySearchView()
+            void this.webdavManager.ensureGlobalSearchIndex(this.selectedAccount)
           })
           //排序按钮
           Button({ type: ButtonType.Circle, stateEffect: true }) {
@@ -2359,36 +2414,75 @@ export struct WebDavMainPage {
   @State filteredList: Array<VideoItem> = []; // 过滤后的歌曲结果
   @State filteredFolderList: Array<FileInfo> = []; // 过滤后的文件夹结果
 
-  // 实时搜索逻辑(带防抖)
-// 实时搜索逻辑(带防抖)
-  private onSearchInput(value: string) {
-    this.searchText = value.trim();
-    let mSearchList: Array<VideoItem> = []
-    mSearchList = this.songs
-
-    // 新增条件判断:空输入时显示所有数据
-    if (this.searchText === '') {
-      this.filteredList = [...mSearchList]; // 创建新数组以触发状态更新
-      this.filteredFolderList = [...this.visibleFoldersState]; // 显示所有文件夹
-    } 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()  ?? "")
-      });
-      
-      // 对文件夹进行过滤
-      this.filteredFolderList = this.visibleFoldersState.filter((folder: FileInfo) => {
-        const regex = new RegExp(this.searchText.toLowerCase().replace(/\s+/g, '.*'), 'i');
-        return regex.test(folder.fileName?.toLowerCase() ?? "") ||
-               regex.test(decodeUrlEncodedString(folder.fileName?.replace('/', '') ?? "").toLowerCase());
-      });
+  private restoreCurrentDirectorySearchView(): void {
+    this.filteredList = this.sortSongsForType([...this.songs], this.sortType)
+    this.filteredFolderList = this.sortFoldersForType([...this.visibleFoldersState], this.sortType)
+    this.isSearchLoading = false
+    this.refreshDisplaySongs(this.filteredList)
+  }
+
+  private shouldShowSearchLocation(): boolean {
+    return this.isSearchMode && this.searchText.length > 0
+  }
+
+  private getSongSearchLocation(song: VideoItem): string {
+    if (!this.shouldShowSearchLocation() || !this.selectedAccount) {
+      return ''
     }
+    return this.webdavManager.getSongSearchLocationLabel(this.selectedAccount, song)
+  }
+
+  private getFolderSearchLocation(folder: FileInfo): string {
+    if (!this.shouldShowSearchLocation() || !this.selectedAccount) {
+      return ''
+    }
+    return this.webdavManager.getFolderSearchLocationLabel(this.selectedAccount, folder)
+  }
 
-    this.updateListData(this.filteredList);
+  private async applyGlobalSearch(keyword: string, ticket: number): Promise<void> {
+    if (!this.selectedAccount) {
+      if (ticket === this.searchTicket) {
+        this.isSearchLoading = false
+        this.filteredList = []
+        this.filteredFolderList = []
+        this.refreshDisplaySongs([])
+      }
+      return
+    }
+    try {
+      const result: RemoteDriveGlobalSearchResult = await this.webdavManager.searchGlobalIndex(this.selectedAccount, keyword);
+      if (ticket !== this.searchTicket) {
+        return
+      }
+      this.filteredList = this.sortSongsForType([...result.songs], this.sortType)
+      this.filteredFolderList = this.sortFoldersForType([...result.folders], this.sortType)
+      this.isSearchLoading = result.isIndexing
+      this.refreshDisplaySongs(this.filteredList)
+    } catch (error) {
+      if (ticket !== this.searchTicket) {
+        return
+      }
+      this.isSearchLoading = false
+      this.filteredList = []
+      this.filteredFolderList = []
+      this.refreshDisplaySongs([])
+      ToastUtil.showToast(`搜索失败: ${(error as Error).message}`)
+    }
+  }
+
+  private onSearchInput(value: string) {
+    const keyword = value.trim();
+    this.searchText = keyword;
+    const ticket = ++this.searchTicket;
+    if (keyword.length === 0) {
+      this.restoreCurrentDirectorySearchView();
+      return;
+    }
+    if (!this.isSearchMode) {
+      this.isSearchMode = true;
+    }
+    this.isSearchLoading = true;
+    void this.applyGlobalSearch(keyword, ticket);
   }
 
   private openSongPropertySheet(song: VideoItem): void {
@@ -2691,6 +2785,20 @@ export struct WebDavMainPage {
         curve: Curve.Smooth // 可选动画曲线
       })
 
+      if (this.isSearchMode && this.searchText.length > 0 && this.isSearchLoading) {
+        Row({ space: 8 }) {
+          LoadingProgress()
+            .width(18)
+            .height(18)
+            .color(this.themeColor)
+          Text('正在构建全局索引,结果会持续补全')
+            .fontSize(12)
+            .fontColor($r('app.color.index_tab_font_color'))
+            .opacity(0.65)
+        }
+        .padding({ left: 20, right: 20, top: this.topSafeHeight + 74, bottom: 6 })
+      }
+
 
       // 文件列表(文件夹 + 歌曲)
       if (this.webDavFiles.length > 0 || this.songs.length > 0) {
@@ -2734,11 +2842,13 @@ export struct WebDavMainPage {
         .margin({ top: 4 })
       } else if (!this.isLoading) {
         Column() {
-          Text('暂无内容')
+          Text(this.isSearchMode && this.searchText.length > 0 ? '没有找到匹配结果' : '暂无内容')
             .fontSize(14)
             .fontColor($r('app.color.index_tab_font_color'))
             .opacity(0.6)
-          Text('点击"左侧菜单"网盘加载')
+          Text(this.isSearchMode && this.searchText.length > 0 ?
+            (this.isSearchLoading ? '正在继续扫描更多目录...' : '可以换个关键词再试') :
+            '点击"左侧菜单"网盘加载')
             .fontSize(12)
             .fontColor($r('app.color.index_tab_font_color'))
             .opacity(0.4)
@@ -2851,6 +2961,14 @@ export struct WebDavMainPage {
             .maxLines(1)
             .textOverflow({ overflow: TextOverflow.Ellipsis })
 
+          Text(this.getFolderSearchLocation(folder))
+            .fontSize(12)
+            .fontColor($r('app.color.index_tab_font_color'))
+            .opacity(0.48)
+            .maxLines(1)
+            .textOverflow({ overflow: TextOverflow.Ellipsis })
+            .visibility(this.getFolderSearchLocation(folder).length > 0 ? Visibility.Visible : Visibility.None)
+
           Text('文件夹')
             .fontSize(13)
             .fontColor($r('app.color.index_tab_font_color'))
@@ -2867,6 +2985,11 @@ export struct WebDavMainPage {
     // .backgroundColor($r('app.color.start_window_background'))
     .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
     .onClick(() => {
+      if (this.isSearchMode && this.searchText.length > 0) {
+        this.isSearchMode = false
+        this.searchController.stopEditing()
+        this.onSearchInput('')
+      }
       this.enterFolder(folder);
       this.breadcrumbs = this.webdavManager.getBreadcrumbs();
     })
@@ -2927,6 +3050,14 @@ export struct WebDavMainPage {
               .textOverflow({ overflow: TextOverflow.Ellipsis })
           }
           .width('90%')
+
+          Text(this.getSongSearchLocation(song))
+            .fontSize(12)
+            .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:$r('app.color.index_tab_font_color'))
+            .opacity(0.42)
+            .maxLines(1)
+            .textOverflow({ overflow: TextOverflow.Ellipsis })
+            .visibility(this.getSongSearchLocation(song).length > 0 ? Visibility.Visible : Visibility.None)
         }
         .alignItems(HorizontalAlign.Start)
         .layoutWeight(1)

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.