Parcourir la source

feat(remote-drive):优化路径管理和面包屑导航功能

chendeben il y a 9 mois
Parent
commit
59993d7ada

+ 106 - 42
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -23,6 +23,11 @@ import MediaTable from './MediaTable';
 
 const TAG = 'heanup RemoteDriveManager';
 
+export interface BreadcrumbItem {
+  label: string;
+  path: string;
+}
+
 // WebDAV认证信息接口
 export interface WebDavAuthInfo {
   headers: Record<string, string>;
@@ -1173,11 +1178,51 @@ export class RemoteDriveManager {
   }
 
   private registerPathLabel(path: string, label: string): void {
-    if (!path || !label) {
+    if (!path || label === undefined) {
       return;
     }
     const normalized = this.normalizeFullPath(path);
-    this.pathDisplayNames.set(normalized, label);
+    const sanitizedLabel = this.sanitizeLabel(label);
+    this.pathDisplayNames.set(normalized, sanitizedLabel);
+  }
+
+  private sanitizeLabel(label: string): string {
+    if (!label) {
+      return '';
+    }
+    if (label === '根目录') {
+      return label;
+    }
+    let sanitized = label.trim();
+    sanitized = sanitized.replace(/\/+$/, '');
+    if (sanitized.length === 0) {
+      sanitized = '/';
+    }
+    try {
+      sanitized = decodeURIComponent(sanitized);
+    } catch (error) {
+      // ignore decode failure, keep original label
+    }
+    return sanitized;
+  }
+
+  private getBasePath(account: WebDavAccount | null = this.currentAccount): string {
+    if (account && account.filepath) {
+      return this.normalizeFullPath(account.filepath);
+    }
+    return '/';
+  }
+
+  private getRelativePath(fullPath: string, basePath: string): string {
+    const normalizedFull = this.normalizeFullPath(fullPath);
+    const normalizedBase = this.normalizeFullPath(basePath);
+    if (normalizedFull === normalizedBase) {
+      return '';
+    }
+    if (normalizedFull.startsWith(normalizedBase) && normalizedBase !== '/') {
+      return normalizedFull.substring(normalizedBase.length).replace(/^\/+/, '');
+    }
+    return normalizedFull.replace(/^\/+/, '');
   }
 
   private normalizeSmbRelativePath(path: string, shareName?: string): string {
@@ -1258,46 +1303,51 @@ export class RemoteDriveManager {
   }
 
   public async enterFolderFromPath(path: string): Promise<void> {
+    const normalizedTarget = this.normalizeFullPath(path);
 
     Logger.info(TAG, '当前路径:', this.currentPath);
-    Logger.info(TAG, '目标路径:', path);
-    // 保存当前路径到历史记录
-    this.pathHistory.push(this.currentPath);
+    Logger.info(TAG, '目标路径:', normalizedTarget);
+
+    this.rebuildPathHistoryForTarget(normalizedTarget);
     Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
-    // 加载文件夹内容
-    await this.loadFilesInfoFromAccount(this.currentAccount, path);
+
+    await this.loadFilesInfoFromAccount(this.currentAccount, normalizedTarget);
   }
 
-  // 在 RemoteDriveManager 类中添加以下方法
-  navigateToBreadcrumb(breadcrumbIndex: number): Promise<string> {
-    return new Promise((resolve, reject) => {
-      try {
-        // 面包屑索引0是"根目录"
-        if (breadcrumbIndex === 0) {
-          resolve('/');
-          return;
-        }
+  private rebuildPathHistoryForTarget(targetPath: string): void {
+    if (!targetPath) {
+      this.pathHistory = [];
+      return;
+    }
 
-        // 根据当前路径构建目标路径
-        const pathParts = this.currentPath.split('/').filter(part => part !== '');
+    const normalizedTarget = this.normalizeFullPath(targetPath);
+    const basePath = this.getBasePath();
 
-        // 验证索引有效性
-        if (breadcrumbIndex > pathParts.length) {
-          reject(new Error('Invalid breadcrumb index'));
-          return;
-        }
+    if (normalizedTarget === basePath) {
+      this.pathHistory = [];
+      return;
+    }
 
-        // 构建目标路径
-        let targetPath = '/';
-        for (let i = 0; i < breadcrumbIndex; i++) {
-          targetPath += pathParts[i] + '/';
-        }
+    const relativePath = this.getRelativePath(normalizedTarget, basePath);
+    if (!relativePath) {
+      this.pathHistory = [];
+      return;
+    }
 
-        resolve(targetPath);
-      } catch (error) {
-        reject(error);
-      }
-    });
+    const parts = relativePath.split('/').filter(part => part.length > 0);
+    const rebuiltHistory: string[] = [];
+
+    // 把账户根路径作为栈底,以便可以返回
+    rebuiltHistory.push(basePath);
+
+    let cumulative = basePath;
+    for (let i = 0; i < parts.length - 1; i++) {
+      const segment = parts[i];
+      cumulative = this.normalizeFullPath(`${cumulative === '/' ? '' : cumulative}/${segment}`);
+      rebuiltHistory.push(cumulative);
+    }
+
+    this.pathHistory = rebuiltHistory;
   }
 
 
@@ -1316,19 +1366,33 @@ export class RemoteDriveManager {
   }
 
   // 获取面包屑路径数组
-  public getBreadcrumbs(): string[] {
-    if (!this.currentPath || this.currentPath === '/') {
-      return ['根目录'];
+  public getBreadcrumbs(): BreadcrumbItem[] {
+    const basePath = this.getBasePath();
+    const breadcrumbs: BreadcrumbItem[] = [{
+      label: '根目录',
+      path: basePath
+    }];
+
+    if (!this.currentPath || this.normalizeFullPath(this.currentPath) === basePath) {
+      return breadcrumbs;
     }
 
-    const parts = this.currentPath.split('/').filter(part => part !== '');
-    const breadcrumbs = ['根目录'];
+    const relativePath = this.getRelativePath(this.currentPath, basePath);
+    if (!relativePath) {
+      return breadcrumbs;
+    }
 
-    let cumulative = '';
+    const parts = relativePath.split('/').filter(part => part.length > 0);
+    let cumulative = basePath;
     for (let i = 0; i < parts.length; i++) {
-      cumulative += `/${parts[i]}`;
-      const label = this.pathDisplayNames.get(cumulative) ?? parts[i];
-      breadcrumbs.push(label);
+      const segment = parts[i];
+      const targetPath = this.normalizeFullPath(`${cumulative === '/' ? '' : cumulative}/${segment}`);
+      const label = this.pathDisplayNames.get(targetPath) ?? segment;
+      breadcrumbs.push({
+        label: this.sanitizeLabel(label),
+        path: targetPath
+      });
+      cumulative = targetPath;
     }
 
     return breadcrumbs;

+ 34 - 32
entry/src/main/ets/dialog/RemoteDriveAccountDialog.ets

@@ -52,10 +52,10 @@ export struct RemoteDriveAccountDialog {
   @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
     ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
 
+  @State portInput: string = '';
   private nameCustomized: boolean = false;
   private portCustomized: boolean = false;
   private navBasePathCustomized: boolean = false;
-  private suppressPortChange: boolean = false;
 
   onColorModeChange() {
     this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
@@ -88,7 +88,7 @@ export struct RemoteDriveAccountDialog {
       }
     } else {
       // this.applyTypeDefaults(this.driveType);
-      this.handleDriveTypeChange(this.driveType);
+      this.handleDriveTypeChange(this.driveType, true);
     }
   }
 
@@ -223,22 +223,24 @@ export struct RemoteDriveAccountDialog {
         Text('端口')
           .fontSize(14)
           .fontColor($r('app.color.index_tab_font_color'));
-        TextInput({ placeholder: this.getPortPlaceholder(), text: this.port.toString() })
+        TextInput({ placeholder: this.getPortPlaceholder(), text: this.portInput })
           .layoutWeight(1)
           .maxLines(1)
           .type(InputType.Number)
           .onChange((value: string) => {
-            if (this.suppressPortChange) {
-              this.suppressPortChange = false;
+            const trimmed = value.trim();
+            this.portInput = trimmed;
+            const defaultPort = this.getDefaultPort();
+            if (trimmed.length === 0) {
+              this.portCustomized = false;
+              this.port = defaultPort;
               return;
             }
-            const parsed = parseInt(value);
-            const defaultPort = this.driveType === RemoteDriveType.Smb ? 445 : (this.enableHttps ? 443 : 5005);
+            const parsed = parseInt(trimmed);
             if (Number.isNaN(parsed) || parsed === -1) {
-              this.updatePortState(defaultPort, false);
-            } else {
-              this.updatePortState(parsed, true);
+              return;
             }
+            this.updatePortState(parsed, true, false);
           });
       }
       .alignItems(VerticalAlign.Center);
@@ -348,7 +350,7 @@ export struct RemoteDriveAccountDialog {
             .onChange((isOn: boolean) => {
               this.enableHttps = isOn;
               if (!this.portCustomized && this.driveType === RemoteDriveType.WebDav) {
-                this.updatePortState(isOn ? 443 : 5005, false);
+                this.updatePortState(this.getDefaultPort(), false);
               }
             });
         }
@@ -437,15 +439,17 @@ export struct RemoteDriveAccountDialog {
       });
   }
 
-  private handleDriveTypeChange(type: RemoteDriveType) {
-    if (this.driveType === type) {
-      return;
+  private handleDriveTypeChange(type: RemoteDriveType, forceApply: boolean = false) {
+    const changed = this.driveType !== type;
+    if (changed) {
+      this.driveType = type;
+      if (type === RemoteDriveType.Smb) {
+        this.enableHttps = false;
+      }
     }
-    this.driveType = type;
-    if (type === RemoteDriveType.Smb) {
-      this.enableHttps = false;
+    if (changed || forceApply) {
+      this.applyTypeDefaults(type, true);
     }
-    this.applyTypeDefaults(type, true);
   }
 
   private applyTypeDefaults(type: RemoteDriveType, forcePort: boolean = false) {
@@ -462,15 +466,7 @@ export struct RemoteDriveAccountDialog {
     if (!force && this.portCustomized) {
       return;
     }
-    if (type === RemoteDriveType.Smb) {
-      this.updatePortState(445, false);
-      return;
-    }
-    if (type === RemoteDriveType.Navidrome) {
-      this.updatePortState(this.enableHttps ? 443 : 4533, false);
-      return;
-    }
-    this.updatePortState(this.enableHttps ? 443 : 5005, false);
+    this.updatePortState(this.getDefaultPort(), false);
   }
 
   private getDefaultAccountName(type: RemoteDriveType): string {
@@ -500,19 +496,25 @@ export struct RemoteDriveAccountDialog {
   }
 
   private getPortPlaceholder(): string {
+    return `默认: ${this.getDefaultPort()}`;
+  }
+
+  private getDefaultPort(): number {
     if (this.driveType === RemoteDriveType.Smb) {
-      return '默认: 445';
+      return 445;
     }
     if (this.driveType === RemoteDriveType.Navidrome) {
-      return this.enableHttps ? '默认: 443' : '默认: 4533';
+      return this.enableHttps ? 443 : 4533;
     }
-    return this.enableHttps ? '默认: 443' : '默认: 5005';
+    return this.enableHttps ? 443 : 5005;
   }
 
-  private updatePortState(value: number, customized: boolean) {
-    this.suppressPortChange = true;
+  private updatePortState(value: number, customized: boolean, syncInput: boolean = true) {
     this.port = value;
     this.portCustomized = customized;
+    if (syncInput) {
+      this.portInput = value > 0 ? value.toString() : '';
+    }
   }
 
   @Builder

+ 12 - 17
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -1,4 +1,4 @@
-import { RemoteDriveManager } from '../common/util/RemoteDriveManager';
+import { BreadcrumbItem, RemoteDriveManager } from '../common/util/RemoteDriveManager';
 import { WebDavAccount } from '../viewmodel/WebDavAccount';
 import { Song } from '../viewmodel/Song';
 import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates';
@@ -86,7 +86,7 @@ export struct WebDavMainPage {
   @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
   @StorageProp('isDarkMode') isDarkMode: boolean = false;
   @State topRectHeight: number = 0; // 顶部安全区高度
-  @State breadcrumbs:string[] = []//面包屑导航
+  @State breadcrumbs:BreadcrumbItem[] = []//面包屑导航
 
   @State visibleFoldersState: FileInfo[] = []; // 改为普通状态变量
   @State isShowFileName: boolean = false//是否显示文件名
@@ -234,6 +234,7 @@ export struct WebDavMainPage {
 
         // 更新可见文件夹列表
         this.updateVisibleFolders();
+        this.breadcrumbs = this.webdavManager.getBreadcrumbs();
 
         // promptAction.showToast({
         //   message: '加载成功: ' + this.webDavFiles.filter(f => f.isDirectory).length + '个文件夹, ' + this.songs.length + '首歌曲'
@@ -516,21 +517,15 @@ export struct WebDavMainPage {
 
 
   // 导航到指定层级的面包屑路径
-  private navigateToBreadcrumb(breadcrumbIndex: number): void {
+  private async navigateToBreadcrumb(crumb: BreadcrumbItem): Promise<void> {
+    if (!crumb) {
+      return;
+    }
     try {
       this.isLoading = true;
-      this.webdavManager.navigateToBreadcrumb(breadcrumbIndex).then((path: string) => {
-        this.webdavManager.enterFolderFromPath(path)
-        this.breadcrumbs = this.webdavManager.getBreadcrumbs();
-        console.info('onecold this.breadcrumbs = ' + JSON.stringify(this.breadcrumbs))
-        console.info('onecold this.webdavManager.currentPath = ' + this.webdavManager.currentPath)
-      })
-        .catch((error: Error) => {
-          Logger.error(TAG, '导航到面包屑路径失败: ' + error.message);
-          this.isLoading = false;
-        });
+      await this.webdavManager.enterFolderFromPath(crumb.path);
     } catch (error) {
-      Logger.error(TAG, '导航到面包屑路径异常: ' + (error as Error).message);
+      Logger.error(TAG, '导航到面包屑路径失败: ' + (error as Error).message);
       this.isLoading = false;
     }
   }
@@ -857,16 +852,16 @@ export struct WebDavMainPage {
           .onClick(() => this.goBack())
 
           Row({ space: 4 }) {
-            ForEach(this.breadcrumbs, (crumb: string, index: number) => {
+            ForEach(this.breadcrumbs, (crumb: BreadcrumbItem, index: number) => {
               Row() {
-                Text(crumb)
+                Text(crumb.label)
                   .fontSize(15)
                   .fontColor(this.themeColor)
                   .maxLines(1)
                   .textOverflow({ overflow: TextOverflow.Ellipsis })
               }
               .onClick(() => {
-                this.navigateToBreadcrumb(index);
+                this.navigateToBreadcrumb(crumb);
               })
 
               // 添加分隔符(除了最后一个元素)