Sfoglia il codice sorgente

Merge feature/webdav into master: resolve conflicts

chendeben 9 mesi fa
parent
commit
69533c1d4e
30 ha cambiato i file con 4607 aggiunte e 117 eliminazioni
  1. 32 0
      entry/src/main/ets/Constants.ets
  2. 2 1
      entry/src/main/ets/common/constants/CommonConstants.ets
  3. 5 0
      entry/src/main/ets/common/enums/SongType.ets
  4. 37 0
      entry/src/main/ets/common/enums/WebdavManagerStates.ets
  5. 62 0
      entry/src/main/ets/common/util/BackgroundManager.ets
  6. 170 0
      entry/src/main/ets/common/util/DataBaseUtil.ets
  7. 141 0
      entry/src/main/ets/common/util/FileManager.ets
  8. 38 3
      entry/src/main/ets/common/util/ImagePickerUtil.ets
  9. 7 2
      entry/src/main/ets/common/util/MediaTable.ets
  10. 75 0
      entry/src/main/ets/common/util/PermissionUtil.ets
  11. 718 0
      entry/src/main/ets/common/util/RcpSocketUtil.ets
  12. 3 1
      entry/src/main/ets/common/util/RdbUtils.ets
  13. 52 0
      entry/src/main/ets/common/util/ReqPermissionUtil.ets
  14. 6 2
      entry/src/main/ets/common/util/Utility.ets
  15. 1160 0
      entry/src/main/ets/common/util/WebdavManager.ets
  16. 48 32
      entry/src/main/ets/dialog/PlaylistDialog.ets
  17. 343 0
      entry/src/main/ets/dialog/WebDavAccountDialog.ets
  18. 44 0
      entry/src/main/ets/entryability/EntryAbility.ets
  19. 413 19
      entry/src/main/ets/pages/NewIndex.ets
  20. 41 7
      entry/src/main/ets/pages/SettingPage.ets
  21. 726 0
      entry/src/main/ets/pages/WebDavMainPage.ets
  22. 318 50
      entry/src/main/ets/view/LocalMusic.ets
  23. 27 0
      entry/src/main/ets/viewmodel/FileInfo.ets
  24. 93 0
      entry/src/main/ets/viewmodel/Song.ets
  25. 2 0
      entry/src/main/ets/viewmodel/VideoItem.ets
  26. 30 0
      entry/src/main/ets/viewmodel/WebDavAccount.ets
  27. 4 0
      entry/src/main/resources/base/element/string.json
  28. 1 0
      entry/src/main/resources/base/media/cloudDisk.svg
  29. 1 0
      entry/src/main/resources/base/media/folder.svg
  30. 8 0
      entry/src/main/resources/dark/element/color.json

+ 32 - 0
entry/src/main/ets/Constants.ets

@@ -1,2 +1,34 @@
 export const APP_ID = "wx263d1c80d204c3c0"
 export const APP_SECRET = "a29d0d08885334589698837e46d4573a"
+
+// WebDAV相关常量
+export class Constants {
+  // WebDAV Preferences名称
+  public static readonly WEBDAV_PREFERENCE_NAME: string = 'WebdavPreferences'
+
+  // 分块加载大小
+  public static readonly CHUNK_SIZE: number = 100
+
+  // 音频文件扩展名
+  public static readonly AUDIO_EXTENSIONS = ['.mp3','.mp3','.wma','.mp2','.mov','.flac',
+    '.midi','.ra','.aac','.ape','.cda','.alac','.m4a','.ogg','.opus','.wv','.aiff','.amr','.aif','.dff'
+    ,'.aif','.au','.eac3','.mlp','.tak','.thd','.tta','.wv','.ac3','.amr','.mka','.mpc','.ra',
+    '.asf','.m2ts','.m4b','.mts','.rm','.wtv','.dts','.av3a','.dsd','.dsf','.wav']
+
+  // 图片扩展名
+  public static readonly IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.webp', '.bmp']
+
+  // 歌词扩展名
+  public static readonly LYRIC_EXTENSIONS = ['.lrc']
+
+  // 默认值
+  public static readonly UNKNOWN_NAME: string = '未命名'
+  public static readonly UNKNOWN_TITLE: string = '未知标题'
+  public static readonly UNKNOWN_ARTIST: string = '未知艺术家'
+  public static readonly UNKNOWN_SAMPLE_RATE: string = '未知采样率'
+  public static readonly UNKNOWN_TRACK_COUNT: string = '未知轨道'
+  public static readonly UNKNOWN_MIME_TYPE: string = 'audio/mpeg'
+
+  // 默认封面
+  public static readonly COMMON_SONG_DEFAULT_IMAGE: ResourceStr = $r('app.media.music_red')
+}

+ 2 - 1
entry/src/main/ets/common/constants/CommonConstants.ets

@@ -91,7 +91,7 @@ export class CommonConstants {
   static readonly REAL_MUSIC_FORMAT = ['.mp3','.mp3','.wma','.mp2','.mov','.flac',
     '.midi','.ra','.aac','.ape','.cda','.lrc','.alac','.m4a','.ogg','.opus','.wv','.aiff','.amr','.aif','.dff'
     ,'.aif','.au','.eac3','.mlp','.tak','.thd','.tta','.wv','.ac3','.amr','.mka','.mpc','.ra',
-    '.asf','.m2ts','.m4b','.mts','.rm','.wtv','.dts','.av3a','.dsd']
+    '.asf','.m2ts','.m4b','.mts','.rm','.wtv','.dts','.av3a','.dsd','.dsf','.wav']
 
 
   // 倍数格式支持列表
@@ -199,6 +199,7 @@ export class CommonConstants {
    * Network video ID.
    */
   static readonly TYPE_INTERNET: number = 1;//网络视频
+  static readonly TYPE_WEBDAV: number = 3;//webdav文件
 
   static readonly TYPE_LOCK: number = 200;//私密视频
   static readonly TYPE_LIKE: number = 201;//我收藏的视频

+ 5 - 0
entry/src/main/ets/common/enums/SongType.ets

@@ -0,0 +1,5 @@
+// 歌曲类型枚举
+export enum SongType {
+  Local = 0,    // 本地歌曲
+  WebDav = 1    // WebDAV云端歌曲
+}

+ 37 - 0
entry/src/main/ets/common/enums/WebdavManagerStates.ets

@@ -0,0 +1,37 @@
+export enum WebdavManagerStates{
+  // 账户管理
+  QueryAccountsSucceed = "QueryAccountsSucceed",
+  QueryAccountsFailed = "QueryAccountsFailed",
+  InsertAccountSucceed = "InsertAccountSucceed",
+  InsertAccountFailed = "InsertAccountFailed",
+  EditAccountSucceed = "EditAccountSucceed",
+  EditAccountFailed = "EditAccountFailed",
+  RemoveAccountSucceed = "RemoveAccountSucceed",
+  RemoveAccountFailed = "RemoveAccountFailed",
+
+  // 文件操作
+  LoadFilesInfoStart = "LoadFilesInfoStart",
+  LoadFilesInfoSucceed = "LoadFilesInfoSucceed",
+  LoadFilesInfoFailed = "LoadFilesInfoFailed",
+  LoadFilesInfoError = "LoadFilesInfoError",
+  CalculateSongsSize = "CalculateSongsSize",
+  QueryWebDavAccounts = "QueryWebDavAccounts",
+  SetSortMode = "SetSortMode",
+  SetWebSongs = "SetWebSongs",
+  LoadWebDavAccountSongs = "LoadWebDavAccountSongs",
+  RenameWebDavSong = "RenameWebDavSong",
+
+  // 下载队列
+  DownloadQueueChanged = "DownloadQueueChanged",
+  SetCurrentDownloadTask = "SetCurrentDownloadTask",
+  ChangeDownloadQueue = "ChangeDownloadQueue",
+  ChangeFinishDownloadQueue = "ChangeFinishDownloadQueue",
+  SetIsPauseDownload = "SetIsPauseDownload",
+
+  // 上传队列
+  UploadQueueChanged = "UploadQueueChanged",
+  SetCurrentUploadTask = "SetCurrentUploadTask",
+  ChangeUploadQueue = "ChangeUploadQueue",
+  ChangeFinishUploadQueue = "ChangeFinishUploadQueue",
+  SetIsPauseUpload = "SetIsPauseUpload",
+}

+ 62 - 0
entry/src/main/ets/common/util/BackgroundManager.ets

@@ -0,0 +1,62 @@
+// BackgroundManager - 后台任务管理器(简化版,用于WebDAV集成)
+import Logger from './Logger';
+import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';
+
+const TAG = 'heanup BackgroundManager';
+
+export class BackgroundManager {
+  private static instance: BackgroundManager;
+  private bgId: number = 0;
+
+  public static getInstance(): BackgroundManager {
+    if (!BackgroundManager.instance) {
+      BackgroundManager.instance = new BackgroundManager();
+    }
+    return BackgroundManager.instance;
+  }
+
+  // 申请后台长时任务
+  public async startBackgroundTask(): Promise<void> {
+    try {
+      Logger.info(TAG, '申请后台长时任务');
+      // TODO: 实现真实的后台任务申请逻辑
+    } catch (error) {
+      Logger.error(TAG, `申请后台任务失败: ${error}`);
+    }
+  }
+
+  // 停止后台长时任务
+  public async stopBackgroundTask(): Promise<void> {
+    try {
+      if (this.bgId > 0) {
+        // stopBackgroundRunning需要context参数,暂时简化处理
+        Logger.info(TAG, '停止后台长时任务');
+        this.bgId = 0;
+      }
+    } catch (error) {
+      Logger.error(TAG, `停止后台任务失败: ${error}`);
+    }
+  }
+
+  // 更新数据传输连续任务
+  public async updateDataTransferContinuousTask(): Promise<void> {
+    try {
+      // 简化版:仅记录日志,不实际更新后台任务
+      // 实际应用中应该更新任务进度通知
+      Logger.info(TAG, '更新数据传输任务');
+    } catch (error) {
+      Logger.error(TAG, `更新数据传输任务失败: ${error}`);
+    }
+  }
+
+  // 取消请求
+  public async cancelRequest(): Promise<void> {
+    try {
+      Logger.info(TAG, '取消请求');
+      // 简化版:仅记录日志,不实际取消请求
+      // 实际应用中应该取消正在进行的网络请求
+    } catch (error) {
+      Logger.error(TAG, `取消请求失败: ${error}`);
+    }
+  }
+}

+ 170 - 0
entry/src/main/ets/common/util/DataBaseUtil.ets

@@ -0,0 +1,170 @@
+// DataBaseUtil - 数据库工具类
+import { relationalStore } from '@kit.ArkData';
+import { common } from '@kit.AbilityKit';
+import Logger from './Logger';
+
+const TAG = 'heanup DataBaseUtil';
+
+export class DataBaseUtil {
+  private static instance: DataBaseUtil;
+  private rdbStore: relationalStore.RdbStore | null = null;
+  private context: common.Context | undefined;
+
+  public static getInstance(): DataBaseUtil {
+    if (!DataBaseUtil.instance) {
+      DataBaseUtil.instance = new DataBaseUtil();
+    }
+    return DataBaseUtil.instance;
+  }
+
+  public setContext(context: common.Context): void {
+    this.context = context;
+  }
+
+  public async getDB(): Promise<relationalStore.RdbStore> {
+    if (this.rdbStore) {
+      return this.rdbStore;
+    }
+
+    if (!this.context) {
+      const err = new Error('Context未设置,请先调用setContext');
+      Logger.error(TAG, err.message);
+      throw err;
+    }
+
+    const config: relationalStore.StoreConfig = {
+      name: 'mediaDB.db',
+      securityLevel: relationalStore.SecurityLevel.S1
+    };
+
+    try {
+      this.rdbStore = await relationalStore.getRdbStore(this.context, config);
+      Logger.info(TAG, '数据库初始化成功');
+      return this.rdbStore;
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, `数据库初始化失败: ${error.message}`);
+      throw error;
+    }
+  }
+
+  // 执行SQL语句
+  public async executeSql(sql: string): Promise<void> {
+    try {
+      const db = await this.getDB();
+      await db.executeSql(sql);
+      Logger.info(TAG, `执行SQL成功: ${sql.substring(0, 50)}...`);
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, `执行SQL失败: ${error.message}`);
+      throw error;
+    }
+  }
+
+  // 插入数据
+  public async insert(tableName: string, values: relationalStore.ValuesBucket): Promise<number> {
+    try {
+      const db = await this.getDB();
+      const rowId = await db.insert(tableName, values);
+      Logger.info(TAG, `插入数据成功,行ID: ${rowId}`);
+      return rowId;
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, `插入数据失败: ${error.message}`);
+      throw error;
+    }
+  }
+
+  // 更新数据
+  public async update(tableName: string, values: relationalStore.ValuesBucket, predicates: relationalStore.RdbPredicates): Promise<number> {
+    try {
+      const db = await this.getDB();
+      const rows = await db.update(values, predicates);
+      Logger.info(TAG, `更新数据成功,影响行数: ${rows}`);
+      return rows;
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, `更新数据失败: ${error.message}`);
+      throw error;
+    }
+  }
+
+  // 删除数据
+  public async delete(predicates: relationalStore.RdbPredicates): Promise<number> {
+    try {
+      const db = await this.getDB();
+      const rows = await db.delete(predicates);
+      Logger.info(TAG, `删除数据成功,影响行数: ${rows}`);
+      return rows;
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, `删除数据失败: ${error.message}`);
+      throw error;
+    }
+  }
+
+  // 查询数据
+  public async query(predicates: relationalStore.RdbPredicates, columns?: Array<string>): Promise<relationalStore.ResultSet> {
+    try {
+      const db = await this.getDB();
+      const resultSet = await db.query(predicates, columns);
+      return resultSet;
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, `查询数据失败: ${error.message}`);
+      throw error;
+    }
+  }
+
+  // 查询数据(简化版,用于WebdavManager)
+  public async queryData(tableName: string, columns: Array<string>, predicates?: relationalStore.RdbPredicates): Promise<relationalStore.ResultSet> {
+    try {
+      const db = await this.getDB();
+      if (predicates) {
+        return await db.query(predicates, columns);
+      } else {
+        const pred = new relationalStore.RdbPredicates(tableName);
+        return await db.query(pred, columns);
+      }
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, `查询数据失败: ${error.message}`);
+      throw error;
+    }
+  }
+
+  // 插入数据(简化版,用于WebdavManager)
+  public async insertData(tableName: string, values: relationalStore.ValuesBucket): Promise<number> {
+    return await this.insert(tableName, values);
+  }
+
+  // 更新数据(简化版,用于WebdavManager)
+  public async updateData(tableName: string, values: relationalStore.ValuesBucket, predicates: relationalStore.RdbPredicates): Promise<number> {
+    return await this.update(tableName, values, predicates);
+  }
+
+  // 删除数据(简化版,用于WebdavManager)
+  public async deleteData(predicates: relationalStore.RdbPredicates): Promise<number> {
+    return await this.delete(predicates);
+  }
+
+  // 查询其他设备的表(简化版占位)
+  public async queryTableFromOtherDevice(tableName: string): Promise<relationalStore.ResultSet | null> {
+    Logger.info(TAG, `查询其他设备表: ${tableName}`);
+    // TODO: 实现跨设备查询逻辑
+    return null;
+  }
+
+  // 关闭数据库
+  public async closeDB(): Promise<void> {
+    if (this.rdbStore) {
+      try {
+        await relationalStore.deleteRdbStore(this.context!, 'mediaDB.db');
+        this.rdbStore = null;
+        Logger.info(TAG, '数据库关闭成功');
+      } catch (error) {
+        Logger.error(TAG, `数据库关闭失败: ${error}`);
+      }
+    }
+  }
+}

+ 141 - 0
entry/src/main/ets/common/util/FileManager.ets

@@ -0,0 +1,141 @@
+// FileManager - 文件管理工具类
+import { fileIo } from '@kit.CoreFileKit';
+import { util } from '@kit.ArkTS';
+import Logger from './Logger';
+
+const TAG = 'heanup FileManager';
+
+class FileManagerClass {
+  private static instance: FileManagerClass;
+
+  public static getInstance(): FileManagerClass {
+    if (!FileManagerClass.instance) {
+      FileManagerClass.instance = new FileManagerClass();
+    }
+    return FileManagerClass.instance;
+  }
+
+  // 创建目录
+  public async createDir(dirPath: string): Promise<void> {
+    try {
+      if (!await this.isExist(dirPath)) {
+        await fileIo.mkdir(dirPath);
+        Logger.info(TAG, `创建目录成功: ${dirPath}`);
+      }
+    } catch (error) {
+      Logger.error(TAG, `创建目录失败: ${error}`);
+    }
+  }
+
+  // 判断文件或目录是否存在
+  public async isExist(path: string): Promise<boolean> {
+    try {
+      await fileIo.stat(path);
+      return true;
+    } catch {
+      return false;
+    }
+  }
+
+  // 删除文件
+  public async deleteFile(filePath: string): Promise<void> {
+    try {
+      if (await this.isExist(filePath)) {
+        await fileIo.unlink(filePath);
+        Logger.info(TAG, `删除文件成功: ${filePath}`);
+      }
+    } catch (error) {
+      Logger.error(TAG, `删除文件失败: ${error}`);
+    }
+  }
+
+  // 获取文件大小
+  public async getFileSize(filePath: string): Promise<number> {
+    try {
+      const stat = await fileIo.stat(filePath);
+      return stat.size;
+    } catch (error) {
+      Logger.error(TAG, `获取文件大小失败: ${error}`);
+      return 0;
+    }
+  }
+
+  // 重命名文件
+  public async renameFile(oldPath: string, newPath: string): Promise<void> {
+    try {
+      await fileIo.rename(oldPath, newPath);
+      Logger.info(TAG, `重命名文件成功: ${oldPath} -> ${newPath}`);
+    } catch (error) {
+      Logger.error(TAG, `重命名文件失败: ${error}`);
+    }
+  }
+
+  // 复制文件
+  public async copyFile(srcPath: string, destPath: string): Promise<void> {
+    try {
+      await fileIo.copyFile(srcPath, destPath);
+      Logger.info(TAG, `复制文件成功: ${srcPath} -> ${destPath}`);
+    } catch (error) {
+      Logger.error(TAG, `复制文件失败: ${error}`);
+    }
+  }
+
+  // 读取文件内容为字符串
+  public async readFileToString(filePath: string): Promise<string> {
+    try {
+      if (!await this.isExist(filePath)) {
+        return '';
+      }
+      const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
+      const stat = await fileIo.stat(filePath);
+      const buffer = new ArrayBuffer(stat.size);
+      await fileIo.read(file.fd, buffer);
+      fileIo.closeSync(file);
+
+      const uint8Array = new Uint8Array(buffer);
+      const decoder = util.TextDecoder.create('utf-8');
+      return decoder.decodeToString(uint8Array);
+    } catch (error) {
+      Logger.error(TAG, `读取文件失败: ${error}`);
+      return '';
+    }
+  }
+
+  // 将字符串写入文件
+  public async writeStringToFilePath(content: string, filePath: string): Promise<void> {
+    try {
+      const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
+      const encoder = util.TextEncoder.create();
+      const uint8Array = encoder.encodeInto(content);
+      await fileIo.write(file.fd, uint8Array.buffer);
+      fileIo.closeSync(file);
+      Logger.info(TAG, `写入文件成功: ${filePath}`);
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, `写入文件失败: ${error.message}`);
+      throw error;
+    }
+  }
+}
+
+// 合并两个路径的工具函数
+export function merge2paths(path1: string, path2: string): string {
+  if (!path1 || !path2) {
+    return path1 || path2 || '';
+  }
+
+  // 移除path1末尾的斜杠
+  if (path1.endsWith('/')) {
+    path1 = path1.slice(0, -1);
+  }
+
+  // 移除path2开头的斜杠
+  if (path2.startsWith('/')) {
+    path2 = path2.slice(1);
+  }
+
+  return `${path1}/${path2}`;
+}
+
+const FileManager = FileManagerClass.getInstance();
+export default FileManager;

+ 38 - 3
entry/src/main/ets/common/util/ImagePickerUtil.ets

@@ -70,17 +70,18 @@ export class ImagePickerUtil {
    * 将图片复制到应用私有目录
    * @param context 应用上下文
    * @param imageUri 图片URI
+   * @param type 图片类型,默认为playlist,可选webdav
    * @returns 返回复制后的图片路径
    */
-  private static async copyImageToAppDir(context: Context, imageUri: string): Promise<string> {
+  private static async copyImageToAppDir(context: Context, imageUri: string, type: string = 'playlist'): Promise<string> {
     try {
       // 生成唯一文件名
       const timestamp = Date.now();
       const randomSuffix = Math.random().toString(36).substring(2, 8);
-      const fileName = `playlist_cover_${timestamp}_${randomSuffix}.jpg`;
+      const fileName = `${type}_cover_${timestamp}_${randomSuffix}.jpg`;
 
       // 目标路径
-      const targetDir = context.filesDir + FileUtil.separator + 'playlist_covers';
+      const targetDir = context.filesDir + FileUtil.separator + `${type}_covers`;
       const targetPath = targetDir + FileUtil.separator + fileName;
 
       // 确保目录存在
@@ -120,6 +121,40 @@ export class ImagePickerUtil {
     }
   }
 
+  /**
+   * 选择单张图片(WebDAV账户封面)
+   * @param context 应用上下文
+   * @returns 返回选择的图片路径,如果取消选择返回null
+   */
+  static async selectSingleWebDavCover(context: Context): Promise<string | null> {
+    try {
+      Logger.info(TAG, '开始选择WebDAV账户封面');
+
+      const photoSelectOptions: photoAccessHelper.PhotoSelectOptions = {
+        MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
+        maxSelectNumber: 1
+      };
+
+      const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
+      const photoSelectResult = await photoViewPicker.select(photoSelectOptions);
+
+      Logger.info(TAG, `选择了 ${photoSelectResult.photoUris.length} 张图片`);
+
+      if (photoSelectResult.photoUris.length === 0) {
+        return null;
+      }
+
+      // 将选中的图片复制到应用私有目录
+      const uri = photoSelectResult.photoUris[0];
+      const copiedPath = await ImagePickerUtil.copyImageToAppDir(context, uri, 'webdav');
+
+      return copiedPath;
+    } catch (error) {
+      Logger.error(TAG, `选择WebDAV账户封面失败: ${(error as Error).message}`);
+      return null;
+    }
+  }
+
   /**
    * 删除歌单封面图片
    * @param imagePath 图片路径

+ 7 - 2
entry/src/main/ets/common/util/MediaTable.ets

@@ -30,6 +30,7 @@ interface DBColumnsInterface {
   LAST_PLAYED_STR: string;
   PLAY_COUNT: string;
   LYRIC_CONTENT: string;
+  WEBDAV_ACCOUNT_ID: string;
 }
 
 /**
@@ -55,7 +56,8 @@ const DB_COLUMNS: DBColumnsInterface = {
   SAMPLE_RATE: 'sampleRate',
   LAST_PLAYED_STR: 'lastPlayedStr',
   PLAY_COUNT: 'playCount',
-  LYRIC_CONTENT: 'lyricContent'
+  LYRIC_CONTENT: 'lyricContent',
+  WEBDAV_ACCOUNT_ID: 'webdav_account_id'
 };
 
 export default class MediaTable {
@@ -622,6 +624,7 @@ export default class MediaTable {
     item.LYRICIST = safeGet('LYRICIST');
     item.COMMENT = safeGet('COMMENT');
     item.disc = safeGet('disc');
+    item.webdav_account_id = safeGet('webdav_account_id');
 
     return item;
   }
@@ -991,7 +994,9 @@ function generateBucket(item: VideoItem): relationalStore.ValuesBucket {
   if(item.disc){
     obj.disc = item.disc;
   }
-
+  if(item.webdav_account_id){
+    obj.webdav_account_id = item.webdav_account_id;
+  }
 
   return obj;
 }

+ 75 - 0
entry/src/main/ets/common/util/PermissionUtil.ets

@@ -0,0 +1,75 @@
+import { fileShare, fileIo as fs, fileUri } from '@kit.CoreFileKit';
+import { FileUtil } from '@pura/harmony-utils';
+
+
+//权限类
+class PermissionUtil {
+  //激活权限
+  async activatePermission(uri: string | undefined): Promise<boolean> {
+    if (!uri) {
+      return false
+    }
+    try {
+      let fd = fs.openSync(uri);
+      fs.closeSync(fd);
+      console.log("onecold activatePermission 无需激活权限");
+      return true
+    } catch {
+      try {
+        if (canIUse('SystemCapability.FileManagement.AppFileService.FolderAuthorization')) {
+          uri = FileUtil.getUriFromPath(uri)
+          console.log('onecold 激活权限 activatePermission uri = '+uri);
+          let policyInfo: fileShare.PolicyInfo = {
+            uri: uri,
+            operationMode: fileShare.OperationMode.READ_MODE | fileShare.OperationMode.WRITE_MODE,
+          };
+          let policies: Array<fileShare.PolicyInfo> = [policyInfo];
+          let results = await fileShare.checkPersistentPermission(policies);
+          for (let i = 0; i < results.length; i++) {
+            console.log('onecold activatePermission 激活权限成功');
+            if (results[i]) {
+              let info: fileShare.PolicyInfo = {
+                uri: policies[i].uri,
+                operationMode: policies[i].operationMode,
+              };
+              let policy: Array<fileShare.PolicyInfo> = [info];
+              await fileShare.activatePermission(policy);
+            }
+          }
+          let fd = fs.openSync(uri);
+          fs.closeSync(fd);
+        }
+        return true
+      } catch (error) {
+        console.error('onecold activatePermission error = '+JSON.stringify(error));
+        if (error.code == 13900001 ) {
+          await this.persistPermission(uri)
+
+        }
+        return false
+      }
+    }
+  }
+
+  //持久化权限
+  async persistPermission(uri: string): Promise<boolean> {
+    try {
+      if (canIUse('SystemCapability.FileManagement.AppFileService.FolderAuthorization')) {
+        let policyInfo: fileShare.PolicyInfo = {
+          uri: uri,
+          operationMode: fileShare.OperationMode.READ_MODE | fileShare.OperationMode.WRITE_MODE,
+        };
+        let policies: Array<fileShare.PolicyInfo> = [policyInfo];
+        fileShare.persistPermission(policies).then(() => {
+        })
+        let fd = await fs.open(uri);
+        await fs.close(fd);
+      }
+    } catch (error) {
+      return true
+    }
+    return false
+  }
+}
+
+export default new PermissionUtil();

+ 718 - 0
entry/src/main/ets/common/util/RcpSocketUtil.ets

@@ -0,0 +1,718 @@
+// rcp通信工具
+import { BusinessError } from '@kit.BasicServicesKit';
+import { buffer, HashMap, JSON, util, xml } from '@kit.ArkTS';
+import { FileInfo } from '../../viewmodel/FileInfo';
+import { rcp } from '@kit.RemoteCommunicationKit';
+import { BackgroundManager } from './BackgroundManager';
+import FileManager, { merge2paths } from './FileManager';
+
+const UtilName = "heanup RcpSocket"
+
+export class RcpSocket {
+  private static instance: RcpSocket;
+  public ErrorMessage: string | BusinessError = ''
+  public filesInfo: FileInfo[] = []
+  private backgroundManager = BackgroundManager.getInstance()
+
+  //private rcpSession : rcp.Session | null = null
+
+  constructor() {
+    console.info(UtilName, 'testTag', 'RcpSocketUtil单例已创建')
+  }
+
+  static getInstance(): RcpSocket {
+    if (!RcpSocket.instance) {
+      RcpSocket.instance = new RcpSocket();
+    }
+    return RcpSocket.instance;
+  }
+
+  public RcpSendHead(host: string, port: number, account: string, password: string, path: string,
+    enableHttps: boolean): Promise<number> {
+    return new Promise<number>((resolve, reject) => {
+      const url = `${enableHttps ? "https" : "http"}://${host}:${port}${path}`;
+      const timeoutDuration: number = 10000;
+      const speedThreshold: number = 5000; // 设置速度测试的时间阈值
+      console.info(UtilName, 'testTag', '发送HEAD的url:' + url)
+      // 创建 RCP 会话配置
+      let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' }
+      let reqCfg: rcp.Configuration = {
+        security: secCfg,
+        transfer: {
+          timeout: {
+            connectMs: timeoutDuration
+          }
+        }
+      }
+      let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg }
+      let rcpSession = rcp.createSession(sessionCfg);
+      // 构造基本认证头部
+      const encodedCredentials = buffer
+        .from(`${account}:${password}`)
+        .toString("base64");
+
+      const headers: rcp.RequestHeaders = {
+        Depth: "1",
+        "Content-Type": "application/xml",
+        Accept: "text/xml",
+        Authorization: `Basic ${encodedCredentials}`,
+      };
+      // 创建请求对象
+      const req = new rcp.Request(url, "HEAD", headers);
+      const startTime = Date.now();
+      // 连接
+      try {
+        rcpSession
+          .fetch(req)
+          .then((response) => {
+            const endTime = Date.now();
+            const elapsedTime = endTime - startTime;
+            // 如果响应时间超过阈值,视为测速过慢
+            if (elapsedTime > speedThreshold) {
+              console.error(UtilName, "testTag", `${host}Response time too slow: ${elapsedTime}ms`);
+              rcpSession?.close()
+              reject("Test speed too slow");
+            } else {
+              console.info(UtilName, "testTag", host + " Connect and test speed succeed");
+              const contentLength = response.headers['content-length'] || '0'
+              let fileSize = 0
+              if (contentLength) {
+                if (Array.isArray(contentLength)) {
+                  const values = contentLength
+                    .map((value) => parseInt(value, 10))
+                    .filter((value) => !isNaN(value));
+                  if (values.length > 0) {
+                    // 取最大值
+                    fileSize = Math.max(...values);
+                  }
+                } else {
+                  fileSize = parseInt(contentLength, 10);
+                }
+              } else {
+                console.info(UtilName, 'testTag', 'Content-Length 头未找到');
+              }
+              rcpSession?.close();
+              resolve(fileSize);
+            }
+          })
+          .catch((err: BusinessError) => {
+            console.error(UtilName, "testTag", `${host}请求失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`);
+            rcpSession?.close()
+            reject(err);
+          });
+      } catch (e) {
+        console.error(UtilName, 'testTag', '发起连接失败', JSON.stringify(e))
+        reject(e)
+      }
+    });
+  }
+
+  public RcpSendDelete(host: string, port: number, account: string, password: string, path: string,
+    enableHttps: boolean): Promise<void> {
+    return new Promise<void>((resolve, reject) => {
+      const url = `${enableHttps ? "https" : "http"}://${host}:${port}${path}`;
+      const timeoutDuration: number = 10000;
+      console.info(UtilName, 'testTag', '发送Delete的url:' + url)
+      // 创建 RCP 会话配置
+      let response = ""
+      const customHttpEventsHandler: rcp.HttpEventsHandler = {
+        onDataReceive: (incomingData: ArrayBuffer) => {
+          response += this.buf2String(incomingData)
+        },
+        onDataEnd: () => {
+        },
+      };
+      const tracingConfig: rcp.TracingConfiguration = {
+        verbose: true,
+        infoToCollect: {
+          textual: true,
+          incomingData: true,
+          outgoingData: true,
+        },
+        collectTimeInfo: true,
+        httpEventsHandler: customHttpEventsHandler
+      };
+      let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' }
+      let reqCfg: rcp.Configuration = {
+        security: secCfg,
+        tracing: tracingConfig,
+        transfer: {
+          timeout: {
+            connectMs: timeoutDuration
+          }
+        }
+      }
+      let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg }
+      let rcpSession = rcp.createSession(sessionCfg);
+      // 构造基本认证头部
+      const encodedCredentials = buffer
+        .from(`${account}:${password}`)
+        .toString("base64");
+
+      const headers: rcp.RequestHeaders = {
+        Authorization: `Basic ${encodedCredentials}`,
+      };
+      // 创建请求对象
+      const req = new rcp.Request(url, "DELETE", headers);
+      // 连接
+      rcpSession
+        .fetch(req)
+        .then(() => {
+          console.info(UtilName, 'testTag', '执行删除的响应', JSON.stringify(response))
+          rcpSession.close()
+          resolve()
+        })
+        .catch((err: BusinessError) => {
+          console.error(UtilName, "testTag", `${host}请求失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`);
+          rcpSession?.close()
+          reject(err);
+        });
+    });
+  }
+
+  public RcpSendMove(host: string, port: number, account: string, password: string, path: string, enableHttps: boolean,
+    newPath: string): Promise<void> {
+    return new Promise<void>((resolve, reject) => {
+      const url = `${enableHttps ? "https" : "http"}://${host}:${port}${path}`;
+      const destinationUrl = `${enableHttps ? "https" : "http"}://${host}:${port}${newPath}`
+      const timeoutDuration: number = 10000;
+      console.info(UtilName, 'testTag', '发送MOVE的url:' + url)
+      // 创建 RCP 会话配置
+      let response = ""
+      const customHttpEventsHandler: rcp.HttpEventsHandler = {
+        onDataReceive: (incomingData: ArrayBuffer) => {
+          response += this.buf2String(incomingData)
+        },
+        onDataEnd: () => {
+        },
+      };
+      const tracingConfig: rcp.TracingConfiguration = {
+        verbose: true,
+        infoToCollect: {
+          textual: true,
+          incomingData: true,
+          outgoingData: true,
+        },
+        collectTimeInfo: true,
+        httpEventsHandler: customHttpEventsHandler
+      };
+      let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' }
+      let reqCfg: rcp.Configuration = {
+        security: secCfg,
+        tracing: tracingConfig,
+        transfer: {
+          timeout: {
+            connectMs: timeoutDuration
+          }
+        }
+      }
+      let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg }
+      let rcpSession = rcp.createSession(sessionCfg);
+      // 构造基本认证头部
+      const encodedCredentials = buffer
+        .from(`${account}:${password}`)
+        .toString("base64");
+
+      const headers: rcp.RequestHeaders = {
+        Authorization: `Basic ${encodedCredentials}`,
+        Destination: destinationUrl
+      };
+      // 创建请求对象
+      const req = new rcp.Request(url, "MOVE", headers);
+      // 连接
+      rcpSession
+        .fetch(req)
+        .then(() => {
+          console.info(UtilName, 'testTag', 'move的响应结果', JSON.stringify(response))
+          rcpSession.close()
+          resolve()
+        })
+        .catch((err: BusinessError) => {
+          console.error(UtilName, "testTag", `${host}请求失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`);
+          rcpSession.close()
+          reject(err);
+        });
+    });
+  }
+
+  // rcp方法
+  public async RcpSendPropFind(
+    host: string,
+    port: number,
+    account: string,
+    password: string,
+    path: string,
+    enableHttps: boolean
+  ): Promise<FileInfo[]> {
+    return new Promise(async (resolve, reject) => {
+      const url = `${enableHttps ? "https" : "http"}://${host}:${port.toString()}${path}`;
+      const timeoutDuration: number = 10000;
+      console.info(UtilName, 'testTag', '发送PROPFIND请求的url:' + url)
+      // 创建 RCP 会话配置
+      let response: string = ''
+      const customHttpEventsHandler: rcp.HttpEventsHandler = {
+        onDataReceive: async (incomingData: ArrayBuffer) => {
+          response += this.buf2String(incomingData)
+          await this.backgroundManager.updateDataTransferContinuousTask()
+        },
+        onDataEnd: () => {
+        },
+      };
+      const tracingConfig: rcp.TracingConfiguration = {
+        verbose: true,
+        infoToCollect: {
+          textual: true,
+          incomingData: true,
+          outgoingData: true,
+        },
+        collectTimeInfo: true,
+        httpEventsHandler: customHttpEventsHandler
+      };
+      let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' }
+      let reqCfg: rcp.Configuration = {}
+      if (enableHttps) {
+        reqCfg = {
+          security: secCfg,
+          tracing: tracingConfig,
+          transfer: {
+            timeout: {
+              connectMs: timeoutDuration,
+              transferMs: timeoutDuration
+            }
+          }
+        }
+      } else {
+        reqCfg = {
+          tracing: tracingConfig,
+          transfer: {
+            timeout: {
+              connectMs: timeoutDuration
+            }
+          }
+        }
+      }
+
+      let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg }
+      let rcpSession = rcp.createSession(sessionCfg);
+
+      // 构造 PROPFIND 请求体
+      const requestBody = `<?xml version="1.0" encoding="UTF-8"?>
+                            <D:propfind xmlns:D="DAV:">
+                              <D:prop>
+                                <D:displayname/>       <!-- 请求文件名 -->
+                                <D:getcontentlength/>  <!-- 请求文件大小 -->
+                                <D:getlastmodified/>   <!-- 请求文件最后修改时间 -->
+                              </D:prop>
+                            </D:propfind>`;
+
+      // 构造基本认证头部
+      const encodedCredentials = buffer
+        .from(`${account}:${password}`)
+        .toString("base64");
+      console.info(UtilName, 'testTag', account, password)
+
+
+      const headers: rcp.RequestHeaders = {
+        "Depth": "1",
+        "Content-Type": "application/xml",
+        "Accept": "text/xml",
+        "Authorization": `Basic ${encodedCredentials}`,
+      };
+      // 创建请求对象
+      const req = new rcp.Request(url, "PROPFIND", headers, requestBody);
+
+      // 发起请求
+      try {
+        await rcpSession.fetch(req)
+          .finally(() => {
+            console.info(UtilName, 'testTag', 'PROPFIND执行完毕')
+            if (response != '') {
+              console.info(UtilName, 'testTag', 'WebDAV响应内容长度:', response.length.toString());
+              console.info(UtilName, 'testTag', 'WebDAV响应前500字符:', response.substring(0, 500));
+              // 提取文件信息
+              const filesInfo = this.extractHrefContents(response, path, url);
+              console.info(UtilName, 'testTag', '解析出文件数量:', filesInfo.length.toString());
+              if (filesInfo.length !== 0) {
+                console.info(UtilName, 'testTag', '请求成功')
+                rcpSession.close()
+                resolve(filesInfo);
+              } else {
+                let message = `请求${host}失败,响应信息:${response}`
+                console.error(UtilName, 'testTag', message)
+                rcpSession.close()
+                reject(message)
+              }
+            } else {
+              let error = `服务器响应信息为空,请求失败`
+              console.error(UtilName, 'testTag', error)
+              rcpSession.close()
+              reject(error);
+            }
+          })
+        // 处理成功响应
+        //console.info(UtilName, 'testTag', JSON.stringify(res));
+      } catch (err) {
+        // 处理错误响应
+        console.error(UtilName, "testTag", `请求${url}失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`);
+        let error = `错误码${err.code},错误信息${err.data}`
+        rcpSession.close()
+        reject(error)
+      }
+    });
+  }
+
+  // PROPFIND递归方法
+  public async RcpSendPropFindInfinity(
+    host: string,
+    port: number,
+    account: string,
+    password: string,
+    path: string,
+    enableHttps: boolean,
+    useCache: boolean,
+    saveCache: boolean,
+    cachePath: string
+  ): Promise<FileInfo[]> {
+    return new Promise(async (resolve, reject) => {
+      const url = `${enableHttps ? "https" : "http"}://${host}:${port.toString()}${path}`;
+      const timeoutDuration: number = 10000;
+      console.info(UtilName, 'testTag', '发送PROPFIND递归请求的url:' + url)
+      let cacheFileInfos_str: string = ''
+      if (useCache) {
+        try {
+          cacheFileInfos_str = await FileManager.readFileToString(cachePath)
+          if (cacheFileInfos_str) {
+            let files = JSON.parse(cacheFileInfos_str) as FileInfo[]
+            console.info(UtilName, 'testTag', '使用缓存')
+            resolve(files)
+            return
+          }
+        } catch (e) {
+          console.error(UtilName, 'testTag', '读取缓存失败', JSON.stringify(e))
+        }
+      }
+      // 创建 RCP 会话配置
+      let response: string = ''
+      const customHttpEventsHandler: rcp.HttpEventsHandler = {
+        onDataReceive: async (incomingData: ArrayBuffer) => {
+          response += this.buf2String(incomingData)
+          //await this.backgroundManager.updateDataTransferContinuousTask(0,0,path)
+        },
+        onDataEnd: () => {
+        },
+      };
+      const tracingConfig: rcp.TracingConfiguration = {
+        verbose: true,
+        infoToCollect: {
+          textual: true,
+          incomingData: true,
+          outgoingData: true,
+        },
+        collectTimeInfo: true,
+        httpEventsHandler: customHttpEventsHandler
+      };
+      let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' }
+      let reqCfg: rcp.Configuration = {}
+      if (enableHttps) {
+        reqCfg = {
+          security: secCfg,
+          tracing: tracingConfig,
+          transfer: {
+            timeout: {
+              connectMs: timeoutDuration,
+              transferMs: timeoutDuration
+            }
+          }
+        }
+      } else {
+        reqCfg = {
+          tracing: tracingConfig,
+          transfer: {
+            timeout: {
+              connectMs: timeoutDuration
+            }
+          }
+        }
+      }
+
+      let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg }
+      let rcpSession = rcp.createSession(sessionCfg);
+
+      // 构造 PROPFIND 请求体
+      const requestBody = `<?xml version="1.0" encoding="UTF-8"?>
+                            <D:propfind xmlns:D="DAV:">
+                              <D:prop>
+                                <D:displayname/>       <!-- 请求文件名 -->
+                                <D:getcontentlength/>  <!-- 请求文件大小 -->
+                                <D:getlastmodified/>   <!-- 请求文件最后修改时间 -->
+                              </D:prop>
+                            </D:propfind>`;
+      // const requestBody = `<D:propfind xmlns:D="DAV:">
+      //                         <D:allprop/>
+      //                       </D:propfind>`;
+
+      // 构造基本认证头部
+      const encodedCredentials = buffer
+        .from(`${account}:${password}`)
+        .toString("base64");
+
+
+      const headers: rcp.RequestHeaders = {
+        "Depth": "1",
+        "Content-Type": "application/xml",
+        "Accept": "text/xml",
+        "Authorization": `Basic ${encodedCredentials}`,
+      };
+      let sendSinglePropfind = async (url: string, root: string): Promise<FileInfo[]> => {
+        return new Promise<FileInfo[]>(async (resolve, reject) => {
+          if (root.includes('%23recycle')) {
+            resolve([])
+            return
+          }
+          // 发起请求
+          try {
+            AppStorage.setOrCreate('CurrentPropfindInfinityRoot', root)
+            response = ''
+            const req = new rcp.Request(url, "PROPFIND", headers, requestBody)
+            await rcpSession.fetch(req)
+              .finally(async () => {
+                if (response != '') {
+                  // 提取文件信息
+                  let filesInfo = this.extractHrefContents(response, root, url);
+                  let folderInfos: FileInfo[] = []
+                  for (const info of filesInfo) {
+                    if (this.isFileFolder(info.name)) {
+                      folderInfos.push(info)
+                    }
+                  }
+                  //console.info(UtilName,'testTag','PROPFIND执行完毕','根目录',root,'子目录数量',folderInfos.length)
+                  if (folderInfos.length > 0) {
+                    for (const folder of folderInfos) {
+                      let sub_url = merge2paths(url, folder.name)
+                      let sub_root = merge2paths(root, folder.name)
+                      let next_infos = await sendSinglePropfind(sub_url, sub_root)
+                      filesInfo = filesInfo.concat(next_infos)
+                    }
+                  }
+                  if (filesInfo.length !== 0) {
+                    resolve(filesInfo);
+                  } else {
+                    resolve([])
+                  }
+                } else {
+                  resolve([])
+                }
+              })
+          } catch (err) {
+            // 处理错误响应
+            console.error(UtilName, "testTag", `请求${url}失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`);
+            let error = `错误码${err.code},错误信息${err.data}`
+            resolve([])
+          }
+        })
+      }
+      // 创建请求对象
+      await sendSinglePropfind(url, path)
+        .then(async (filesInfo: FileInfo[]) => {
+          console.info(UtilName, 'testTag', 'PROPFIND目录递归结果', filesInfo.length)
+          rcpSession?.close()
+          if (saveCache) {
+            let str = JSON.stringify(filesInfo)
+            if (str !== cacheFileInfos_str) {
+              try {
+                await FileManager.writeStringToFilePath(str, cachePath)
+                console.info(UtilName, 'testTag', '保存缓存成功')
+              } catch (e) {
+                console.info(UtilName, 'testTag', '保存缓存失败', JSON.stringify(e))
+              }
+            }
+          }
+          resolve(filesInfo)
+        }).catch((err: BusinessError) => {
+          rcpSession?.close()
+          console.error(UtilName, 'testTag', 'PROPFIND目录递归失败', JSON.stringify(err))
+          reject(err)
+        })
+    });
+  }
+
+  //ArrayBuffer转utf8字符串
+  buf2String(buf: ArrayBuffer) {
+    let msgArray = new Uint8Array(buf);
+    let textDecoder = util.TextDecoder.create("utf-8");
+    return textDecoder.decodeToString(msgArray)
+  }
+
+  // 提取XML
+  extractXmlContent(httpResponse: string): string {
+    const xmlStart = httpResponse.indexOf('<?xml');
+    if (xmlStart !== -1) {
+      return httpResponse.substring(xmlStart);
+    }
+    return '';
+  }
+
+  stringToNumber(str: string): number {
+    let result: number = 0;
+    for (let i = 0; i < str.length; i++) {
+      result += str.charCodeAt(i);
+    }
+    return result;
+  }
+
+  // 使用正则表达式提取所有 <D:href> 标签的内容
+  extractHrefContents(xmlContent: string, rootpath: string, url: string): FileInfo[] {
+    const filesInfo: FileInfo[] = [];
+    // 匹配每个 <D:response>
+    const responseRegex = /<D:response\b[^>]*>([\s\S]*?)<\/D:response>/gi;
+    let responseMatch: RegExpExecArray | null;
+
+    while ((responseMatch = responseRegex.exec(xmlContent)) !== null) {
+      //console.info(UtilName,'testTag',JSON.stringify(responseMatch))
+      const responseBlock = responseMatch[1];
+
+      // 提取 <D:href> 内容
+      const hrefMatch = responseBlock.match(/<D:href>(.*?)<\/D:href>/i);
+      if (!hrefMatch) {
+        continue;
+      }
+
+      // 获取完整的href路径
+      const fullHref = this.decodeXMLEntities(decodeURI(hrefMatch[1]));
+
+      // 尝试提取 <D:displayname> 作为文件名
+      let displayNameMatch = responseBlock.match(/<D:displayname>(.*?)<\/D:displayname>/i);
+      if (!displayNameMatch) {
+        displayNameMatch = responseBlock.match(/<lp1:displayname>(.*?)<\/lp1:displayname>/i);
+      }
+
+      let name = '';
+      if (displayNameMatch && displayNameMatch[1]) {
+        // 如果有 displayname,使用它
+        name = this.decodeXMLEntities(displayNameMatch[1]);
+        console.info(UtilName, 'testTag', '使用displayname作为文件名:', name);
+      } else {
+        // 否则从href中提取文件名(最后一个/后的部分)
+        name = fullHref;
+        const lastSlashIndex = fullHref.lastIndexOf('/');
+        if (lastSlashIndex >= 0 && lastSlashIndex < fullHref.length - 1) {
+          name = fullHref.substring(lastSlashIndex + 1);
+        } else if (fullHref.endsWith('/')) {
+          // 如果是目录(以/结尾),取倒数第二段
+          const withoutTrailingSlash = fullHref.substring(0, fullHref.length - 1);
+          const secondLastSlash = withoutTrailingSlash.lastIndexOf('/');
+          if (secondLastSlash >= 0) {
+            name = withoutTrailingSlash.substring(secondLastSlash + 1) + '/';
+          }
+        }
+      }
+
+      // 跳过根目录本身
+      if (name === '' || name === '/') {
+        continue;
+      }
+
+      // 提取<lp1:getcontentlength> 或 <D:getcontentlength>
+      let sizeMatch = responseBlock.match(/<lp1:getcontentlength>(.*?)<\/lp1:getcontentlength>/i);
+      if (!sizeMatch) {
+        sizeMatch = responseBlock.match(/<D:getcontentlength>(.*?)<\/D:getcontentlength>/i);
+      }
+
+      // 提取<D:getlastmodified> 内容
+      let lastModifiedMatch = responseBlock.match(/<D:getlastmodified>(.*?)<\/D:getlastmodified>/i);
+      if (!lastModifiedMatch) {
+        lastModifiedMatch = responseBlock.match(/<lp1:getlastmodified>(.*?)<\/lp1:getlastmodified>/i);
+      }
+      const lastModified = lastModifiedMatch ? this.convertToUnixTimestamp(lastModifiedMatch[1]) : 0;
+      const size = sizeMatch ? Number(sizeMatch[1]) : 0;
+
+      // 创建FileInfo并设置WebDAV属性
+      const fileInfo = new FileInfo(rootpath, name, size, lastModified);
+      fileInfo.href = fullHref;
+      fileInfo.contentLength = size;
+      // 判断是否为文件夹(以/结尾或没有contentLength)
+      fileInfo.isDirectory = fullHref.endsWith('/') || size === 0;
+      filesInfo.push(fileInfo);
+    }
+
+    return filesInfo;
+  }
+
+  /**
+   * 获取文件列表
+   * @param host 主机名
+   * @param localHost 本地主机名
+   * @param isUseLocalHost 是否使用本地主机名
+   * @param port 端口号
+   * @param path 路径
+   * @param account 用户名
+   * @param password 密码
+   * @param enableHttps 是否启用HTTPS
+   * @returns
+   */
+  public async getFileList(
+    host: string,
+    localHost: string,
+    isUseLocalHost: boolean,
+    port: number,
+    path: string,
+    account: string,
+    password: string,
+    enableHttps: boolean
+  ): Promise<FileInfo[]> {
+    const actualHost = isUseLocalHost ? localHost : host;
+    return await this.RcpSendPropFind(actualHost, port, account, password, path, enableHttps);
+  }
+
+  // 订阅HTTP数据传输事件
+  public subscribeHTTPDataTransfer(callback: (event: string) => void): void {
+    // 简化版:目前不实现具体订阅逻辑
+    console.info(UtilName, 'testTag', '订阅HTTP数据传输事件(占位)');
+  }
+
+  // 获取文件列表(简化版包装方法)
+
+  // 判断文件是否属于文件夹
+  /**
+   * 判断文件是否属于文件夹
+   * @param filename 文件名
+   * @returns
+   */
+  private isFileFolder(filename: string): boolean {
+    return filename.toLowerCase().endsWith('/')
+  }
+
+  // 将 HTTP 日期字符串转换为 Unix 时间戳
+  private convertToUnixTimestamp(dateString: string): number {
+    const date = new Date(dateString);
+    if (isNaN(date.getTime())) {
+      console.error(UtilName, 'testTag', "非法日期字符串:", dateString);
+      return 0;
+    }
+    return Math.floor(date.getTime() / 1000);
+  }
+
+  private decodeXMLEntities(str: string): string {
+    const entityMap: HashMap<string, string> = new HashMap()
+    entityMap.set('&amp;', '&')
+    entityMap.set('&lt;', '<')
+    entityMap.set('&gt;', '>')
+    entityMap.set('&quot;', '"')
+    entityMap.set('&apos;', "'")
+
+    // 先替换 XML 实体
+    let result = str.replace(/&(amp|lt|gt|quot|apos);/g, (match: string, entity: string) => {
+      const decoded = entityMap.get(`&${entity};`);
+      return decoded ? decoded : match;
+    });
+
+    // 再进行 URL 解码
+    try {
+      result = decodeURIComponent(result);
+    } catch (error) {
+      // 如果解码失败,保持原样
+    }
+
+    return result;
+  }
+}

+ 3 - 1
entry/src/main/ets/common/util/RdbUtils.ets

@@ -80,6 +80,7 @@ export default class RdbUtils {
       '        LYRICIST TEXT,\n' +
       '        COMMENT TEXT,\n' +
       '        disc TEXT,\n' +
+      '        webdav_account_id TEXT,\n' +
 
       '        mimeType TEXT' +
       ')',
@@ -88,7 +89,7 @@ export default class RdbUtils {
       'duration',   'sampleRate','playCount',  'lastPlayedStr', 'trackCount',
       'lyricContent','md5Str','extra_json','pyStr','bit_rate','probe_score','year','nb_streams','nb_programs',
       'genre','track',  'bits_per_raw_sample','channels',  'channel_layout','start_time',
-      'ALBUMARTIST',  'COMPOSER','LYRICIST','COMMENT','COMMENT','disc',
+      'ALBUMARTIST',  'COMPOSER','LYRICIST','COMMENT','COMMENT','disc','webdav_account_id',
       'mimeType']
   };
 
@@ -216,6 +217,7 @@ export default class RdbUtils {
             'LYRICIST': 'TEXT',
             'COMMENT': 'TEXT',
             'disc': 'TEXT',
+            'webdav_account_id': 'TEXT',
           };
 
           // 逐个添加列,不依赖于检查结果

+ 52 - 0
entry/src/main/ets/common/util/ReqPermissionUtil.ets

@@ -0,0 +1,52 @@
+import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';
+import { fileShare, fileIo as fs, fileUri } from '@kit.CoreFileKit';
+import { hilog } from '@kit.PerformanceAnalysisKit';
+import { BusinessError } from '@kit.BasicServicesKit';
+
+// 重新设置权限类,用于每次启动重新设置权限
+class ReqPermission {
+  public permissions: Permissions[] = ['ohos.permission.FILE_ACCESS_PERSIST'];
+
+  // 重新申请权限
+  reqPermissionsFromUser(permissions: Permissions[], context: common.UIAbilityContext): void {
+    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
+    atManager.requestPermissionsFromUser(context, permissions).then((data) => {
+      let grantStatus: number[] = data.authResults;
+      let length: number = grantStatus.length;
+      for (let i = 0; i < length; i++) {
+        if (grantStatus[i] === 0) {
+        } else {
+          return;
+        }
+      }
+    })
+  }
+  //持久化权限
+  async persistPermission(uri: string): Promise<boolean> {
+    try {
+      if (canIUse('SystemCapability.FileManagement.AppFileService.FolderAuthorization')) {
+        if(uri.startsWith('file://media/Photo')){
+          uri = new fileUri.FileUri(uri).path
+        }
+        console.info('onecold 持久化权限persistPermission uri : ', uri);
+        let policyInfo: fileShare.PolicyInfo = {
+          uri: uri,
+          operationMode: fileShare.OperationMode.READ_MODE | fileShare.OperationMode.WRITE_MODE,
+        };
+        let policies: Array<fileShare.PolicyInfo> = [policyInfo];
+        fileShare.persistPermission(policies).then(() => {
+          console.log("onecold 持久化权限persistPermission success");
+        }).catch((err: BusinessError<Array<fileShare.PolicyErrorResult>>) => {
+          console.log("onecold persistPermission failed   err.message=" + JSON.stringify(err));
+        });
+        let fd = await fs.open(uri);
+        await fs.close(fd);
+      }
+    } catch (error) {
+      return true
+    }
+    return false
+  }
+}
+
+export default new ReqPermission()

+ 6 - 2
entry/src/main/ets/common/util/Utility.ets

@@ -24,6 +24,7 @@ import { pinyin4js } from '@ohos/pinyin4js';
 import { VipData } from '../../viewmodel/VipData';
 import { VipPage } from '../../pages/VipPage';
 import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
+import ReqPermissionUtil from './ReqPermissionUtil';
 
 
 export interface FFMpegTags {
@@ -649,6 +650,7 @@ export class Utility {
 
   //获取音乐资源的属性值,
   static async uriGetMusicAssetsFromFile(context:Context,uri:string,type:number,autoParseMusicName?:boolean): Promise<VideoItem> {
+    await ReqPermissionUtil.persistPermission(uri);
     if(StrUtil.isNotEmpty(uri)&&uri.toLowerCase().endsWith('.cue')){
       return  new VideoItem(FileUtil.getFileName(uri),uri,uri,type,0,'')
     }
@@ -832,6 +834,7 @@ export class Utility {
 
   static async  readMetaInfoFFmpeg(context:Context,inputPath: string,type:number,autoParseMusicName?:boolean): Promise<VideoItem> {
     return new Promise((resolve, reject) => {
+      inputPath = FileUtil.getFilePath(inputPath)
       let commands = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", inputPath];
       let outputJson = "";
 
@@ -1648,7 +1651,7 @@ function formatDuration(seconds: string, forceHHMMSS: boolean = false): string {
 /**
  * 定义解析结果的数据结构
  */
-class MusicInfo {
+export class MusicInfo {
   artist: string = "";  // 艺术家名称
   title: string = "";   // 歌曲名称
   isValid: boolean = false;  // 格式是否有效
@@ -1691,7 +1694,7 @@ const ARTIST_KEYWORDS = [
 /**
  * 智能解析音乐文件名
  */
-function parseMusicFileName(fileName: string): MusicInfo {
+export function parseMusicFileName(fileName: string): MusicInfo {
   const result: MusicInfo = new MusicInfo();
   if (!fileName) return result;
 
@@ -1907,6 +1910,7 @@ async function extractLyricsContent(inputPath: string): Promise<string> {
  */
 async function parseAudioMetadata(inputPath: string): Promise<VideoItem> {
   return new Promise(async (resolve, reject) => {
+    inputPath = FileUtil.getFilePath(inputPath)
     try {
       // 1. 执行FFprobe命令
       const commands: string[] = [

+ 1160 - 0
entry/src/main/ets/common/util/WebdavManager.ets

@@ -0,0 +1,1160 @@
+// WebdavManager - WebDAV管理器(简化版)
+import { VideoItem } from '../../viewmodel/VideoItem';
+import { common } from '@kit.AbilityKit';
+import { FileInfo } from '../../viewmodel/FileInfo';
+import FileManager, { merge2paths } from './FileManager';
+import { BusinessError } from '@kit.BasicServicesKit';
+import { WebdavManagerStates } from '../enums/WebdavManagerStates';
+import { RcpSocket } from './RcpSocketUtil';
+import { DataBaseUtil } from './DataBaseUtil';
+import { WebDavAccount } from '../../viewmodel/WebDavAccount';
+import { relationalStore } from '@kit.ArkData';
+import { Constants } from '../../Constants';
+import Logger from './Logger';
+import { buffer } from '@kit.ArkTS';
+import { CommonConstants } from '../constants/CommonConstants';
+import { GlobalContext } from '@pura/harmony-utils';
+import { MusicInfo, parseMusicFileName, Utility } from './Utility';
+
+const TAG = 'heanup WebdavManager';
+
+// WebDAV认证信息接口
+export interface WebDavAuthInfo {
+  headers: Record<string, string>;
+  url: string;
+  accountId?: string; // 添加账号ID字段,用于标识认证信息对应的账号
+}
+
+// 流媒体认证信息接口
+export interface StreamAuthInfo {
+  url: string;
+  headers: Record<string, string>;
+}
+
+export interface TransferTask {
+  song: VideoItem;
+  account: WebDavAccount;
+}
+
+@Observed
+export class WebdavManager {
+  public rcpSocket: RcpSocket = RcpSocket.getInstance();
+  private dataBaseUtil = DataBaseUtil.getInstance();
+  public observers: Array<(event: string) => void> = [];
+  public static instance: WebdavManager;
+  public context: common.Context | undefined;
+  public DownloadDirectoryFilePath: string = '';
+  public audioExtensions = Constants.AUDIO_EXTENSIONS;
+  public lyricExtensions = Constants.LYRIC_EXTENSIONS;
+  public imageExtensions = Constants.IMAGE_EXTENSIONS;
+
+  // 数据表名称
+  public preferenceName = Constants.WEBDAV_PREFERENCE_NAME;
+  public webDavTable = 'WebDavAccount';
+
+  public receivedSize: number = 0;
+  public totalSize: number = 0;
+
+  public webDavAccounts: WebDavAccount[] = [];
+  public webDavSongs: VideoItem[] = [];
+  public webDavFiles: FileInfo[] = [];  // 当前目录的所有文件(包括文件夹)
+
+  // 路径导航
+  public currentPath: string = '';  // 当前浏览的路径
+  public pathHistory: string[] = [];  // 路径历史记录
+
+  // 错误信息
+  public ErrorMessage: string | BusinessError = '';
+
+  // 下载队列
+  public downloadQueue: TransferTask[] = [];
+  public finishDownloadQueue: TransferTask[] = [];
+  public isProcessingQueue: boolean = false;
+  public currentDownloadTask: TransferTask | null = null;
+  public isPauseDownload: boolean = true;
+
+  // 上传队列
+  public uploadQueue: TransferTask[] = [];
+  public finishUploadQueue: TransferTask[] = [];
+  public isProcessingUploadQueue: boolean = false;
+  public currentUploadTask: TransferTask | null = null;
+  public isPauseUpload: boolean = true;
+
+  public currentAccount:WebDavAccount = new WebDavAccount();
+
+  public constructor() {
+    this.rcpSocket.subscribeHTTPDataTransfer((event: string) => {
+      this.notifyObservers(event);
+    });
+  }
+
+  public static getInstance(): WebdavManager {
+    if (!WebdavManager.instance) {
+      WebdavManager.instance = new WebdavManager();
+    }
+    return WebdavManager.instance;
+  }
+
+  public setContext(context: common.Context): void {
+    this.context = context;
+    this.DownloadDirectoryFilePath = merge2paths(context.filesDir, 'Download');
+  }
+
+  // ==================== 观察者模式 ====================
+
+  public subscribe(callback: (event: string) => void): void {
+    this.observers.push(callback);
+  }
+
+  public unsubscribe(callback: (event: string) => void): void {
+    const index = this.observers.indexOf(callback);
+    if (index > -1) {
+      this.observers.splice(index, 1);
+    }
+  }
+
+  public notifyObservers(event: string): void {
+    Logger.info(TAG, '通知观察者:', event);
+    Logger.info(TAG, '观察者数量:', this.observers.length.toString());
+    for (let i = 0; i < this.observers.length; i++) {
+      const observer = this.observers[i];
+      Logger.info(TAG, '调用观察者', i.toString(), ',事件:', event);
+      observer(event);
+    }
+  }
+
+  // ==================== 数据库操作 ====================
+
+  /**
+   * 根据webdav_account_id查询WebDAV账号信息
+   * @param accountId WebDAV账号ID
+   * @returns Promise<WebDavAccount | null> 返回WebDAV账号信息或null
+   */
+  public async getWebDavAccountById(accountId: string): Promise<WebDavAccount | null> {
+    if (!accountId) {
+      Logger.warn(TAG, 'webdav_account_id为空,无法查询账号信息');
+      return null;
+    }
+
+    try {
+      const db = await this.dataBaseUtil.getDB();
+      const predicates = new relationalStore.RdbPredicates(this.webDavTable);
+      predicates.equalTo('id', accountId);
+
+      const resultSet = await db.query(predicates);
+      if (resultSet.rowCount > 0) {
+        resultSet.goToFirstRow();
+        const account = new WebDavAccount();
+        account.id = resultSet.getLong(resultSet.getColumnIndex('id'));
+        account.name = resultSet.getString(resultSet.getColumnIndex('name'));
+        account.host = resultSet.getString(resultSet.getColumnIndex('host'));
+        account.localHost = resultSet.getString(resultSet.getColumnIndex('localHost'));
+        account.isUseLocalHost = resultSet.getLong(resultSet.getColumnIndex('isUseLocalHost')) === 1;
+        account.port = resultSet.getLong(resultSet.getColumnIndex('port'));
+        account.filepath = resultSet.getString(resultSet.getColumnIndex('filepath'));
+        account.imageFilePath = resultSet.getString(resultSet.getColumnIndex('imageFilePath'));
+        account.lyricFilePath = resultSet.getString(resultSet.getColumnIndex('lyricFilePath'));
+        account.uploadFilePath = resultSet.getString(resultSet.getColumnIndex('uploadFilePath'));
+        account.account = resultSet.getString(resultSet.getColumnIndex('account'));
+        account.password = resultSet.getString(resultSet.getColumnIndex('password'));
+        account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
+        account.coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath'));
+        account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
+
+        resultSet.close();
+        Logger.info(TAG, `成功查询到WebDAV账号: ${account.name}, ID: ${account.id}`);
+        return account;
+      } else {
+        Logger.warn(TAG, `未找到ID为 ${accountId} 的WebDAV账号`);
+        resultSet.close();
+        return null;
+      }
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `查询WebDAV账号失败: ${err.message}`);
+      return null;
+    }
+  }
+
+  /**
+   * 根据WebDAV账号构建认证信息
+   * @param accountId WebDAV账号ID
+   * @returns Promise<WebDavAuthInfo | null> 返回认证信息或null
+   */
+  public async buildAuthInfoByAccountId(accountId: string): Promise<WebDavAuthInfo | null> {
+    const account = await this.getWebDavAccountById(accountId);
+    if (!account) {
+      Logger.warn(TAG, `无法构建认证信息,账号ID ${accountId} 对应的账号不存在`);
+      return null;
+    }
+
+    try {
+      // 构建基础URL
+      const protocol = account.enableHttps ? 'https' : 'http';
+      const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+      const port = account.port && account.port !== 80 && account.port !== 443 ? `:${account.port}` : '';
+      const baseUrl = `${protocol}://${host}${port}`;
+
+      // 构建认证头
+      const headers: Record<string, string> = {};
+      if (account.account && account.password) {
+        const credentials = `${account.account}:${account.password}`;
+        // 简化base64编码实现,避免复杂的类型转换
+        try {
+          const bufferObj = buffer.from(credentials, 'utf-8');
+          const base64Credentials = bufferObj.toString('base64');
+          headers['Authorization'] = `Basic ${base64Credentials}`;
+        } catch (error) {
+          const err = error as Error;
+          Logger.warn(TAG, `base64编码失败,使用原始凭据: ${err.message}`);
+          headers['Authorization'] = `Basic ${credentials}`;
+        }
+      }
+
+      Logger.info(TAG, `成功构建WebDAV认证信息,账号: ${account.name}, URL: ${baseUrl}`);
+
+      return {
+        headers: headers,
+        url: baseUrl,
+        accountId: accountId
+      };
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `构建WebDAV认证信息失败: ${err.message}`);
+      return null;
+    }
+  }
+
+  /**
+   * 构建HTTP请求头,通过webdav_account_id查询认证信息
+   * @param currentSong - 当前播放的歌曲信息
+   * @param videoUrl - 当前歌曲的URL
+   * @returns Promise<Map<string, string>> HTTP请求头
+   */
+  public async buildHttpHeadersWithAccountId(
+    currentSong: VideoItem | undefined,
+    videoUrl: string
+  ): Promise<Map<string, string>> {
+    const headers = new Map<string, string>();
+
+    if (!currentSong) {
+      return headers;
+    }
+
+    Logger.info(`heanup 当前歌曲信息 - name: ${currentSong.name}, type: ${currentSong.type}, filePath: ${currentSong.filePath}`);
+
+    // 检查是否为WebDAV歌曲
+    if (currentSong.type === CommonConstants.TYPE_WEBDAV) {
+      // 优先使用webdav_account_id查询认证信息
+      if (currentSong.webdav_account_id) {
+        Logger.info(`heanup 使用webdav_account_id查询认证信息,账号ID: ${currentSong.webdav_account_id}`);
+
+        try {
+          const authInfo = await this.buildAuthInfoByAccountId(currentSong.webdav_account_id);
+          if (authInfo) {
+            // 添加WebDAV认证头
+            Object.keys(authInfo.headers).forEach(key => {
+              headers.set(key, authInfo.headers[key]);
+            });
+            Logger.info(`heanup 成功通过webdav_account_id获取WebDAV认证信息`);
+          } else {
+            Logger.warn(`heanup 无法通过webdav_account_id ${currentSong.webdav_account_id} 获取认证信息`);
+          }
+        } catch (error) {
+          const err = error as Error;
+          Logger.error(`heanup 查询webdav_account_id认证信息时出错: ${err.message}`);
+        }
+      } else {
+        Logger.warn(`heanup WebDAV歌曲缺少webdav_account_id字段,无法查询认证信息`);
+      }
+
+      // 如果没有认证信息,记录警告
+      if (headers.size === 0) {
+        Logger.warn(`heanup WebDAV歌曲缺少认证信息,可能需要手动配置WebDAV账户`);
+      }
+    }
+
+    return headers;
+  }
+
+  /**
+   * 更新现有WebDAV歌曲的webdav_account_id字段
+   * 用于修复旧版本数据库中缺少webdav_account_id的记录
+   * @param accountId WebDAV账号ID
+   */
+  public async updateWebDavSongsAccountId(accountId: string): Promise<void> {
+    try {
+      const db = await this.dataBaseUtil.getDB();
+
+      // 查询所有TYPE_WEBDAV且webdav_account_id为空或null的记录
+      const querySql = `
+        SELECT id, filePath FROM mediaTable
+        WHERE mtype = ${CommonConstants.TYPE_WEBDAV}
+        AND (webdav_account_id IS NULL OR webdav_account_id = '')
+      `;
+
+      const resultSet = await db.querySql(querySql);
+      const updateCount = resultSet.rowCount;
+
+      if (updateCount > 0) {
+        Logger.info(TAG, `发现 ${updateCount} 个WebDAV歌曲缺少webdav_account_id,开始更新`);
+
+        // 更新这些记录的webdav_account_id
+        const updateSql = `
+          UPDATE mediaTable
+          SET webdav_account_id = ?
+          WHERE mtype = ${CommonConstants.TYPE_WEBDAV}
+          AND (webdav_account_id IS NULL OR webdav_account_id = '')
+        `;
+
+        await db.executeSql(updateSql, [accountId]);
+        Logger.info(TAG, `成功更新 ${updateCount} 个WebDAV歌曲的webdav_account_id为: ${accountId}`);
+      } else {
+        Logger.info(TAG, `所有WebDAV歌曲都已有webdav_account_id,无需更新`);
+      }
+
+      resultSet.close();
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `更新WebDAV歌曲webdav_account_id失败: ${err.message}`);
+    }
+  }
+
+  /**
+   * 修复所有WebDAV歌曲的webdav_account_id字段
+   * 用于全局修复旧版本数据库中缺少webdav_account_id的记录
+   * @param accountId WebDAV账号ID(可选,如果不提供则使用当前激活的账号)
+   */
+  public async fixAllWebDavSongsAccountId(accountId?: string): Promise<void> {
+    try {
+      // 如果没有提供accountId,尝试使用当前激活的账号
+      if (!accountId) {
+        const activeAccount = await this.getActiveWebDavAccount();
+        if (activeAccount && activeAccount.id) {
+          accountId = activeAccount.id.toString();
+        } else {
+          Logger.warn(TAG, `无法找到激活的WebDAV账号来修复歌曲记录`);
+          return;
+        }
+      }
+
+      Logger.info(TAG, `开始修复所有WebDAV歌曲的webdav_account_id,使用账号ID: ${accountId}`);
+      await this.updateWebDavSongsAccountId(accountId);
+      Logger.info(TAG, `WebDAV歌曲webdav_account_id修复完成`);
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `修复WebDAV歌曲webdav_account_id失败: ${err.message}`);
+    }
+  }
+
+  /**
+   * 获取当前激活的WebDAV账号
+   */
+  private async getActiveWebDavAccount(): Promise<WebDavAccount | null> {
+    try {
+      const db = await this.dataBaseUtil.getDB();
+      const predicates = new relationalStore.RdbPredicates(this.webDavTable);
+      predicates.equalTo('isActivate', 1);
+      predicates.limitAs(1);
+
+      const resultSet = await db.query(predicates);
+      if (resultSet.rowCount > 0) {
+        resultSet.goToFirstRow();
+        const account = new WebDavAccount();
+        account.id = resultSet.getLong(resultSet.getColumnIndex('id'));
+        account.name = resultSet.getString(resultSet.getColumnIndex('name'));
+        account.host = resultSet.getString(resultSet.getColumnIndex('host'));
+        account.localHost = resultSet.getString(resultSet.getColumnIndex('localHost'));
+        account.isUseLocalHost = resultSet.getLong(resultSet.getColumnIndex('isUseLocalHost')) === 1;
+        account.port = resultSet.getLong(resultSet.getColumnIndex('port'));
+        account.filepath = resultSet.getString(resultSet.getColumnIndex('filepath'));
+        account.imageFilePath = resultSet.getString(resultSet.getColumnIndex('imageFilePath'));
+        account.lyricFilePath = resultSet.getString(resultSet.getColumnIndex('lyricFilePath'));
+        account.uploadFilePath = resultSet.getString(resultSet.getColumnIndex('uploadFilePath'));
+        account.account = resultSet.getString(resultSet.getColumnIndex('account'));
+        account.password = resultSet.getString(resultSet.getColumnIndex('password'));
+        account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
+        account.coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath'));
+        account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
+
+        resultSet.close();
+        return account;
+      } else {
+        resultSet.close();
+        return null;
+      }
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `获取激活的WebDAV账号失败: ${err.message}`);
+      return null;
+    }
+  }
+
+  // 创建WebDAV账户表
+  public createWebDavTableInDB(): Promise<void> {
+    const createTableSql = `CREATE TABLE IF NOT EXISTS ${this.webDavTable} (
+      id INTEGER PRIMARY KEY AUTOINCREMENT,
+      name TEXT,
+      isActivate INTEGER DEFAULT 1,
+      host TEXT,
+      localHost TEXT,
+      isUseLocalHost INTEGER DEFAULT 0,
+      port INTEGER,
+      filepath TEXT,
+      imageFilePath TEXT,
+      lyricFilePath TEXT,
+      uploadFilePath TEXT,
+      account TEXT,
+      password TEXT,
+      enableHttps INTEGER DEFAULT 0,
+      coverPath TEXT
+    )`;
+
+    return this.dataBaseUtil.executeSql(createTableSql)
+      .then(() => {
+        Logger.info(TAG, 'WebDAV账户表创建成功');
+
+        // 检查并添加新字段(用于数据库升级)
+        return this.upgradeWebDavTable();
+      })
+      .catch((err: Error) => {
+        Logger.error(TAG, '创建WebDAV账户表失败:', err.message);
+        throw err;
+      });
+  }
+
+  // 升级WebDAV表结构
+  private async upgradeWebDavTable(): Promise<void> {
+    try {
+      // 直接尝试添加coverPath字段,如果字段已存在会失败但不影响应用运行
+      Logger.info(TAG, '检查数据库表结构,尝试添加coverPath字段...');
+      const addColumnSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN coverPath TEXT`;
+
+      await this.dataBaseUtil.executeSql(addColumnSql);
+      Logger.info(TAG, 'coverPath字段添加成功,数据库升级完成');
+    } catch (error) {
+      // 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
+      Logger.info(TAG, 'coverPath字段可能已存在或添加失败,继续正常运行');
+    }
+  }
+
+  // 从数据库查询所有账户
+  public async queryWebDavAccountsFromDB(): Promise<void> {
+    try {
+      // 确保表结构是最新的
+      await this.upgradeWebDavTable();
+
+      const predicates = new relationalStore.RdbPredicates(this.webDavTable);
+      // 先查询基础字段(确保这些字段在旧版本中存在)
+      const baseColumns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
+        'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
+        'account', 'password', 'enableHttps'];
+
+      const resultSet = await this.dataBaseUtil.queryData(this.webDavTable, baseColumns, predicates);
+
+      this.webDavAccounts = [];
+      while (resultSet.goToNextRow()) {
+        const account = new WebDavAccount();
+        account.id = resultSet.getLong(resultSet.getColumnIndex('id'));
+        account.name = resultSet.getString(resultSet.getColumnIndex('name'));
+        account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
+        account.host = resultSet.getString(resultSet.getColumnIndex('host'));
+        account.localHost = resultSet.getString(resultSet.getColumnIndex('localHost'));
+        account.isUseLocalHost = resultSet.getLong(resultSet.getColumnIndex('isUseLocalHost')) === 1;
+        account.port = resultSet.getLong(resultSet.getColumnIndex('port'));
+        account.filepath = resultSet.getString(resultSet.getColumnIndex('filepath'));
+        account.imageFilePath = resultSet.getString(resultSet.getColumnIndex('imageFilePath'));
+        account.lyricFilePath = resultSet.getString(resultSet.getColumnIndex('lyricFilePath'));
+        account.uploadFilePath = resultSet.getString(resultSet.getColumnIndex('uploadFilePath'));
+        account.account = resultSet.getString(resultSet.getColumnIndex('account'));
+        account.password = resultSet.getString(resultSet.getColumnIndex('password'));
+        account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
+        // 设置coverPath为默认值undefined,稍后会尝试更新
+        account.coverPath = undefined;
+
+        this.webDavAccounts.push(account);
+      }
+      resultSet.close();
+
+      // 尝试查询coverPath字段(如果升级成功)
+      try {
+        const coverPathResultSet = await this.dataBaseUtil.queryData(this.webDavTable, ['id', 'coverPath'], predicates);
+        if (coverPathResultSet.goToFirstRow()) {
+          // 创建一个映射来存储coverPath
+          const coverPathMap = new Map<number, string>();
+          do {
+            const accountId = coverPathResultSet.getLong(coverPathResultSet.getColumnIndex('id'));
+            const coverPathIndex = coverPathResultSet.getColumnIndex('coverPath');
+            const coverPath = coverPathIndex >= 0 ? coverPathResultSet.getString(coverPathIndex) : undefined;
+            if (coverPath) {
+              coverPathMap.set(accountId, coverPath);
+            }
+          } while (coverPathResultSet.goToNextRow());
+
+          // 将coverPath值赋给对应的账户
+          for (const account of this.webDavAccounts) {
+            if (coverPathMap.has(account.id)) {
+              account.coverPath = coverPathMap.get(account.id);
+            }
+          }
+        }
+        coverPathResultSet.close();
+      } catch (error) {
+        // 如果查询coverPath失败,说明字段可能不存在,忽略错误
+        Logger.info(TAG, 'coverPath字段不存在或查询失败,使用默认值');
+      }
+
+      Logger.info(TAG, '从数据库加载了', this.webDavAccounts.length.toString(), '个WebDAV账户');
+      this.notifyObservers(WebdavManagerStates.QueryAccountsSucceed);
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, '查询WebDAV账户失败:', error.message);
+      this.notifyObservers(WebdavManagerStates.QueryAccountsFailed);
+      throw error;
+    }
+  }
+
+  // 插入新账户
+  public async insertAccount(
+    name: string,
+    host: string,
+    localHost: string,
+    isUseLocalHost: boolean,
+    port: number,
+    filepath: string,
+    lyricFilePath: string,
+    uploadFilePath: string,
+    imageFilePath: string,
+    account: string,
+    password: string,
+    enableHttps: boolean,
+    coverPath?: string
+  ): Promise<void> {
+    try {
+      const values: relationalStore.ValuesBucket = {
+        'name': name,
+        'isActivate': 1,
+        'host': host,
+        'localHost': localHost,
+        'isUseLocalHost': isUseLocalHost ? 1 : 0,
+        'port': port,
+        'filepath': filepath,
+        'imageFilePath': imageFilePath,
+        'lyricFilePath': lyricFilePath,
+        'uploadFilePath': uploadFilePath,
+        'account': account,
+        'password': password,
+        'enableHttps': enableHttps ? 1 : 0,
+        'coverPath': coverPath || null
+      };
+
+      await this.dataBaseUtil.insertData(this.webDavTable, values);
+      Logger.info(TAG, '插入WebDAV账户成功:', name);
+
+      await this.queryWebDavAccountsFromDB();
+      this.notifyObservers(WebdavManagerStates.InsertAccountSucceed);
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, '插入WebDAV账户失败:', error.message);
+      this.notifyObservers(WebdavManagerStates.InsertAccountFailed);
+      throw error;
+    }
+  }
+
+  // 编辑账户
+  public async editAccount(account: WebDavAccount): Promise<void> {
+    try {
+      const values: relationalStore.ValuesBucket = {
+        'name': account.name,
+        'isActivate': account.isActivate ? 1 : 0,
+        'host': account.host,
+        'localHost': account.localHost,
+        'isUseLocalHost': account.isUseLocalHost ? 1 : 0,
+        'port': account.port,
+        'filepath': account.filepath,
+        'imageFilePath': account.imageFilePath,
+        'lyricFilePath': account.lyricFilePath,
+        'uploadFilePath': account.uploadFilePath,
+        'account': account.account,
+        'password': account.password,
+        'enableHttps': account.enableHttps ? 1 : 0,
+        'coverPath': account.coverPath || null
+      };
+
+      const predicates = new relationalStore.RdbPredicates(this.webDavTable);
+      predicates.equalTo('id', account.id);
+
+      await this.dataBaseUtil.updateData(this.webDavTable, values, predicates);
+      Logger.info(TAG, '更新WebDAV账户成功:', account.name);
+
+      await this.queryWebDavAccountsFromDB();
+      this.notifyObservers(WebdavManagerStates.EditAccountSucceed);
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, '更新WebDAV账户失败:', error.message);
+      this.notifyObservers(WebdavManagerStates.EditAccountFailed);
+      throw error;
+    }
+  }
+
+  // 删除账户
+  public async removeAccount(account: WebDavAccount): Promise<void> {
+    try {
+      const predicates = new relationalStore.RdbPredicates(this.webDavTable);
+      predicates.equalTo('id', account.id);
+
+      await this.dataBaseUtil.deleteData(predicates);
+      Logger.info(TAG, '删除WebDAV账户成功:', account.name);
+
+      await this.queryWebDavAccountsFromDB();
+      this.notifyObservers(WebdavManagerStates.RemoveAccountSucceed);
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, '删除WebDAV账户失败:', error.message);
+      this.notifyObservers(WebdavManagerStates.RemoveAccountFailed);
+      throw error;
+    }
+  }
+
+  // 获取所有账户
+  public getAllWebDavAccounts(): WebDavAccount[] {
+    return this.webDavAccounts;
+  }
+
+  // 获取激活的账户
+  public getActivatedWebDavAccount(): WebDavAccount | null {
+    if(this.currentAccount)
+      return this.currentAccount;
+    for (let i = 0; i < this.webDavAccounts.length; i++) {
+      const account = this.webDavAccounts[i];
+      if (account.isActivate) {
+        return account;
+      }
+    }
+    return null;
+  }
+
+  // ==================== 文件操作 ====================
+
+  // 从WebDAV加载文件列表
+  public async loadFilesInfoFromWebdav(customPath?: string): Promise<void> {
+
+    const account = this.getActivatedWebDavAccount();
+    if (!account) {
+      Logger.error(TAG, '没有激活的WebDAV账户');
+      this.notifyObservers(WebdavManagerStates.LoadFilesInfoFailed);
+      return;
+    }
+
+    try {
+      this.notifyObservers(WebdavManagerStates.LoadFilesInfoStart);
+
+      // 使用自定义路径或账户默认路径
+      const path = customPath !== undefined ? customPath : account.filepath;
+      this.currentPath = path;
+
+      const files = await this.rcpSocket.getFileList(
+        account.host,
+        account.localHost,
+        account.isUseLocalHost,
+        account.port,
+        path,
+        account.account,
+        account.password,
+        account.enableHttps
+      );
+
+      // 保存所有文件(包括文件夹)
+      // 直接使用从RcpSocketUtil返回的FileInfo对象
+      this.webDavFiles = [];
+      for (let i = 0; i < files.length; i++) {
+        const file = files[i];
+        // 直接添加原始文件对象
+        this.webDavFiles.push(file);
+      }
+
+      // 调试:输出获取到的文件总数
+      Logger.info(TAG, '从WebDAV获取到 ' + files.length + ' 个文件/文件夹');
+
+      // 分别统计文件夹和音频文件
+      let folderCount = 0;
+      let audioCount = 0;
+
+      // 过滤音频文件
+      this.webDavSongs = [];
+      for (let i = 0; i < files.length; i++) {
+        const file = files[i];
+        const fileName = file.fileName;
+
+        if (file.isDirectory) {
+          folderCount++;
+        } else if (this.isAudioFile(fileName)) {
+          audioCount++;
+          const song = this.fileInfoToVideoItem(file, account);
+          this.webDavSongs.push(song);
+        }
+      }
+
+      this.notifyObservers(WebdavManagerStates.LoadFilesInfoSucceed);
+    } catch (error) {
+      this.ErrorMessage = error as BusinessError;
+      this.notifyObservers(WebdavManagerStates.LoadFilesInfoFailed);
+    }
+  }
+
+
+  public async loadFilesInfoFromAccount(account: WebDavAccount,customPath?: string): Promise<void> {
+    this.currentAccount = account;
+
+    // 更新现有WebDAV歌曲的webdav_account_id字段
+    if (account && account.id) {
+      await this.updateWebDavSongsAccountId(account.id.toString());
+    }
+
+    try {
+      this.notifyObservers(WebdavManagerStates.LoadFilesInfoStart);
+
+      // 使用自定义路径或账户默认路径
+      const path = customPath !== undefined ? customPath : account.filepath;
+      this.currentPath = path;
+
+      const files = await this.rcpSocket.getFileList(
+        account.host,
+        account.localHost,
+        account.isUseLocalHost,
+        account.port,
+        path,
+        account.account,
+        account.password,
+        account.enableHttps
+      );
+
+      // 保存所有文件(包括文件夹)
+      // 直接使用从RcpSocketUtil返回的FileInfo对象
+      this.webDavFiles = [];
+      for (let i = 0; i < files.length; i++) {
+        const file = files[i];
+        // 直接添加原始文件对象
+        this.webDavFiles.push(file);
+      }
+
+      // 调试:输出获取到的文件总数
+      Logger.info(TAG, '从WebDAV获取到 ' + files.length + ' 个文件/文件夹');
+
+      // 分别统计文件夹和音频文件
+      let folderCount = 0;
+      let audioCount = 0;
+
+      // 过滤音频文件
+      this.webDavSongs = [];
+      for (let i = 0; i < files.length; i++) {
+        const file = files[i];
+        const fileName = file.fileName;
+
+        if (file.isDirectory) {
+          folderCount++;
+        } else if (this.isAudioFile(fileName)) {
+          audioCount++;
+          const song = this.fileInfoToVideoItem(file, account);
+          this.webDavSongs.push(song);
+        }
+      }
+
+      this.notifyObservers(WebdavManagerStates.LoadFilesInfoSucceed);
+    } catch (error) {
+      this.ErrorMessage = error as BusinessError;
+      this.notifyObservers(WebdavManagerStates.LoadFilesInfoFailed);
+    }
+  }
+
+  // 将FileInfo转换为VideoItem
+  private fileInfoToVideoItem(fileInfo: FileInfo, account: WebDavAccount): VideoItem {
+    // 构建安全的WebDAV URL(不包含认证信息)
+    const protocol = account.enableHttps ? 'https' : 'http';
+    const host = account.isUseLocalHost ? account.localHost : account.host;
+    const port = account.port;
+
+    // 构建基础URL,确保路径重新编码(目录/文件名中的中文与空格)
+    const encodedHref = encodeURI(fileInfo.href); // 仅编码非保留字符,已编码的百分号不重复编码
+    const filePath = `${protocol}://${host}:${port}${encodedHref}`;
+    Logger.info(TAG, '编码后的WebDAV URL: ' + filePath);
+
+    // 创建VideoItem对象
+    // 构造函数签名: (name, id, filePath, type, videoSize, cTime, pixelMap?, size?, pixelMapPath?, artist?, album?, fileName?, lastPlayed?)
+
+    const videoItem = new VideoItem(
+      this.getFileNameWithoutExtension(fileInfo.fileName), // name: 歌曲名
+      '', // id: 空字符串,WebDAV文件无本地ID
+      filePath, // filePath: 文件路径
+      CommonConstants.TYPE_WEBDAV, // type: WebDAV类型
+      fileInfo.contentLength, // videoSize: 文件大小
+      Utility.getFormatDateStr(fileInfo.time,'yyyy-MM-dd HH:mm'), // cTime: 修改时间
+      undefined, // pixelMap
+      undefined, // size
+      undefined, // pixelMapPath: 对于WebDAV歌曲不设置图片路径
+      Constants.UNKNOWN_ARTIST, // artist: 艺术家
+      undefined, // album: 专辑
+      fileInfo.fileName // fileName: 真实文件名
+    );
+    videoItem.size = Utility.formatFSize(fileInfo.contentLength)
+    //根据文件名解析艺术家
+    const musicData:MusicInfo = parseMusicFileName(fileInfo.fileName);
+    if (musicData.isValid)  {
+      console.log('onecold parseMusicFileName  artist:', musicData.artist)
+      videoItem.artist = musicData.artist
+      videoItem.name = musicData.title
+    }
+
+    // 设置WebDAV账号ID,用于后续认证信息查询
+    if (account && account.id) {
+      videoItem.webdav_account_id = account.id.toString();
+      Logger.info(TAG, `设置WebDAV歌曲 ${videoItem.name} 的账号ID: ${videoItem.webdav_account_id}`);
+    }
+
+    return videoItem;
+  }
+
+  // 判断是否为音频文件
+  private isAudioFile(fileName: string): boolean {
+    const ext = this.getFileExtension(fileName).toLowerCase();
+    for (let i = 0; i < this.audioExtensions.length; i++) {
+      if (ext === this.audioExtensions[i]) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // 获取文件扩展名
+  private getFileExtension(fileName: string): string {
+    const lastDotIndex = fileName.lastIndexOf('.');
+    if (lastDotIndex === -1) {
+      return '';
+    }
+    return fileName.substring(lastDotIndex);
+  }
+
+  // 获取不带扩展名的文件名
+  private getFileNameWithoutExtension(fileName: string): string {
+    const lastDotIndex = fileName.lastIndexOf('.');
+    if (lastDotIndex === -1) {
+      return fileName;
+    }
+    return fileName.substring(0, lastDotIndex);
+  }
+
+  // 进入文件夹
+  public async enterFolder(folder: FileInfo): Promise<void> {
+    if (!folder.isDirectory) {
+      Logger.error(TAG, '不是文件夹,无法进入');
+      return;
+    }
+
+    Logger.info(TAG, '准备进入文件夹:', folder.fileName);
+    Logger.info(TAG, '当前路径:', this.currentPath);
+    Logger.info(TAG, '目标路径:', folder.href);
+
+    // 保存当前路径到历史记录
+    this.pathHistory.push(this.currentPath);
+    Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
+
+    // 加载文件夹内容
+    await this.loadFilesInfoFromWebdav(folder.href);
+  }
+
+  public async enterFolderFromPath(path: string): Promise<void> {
+
+    Logger.info(TAG, '当前路径:', this.currentPath);
+    Logger.info(TAG, '目标路径:', path);
+    // 保存当前路径到历史记录
+    this.pathHistory.push(this.currentPath);
+    Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
+    // 加载文件夹内容
+    await this.loadFilesInfoFromWebdav(path);
+  }
+
+  // 在 WebdavManager 类中添加以下方法
+  navigateToBreadcrumb(breadcrumbIndex: number): Promise<string> {
+    return new Promise((resolve, reject) => {
+      try {
+        // 面包屑索引0是"根目录"
+        if (breadcrumbIndex === 0) {
+          resolve('/');
+          return;
+        }
+
+        // 根据当前路径构建目标路径
+        const pathParts = this.currentPath.split('/').filter(part => part !== '');
+
+        // 验证索引有效性
+        if (breadcrumbIndex > pathParts.length) {
+          reject(new Error('Invalid breadcrumb index'));
+          return;
+        }
+
+        // 构建目标路径
+        let targetPath = '/';
+        for (let i = 0; i < breadcrumbIndex; i++) {
+          targetPath += pathParts[i] + '/';
+        }
+
+        resolve(targetPath);
+      } catch (error) {
+        reject(error);
+      }
+    });
+  }
+
+
+  // 返回上级目录
+  public async goBack(): Promise<void> {
+    if (this.pathHistory.length === 0) {
+      Logger.info(TAG, '已经在根目录,无法返回');
+      return;
+    }
+
+    // 从历史记录中取出上一级路径
+    const previousPath = this.pathHistory.pop();
+    if (previousPath !== undefined) {
+      await this.loadFilesInfoFromWebdav(previousPath);
+    }
+  }
+
+  // 获取面包屑路径数组
+  public getBreadcrumbs(): string[] {
+    if (!this.currentPath || this.currentPath === '/') {
+      return ['根目录'];
+    }
+
+    const parts = this.currentPath.split('/').filter(part => part !== '');
+    const breadcrumbs = ['根目录'];
+
+    for (let i = 0; i < parts.length; i++) {
+      breadcrumbs.push(parts[i]);
+    }
+
+    return breadcrumbs;
+  }
+
+  // 是否可以返回上级
+  public canGoBack(): boolean {
+    return this.pathHistory.length > 0;
+  }
+
+  // ==================== Preferences操作 ====================
+
+  // 加载历史数据(从Preferences迁移)
+  public async loadInfo(): Promise<void> {
+    try {
+      // 这里可以添加从Preferences加载历史配置的逻辑
+      Logger.info(TAG, '从Preferences加载配置');
+    } catch (error) {
+      Logger.error(TAG, '从Preferences加载配置失败:', error.toString());
+    }
+  }
+
+  // 保存配置到Preferences
+  public async saveInfo(): Promise<void> {
+    try {
+      // 这里可以添加保存配置到Preferences的逻辑
+      Logger.info(TAG, '保存配置到Preferences');
+    } catch (error) {
+      Logger.error(TAG, '保存配置到Preferences失败:', error.toString());
+    }
+  }
+
+  // ==================== 下载队列管理 ====================
+
+  // 添加到下载队列
+  public addToDownloadQueue(song: VideoItem, account: WebDavAccount): void {
+    const task: TransferTask = { song, account };
+    this.downloadQueue.push(task);
+    Logger.info(TAG, '添加到下载队列:', song.name);
+    this.notifyObservers(WebdavManagerStates.DownloadQueueChanged);
+  }
+
+  // 从下载队列移除
+  public removeFromDownloadQueue(index: number): void {
+    if (index >= 0 && index < this.downloadQueue.length) {
+      const task = this.downloadQueue[index];
+      this.downloadQueue.splice(index, 1);
+      Logger.info(TAG, '从下载队列移除:', task.song.name);
+      this.notifyObservers(WebdavManagerStates.DownloadQueueChanged);
+    }
+  }
+
+  // 清空下载队列
+  public clearDownloadQueue(): void {
+    this.downloadQueue = [];
+    Logger.info(TAG, '清空下载队列');
+    this.notifyObservers(WebdavManagerStates.DownloadQueueChanged);
+  }
+
+  // ==================== 上传队列管理 ====================
+
+  // 添加到上传队列
+  public addToUploadQueue(song: VideoItem, account: WebDavAccount): void {
+    const task: TransferTask = { song, account };
+    this.uploadQueue.push(task);
+    Logger.info(TAG, '添加到上传队列:', song.name);
+    this.notifyObservers(WebdavManagerStates.UploadQueueChanged);
+  }
+
+  // 从上传队列移除
+  public removeFromUploadQueue(index: number): void {
+    if (index >= 0 && index < this.uploadQueue.length) {
+      const task = this.uploadQueue[index];
+      this.uploadQueue.splice(index, 1);
+      Logger.info(TAG, '从上传队列移除:', task.song.name);
+      this.notifyObservers(WebdavManagerStates.UploadQueueChanged);
+    }
+  }
+
+  // 清空上传队列
+  public clearUploadQueue(): void {
+    this.uploadQueue = [];
+    Logger.info(TAG, '清空上传队列');
+    this.notifyObservers(WebdavManagerStates.UploadQueueChanged);
+  }
+
+  // ==================== 安全认证方法 ====================
+
+  // 获取WebDAV认证信息(用于播放器)
+  public async getWebDavAuthHeaders(accountId: string): Promise<WebDavAuthInfo | null> {
+    const account = await this.getWebDavAccountById(accountId);
+    if (!account || !account.account || !account.password) {
+      Logger.error(TAG, '无效的WebDAV账户或缺少认证信息');
+      return null;
+    }
+
+    // 构建基础URL
+    const protocol: string = account.enableHttps ? 'https' : 'http';
+    const host: string = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+    const port: number = account.port;
+
+    // 构建认证头
+    const credentials = buffer
+      .from(`${account.account}:${account.password}`)
+      .toString("base64");
+
+    const authInfo: WebDavAuthInfo = {
+      headers: {
+        'Authorization': `Basic ${credentials}`,
+        'User-Agent': 'TTMusic/1.0'
+      },
+      url: `${protocol}://${host}:${port}`,
+      accountId: accountId
+    };
+    return authInfo;
+  }
+
+}
+
+
+
+/**
+ * 构建HTTP请求头,特别处理WebDAV认证
+ * @param currentSong - 当前播放的歌曲信息
+ * @param videoUrl - 当前歌曲的URL
+ * @param webDavAuthItem - 当前WebDAV认证信息(实例变量)
+ * @returns Promise<Map<string, string>> HTTP请求头
+ */
+export async function buildHttpHeadersWithWebDav(
+  currentSong: VideoItem | undefined,
+  videoUrl: string,
+  webDavAuthItem: WebDavAuthItem
+): Promise<Map<string, string>> {
+  const headers = new Map<string, string>();
+
+  let isWebDavSong = false;
+
+  if (currentSong) {
+    Logger.info(`heanup 当前歌曲信息 - name: ${currentSong.name}, type: ${currentSong.type}, filePath: ${currentSong.filePath}`);
+
+    // 检查是否为WebDAV歌曲
+    if (currentSong.type === CommonConstants.TYPE_WEBDAV) {
+      isWebDavSong = true;
+
+      // 优先从实例变量获取认证信息
+      if (webDavAuthItem) {
+        Logger.info(`heanup 从实例变量获取到WebDAV认证信息,账户ID: ${webDavAuthItem.accountId}`);
+      } else {
+        // 回退到全局上下文
+        try {
+          const globalContext = GlobalContext.getContext();
+          webDavAuthItem = globalContext.getObject('webDavAuthInfo') as WebDavAuthItem;
+          if (webDavAuthItem) {
+            Logger.info(`heanup 从全局上下文获取到WebDAV认证信息,账户ID: ${webDavAuthItem.accountId}`);
+          } else {
+            Logger.warn(`heanup 全局上下文中没有WebDAV认证信息`);
+          }
+        } catch (error) {
+          Logger.error(`heanup 获取全局上下文失败:`, error.toString());
+        }
+      }
+
+      // 如果识别为WebDAV但没有认证信息,记录警告
+      if (!webDavAuthItem) {
+        Logger.warn(`heanup 识别为WebDAV播放但缺少认证信息,可能需要手动配置WebDAV账户`);
+      }
+    }
+
+    if (isWebDavSong && webDavAuthItem) {
+      Logger.info(`heanup 检测到WebDAV播放,添加安全认证头`);
+      Logger.info(`heanup 歌曲URL: ${videoUrl}`);
+
+      try {
+        if (webDavAuthItem && webDavAuthItem.accountId) {
+          Logger.info(`heanup 找到WebDAV认证信息,账户ID: ${webDavAuthItem.accountId}`);
+          // 使用WebdavManager获取认证头
+          const webdavManager = WebdavManager.getInstance();
+          const authHeaders = await webdavManager.getWebDavAuthHeaders(webDavAuthItem.accountId.toString());
+
+          if (authHeaders) {
+            // 添加Basic认证头(使用规范字段名)
+            headers.set("Authorization", authHeaders.headers.Authorization);
+          } else {
+            Logger.error(`heanup 无法获取WebDAV认证头`);
+          }
+        } else {
+          Logger.error(`heanup WebDAV认证信息无效或缺少accountId`);
+        }
+      } catch (error) {
+        Logger.error(`heanup 获取WebDAV认证信息失败:`, error.toString());
+      }
+
+      // 添加标准的WebDAV请求头
+      headers.set("User-Agent", "TTMusic-WebDAV/1.0");
+      headers.set("Accept", "*/*");
+      // 请求首段数据,触发服务器返回 206(部分内容)以适配流式播放
+      headers.set("Range", "bytes=0-");
+
+      Logger.info(`heanup WebDAV安全认证头设置完成`);
+    }
+  }
+
+  // 输出所有设置的头部信息用于调试
+  console.log(`heanup 设置的HTTP头部信息:`);
+  const headerIterator = headers.entries();
+  let headerEntry = headerIterator.next();
+  while (!headerEntry.done) {
+    const key = headerEntry.value[0];
+    const value = headerEntry.value[1];
+    console.log(`heanup ${key}: ${value}`);
+    headerEntry = headerIterator.next();
+  }
+  
+  return headers;
+}
+
+/**
+ * WebDAV认证信息(LocalMusic专用)
+ */
+export interface WebDavAuthItem {
+  accountId: number;
+  host: string;
+  port: number;
+  account: string;
+  password: string;
+  enableHttps: boolean;
+}

+ 48 - 32
entry/src/main/ets/dialog/PlaylistDialog.ets

@@ -60,6 +60,17 @@ struct PlaylistDialogContent {
   }
 
   build() {
+    Scroll(){
+      this.contentBuilder()
+    }
+    .width('100%')
+    .scrollBar(BarState.Off)
+
+
+  }
+
+  @Builder
+  contentBuilder() {
     Column({ space: 20 }) {
       // 标题
       // Text(this.isEditMode ? '编辑歌单' : '创建歌单')
@@ -68,14 +79,13 @@ struct PlaylistDialogContent {
       //   .fontColor($r('app.color.text_color'))
       //   .visibility(this.isEditMode?Visibility.None:Visibility.Visible)
       //   .margin({ top: 20 })
-
       // 歌单封面选择
       Column({ space: 8 }) {
         Text('歌单封面(可选)')
           .fontSize(14)
           .fontColor(this.isDarkMode ? '#EBEBF5' : '#666666')
           .fontWeight(FontWeight.Medium)
-          .alignSelf(ItemAlign.Start)
+          .alignSelf(ItemAlign.Start);
 
         // 封面选择区域
         Row({ space: 12 }) {
@@ -86,25 +96,25 @@ struct PlaylistDialogContent {
                 .width(80)
                 .height(80)
                 .borderRadius(8)
-                .objectFit(ImageFit.Cover)
+                .objectFit(ImageFit.Cover);
             } else {
               // 默认封面图标
               Column() {
                 Image($r('app.media.hm_music'))
                   .width(40)
                   .height(40)
-                  .fillColor(this.isDarkMode ? Color.White : this.themeColor)
+                  .fillColor(this.isDarkMode ? Color.White : this.themeColor);
               }
               .width(80)
               .height(80)
               .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))
               .borderRadius(8)
-              .justifyContent(FlexAlign.Center)
+              .justifyContent(FlexAlign.Center);
             }
           }
           .onClick(() => {
-            this.handleSelectCover()
-          })
+            this.handleSelectCover();
+          });
 
           // 操作按钮
           Column({ space: 8 }) {
@@ -115,8 +125,8 @@ struct PlaylistDialogContent {
               .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color'))
               .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))
               .onClick(() => {
-                this.handleSelectCover()
-              })
+                this.handleSelectCover();
+              });
 
             if (this.coverPath) {
               Button('移除封面')
@@ -126,17 +136,17 @@ struct PlaylistDialogContent {
                 .fontColor(this.isDarkMode ? '#FF453A' : Color.Red)
                 .backgroundColor(this.isDarkMode ? 'rgba(255,69,58,0.2)' : '#FFE5E5')
                 .onClick(() => {
-                  this.handleRemoveCover()
-                })
+                  this.handleRemoveCover();
+                });
             }
           }
-          .alignItems(HorizontalAlign.Start)
+          .alignItems(HorizontalAlign.Start);
         }
         .width('100%')
-        .justifyContent(FlexAlign.Start)
+        .justifyContent(FlexAlign.Start);
       }
       .width('100%')
-      .alignItems(HorizontalAlign.Start)
+      .alignItems(HorizontalAlign.Start);
 
       // 歌单名称输入框
       Column({ space: 8 }) {
@@ -144,7 +154,7 @@ struct PlaylistDialogContent {
           .fontSize(14)
           .fontColor(this.isDarkMode ? '#EBEBF5' : '#666666')
           .fontWeight(FontWeight.Medium)
-          .alignSelf(ItemAlign.Start)
+          .alignSelf(ItemAlign.Start);
 
         TextInput({ placeholder: '请输入歌单名称', text: this.playlistName })
           .width('100%')
@@ -156,11 +166,11 @@ struct PlaylistDialogContent {
           .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color'))
           .placeholderColor(this.isDarkMode ? '#8E8E93' : '#999999')
           .onChange((value: string) => {
-            this.playlistName = value
-          })
+            this.playlistName = value;
+          });
       }
       .width('100%')
-      .alignItems(HorizontalAlign.Start)
+      .alignItems(HorizontalAlign.Start);
 
       // 歌单描述输入框
       Column({ space: 8 }) {
@@ -168,27 +178,32 @@ struct PlaylistDialogContent {
           .fontSize(14)
           .fontColor(this.isDarkMode ? '#EBEBF5' : '#666666')
           .fontWeight(FontWeight.Medium)
-          .alignSelf(ItemAlign.Start)
+          .alignSelf(ItemAlign.Start);
 
         TextArea({ placeholder: '请输入歌单描述', text: this.playlistDescription })
           .width('100%')
           .height(80)
           // .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))
           .borderRadius(8)
-          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
+          .padding({
+            left: 12,
+            right: 12,
+            top: 8,
+            bottom: 8
+          })
           .fontSize(14)
           .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color'))
           .placeholderColor(this.isDarkMode ? '#8E8E93' : '#999999')
           .onChange((value: string) => {
-            this.playlistDescription = value
-          })
+            this.playlistDescription = value;
+          });
       }
       .width('100%')
-      .alignItems(HorizontalAlign.Start)
+      .alignItems(HorizontalAlign.Start);
 
       // 按钮区域
       Row({ space: 12 }) {
-        Button('取消',{ type: ButtonType.Capsule, stateEffect: true })
+        Button('取消', { type: ButtonType.Capsule, stateEffect: true })
           .width('45%')
           .height(40)
           .backgroundColor(this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.8, this.isDarkMode)
@@ -197,25 +212,26 @@ struct PlaylistDialogContent {
           .fontSize(14)
           .fontColor($r('app.color.cancel_button_text'))
           .onClick(() => {
-            this.onCancel?.()
+            this.onCancel?.();
             // 关闭对话框
-            DialogHelper.closeDialog(this.isEditMode ? 'editPlaylistDialog' : 'createPlaylistDialog')
-          })
+            DialogHelper.closeDialog(this.isEditMode ? 'editPlaylistDialog' : 'createPlaylistDialog');
+          });
 
-        Button(this.isEditMode ? '保存' : '创建',{ type: ButtonType.Capsule, stateEffect: true })
+        Button(this.isEditMode ? '保存' : '创建', { type: ButtonType.Capsule, stateEffect: true })
           .width('45%')
           .height(40)
-          .backgroundColor(this.isDarkMode ? $r('app.color.silvery'):this.themeColor)
+          .backgroundColor(this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.8, this.isDarkMode) :
+            this.themeColor)
           .borderRadius(8)
           .fontSize(14)
           .fontColor(Color.White)
           .onClick(() => {
-            this.handleConfirm()
-          })
+            this.handleConfirm();
+          });
       }
       .width('100%')
       .justifyContent(FlexAlign.SpaceBetween)
-      .margin({ top: 20, bottom: 20 })
+      .margin({ top: 20, bottom: 20 });
     }
     .width('100%')
     // .backgroundColor(this.isDarkMode ? '#1C1C1E' : $r('app.color.dialog_background'))

+ 343 - 0
entry/src/main/ets/dialog/WebDavAccountDialog.ets

@@ -0,0 +1,343 @@
+import { ToastUtil } from '@pura/harmony-utils';
+import { CommonConstants } from '../common/constants/CommonConstants';
+import { WebDavAccount } from '../viewmodel/WebDavAccount';
+import { ImagePickerUtil } from '../common/util/ImagePickerUtil';
+import Logger from '../common/util/Logger';
+import { ConfigurationConstant, Context } from '@kit.AbilityKit';
+
+// 工具函数:将 #RRGGBB 字符串和透明度转为 'rgba(r,g,b,a)' 字符串,深色模式下可返回深色变体
+function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: boolean = false): string {
+  if (isDarkMode) {
+    // 深色模式下返回更深的灰色或半透明黑色
+    return `rgba(34,34,34,${alpha + 0.2 > 1 ? 1 : alpha + 0.2})`;
+  }
+  const color = themeColor.replace('#', '');
+  const r = parseInt(color.substring(0, 2), 16);
+  const g = parseInt(color.substring(2, 4), 16);
+  const b = parseInt(color.substring(4, 6), 16);
+  return `rgba(${r},${g},${b},${alpha})`;
+}
+
+// WebDAV账户对话框
+@Component
+export struct WebDavAccountDialog {
+  @Prop isEditMode: boolean = false;
+  @Prop account: WebDavAccount;
+  onConfirm?: (account: WebDavAccount) => void;
+  onCancel?: () => void;
+  @State accountName: string = 'demo';
+  @State host: string = 'myhome.ss5.xyz';
+  @State port: number = 5005;
+  @State filepath: string = '/';
+  @State username: string = 'chendeben';
+  @State password: string = 'chen384626WYT';
+  @State enableHttps: boolean = false;
+  @State coverPath: string = '';
+  @State isDarkMode: boolean = false;
+  @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
+  @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
+    ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
+
+  onColorModeChange() {
+    this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
+  }
+
+  aboutToAppear(): void {
+    // 初始化深色模式状态
+    this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
+
+    if(this.isEditMode){
+      this.accountName = this.account.name;
+      this.host = this.account.host;
+      this.port = this.account.port;
+      this.filepath = this.account.filepath;
+      this.username = this.account.account;
+      this.password = this.account.password;
+      this.enableHttps = this.account.enableHttps;
+      this.coverPath = this.account.coverPath || '';
+      Logger.info('heanup WebDavAccountDialog', `编辑模式加载账户: ${this.accountName}, 封面路径: ${this.coverPath}`);
+      if (this.coverPath) {
+        Logger.info('heanup WebDavAccountDialog', `封面文件是否存在: ${ImagePickerUtil.imageExists(this.coverPath)}`);
+      }
+    }
+  }
+
+  build() {
+    Scroll(){
+      this.contentBuilder()
+    }
+    .height('100%')
+    .width('100%')
+    .scrollBar(BarState.Off)
+
+  }
+
+  @Builder
+  contentBuilder() {
+    Column({ space: 16 }) {
+      // 标题
+      Text(this.isEditMode ? '编辑账户' : '添加账户')
+        .fontSize(18)
+        .fontWeight(FontWeight.Bold)
+        .fontColor($r('app.color.index_tab_font_color'));
+
+      // 账户封面选择
+      Column({ space: 8 }) {
+        Text('账户封面(可选)')
+          .fontSize(14)
+          .fontColor(this.isDarkMode ? '#EBEBF5' : '#666666')
+          .fontWeight(FontWeight.Medium)
+          .alignSelf(ItemAlign.Start);
+
+        // 封面选择区域
+        Row({ space: 12 }) {
+          // 封面预览
+          Stack() {
+            if (this.coverPath) {
+              Image(this.coverPath)
+                .width(80)
+                .height(80)
+                .borderRadius(8)
+                .objectFit(ImageFit.Cover);
+            } else {
+              // 默认封面图标
+              Column() {
+                Image($r('app.media.cloudDisk'))
+                  .width(40)
+                  .height(40)
+                  .fillColor(this.isDarkMode ? Color.White : this.themeColor);
+              }
+              .width(80)
+              .height(80)
+              .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))
+              .borderRadius(8)
+              .justifyContent(FlexAlign.Center);
+            }
+          }
+          .onClick(() => {
+            this.handleSelectCover();
+          });
+
+          // 操作按钮
+          Column({ space: 8 }) {
+            Button('选择封面')
+              .width(120)
+              .height(36)
+              .fontSize(12)
+              .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color'))
+              .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))
+              .onClick(() => {
+                this.handleSelectCover();
+              });
+
+            if (this.coverPath) {
+              Button('移除封面')
+                .width(120)
+                .height(36)
+                .fontSize(12)
+                .fontColor(this.isDarkMode ? '#FF453A' : Color.Red)
+                .backgroundColor(this.isDarkMode ? 'rgba(255,69,58,0.2)' : '#FFE5E5')
+                .onClick(() => {
+                  this.handleRemoveCover();
+                });
+            }
+          }
+          .alignItems(HorizontalAlign.Start);
+        }
+        .width('100%')
+        .justifyContent(FlexAlign.Start);
+      }
+      .width('100%')
+      .alignItems(HorizontalAlign.Start);
+
+      // 账户名称
+      Row({ space: 8 }) {
+        Text('名称')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'));
+        TextArea({ placeholder: '请输入账户名称', text: this.accountName })
+          .layoutWeight(1)
+          .maxLines(1)
+          .onChange((value: string) => {
+            this.accountName = value;
+          });
+      }
+      .width('100%')
+      .alignItems(VerticalAlign.Center);
+
+      // 服务器地址
+      Row({ space: 8 }) {
+        Text('服务器')
+          .fontSize(14)
+          .maxLines(2)
+          .fontColor($r('app.color.index_tab_font_color'));
+        TextArea({ placeholder: '例如: example.com', text: this.host })
+          .layoutWeight(1)
+          .onChange((value: string) => {
+            this.host = value;
+          });
+      }
+      .alignItems(VerticalAlign.Center);
+
+      // 端口
+      Row({ space: 8 }) {
+        Text('端口')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'));
+        TextInput({ placeholder: '默认: 80', text: this.port.toString() })
+          .layoutWeight(1)
+          .maxLines(1)
+          .type(InputType.Number)
+          .onChange((value: string) => {
+            this.port = parseInt(value) || 80;
+          });
+      }
+      .alignItems(VerticalAlign.Center);
+
+      // 文件目录
+      Row({ space: 8 }) {
+        Text('文件目录')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'));
+        TextInput({ placeholder: '例如: /music', text: this.filepath })
+          .layoutWeight(1)
+          .maxLines(1)
+          .onChange((value: string) => {
+            this.filepath = value;
+          });
+      }
+      .alignItems(VerticalAlign.Center);
+
+      // 用户名
+      Row({ space: 8 }) {
+        Text('用户名')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'));
+        TextInput({ placeholder: '请输入用户名', text: this.username })
+          .layoutWeight(1)
+          .maxLines(1)
+          .onChange((value: string) => {
+            this.username = value;
+          });
+      }
+      .alignItems(VerticalAlign.Center);
+
+      // 密码
+      Row({ space: 8 }) {
+        Text('密码')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'));
+        TextInput({ placeholder: '请输入密码', text: this.password })
+          .type(InputType.Password)
+          .layoutWeight(1)
+          .maxLines(2)
+          .onChange((value: string) => {
+            this.password = value;
+          });
+      }
+      .alignItems(VerticalAlign.Center);
+
+      // HTTPS开关
+      Row() {
+        Text('启用HTTPS')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'));
+        Blank();
+        Toggle({ type: ToggleType.Switch, isOn: this.enableHttps })
+          .selectedColor(this.themeColor)
+          .onChange((isOn: boolean) => {
+            this.enableHttps = isOn;
+          });
+      }
+      .width('100%');
+
+      // 按钮
+      Row({ space: 12 }) {
+        Button('取消', { type: ButtonType.Capsule })
+          .backgroundColor($r('app.color.cancel_button_background'))
+          .fontColor($r('app.color.cancel_button_text'))
+          .layoutWeight(1)
+          .onClick(() => {
+            this.onCancel?.();
+          });
+
+        Button(this.isEditMode ? '保存' : '添加', { type: ButtonType.Capsule })
+          .backgroundColor(this.themeColor)
+          .layoutWeight(1)
+          .onClick(() => {
+            if (!this.accountName || !this.host) {
+              ToastUtil.showToast('请填写账户名称和服务器地址');
+              return;
+            }
+
+            const updatedAccount = new WebDavAccount();
+            if (this.isEditMode) {
+              updatedAccount.id = this.account.id;
+            }
+            updatedAccount.name = this.accountName;
+            updatedAccount.host = this.host;
+            updatedAccount.port = this.port;
+            updatedAccount.filepath = this.filepath;
+            updatedAccount.account = this.username;
+            updatedAccount.password = this.password;
+            updatedAccount.enableHttps = this.enableHttps;
+            updatedAccount.coverPath = this.coverPath;
+            updatedAccount.isActivate = true;
+            updatedAccount.localHost = '';
+            updatedAccount.isUseLocalHost = false;
+            updatedAccount.lyricFilePath = '';
+            updatedAccount.uploadFilePath = '';
+            updatedAccount.imageFilePath = '';
+            this.onConfirm?.(updatedAccount);
+
+          });
+      }
+      .width('100%')
+      .margin({ top: 8 });
+    }
+    .padding(24)
+    .width('90%')
+    .borderRadius(16)
+  }
+
+  /**
+   * 处理选择封面
+   */
+  private async handleSelectCover() {
+    try {
+      // 需要获取上下文,这里通过全局上下文获取
+      const context = getContext(this) as Context
+      if (!context) {
+        ToastUtil.showToast('获取应用上下文失败')
+        return
+      }
+
+      const selectedPath = await ImagePickerUtil.selectSingleWebDavCover(context)
+      if (selectedPath) {
+        // 如果之前有封面,删除旧封面
+        if (this.coverPath && this.coverPath !== this.account.coverPath) {
+          ImagePickerUtil.deleteImage(this.coverPath)
+        }
+        this.coverPath = selectedPath
+        Logger.info('heanup WebDavAccountDialog', `选择封面成功: ${selectedPath}`)
+      }
+    } catch (error) {
+        Logger.error('heanup WebDavAccountDialog', `选择封面失败: ${(error as Error).message}`)
+        ToastUtil.showToast('选择封面失败')
+    }
+  }
+
+  /**
+   * 处理移除封面
+   */
+  private handleRemoveCover() {
+    if (this.coverPath) {
+      // 只有当封面不是原来的封面时才删除文件
+      if (this.coverPath !== this.account.coverPath) {
+        ImagePickerUtil.deleteImage(this.coverPath)
+      }
+      this.coverPath = ''
+      Logger.info('heanup WebDavAccountDialog', '移除封面成功')
+    }
+  }
+}

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

@@ -98,6 +98,10 @@ export default class EntryAbility extends UIAbility {
     async onCreate(want:Want, launchParam:AbilityConstant.LaunchParam) {
         AppStorage.setOrCreate('context', this.context);
         hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
+
+        // 初始化WebDAV管理器
+        this.initWebDAV();
+
         setTimeout(async ()=>{
             this.loadDoWant(want)
             await this.handleParam(want)
@@ -109,6 +113,46 @@ export default class EntryAbility extends UIAbility {
 
     }
 
+    /**
+     * 初始化WebDAV管理器
+     * 功能:
+     * 1. 设置Context
+     * 2. 初始化数据库和Preferences
+     * 3. 创建WebDAV数据库表
+     * 4. 加载WebDAV账户信息
+     */
+    private async initWebDAV() {
+        try {
+            Logger.info('heanup EntryAbility', '开始初始化WebDAV管理器');
+
+            // 1. 初始化DataBaseUtil
+            const DataBaseUtil = (await import('../common/util/DataBaseUtil')).DataBaseUtil;
+            DataBaseUtil.getInstance().setContext(this.context);
+
+            // 2. 初始化PreferencesUtil
+            // const PreferencesUtil = (await import('../common/util/PreferencesUtil')).default;
+            // PreferencesUtil.getInstance().setContext(this.context);
+
+            // 3. 初始化WebdavManager
+            const WebdavManagerModule = await import('../common/util/WebdavManager');
+            const webdavManager = WebdavManagerModule.WebdavManager.getInstance();
+            webdavManager.setContext(this.context);
+
+            // 4. 创建WebDAV数据库表
+            await webdavManager.createWebDavTableInDB();
+
+            // 5. 从数据库加载账户
+            await webdavManager.queryWebDavAccountsFromDB();
+
+            // 6. 加载历史数据(如果有迁移需求)
+            await webdavManager.loadInfo();
+
+            Logger.info('heanup EntryAbility', 'WebDAV管理器初始化成功');
+        } catch (error) {
+            Logger.error('heanup EntryAbility', `WebDAV管理器初始化失败: ${error}`);
+        }
+    }
+
     async onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
         hilog.info(0x0000, 'testTag', `onNewWant, want=${JSON.stringify(want)}`);
         super.onNewWant(want, launchParam);

+ 413 - 19
entry/src/main/ets/pages/NewIndex.ets

@@ -1,6 +1,10 @@
 import { AppUtil, ArrayUtil, DeviceUtil, DisplayUtil, LogUtil, PreferencesUtil, ToastUtil } from '@pura/harmony-utils';
 import TitleBar from '../view/TitleBar';
-import { curves, LengthMetrics, router, SegmentButton, SegmentButtonOptions, SymbolGlyphModifier } from '@kit.ArkUI';
+import { curves,
+  ImmersiveMode,
+  LengthMetrics,
+  LevelMode,
+  router, SegmentButton, SegmentButtonOptions, SymbolGlyphModifier } from '@kit.ArkUI';
 import mainViewModel, { MainViewModel } from '../viewmodel/MainViewModel';
 import ItemData from '../viewmodel/ItemData';
 import type { sysResource } from '../viewmodel/ItemData';
@@ -40,7 +44,11 @@ import { convertPlaylistSongsToVideoItems } from './PlaylistDetailPage';
 import MediaTable from '../common/util/MediaTable';
 import { showCreatePlaylistDialog } from '../dialog/PlaylistDialog';
 import { ImagePickerUtil } from '../common/util/ImagePickerUtil';
-import { ParamInfo } from '../viewmodel/ParamInfo';
+import { WebDavMainPage } from './WebDavMainPage';
+import { WebDavAccount } from '../viewmodel/WebDavAccount';
+import { WebdavManager } from '../common/util/WebdavManager';
+import { WebDavAccountDialog } from '../dialog/WebDavAccountDialog';
+import ReqPermissionUtil from '../common/util/ReqPermissionUtil';
 
 // import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
 
@@ -91,7 +99,6 @@ struct NewIndex {
   @StorageProp('isHiCarStatus') isHiCarStatus: boolean = false;
   @StorageProp('curDisplayIsHiCar') curDisplayIsHiCar: boolean = false;
   @State idDefaultMediaKu: boolean = false
-
   /**
    * 是否显示更新日志开关
    */
@@ -144,7 +151,7 @@ struct NewIndex {
   private backTime: number = 0;
 
   @State tabOptions: SegmentButtonOptions = SegmentButtonOptions.tab({
-    buttons: [{ text: '分类' }, { text: '歌单' }],
+    buttons: [{ text: '分类' }, { text: '歌单' }, { text: '网盘' }],
     direction: Direction.Ltr,
     backgroundColor: $r('app.color.index_background'),
     selectedBackgroundColor:$r('app.color.start_window_background'),
@@ -159,8 +166,10 @@ struct NewIndex {
   tabSelectedIndexesChanged() {
     if(this.tabSelectedIndexes[0]==1){
       this.mType = 0
+    } else if(this.tabSelectedIndexes[0]==2){
+      // 网盘选项卡
+      this.mType = 6
     }
-
   }
   // 歌单相关状态变量
   @State playlistList: Playlist[] = []
@@ -168,6 +177,11 @@ struct NewIndex {
   @State selectedPlaylist: Playlist | null = null
   private playlistTable: PlaylistTable | null = null
 
+  // WebDAV账户相关状态变量
+  @State webDavAccounts: WebDavAccount[] = []
+  private webdavManager: WebdavManager = WebdavManager.getInstance()
+  private _webDavLoading: boolean = false // 防止重复加载WebDAV账户的标志
+  @State selectedAccount: WebDavAccount = new WebDavAccount()
   /**
    * 返回键处理逻辑:
    * - 如果不是根目录或有历史记录,发送广播通知更新列表
@@ -216,9 +230,11 @@ struct NewIndex {
 
 
   onPageShow() {
-    LogUtil.info('heanup NewIndex', 'onPageShow 被调用,刷新歌单列表')
+    LogUtil.info('heanup NewIndex', 'onPageShow 被调用,刷新歌单列表和WebDAV账户列表')
     // 加载歌单列表
     this.loadPlaylistList()
+    // 加载WebDAV账户列表
+    this.loadWebDavAccounts()
   }
   /**
    * 页面显示生命周期钩子
@@ -230,9 +246,7 @@ struct NewIndex {
    */
 
   async aboutToAppear() {
-    let params = this.getUIContext().getRouter().getParams() as ParamInfo;
-    this.videoLocalList = params.videoList as VideoItem[];
-    console.info('onecold NewIndex aboutToAppear 被调用,length:'+ this.videoLocalList.length)
+    ReqPermissionUtil.reqPermissionsFromUser(ReqPermissionUtil.permissions, this.context);
     this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true)
     this.idDefaultMediaKu = PreferencesUtil.getBooleanSync('idDefaultMediaKu', false)
     if(this.idDefaultMediaKu){
@@ -245,7 +259,21 @@ struct NewIndex {
     })
     this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
     this.breakpointSystem.register();
+    let params = router.getParams() as Record<string, Object>;
+    if (params && params.videoList) {
+      this.videoLocalList = params.videoList as VideoItem[];
+      LogUtil.info('heanup NewIndex', '从路由参数获取到播放列表,长度:' + this.videoLocalList.length)
+    } else {
+      this.videoLocalList = []
+      LogUtil.info('heanup NewIndex', '路由参数中没有播放列表,使用空数组')
+    }
 
+    // 检查是否从WebDavPage返回
+    if (params && params.fromWebDavPage) {
+      LogUtil.info('heanup NewIndex', '从WebDavPage返回,切换到网盘标签页')
+      this.mType = 6
+      this.tabSelectedIndexes = [2] // 切换到网盘标签
+    }
     // Utility.setStatusBarLight()
     ScreenUtil.setScreenSize();
     this.bundleName = AppUtil.getBundleName()
@@ -292,6 +320,9 @@ struct NewIndex {
     // 初始化歌单数据库
     await this.initPlaylistTable();
 
+    // 初始化WebDAV管理器
+    await this.initWebDavManager();
+
     // 监听歌单刷新事件
     emitter.on({ eventId: EventConstants.EVENT_PLAYLIST_REFRESH }, (eventData: emitter.EventData) => {
       LogUtil.info('heanup NewIndex', '收到歌单刷新事件,开始刷新歌单列表')
@@ -327,10 +358,14 @@ struct NewIndex {
     this.breakpointSystem.unregister();
     emitter.off(EventConstants.EVENT_SWIPE_BACK_UPDATE);
     emitter.off(EventConstants.EVENT_USER_STATE_CHANGE);
-    
+
     // 监听歌单刷新事件
     emitter.off(EventConstants.EVENT_PLAYLIST_REFRESH);
 
+    // 清理WebDAV管理器订阅
+    if (this.webdavManager) {
+      this.webdavManager.observers = [];
+    }
   }
 
   build() {
@@ -360,6 +395,14 @@ struct NewIndex {
             .visibility(this.mType === 4 ? Visibility.Visible : Visibility.None)
           AboutPage()
             .visibility(this.mType === 5 ? Visibility.Visible : Visibility.None)
+
+          WebDavMainPage({
+            offsetX:this.offsetX,
+            isShowDrawer:this.isShowDrawer,
+            mType:this.mType,
+            selectedAccount:this.selectedAccount
+          })
+            .visibility(this.mType === 6 ? Visibility.Visible : Visibility.None)
           
           // 抽屉打开时的遮罩层,用于拦截点击事件  这个只能正常尺寸的手机竖屏的才能生效
           if (this.isShowDrawer&&this.isPhonePortrait()) {
@@ -524,7 +567,7 @@ struct NewIndex {
         SegmentButton({ options: this.tabOptions, selectedIndexes: $tabSelectedIndexes })
       }
       .padding({ bottom:10 })
-      .width('80%')
+      .width('90%')
     }
 
   }
@@ -644,8 +687,10 @@ struct NewIndex {
       ListItemGroup({ header: this.buildUserInfoCard() }) {
         if(this.tabSelectedIndexes[0]==0){//分类
           this.buildTabCate()
-        }else{//歌单
+        }else if(this.tabSelectedIndexes[0]==1){//歌单
           this.buildPlaylistTab()
+        }else if(this.tabSelectedIndexes[0]==2){//网盘
+          this.buildCloudStorageTab()
         }
 
       }
@@ -681,7 +726,7 @@ struct NewIndex {
                 .fontColor([this.themeColor])
                 .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
                 .alignSelf(ItemAlign.Center)
-                .margin({ left: 25 })
+                .margin({ left: 15 })
             } else {
               Image(item.img as Resource)
                 .height(22)
@@ -1026,7 +1071,7 @@ struct NewIndex {
           
           Text('创建歌单')
             .margin({ left: 10, right: 20 })
-            .fontSize(15)
+            .fontSize(14)
             .fontColor($r('app.color.index_tab_font_color'))
             .fontWeight(480)
           
@@ -1118,6 +1163,142 @@ struct NewIndex {
     })
   }
 
+  /**
+   * 构建网盘tab内容 - 从数据库获取所有网盘账户
+   * 支持多种类型的网盘账户(目前支持WebDAV,将来可扩展其他类型)
+   */
+  @Builder
+  buildCloudStorageTab() {
+    // 当前只支持WebDAV账户,将来可以在这里添加其他类型的网盘账户
+    // 例如:OneDrive, Google Drive, Dropbox等
+    // 添加新账户按钮
+    ListItem() {
+      Button({ type: ButtonType.Capsule, stateEffect: true }) {
+        Row() {
+          SymbolGlyph($r('sys.symbol.plus_circle'))
+            .fontSize(22)
+            .fontColor([this.themeColor])
+            .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
+            .alignSelf(ItemAlign.Center)
+            .margin({ left: 25 })
+
+          Text('添加WebDAV账户')
+            .margin({ left: 10, right: 20 })
+            .fontSize(14)
+            .fontColor($r('app.color.index_tab_font_color'))
+            .fontWeight(480)
+
+          Blank()
+
+          Image($r('app.media.arrow_right'))
+            .width(22)
+            .height(22)
+            .margin({ left: 20, right: 0 })
+            .align(Alignment.Center)
+        }
+        .width('100%')
+        .height(55)
+      }
+      .backgroundColor(Color.Transparent)
+      .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
+      .onClick(() => {
+        console.info('heanup', '点击添加WebDAV账户')
+        this.showAddWebDavAccountDialog(false)
+      })
+    }
+    // 显示所有WebDAV账户
+    ForEach(this.webDavAccounts, (account: WebDavAccount) => {
+      ListItem() {
+        Button({ type: ButtonType.Capsule, stateEffect: true }) {
+          Row() {
+            // 账户封面或默认图标
+            Stack() {
+              Image(account.coverPath?account.coverPath:$r('app.media.cloudDisk'))
+                  .width(20)
+                  .height(20)
+                  .borderRadius(4)
+                  .fillColor(this.themeColor)
+                  .objectFit(ImageFit.Cover)
+            }
+            .margin({ left: 20 })
+
+            Column() {
+              // 账户名称
+              Text(account.name)
+                .margin({ left: 8, right: 20 })
+                .fontSize(15)
+                .fontColor(this.selectedAccount==account?this.themeColor:$r('app.color.text_color'))
+                .fontWeight(480)
+                .maxLines(1)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+
+              Row() {
+                // 账户类型标签
+                Text('WebDAV')
+                  .fontSize(10)
+                  .fontColor(this.selectedAccount==account?this.themeColor:$r('app.color.index_tab_font_color'))
+                  .opacity(0.8)
+                  .padding({ left: 8, top: 2, bottom: 2 })
+                  .borderRadius(3)
+
+                // 服务器地址
+                Text(`@${account.isUseLocalHost ? account.localHost : account.host}:${account.port}`)
+                  .fontSize(12)
+                  .fontColor(this.selectedAccount==account?this.themeColor:$r('app.color.index_tab_font_color'))
+                  .opacity(0.7)
+                  .maxLines(1)
+                  .padding({ right: 8, top: 2, bottom: 2 })
+                  .textOverflow({ overflow: TextOverflow.Ellipsis })
+              }
+            }
+            .alignItems(HorizontalAlign.Start)
+
+            Blank()
+
+            Image($r('app.media.arrow_right'))
+              .width(22)
+              .height(22)
+              .margin({ left: 20, right: 0 })
+              .align(Alignment.Center)
+          }
+          .width('100%')
+          .height(60)
+        }
+        .backgroundColor(Color.Transparent)
+        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
+        .onClick(() => {
+          console.info('heanup', '点击WebDAV账户:', account.name)
+          this.selectWebDavAccount(account)
+        })
+        .bindContextMenu(this.MenuDavBuilder(account), ResponseType.LongPress,
+          {
+            preview: MenuPreviewMode.IMAGE,
+            previewAnimationOptions: { scale: [0.8, 1.0] },
+          })
+        .bindContextMenu(this.MenuDavBuilder(account), ResponseType.RightClick,
+          {
+            preview: MenuPreviewMode.IMAGE,
+            previewAnimationOptions: { scale: [0.8, 1.0] },
+          })
+      }
+    })
+
+
+
+    // 如果没有账户,显示提示信息
+    if (this.webDavAccounts.length === 0) {
+      ListItem() {
+        Text('暂无WebDAV账户,点击上方按钮添加')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'))
+          .opacity(0.6)
+          .textAlign(TextAlign.Center)
+          .width('100%')
+          .padding(20)
+      }
+    }
+  }
+
   @Builder
   MenuBuilder(playlist: Playlist) {
     Menu(){
@@ -1142,6 +1323,66 @@ struct NewIndex {
 
   }
 
+
+  @Builder
+  MenuDavBuilder(account: WebDavAccount) {
+    Menu(){
+      MenuItem({
+        symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.square_and_pencil')),
+        content: '编辑'
+      })
+        .onClick(async() => {
+         this.showAddWebDavAccountDialog(true,account)
+        })
+
+      MenuItem({
+        symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.trash')),
+        content: '删除'
+      })
+        .onClick(async() => {
+          this.deleteWebDavAccount(account)
+        })
+    }
+
+  }
+
+
+  /**
+   * 删除WebDAV账户
+   */
+  async deleteWebDavAccount(account: WebDavAccount) {
+    try {
+      // 显示确认对话框
+      this.getUIContext().showAlertDialog({
+        title: '删除账户',
+        message: `确定要删除webdav账户"${account.name}"吗?`,
+        primaryButton: {
+          value: '取消',
+          action: () => {}
+        },
+        secondaryButton: {
+          value: '删除',
+          fontColor: Color.Red,
+          action: async () => {
+            try {
+              await this.webdavManager.removeAccount(account)
+              ToastUtil.showToast('WebDAV账户删除成功')
+              // 重新加载WebDAV账户列表
+              await this.loadWebDavAccounts()
+              LogUtil.info('heanup NewIndex', 'WebDAV账户删除成功:', account.name)
+            } catch (error) {
+              LogUtil.error('heanup NewIndex', `删除WebDAV账户失败: ${(error as Error).message}`)
+              ToastUtil.showToast('删除失败')
+            }
+          }
+        }
+      })
+    } catch (error) {
+      LogUtil.error('heanup NewIndex', `删除WebDAV账户操作失败: ${(error as Error).message}`)
+      ToastUtil.showToast('操作失败')
+    }
+  }
+
   /**
    * 删除歌单
    */
@@ -1254,11 +1495,8 @@ struct NewIndex {
       if (this.playlistTable) {
         const playlists = await this.playlistTable.queryAllPlaylists()
         // 强制触发UI更新
-        this.playlistList = [...playlists]
+        this.playlistList = playlists.slice()
         LogUtil.info('heanup NewIndex', `成功加载 ${playlists.length} 个歌单`)
-        playlists.forEach((playlist, index) => {
-          LogUtil.info('heanup NewIndex', `歌单${index + 1}: ${playlist.name}, 歌曲数: ${playlist.songCount}`)
-        })
       } else {
         LogUtil.warn('heanup NewIndex', '歌单表未初始化')
       }
@@ -1266,8 +1504,165 @@ struct NewIndex {
       LogUtil.error('heanup NewIndex', `加载歌单列表失败: ${(error as Error).message}`)
     }
   }
+
+  /**
+   * 初始化WebDAV管理器
+   */
+  async initWebDavManager() {
+    try {
+      // 设置WebDAV管理器的上下文
+      this.webdavManager.setContext(this.context)
+
+      // 创建WebDAV账户表
+      await this.webdavManager.createWebDavTableInDB()
+
+      // 订阅WebDAV管理器事件
+      this.webdavManager.subscribe((event: string) => {
+        LogUtil.info('heanup NewIndex', '收到WebDAV事件:', event)
+        if (event === 'QueryAccountsSucceed') {
+          this.webDavAccounts = this.webdavManager.getAllWebDavAccounts().slice()
+        }
+      })
+
+      // 首页EntryAblility去加载WebDAV账户了。所以这边不加载
+      // await this.loadWebDavAccounts()
+
+      LogUtil.info('heanup NewIndex', 'WebDAV管理器初始化成功')
+    } catch (error) {
+      LogUtil.error('heanup NewIndex', `初始化WebDAV管理器失败: ${(error as Error).message}`)
+    }
+  }
+
+  /**
+   * 加载WebDAV账户列表
+   */
+  async loadWebDavAccounts() {
+    try {
+      LogUtil.info('heanup NewIndex', '开始加载WebDAV账户列表')
+      await this.webdavManager.queryWebDavAccountsFromDB()
+      // 强制触发UI更新
+      this.webDavAccounts = this.webdavManager.getAllWebDavAccounts().slice()
+      LogUtil.info('heanup NewIndex', `成功加载 ${this.webDavAccounts.length} 个WebDAV账户`)
+    } catch (error) {
+      LogUtil.error('heanup NewIndex', `加载WebDAV账户列表失败: ${(error as Error).message}`)
+    }
+  }
+
+  /**
+   * 选择WebDAV账户
+   */
+  selectWebDavAccount(account: WebDavAccount) {
+    try {
+      LogUtil.info('heanup NewIndex', '选择WebDAV账户:', account.name)
+      // 如果账户未激活,先激活它
+      if (!account.isActivate) {
+        // 先将所有账户设为未激活
+        this.webDavAccounts.forEach(acc => {
+          acc.isActivate = false
+        })
+
+        // 激活选中的账户
+        account.isActivate = true
+        // 更新数据库中的激活状态
+        this.webdavManager.editAccount(account).then(() => {
+          LogUtil.info('heanup NewIndex', 'WebDAV账户编辑成功:', account.name)
+
+
+        }).catch((error: Error) => {
+          LogUtil.error('heanup NewIndex', `编辑WebDAV账户失败: ${error.message}`)
+          ToastUtil.showToast('编辑账户失败')
+        })
+      }
+      this.selectedAccount = account
+      // 切换到WebDAV页面
+      this.mType = 6
+      this.doShowDrawer()
+    } catch (error) {
+      LogUtil.error('heanup NewIndex', `选择WebDAV账户失败: ${(error as Error).message}`)
+      ToastUtil.showToast('选择账户失败')
+    }
+  }
+
+  /**
+   * 显示添加WebDAV账户对话框
+   */
+  @State addDavDialogId:number = 1
+  showAddWebDavAccountDialog(isEditMode?: boolean,account?: WebDavAccount) {
+
+    const node: FrameNode | null = this.getUIContext().getFrameNodeById("test_text") || null;
+    this.getUIContext().getPromptAction().openCustomDialog({
+      builder: () => {
+        this.webDavAccountBuilder(isEditMode,account)
+      },
+      levelMode: LevelMode.EMBEDDED, // 启用页面级弹出框
+      levelUniqueId: node?.getUniqueId(), // 设置页面级弹出框所在页面的任意节点ID
+      immersiveMode: ImmersiveMode.EXTEND, // 设置页面级弹出框蒙层的显示模式
+    }).then((dialogId: number) => {
+      this.addDavDialogId = dialogId;
+    })
+
+  }
+
+  @Builder
+  webDavAccountBuilder(isEditMode?: boolean,account?: WebDavAccount) {
+     WebDavAccountDialog({
+       isEditMode: isEditMode,
+       account: account,
+       onCancel: () => {
+         // 取消添加
+         this.getUIContext().getPromptAction().closeCustomDialog(this.addDavDialogId)
+       },
+       onConfirm: (account: WebDavAccount) => {
+         this.getUIContext().getPromptAction().closeCustomDialog(this.addDavDialogId)
+
+         if (isEditMode && account?.id) {
+           // 编辑模式:更新现有账户
+           this.webdavManager.editAccount(account).then(() => {
+             ToastUtil.showToast('修改成功')
+             // 重新加载WebDAV账户列表,观察着模式去更新了,所以这里不需要更新,先注释掉
+             // this.loadWebDavAccounts()
+             LogUtil.info('heanup NewIndex', 'WebDAV账户修改成功:', account.name)
+           }).catch((error: Error) => {
+             LogUtil.error('heanup NewIndex', `修改WebDAV账户失败: ${error.message}`)
+             ToastUtil.showToast('修改失败')
+           })
+         } else {
+           // 添加模式:创建新账户
+           this.webdavManager.insertAccount(
+             account.name,
+             account.host,
+             account.localHost,
+             account.isUseLocalHost,
+             account.port,
+             account.filepath,
+             account.lyricFilePath,
+             account.uploadFilePath,
+             account.imageFilePath,
+             account.account,
+             account.password,
+             account.enableHttps,
+             account.coverPath
+           ).then(() => {
+             ToastUtil.showToast('添加成功')
+             // 重新加载WebDAV账户列表 观察着模式去更新了,所以这里不需要更新,先注释掉
+             // this.loadWebDavAccounts()
+             LogUtil.info('heanup NewIndex', 'WebDAV账户添加成功:', account.name)
+           }).catch((error: Error) => {
+             LogUtil.error('heanup NewIndex', `添加WebDAV账户失败: ${error.message}`)
+             ToastUtil.showToast('添加失败')
+           })
+         }
+       }
+
+     });
+
+  }
+
 }
 
+
+
+
 // 1. 工具函数:将 #RRGGBB 字符串和透明度转为 'rgba(r,g,b,a)' 字符串,深色模式下可返回深色变体
 function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: boolean = false): string {
   if (isDarkMode) {
@@ -1287,4 +1682,3 @@ interface HiCarAspectRatio {
   playlistId: string;
 }
 
-

+ 41 - 7
entry/src/main/ets/pages/SettingPage.ets

@@ -22,6 +22,8 @@ import { FastForwardSecondInterface } from './FastForwardSecondInterface'
 // @Entry
 @Component
 export struct SettingPage {
+  @State isCopyFileToDownLoad: boolean = false
+  static readonly IS_COPYFILE_TO_DOWNLOAD: string = 'isCopyFileToDownLoad';
   @State fastForwardSeconds: string = '10'
   @State isShowBackFast: boolean = true//快进快退按钮
   @Consume isShowDrawer: boolean;
@@ -261,6 +263,7 @@ export struct SettingPage {
     this.isLongNameRoLL = PreferencesUtil.getBooleanSync('isLongNameRoLL', true)
     this.isShowBackFast  = PreferencesUtil.getBooleanSync('isShowBackFast', false)
     this.fastForwardSeconds  = PreferencesUtil.getStringSync('fastForwardSeconds', '10')
+    this.isCopyFileToDownLoad = PreferencesUtil.getBooleanSync(SettingPage.IS_COPYFILE_TO_DOWNLOAD, false)
     // 只用 themeMode 控制主题
     this.themeMode = PreferencesUtil.getNumberSync(SettingPage.THEME_MODE, 0)
     this.applyThemeMode(this.themeMode)
@@ -585,13 +588,6 @@ export struct SettingPage {
       Scroll() {
         Column() {
 
-
-          // API设置分组
-          this.apiBuilder()
-          //设置教程
-          this.jcBuilder()
-
-
           // 主题设置分组
           Column() {
             Row() {
@@ -797,6 +793,39 @@ export struct SettingPage {
             .height(55)
             .clickEffect({ level: ClickEffectLevel.HEAVY })
 
+            Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
+
+            //导入视频复制一份到DownLoad目录
+            Row() {
+              SymbolGlyph($r('sys.symbol.save'))
+                .fontSize(20)
+                .fontColor([this.themeColor])
+                .alignSelf(ItemAlign.Center)
+                .margin({ left: 15 })
+              Text('导入音乐复制到DownLoad目录')
+                .margin({ left: 8 })
+                .fontSize(15)
+                .fontColor(Color.Gray)
+                .fontWeight(480)
+                .layoutWeight(1)
+              Toggle({ type: ToggleType.Switch, isOn: this.isCopyFileToDownLoad })
+                .selectedColor(this.themeColor)
+                .switchPointColor(Color.White)
+                .margin({ right: 18 })
+                .onChange((checked: boolean) => {
+                  // if(!this.isCopyFileToDownLoad){
+                  //   ToastUtil.showToast("读取内嵌封面和歌词功能需要开启导入复制开关,不然没有权限读取。")
+                  // }
+                  this.isCopyFileToDownLoad = checked;
+                  PreferencesUtil.put(SettingPage.IS_COPYFILE_TO_DOWNLOAD, this.isCopyFileToDownLoad)
+                  this.sendChangeEvent()
+                })
+                .width(50)
+                .height(30);
+            }
+            .height(55)
+            .clickEffect({level:ClickEffectLevel.HEAVY})
+
             Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
             //播放列表透明
             Row() {
@@ -1546,6 +1575,11 @@ export struct SettingPage {
           })
           .padding(0)
 
+
+          // API设置分组
+          this.apiBuilder()
+          //设置教程
+          this.jcBuilder()
           //显示与隐藏设置
           this.xsBuilder()
 

+ 726 - 0
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -0,0 +1,726 @@
+import { WebdavManager } from '../common/util/WebdavManager';
+import { WebDavAccount } from '../viewmodel/WebDavAccount';
+import { Song } from '../viewmodel/Song';
+import { WebdavManagerStates } from '../common/enums/WebdavManagerStates';
+import Logger from '../common/util/Logger';
+import { promptAction, router, window } from '@kit.ArkUI';
+import { CommonConstants } from '../common/constants/CommonConstants';
+import { VideoItem } from '../viewmodel/VideoItem';
+import { GlobalContext } from '../common/util/GlobalContext';
+import { display } from '@kit.ArkUI';
+import { FileInfo } from '../viewmodel/FileInfo';
+import { emitter } from '@kit.BasicServicesKit';
+import { EventConstants } from '../common/constants/EventConstants';
+import { LazyDataSource } from '../common/util/LazyDataSource';
+import { PreferencesUtil } from '@pura/harmony-utils';
+
+/**
+ * 歌单播放事件数据
+ */
+interface PlaylistEventData {
+  playlistId: string;
+  playlistName: string;
+  songCount: number;
+  startIndex: number;
+  songFilePaths: string[];
+  // 移除webDavAuthInfo,现在直接使用videoItems中的webdav_account_id
+}
+
+const TAG = 'heanup WebDavMainPage';
+
+// WebDAV歌曲数据全局内存存储
+let globalWebdavVideoItems: VideoItem[] = [];
+let globalWebdavCurrentPlayIndex: number = 0;
+
+// 导出函数供LocalMusic访问
+export function getWebdavVideoItems(): VideoItem[] {
+  return globalWebdavVideoItems;
+}
+
+export function getWebdavCurrentPlayIndex(): number {
+  return globalWebdavCurrentPlayIndex;
+}
+
+export function clearWebdavVideoItems(): void {
+  globalWebdavVideoItems = [];
+  globalWebdavCurrentPlayIndex = 0;
+}
+
+// URL解码函数
+function decodeUrlEncodedString(encodedStr: string): string {
+  try {
+    return decodeURIComponent(encodedStr);
+  } catch (error) {
+    // 如果解码失败,返回原始字符串
+    return encodedStr;
+  }
+}
+
+@Preview
+@Entry
+@Component
+export struct WebDavMainPage {
+  @State webdavManager: WebdavManager = WebdavManager.getInstance();
+  @State accounts: WebDavAccount[] = [];
+  @Prop @Watch('onSwitchAccount') selectedAccount: WebDavAccount;
+  @State songs: VideoItem[] = [];
+  @State  dataSource:LazyDataSource<VideoItem> = new LazyDataSource(this.songs)
+  @Link mType: number;
+  @Link offsetX: number;
+  @Link isShowDrawer: boolean;
+  @State isLoading: boolean = false;
+  @State webDavFiles: FileInfo[] = []; // 直接在页面中保存文件列表副本
+  @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
+  @StorageProp('isDarkMode') isDarkMode: boolean = false;
+  @State topRectHeight: number = 0; // 顶部安全区高度
+  @State breadcrumbs:string[] = []//面包屑导航
+
+  @State visibleFoldersState: FileInfo[] = []; // 改为普通状态变量
+  @State isShowFileName: boolean = false//是否显示文件名
+  @State isLongNameRoLL: boolean = true//长歌名滚动
+
+  async onSwitchAccount(){
+    console.log('onecold 切换账户:', this.selectedAccount.name);
+    this.songs = [];
+    this.visibleFoldersState = [];
+    this.updateListData(this.songs)
+    
+    // 注意:不再需要清空全局上下文,因为LocalMusic已改用实例变量缓存
+    // 当新账户加载时,新的认证信息会自动覆盖旧的
+    Logger.info(TAG, 'heanup 切换账户,准备加载新账户文件');
+    
+    this.isLoading = true;
+    await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount)
+      .catch((error: Error) => {
+        Logger.error(TAG, '加载文件失败: ' + error.message);
+        this.isLoading = false;
+      });
+    this.breadcrumbs = this.webdavManager.getBreadcrumbs();
+  }
+
+  updateListData(mList:Array<VideoItem>){
+    this.dataSource.pushArrayData(mList)
+  }
+
+  // 更新可见文件夹列表
+  private updateVisibleFolders(): void {
+    try {
+      // 安全检查webDavFiles
+      if (!this.webDavFiles || !Array.isArray(this.webDavFiles)) {
+        this.visibleFoldersState = [];
+        return;
+      }
+
+      const allFolders = this.webDavFiles.filter(f => f.isDirectory);
+      const visible: FileInfo[] = [];
+
+      for (let i = 0; i < allFolders.length; i++) {
+        const folder = allFolders[i];
+
+        // 安全检查folder对象
+        if (!folder || typeof folder.fileName !== 'string') {
+          continue;
+        }
+
+        let shouldShow = this.isDirectChildOfCurrentPath(folder);
+
+        if (shouldShow) {
+          visible.push(folder);
+        }
+      }
+      console.log('更新文件夹列表:', visible);
+
+      this.visibleFoldersState = visible;
+    } catch (error) {
+      Logger.error(TAG, '更新文件夹列表失败:', error.toString());
+      this.visibleFoldersState = [];
+    }
+  }
+
+  // 对话框控制器
+  private accountDialogController: CustomDialogController | null = null;
+  // 保存事件处理器引用,用于取消订阅
+  private eventHandler: (event: string) => void = (event: string) => {
+    this.handleWebdavEvent(event);
+  };
+
+  aboutToAppear(): void {
+    this.isShowFileName = PreferencesUtil.getBooleanSync('isShowFileName', false)
+    this.isLongNameRoLL = PreferencesUtil.getBooleanSync('isLongNameRoLL', true)
+    // 获取顶部安全区高度
+    this.getTopRectHeight();
+
+    // 加载账户列表
+    this.loadAccounts();
+
+    this.loadFiles()
+
+    this.breadcrumbs = this.webdavManager.getBreadcrumbs();
+
+    // 订阅WebDAV状态变化
+    this.webdavManager.subscribe(this.eventHandler);
+  }
+
+  // 获取顶部安全区高度
+  private getTopRectHeight(): void {
+    window.getLastWindow(getContext(this), (err, data) => {
+      if (err.code) {
+        Logger.error(TAG, '获取窗口失败: ' + JSON.stringify(err));
+        return;
+      }
+      const area = data.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
+      this.topRectHeight = px2vp(area.topRect.height);
+      Logger.info(TAG, '顶部安全区高度: ' + this.topRectHeight);
+    });
+  }
+
+  aboutToDisappear(): void {
+    // 取消订阅
+    this.webdavManager.unsubscribe(this.eventHandler);
+  }
+
+  // 处理WebDAV事件
+  private handleWebdavEvent(event: string): void {
+    switch (event) {
+      case WebdavManagerStates.LoadFilesInfoSucceed:
+        this.songs = this.webdavManager.webDavSongs;
+        this.updateListData(this.songs)
+        // 直接引用webdavManager的数组,避免@Observed序列化问题
+        this.webDavFiles = this.webdavManager.webDavFiles;
+        this.isLoading = false;
+
+        // 更新可见文件夹列表
+        this.updateVisibleFolders();
+
+        promptAction.showToast({
+          message: '加载成功: ' + this.webDavFiles.filter(f => f.isDirectory).length + '个文件夹, ' + this.songs.length + '首歌曲'
+        });
+        break;
+      case WebdavManagerStates.LoadFilesInfoFailed:
+        this.isLoading = false;
+        this.getUIContext().getPromptAction().showToast({ message: '加载失败' });
+        break;
+      case WebdavManagerStates.InsertAccountSucceed:
+      case WebdavManagerStates.EditAccountSucceed:
+      case WebdavManagerStates.RemoveAccountSucceed:
+        this.loadAccounts();
+        break;
+    }
+  }
+
+  // 加载账户列表
+  private loadAccounts(): void {
+    this.accounts = this.webdavManager.getAllWebDavAccounts();
+
+  }
+
+  // 加载文件列表
+  private loadFiles(): void {
+    if (!this.selectedAccount) {
+      this.getUIContext().getPromptAction().showToast({ message: '请先选择账户' });
+      return;
+    }
+
+    this.isLoading = true;
+    this.webdavManager.loadFilesInfoFromWebdav()
+      .catch((error: Error) => {
+        Logger.error(TAG, '加载文件失败: ' + error.message);
+        this.isLoading = false;
+      });
+  }
+
+  // 进入文件夹
+  private enterFolder(folder: FileInfo): void {
+    this.isLoading = true;
+    this.webdavManager.enterFolder(folder)
+      .catch((error: Error) => {
+        Logger.error(TAG, '进入文件夹失败: ' + error.message);
+        this.isLoading = false;
+      });
+  }
+
+  // 检查是否为当前目录的直接子项
+  private isDirectChildOfCurrentPath(folder: FileInfo): boolean {
+    const currentPath = this.webdavManager.currentPath || '';
+
+    // 如果在根目录(currentPath为空或'/'),显示所有第一级文件夹
+    if (currentPath === '' || currentPath === '/') {
+      const folderPath = folder.href.replace(/\/$/, ''); // 去掉尾部斜杠
+      return folder.href.startsWith('/') &&
+             folder.href !== '/' &&
+             !folderPath.substring(1).includes('/');
+    }
+
+    // 非根目录情况,计算相对路径
+    let relativePath = folder.href;
+    if (currentPath !== '/') {
+      relativePath = folder.href.replace(currentPath, '');
+    }
+    relativePath = relativePath.replace(/^\//, '').replace(/\/$/, '');
+
+    // 只有相对路径不为空且不包含/时才认为是直接子项
+    return relativePath !== '' && !relativePath.includes('/');
+  }
+
+  // 返回上级目录
+  private goBack(): void {
+    if(this.webdavManager.canGoBack()){
+      this.isLoading = true;
+      this.webdavManager.goBack()
+        .catch((error: Error) => {
+          Logger.error(TAG, '返回失败: ' + error.message);
+          this.isLoading = false;
+        });
+      this.breadcrumbs = this.webdavManager.getBreadcrumbs();
+    }
+
+  }
+
+  // 切换账户
+  private switchAccount(account: WebDavAccount): void {
+    this.selectedAccount = account;
+    this.songs = [];
+    this.updateListData(this.songs)
+  }
+
+  // 播放WebDAV歌曲
+  private playSong(song: VideoItem, index: number): void {
+    try {
+      Logger.info(TAG, `heanup === WebDAV playSong 方法被调用 ===`);
+      Logger.info(TAG, 'heanup 播放指定歌曲: ' + song.name + ', 索引: ' + index);
+      Logger.info(TAG, 'heanup 歌曲列表长度: ' + this.songs.length);
+
+      // 检查歌曲是否有webdav_account_id
+      Logger.info(TAG, `heanup 播放歌曲的webdav_account_id: ${song.webdav_account_id || '未设置'}`);
+
+      // 确保所有歌曲都设置了正确的webdav_account_id
+      if (this.selectedAccount && this.selectedAccount.id) {
+        const accountIds = this.songs.map(s => s.webdav_account_id).filter(id => id);
+        Logger.info(TAG, `heanup 当前歌曲列表中有 ${accountIds.length}/${this.songs.length} 首歌曲设置了webdav_account_id`);
+
+        // 如果发现歌曲缺少webdav_account_id,立即设置
+        this.songs.forEach((item, idx) => {
+          if (!item.webdav_account_id) {
+            item.webdav_account_id = this.selectedAccount!.id.toString();
+            Logger.info(TAG, `heanup 为歌曲 "${item.name}" 设置webdav_account_id: ${item.webdav_account_id}`);
+          }
+        });
+      }
+
+      // 直接使用当前的VideoItem数组
+      const videoItems: VideoItem[] = this.songs;
+      const songFilePaths: string[] = [];
+
+      for (let i = 0; i < this.songs.length; i++) {
+        const item = this.songs[i];
+        songFilePaths.push(item.filePath); // 使用filePath作为文件路径
+      }
+
+      // 直接通过事件传递videoItems数据,不使用GlobalContext
+      const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
+
+      const playlistData: PlaylistEventData = {
+        playlistId: 'webdav-playlist', // 使用特殊的ID标识WebDAV播放列表
+        playlistName: 'WebDAV - ' + (this.selectedAccount?.name || '未知账户'),
+        songCount: this.songs.length,
+        startIndex: index,
+        songFilePaths: songFilePaths
+      };
+
+      // 保存videoItems到全局内存
+      globalWebdavVideoItems = videoItems;
+      globalWebdavCurrentPlayIndex = index;
+
+      Logger.info(TAG, `heanup 保存WebDAV歌曲到全局内存,长度: ${videoItems.length}, 索引: ${index}`);
+
+      // 发送播放请求事件,只传递索引信息
+      emitter.emit(eventPlaylistPlay, { data: playlistData });
+
+      Logger.info(TAG, `heanup 发送WebDAV播放事件,只传递索引: ${index}`);
+
+      // 跳转到首页播放器
+      this.getUIContext()?.animateTo({ duration: 555 }, () => {
+        this.mType = 0
+      })
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, '播放歌曲失败: ' + err.message);
+      this.getUIContext().getPromptAction().showToast({ message: '播放失败' });
+    }
+  }
+
+
+  // 导航到指定层级的面包屑路径
+  private navigateToBreadcrumb(breadcrumbIndex: number): void {
+    try {
+      this.isLoading = true;
+      this.webdavManager.navigateToBreadcrumb(breadcrumbIndex).then((path: string) => {
+        this.webdavManager.enterFolderFromPath(path)
+        this.breadcrumbs = this.webdavManager.getBreadcrumbs();
+      })
+        .catch((error: Error) => {
+          Logger.error(TAG, '导航到面包屑路径失败: ' + error.message);
+          this.isLoading = false;
+        });
+    } catch (error) {
+      Logger.error(TAG, '导航到面包屑路径异常: ' + (error as Error).message);
+      this.isLoading = false;
+    }
+  }
+
+  build() {
+    Column() {
+      // 顶部安全区和标题栏
+      Column() {
+        Blank()
+          .height(this.topRectHeight + 5)
+          .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
+          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
+
+        // 标题栏
+        Row() {
+          Image($r('app.media.menu'))
+            .width(24)
+            .height(24)
+            .margin({ left: 12, right: 8 })
+            .onClick(() => {
+              this.getUIContext()?.animateTo({ duration: 555 }, () => {
+                this.isShowDrawer = !this.isShowDrawer
+                this.offsetX = 0
+              })
+            });
+
+
+          Text(this.selectedAccount.name || 'WebDav')
+            .fontSize(18)
+            .fontColor(Color.White)
+            .fontWeight(FontWeight.Medium)
+            .textAlign(TextAlign.Center)
+
+        }
+        .height(48)
+        .width('100%')
+        .alignItems(VerticalAlign.Center)
+        .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
+      }
+
+      // 内容区域
+      if (this.accounts.length === 0) {
+        this.buildEmptyView();
+      } else {
+        this.buildContentView();
+      }
+    }
+    .width('100%')
+    .height('100%')
+    .backgroundColor($r('app.color.start_window_background'))
+  }
+
+  // 空状态视图
+  @Builder
+  buildEmptyView() {
+    Column({ space: 20 }) {
+      Image($r('app.media.cloudDisk'))
+        .width(120)
+        .height(120)
+        .opacity(0.3)
+
+      Text('暂无WebDAV账户')
+        .fontSize(16)
+        .fontColor($r('app.color.index_tab_font_color'))
+        .opacity(0.6)
+
+    }
+    .justifyContent(FlexAlign.Center)
+    .width('100%')
+    .layoutWeight(1)
+  }
+
+  // 内容视图
+  @Builder
+  buildContentView() {
+    Column() {
+
+
+      // 加载按钮和面包屑导航
+      Column({ space: 8 }) {
+
+        // 账户信息显示
+        // if (this.selectedAccount) {
+        //   Row({ space: 12 }) {
+        //     // 账户封面
+        //     Stack() {
+        //       if (this.selectedAccount.coverPath) {
+        //         Image(this.selectedAccount.coverPath)
+        //           .width(20)
+        //           .height(20)
+        //           .borderRadius(10)
+        //           .objectFit(ImageFit.Cover)
+        //           .border({ width: 2, color: this.themeColor })
+        //       } else {
+        //
+        //         Image($r('app.media.cloudDisk'))
+        //           .width(20)
+        //           .height(20)
+        //           .fillColor(this.themeColor)
+        //       }
+        //     }
+        //
+        //     // 账户信息
+        //     Column({ space: 4 }) {
+        //       Text(this.selectedAccount.name || '未知账户')
+        //         .fontSize(16)
+        //         .fontWeight(FontWeight.Medium)
+        //         .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color'))
+        //         .maxLines(1)
+        //         .textOverflow({ overflow: TextOverflow.Ellipsis })
+        //
+        //       Text(`${this.selectedAccount.host}:${this.selectedAccount.port}`)
+        //         .fontSize(12)
+        //         .fontColor(this.isDarkMode ? '#8E8E93' : $r('app.color.index_tab_font_color'))
+        //         .opacity(0.7)
+        //         .maxLines(1)
+        //         .textOverflow({ overflow: TextOverflow.Ellipsis })
+        //     }
+        //     .alignItems(HorizontalAlign.Start)
+        //     .layoutWeight(1)
+        //
+        //   }
+        //   .width('100%')
+        //   .padding({ left: 4, right: 4, top: 8, bottom: 8 })
+        //   .backgroundColor(this.isDarkMode ? 'rgba(44,44,46,0.8)' : 'rgba(248,248,248,0.8)')
+        //   .borderRadius(8)
+        //   .margin({ bottom: 8 })
+        // }
+        //
+
+        // 面包屑导航
+        if (this.webdavManager.currentPath !== '') {
+          Row({ space: 8 }) {
+            Button({ type: ButtonType.Circle }) {
+              Image(this.webdavManager.canGoBack()?$r('app.media.back'):$r('app.media.cloudDisk'))
+                .width(15)
+                .height(15)
+                .fillColor(Color.White)
+            }
+            .width(20)
+            .height(20)
+            .backgroundColor(this.themeColor)
+            .onClick(() => this.goBack())
+
+            Row({ space: 4 }) {
+              ForEach(this.breadcrumbs, (crumb: string, index: number) => {
+                Row() {
+                  Text(crumb)
+                    .fontSize(15)
+                    .fontColor(this.themeColor)
+                    .maxLines(1)
+                    .textOverflow({ overflow: TextOverflow.Ellipsis })
+                }
+                .onClick(() => {
+                  this.navigateToBreadcrumb(index);
+                })
+
+                // 添加分隔符(除了最后一个元素)
+                if (index < this.breadcrumbs.length - 1) {
+                  Text('/')
+                    .fontSize(15)
+                    .fontColor($r('app.color.index_tab_font_color'))
+                    .opacity(0.6)
+                }
+              })
+            }
+            .layoutWeight(1)
+
+          }
+          .width('100%')
+          .padding({ left: 4, right: 4 })
+        }
+
+        // 统计信息
+        if (this.webDavFiles.length > 0) {
+          Row() {
+            Text(this.visibleFoldersState.length + ' 个文件夹, ' + this.songs.length + ' 首歌曲')
+              .fontSize(13)
+              .fontColor($r('app.color.index_tab_font_color'))
+              .opacity(0.6)
+              .layoutWeight(1)
+              .textAlign(TextAlign.Start)
+            Blank()
+          }
+          .padding({ left: 4, right: 4 })
+        }
+      }
+      .width('100%')
+      .padding(12)
+      .margin({ top: 8 })
+
+      // 加载状态
+
+      Row() {
+        LoadingProgress()
+          .width(30)
+          .height(30)
+          .color(this.themeColor)
+        Text('加载中...')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'))
+          .margin({ left: 12 })
+      }
+      .padding(20)
+      .visibility(this.isLoading?Visibility.Visible:Visibility.None)
+      .opacity(this.isLoading ? 1 : 0)
+      .animation({
+        duration: 500,
+        curve: 'ease-in-out' // 可选动画曲线
+      })
+
+
+      // 文件列表(文件夹 + 歌曲)
+      if (this.webDavFiles.length > 0) {
+        List({ space: 0 }) {
+          // 显示文件夹 - 只显示当前目录下的直接子文件夹
+          ForEach(this.visibleFoldersState, (folder: FileInfo) => {
+            ListItem() {
+              this.buildFolderItem(folder)
+            }
+            .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 600 }),
+              TransitionEffect.scale({ x: 0, y: 0 })))
+            .clickEffect({ level: ClickEffectLevel.MIDDLE })
+          })
+
+          // 显示歌曲
+          LazyForEach(this.dataSource, (song: VideoItem, index: number) => {
+            ListItem() {
+              this.buildSongItem(song, index)
+            }
+            .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 600 }),
+              TransitionEffect.scale({ x: 0, y: 0 })))
+            .clickEffect({ level: ClickEffectLevel.MIDDLE })
+          })
+        }
+        .layoutWeight(1)
+        .divider({ strokeWidth: 1, color: this.isDarkMode ? '#333333' :'#EEEEEE' })
+        .margin({ top: 4 })
+      } else if (!this.isLoading) {
+        Column() {
+          Text('暂无内容')
+            .fontSize(14)
+            .fontColor($r('app.color.index_tab_font_color'))
+            .opacity(0.6)
+          Text('点击"加载文件列表"按钮加载')
+            .fontSize(12)
+            .fontColor($r('app.color.index_tab_font_color'))
+            .opacity(0.4)
+            .margin({ top: 8 })
+        }
+        .justifyContent(FlexAlign.Center)
+        .layoutWeight(1)
+      }
+    }
+    .layoutWeight(1)
+  }
+
+  // 文件夹列表项
+  @Builder
+  buildFolderItem(folder: FileInfo) {
+    Button({ type: ButtonType.Normal, stateEffect: false }) {
+      Row({ space: 2 }) {
+        SymbolGlyph($r('sys.symbol.folder'))
+          .fontSize(48)
+          .fontColor([this.themeColor])
+          .alignSelf(ItemAlign.Center)
+          .margin({ left: 8, right: 6 })
+
+        // 文件夹信息
+        Column({ space: 4 }) {
+          Text(decodeUrlEncodedString(folder.fileName.replace('/', '')))
+            .fontSize(15)
+            .fontColor($r('app.color.index_tab_font_color'))
+            .maxLines(1)
+            .textOverflow({ overflow: TextOverflow.Ellipsis })
+
+          Text('文件夹')
+            .fontSize(13)
+            .fontColor($r('app.color.index_tab_font_color'))
+            .opacity(0.6)
+        }
+        .alignItems(HorizontalAlign.Start)
+        .layoutWeight(1)
+      }
+    }
+    .reuseId('dir_item')
+    .width('100%')
+    .padding(12)
+    .backgroundColor(Color.Transparent)
+    // .backgroundColor($r('app.color.start_window_background'))
+    .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
+    .onClick(() => {
+      this.enterFolder(folder);
+      this.breadcrumbs = this.webdavManager.getBreadcrumbs();
+    })
+  }
+
+  // 歌曲列表项
+  @Builder
+  buildSongItem(song: VideoItem, index: number) {
+    Button({ type: ButtonType.Normal, stateEffect: false }) {
+      Row({ space: 12 }) {
+        // 序号
+        // 歌曲封面
+        Image(song.pixelMap)
+          .width(48)
+          .height(48)
+          .borderRadius(4)
+          .alt($r('app.media.music_red'))
+          .fillColor(this.themeColor)
+          .objectFit(ImageFit.Cover)
+          .margin({ left: 8 })
+
+        // 歌曲信息
+        Column({ space: 4 }) {
+          Text(this.isShowFileName?song.fileName :song.name)
+            .fontSize(15)
+            .textOverflow({ overflow: this.isLongNameRoLL?TextOverflow.MARQUEE:TextOverflow.Ellipsis })//超长滚动
+            .fontColor($r('app.color.index_tab_font_color'))
+            .maxLines(1)
+            .textOverflow({ overflow: TextOverflow.Ellipsis })
+
+          Row(){
+            Text(song.artist+"  ")
+              .fontSize(13)
+              .fontColor($r('app.color.index_tab_font_color'))
+              .opacity(0.6)
+              .maxLines(1)
+              .visibility(song.artist?Visibility.Visible:Visibility.None)
+              .textOverflow({ overflow: TextOverflow.Ellipsis })
+            Text(decodeUrlEncodedString(song.size||"")+'  '+song.cTime)
+              .fontSize(13)
+              .fontColor($r('app.color.index_tab_font_color'))
+              .opacity(0.6)
+              .maxLines(1)
+              .textOverflow({ overflow: TextOverflow.Ellipsis })
+          }
+
+
+        }
+        .alignItems(HorizontalAlign.Start)
+        .layoutWeight(1)
+
+        // 播放图标
+        Image($r('app.media.ic_play'))
+          .width(20)
+          .height(20)
+          .fillColor($r('app.color.index_tab_font_color'))
+          .opacity(0.4)
+      }
+    }
+    .width('100%')
+    .padding(12)
+    .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
+    .backgroundColor(Color.Transparent)
+    .onClick(() => {
+      this.playSong(song, index);
+    })
+  }
+}

+ 318 - 50
entry/src/main/ets/view/LocalMusic.ets

@@ -2,6 +2,7 @@ import TitleBar from './TitleBar'
 import { curves, display, PiPWindow, promptAction, router, SymbolGlyphModifier, window } from '@kit.ArkUI';
 import { VideoItem } from '../viewmodel/VideoItem';
 import {  LengthMetrics, SegmentButton,borderRadiuses, SegmentButtonOptions } from '@kit.ArkUI';
+import { getWebdavVideoItems, getWebdavCurrentPlayIndex, clearWebdavVideoItems } from '../pages/WebDavMainPage';
 import {
   AppUtil,
   ArrayUtil,
@@ -10,6 +11,7 @@ import {
   DeviceUtil,
   DisplayUtil,
   FileUtil,
+  GlobalContext,
   ImageUtil,
   LogUtil,
   MD5,
@@ -31,6 +33,7 @@ import { common, ConfigurationConstant } from '@kit.AbilityKit';
 import { AnimationHelper, DialogAction, DialogHelper } from '@pura/harmony-dialog';
 import { fileIo, fileUri, picker } from '@kit.CoreFileKit';
 import { MessageEvents, util, worker, ErrorEvent } from '@kit.ArkTS';
+import Base64 from '@ohos.util';
 import { Verify } from './Verify';
 import { taskpool } from '@kit.ArkTS';
 import { RotatingCover } from './RotatingCover';
@@ -92,7 +95,11 @@ import { showAddToPlaylistDialog } from '../dialog/AddToPlaylistDialog';
 import { showCreatePlaylistDialog } from '../dialog/PlaylistDialog';
 import { convertPlaylistSongsToVideoItems, emptyView } from '../pages/PlaylistDetailPage';
 import { Playlist, PlaylistSong } from '../viewmodel/Playlist';
-import { updateAllSongsSortOrder } from '../pages/PlaylistDetailPage';
+import { Song } from '../viewmodel/Song';
+import { SongType } from '../common/enums/SongType';
+import { WebdavManager, WebDavAuthInfo as WebDavManagerAuthInfo,
+  buildHttpHeadersWithWebDav,
+  WebDavAuthItem} from '../common/util/WebdavManager';
 const TAG = 'LocalMusic';
 
 /**
@@ -104,8 +111,11 @@ interface PlaylistEventData {
   songCount: number;
   startIndex: number;
   songFilePaths: string[];
+  webDavAuthInfo?: WebDavAuthItem; // 新增WebDAV认证信息
 }
 
+
+
 const DEFAULT_INDEX =
   ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
     'X', 'Y', 'Z']
@@ -164,6 +174,7 @@ const ITEM_HEIGHT_BIG: number = 78; // 列表项大高度
 @Preview
 @Component
 export struct LocalMusic {
+  @State isCopyFileToDownLoad: boolean = false
   @State offHeight: number = 75
   @State currentSongList: Array<VideoItem> = []//当前歌单
   @Consume currentSongListName:string //当前歌单名称
@@ -171,6 +182,8 @@ export struct LocalMusic {
   @State isRefreshing: boolean = false;
   @State maxRefreshingHeight: number = 100.0;
   private contentNode?: ComponentContent<Object> = undefined;
+
+  // WebDAV相关实例变量(现在改为从内存读取,不再需要存储)
   @State ratioL: number = 1;
   @State mediaKuCount: number = 0;
   @State albumCount: number = 0;
@@ -267,6 +280,9 @@ export struct LocalMusic {
   @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
     ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
 
+  // WebDAV认证信息缓存(作为实例变量,比全局上下文更可靠)
+  private currentWebDavAuthInfo: WebDavAuthItem | null = null;
+
   //瀑布流的列数横竖屏动态切换
   onIsLandscapeChange() {
      if(this.isLandscape){
@@ -592,7 +608,6 @@ export struct LocalMusic {
     this.getSortedFiles(this.currentPath)
 
   }
-
   // 组件生命周期
   aboutToAppear() {
     if(ArrayUtil.isNotEmpty(this.videoLocalList)&&this.modeType==0){
@@ -651,10 +666,7 @@ export struct LocalMusic {
 
     // 监听歌单播放请求事件
     let eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }
-    emitter.on(eventPlaylistPlay, (eventData: emitter.EventData) => {
-      Logger.info('heanup eventPlaylistPlay received - full eventData: ' + JSON.stringify(eventData))
-      Logger.info('heanup eventPlaylistPlay received - eventData.data: ' + JSON.stringify(eventData.data))
-
+    emitter.on(eventPlaylistPlay, async (eventData: emitter.EventData) => {
       const data = eventData.data as Record<string, Object>
 
       if (!data) {
@@ -664,8 +676,6 @@ export struct LocalMusic {
 
       // 检查歌单播放数据结构
       if (data.playlistId && data.songFilePaths && data.startIndex !== undefined) {
-        Logger.info('heanup eventPlaylistPlay: 接收到歌单播放数据')
-        // 手动构建数据对象以避免类型转换问题
         const playlistData: PlaylistEventData = {
           playlistId: data.playlistId as string,
           playlistName: data.playlistName as string,
@@ -673,8 +683,8 @@ export struct LocalMusic {
           startIndex: data.startIndex as number,
           songFilePaths: data.songFilePaths as string[]
         }
-        // 根据文件路径重新构建歌曲列表
-        this.handlePlaylistPlayRequest(
+
+        await this.handlePlaylistPlayRequest(
           playlistData.playlistId,
           playlistData.playlistName,
           playlistData.songFilePaths,
@@ -776,7 +786,6 @@ export struct LocalMusic {
     });
 
   }
-
   //载入媒体库,艺术家,专辑等缓存
   initLoadCache(){
     this.mediaKuCount = PreferencesUtil.getNumberSync('mediaKuCount', 0)
@@ -877,6 +886,7 @@ export struct LocalMusic {
     this.isLongNameRoLL = PreferencesUtil.getBooleanSync('isLongNameRoLL', true)
     this.fastForwardSeconds  = PreferencesUtil.getStringSync('fastForwardSeconds', '10')
     this.isShowBackFast  = PreferencesUtil.getBooleanSync('isShowBackFast', false)
+    this.isCopyFileToDownLoad = PreferencesUtil.getBooleanSync(SettingPage.IS_COPYFILE_TO_DOWNLOAD, false)
     if(this.volumeSmall){
       this.volume = 0.5
     }else{
@@ -1117,7 +1127,11 @@ export struct LocalMusic {
           this.isFirstStartPlay = false
           this.currentSong = this.songList[0]
         }
-        this.videoUrl = this.currentSong.filePath
+        if(this.currentSong.type==CommonConstants.TYPE_WEBDAV){
+          this.videoUrl = this.currentSong.filePath.replace(/ /g, '%20');
+        }else{
+          this.videoUrl =  this.currentSong.filePath
+        }
         this.name = this.currentSong.name
         this.cover = this.currentSong.pixelMapPath
         AppStorage.setOrCreate('currentSong',this.currentSong) ;
@@ -1373,7 +1387,6 @@ export struct LocalMusic {
     }
     return mediaItems;
   }
-
   updateListData(mList: Array<VideoItem>, noSort?: boolean) {
 
     animateTo({ duration: 666 }, () => {
@@ -1500,7 +1513,6 @@ export struct LocalMusic {
       this.isHasDir = false
     }
   }
-
   showRankDialog() {
     if ((this.modeType === 2&&!this.isCanBack) || (this.modeType === 3&&!this.isCanBack)) {
       //动作面板
@@ -1703,9 +1715,8 @@ export struct LocalMusic {
     }
     DialogHelper.showActionSheetDialog({
       title: "请选择添加方式(建议歌词文件一起导入)",
-      // sheets: ["新建歌单", "手动导入", "自动导入"],
       sheets: [
-        { value: "新建歌单", fontColor: $r('app.color.text_color') },
+        { value: "新建文件夹", fontColor: $r('app.color.text_color') },
         { value: "手动导入", fontColor: $r('app.color.text_color') },
         { value: "自动导入", fontColor: $r('app.color.text_color') },
         { value: "全选导入", fontColor: $r('app.color.text_color') },
@@ -1719,7 +1730,7 @@ export struct LocalMusic {
             if (this.modeType === 0) {
               this.showMkDialog()
             } else {
-              ToastUtil.showToast('请切换到首页才能新建歌单!')
+              ToastUtil.showToast('请切换到首页才能新建文件夹!')
             }
 
             break;
@@ -1818,8 +1829,76 @@ export struct LocalMusic {
   }
 
   @State progress: number = 0;
+  async saveVideoDatas(uris:string[],isOpen?:boolean){
+    if (ArrayUtil.isEmpty(uris))  {
+      return;
+    }
+
+    console.info('onecold this.isCopyFileToDownLoad = ' + this.isCopyFileToDownLoad);
+    if(this.isCopyFileToDownLoad||this.currentPath==this.lockPath){
+      this.saveVideoDatasToDownLoad(uris,isOpen)
+      return
+    }
+
+
+    // 初始化进度条
+    this.progress  = 0;
+    DialogHelper.showLoadingProgress({
+      progress: this.progress,
+      backCancel: false,
+      autoCancel: false,
+      loadColor: $r('app.color.title_bar_bg'),
+      fontColor: $r('app.color.title_bar_bg')
+    });
+
+    // 计算处理总数用于进度计算
+    const totalItems = uris.length;
+    let processedItems = 0;
+
+    for (let i = 0; i < uris.length;  i++) {
+      let filePath = uris[i];
+      try {
+        if (this.currentPath  !== this.lockPath)  { // 判断不是私密音乐,才入库
+          if (!filePath.endsWith('.lrc')  && Utility.isMeidaByExtension(filePath)  && !filePath.endsWith('.srt'))  {
+            let mediaItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context,
+              filePath, CommonConstants.TYPE_LOCAL, this.autoParseMusicName);
+            console.info('onecold  saveVideoDatas filePath = ' + filePath)
+            if (!filePath.includes(this.rootPath))  {
+              console.info('onecold  saveVideoDatas mediaItem.parentPath111  = ' + mediaItem.parentPath)
+              mediaItem.parentPath  = this.currentPath;
+            }
+            console.info('onecold  saveVideoDatas mediaItem.parentPath  = ' + mediaItem.parentPath)
+            this.table.insert(mediaItem,  (id: number) => {
+              // 插入完成回调
+            });
+          }
+        }
+
+        // 更新进度
+        processedItems++;
+        this.progress  = Math.floor((processedItems  / totalItems) * 100);
+        DialogHelper.updateLoading(' 正在处理', this.progress);
+
+      } catch (error) {
+        Logger.error(TAG,  'saveVideoDatas failed with err: ' + JSON.stringify(error));
+        // 即使出错也更新进度
+        processedItems++;
+        this.progress  = Math.floor((processedItems  / totalItems) * 100);
+        DialogHelper.updateLoading(' 正在处理', this.progress);
+      }
+    }
+    // 关闭进度条
+    DialogHelper.closeLoading();
+    this.isZero = false
+    // 删除目标路径缓存
+    setTimeout(() => {
+      this.cache.delete(this.currentPath);
+      this.getSortedFiles(this.currentPath,false,uris[0],isOpen)
+      this.setButtonStatus()
+    }, 500);
 
-  async saveVideoDatas(uris: string[], isOpen?: boolean) {
+  }
+  async saveVideoDatasToDownLoad(uris: string[], isOpen?: boolean) {
     if (ArrayUtil.isEmpty(uris)) {
       return;
     }
@@ -2112,7 +2191,6 @@ export struct LocalMusic {
 
 
   }
-
   //复制 到另个文件夹的对话框
 
   showCopyDialog(isCurrent: boolean, item: VideoItem, index: number, id: string) {
@@ -2857,7 +2935,6 @@ export struct LocalMusic {
     }
 
   }
-
   @Builder
   private DirItem(item: VideoItem, index?: number) {
     Button({ type: ButtonType.Normal, stateEffect: true }) {
@@ -4857,7 +4934,6 @@ export struct LocalMusic {
     this.pinchValue = 1;
     this.scaleValue = 1;
   }
-
   @Builder
   getList(){
     Refresh({ refreshing: $$this.isRefreshing, refreshingContent: this.contentNode }) {
@@ -5518,6 +5594,55 @@ export struct LocalMusic {
         this.startPlayOrResumePlay()
         break;
 
+      case CommonConstants.TYPE_WEBDAV:
+        // 处理网络音频播放(WebDAV)
+        Logger.info(`heanup 处理网络音频播放: ${item.name}, URL: ${item.filePath}`)
+
+        if (this.CONTROL_PlayStatus !== PlayStatus.INIT) {
+          this.stop();
+        }
+
+        if (isOpen) {
+          this.currentSong = item
+          if (index !== undefined) {
+            this.curIndex = index
+          }
+          this.songList = []
+          this.songList.push(item)
+          this.sonDataSource.pushArrayData(this.songList)
+        } else if (isFromSonPlayList) {
+          // 点击来自右下角的播放列表
+          this.currentSong = item
+          if (index !== undefined) {
+            this.curIndex = index
+          }
+          // 确保播放列表是完整的歌单歌曲列表(已在finishLoadingPlaylist中设置)
+          Logger.info(`heanup 网络音频 isFromSonPlayList分支 - 当前播放列表长度: ${this.songList.length}, 当前索引: ${this.curIndex}`)
+        } else {
+          // 从全局网络音频列表中查找
+          let globalVideoList = this.videoLocalList.filter(video => video.type === CommonConstants.TYPE_WEBDAV) as VideoItem[];
+          this.curIndex = globalVideoList.findIndex(video => video.filePath === item.filePath);
+          if (this.curIndex === -1 && index !== undefined) {
+            this.curIndex = index;
+          }
+          this.songList = globalVideoList.length > 0 ? globalVideoList : [item];
+          this.currentSong = this.songList[this.curIndex];
+
+          this.sonDataSource.pushArrayData(this.songList)
+        }
+
+        AppStorage.setOrCreate('currentSong', this.currentSong);
+        // 对WebDAV URL进行编码处理,确保空格等特殊字符被正确编码
+        this.videoUrl = this.currentSong.filePath.replace(/ /g, '%20');
+        this.name = this.currentSong.name
+        this.cover = this.currentSong.pixelMapPath
+        this.artist = this.currentSong.artist
+
+        Logger.info(`heanup 准备播放网络音频 - 歌名: ${this.name}, 艺术家: ${this.artist}, 编码后URL: ${this.videoUrl}`)
+
+        this.startPlayOrResumePlay()
+        break;
+
     }
 
 
@@ -5571,7 +5696,11 @@ export struct LocalMusic {
           }
           this.songList = globalVideoList
           this.sonDataSource.pushArrayData(this.songList)
-          this.videoUrl = this.currentSong.filePath
+          if(this.currentSong.type==CommonConstants.TYPE_WEBDAV){
+            this.videoUrl = this.currentSong.filePath.replace(/ /g, '%20');
+          }else{
+            this.videoUrl =  this.currentSong.filePath
+          }
           this.name = item.title||this.currentSong.name
           this.cover = this.currentSong.pixelMapPath
           this.artist = item.performer||this.currentSong.artist
@@ -5585,8 +5714,6 @@ export struct LocalMusic {
       }
     })
   }
-
-
   @Builder
   PlayController() {
     Column() {
@@ -5978,9 +6105,6 @@ export struct LocalMusic {
     .width('100%')
     .height('100%')
   }
-
-
-
   @Builder
   editDetail(item: VideoItem) {
     Column() {
@@ -9420,7 +9544,7 @@ export struct LocalMusic {
         .width('99%')
         .textAlign(TextAlign.Center)
         .fontColor(Color.White)
-      Text(this.artist + '  ' + this.currentSong?.album)
+      Text(this.artist + '  ' + this.currentSong?.album||'')
         .fontSize(this.isCoverOpacity() ? 12 : 15)
         .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动
         .fontColor(Color.White)
@@ -11498,6 +11622,12 @@ export struct LocalMusic {
   }
 
   updateLastPlayTimeStr(filePath: string) {
+    // 检查是否为网络音频(WebDAV),如果是则不更新本地数据库
+    if (this.currentSong && this.currentSong.type === CommonConstants.TYPE_WEBDAV) {
+      Logger.info(`heanup 检测到网络音频播放,跳过数据库更新: ${filePath}`)
+      return;
+    }
+
     let lastPlayTime = DateUtil.getTodayStr('yyyy-MM-dd HH:mm:ss')
     this.table.updateLastPlayedStrByFilePath(filePath, lastPlayTime, (success: boolean, error?: string) => {
       if (success) {
@@ -11642,6 +11772,7 @@ export struct LocalMusic {
     this.loadingVisible = Visibility.None;
     this.replayVisible = Visibility.Visible;
   }
+
   private async play(url: string,startOffset?:number) {
     let that = this;
     that.showLoadIng();
@@ -11671,12 +11802,66 @@ export struct LocalMusic {
 
     //设置视频源
     this.mIjkMediaPlayer.setDataSource(url);
-    //设置视频源http请求头
-    let headers = new Map([
-      ["user_agent", "Mozilla/5.0 BiliDroid/7.30.0 (bbcallen@gmail.com)"],
-      ["referer", "https://www.bilibili.com"]
-    ]);
+    // 构建规范的HTTP请求头(统一一次性设置,避免未带认证提前发起连接)
+    const headers = new Map<string, string>();
+    headers.set("User-Agent", "TTMusic-WebDAV/1.0");
+    headers.set("Accept", "*/*");
+    headers.set("Range", "bytes=0-");
+    if (this.currentSong&&StrUtil.isEmpty(this.currentSong.pixelMapPath)
+    &&PreferencesUtil.getStringSync('COVER_API',  '') != '') {
+      //如果封面为空,则搜索封面
+      this.searchCover(this.currentSong,  this.currentSong.name,  this.currentSong.artist||'')
+    }
+
+    // 如果是WebDAV网络音频,使用webdav_account_id获取认证信息(等待完成后再设置一次性头部)
+    if (this.currentSong && this.currentSong.type === CommonConstants.TYPE_WEBDAV) {
+      Logger.info(`heanup WebDAV歌曲认证 - 歌曲名: ${this.currentSong.name}, webdav_account_id: ${this.currentSong.webdav_account_id || '未设置'}`);
+      if (this.currentSong.webdav_account_id) {
+        try {
+          const webdavManager = WebdavManager.getInstance();
+          const webDavHeaders = await webdavManager.buildHttpHeadersWithAccountId(this.currentSong, this.videoUrl);
+          if (webDavHeaders && webDavHeaders.size > 0) {
+            webDavHeaders.forEach((value, key) => headers.set(key, value));
+            Logger.info(`heanup 成功通过webdav_account_id获取并合并WebDAV认证头`);
+          } else {
+            Logger.warn(`heanup 通过webdav_account_id获取认证头失败,播放可能无法继续`);
+          }
+        } catch (error) {
+          const err = error as Error;
+          Logger.error(`heanup 获取WebDAV认证头时出错: ${err.message}`);
+        }
+      } else {
+        Logger.error(`heanup WebDAV歌曲 "${this.currentSong.name}" 缺少webdav_account_id,无法进行认证`);
+      }
+    }
+    // 统一设置带认证的请求头
     this.mIjkMediaPlayer.setDataSourceHeader(headers);
+    if (this.currentSong&&this.currentSong.type === CommonConstants.TYPE_WEBDAV) {
+      console.log(`heanup 为WebDAV播放设置IjkPlayer选项`);
+      // 网络超时设置(单位:微秒,文档显示应该是字符串格式)
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", "30000000"); // 30秒
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "connect_timeout", "30000000"); // 连接超时30秒
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "listen_timeout", "30000000"); // 监听超时30秒
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", "30000000"); // DNS缓存超时30秒
+
+      // 缓冲和播放优化设置
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", "1024000"); // 增大缓冲区
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", "50"); // 减少最小帧数
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", "1"); // 预加载启动
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", "0"); // 无缓冲播放
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", "30000"); // 最大缓存30秒
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", "1"); // 无限制收流
+
+      // 网络相关设置
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http_redirect", "1"); // 启用HTTP重定向
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "user_agent", "TTMusic-WebDAV/1.0"); // 用户代理
+
+      // 重连设置
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", "3"); // 重连3次
+
+      console.log(`heanup WebDAV IjkPlayer选项设置完成`);
+    }
+
     // if(PreferencesUtil.getBooleanSync(SettingPage.IS_MIDIACODEC_OPEN,false)){
     //   this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", "1"); // 启用硬件解码,会导致无法顺序播放
     // }
@@ -11975,17 +12160,55 @@ export struct LocalMusic {
       onError: (what: number, extra: number) => {
         this.stopProgressTask();
         LogUtils.getInstance().LOGI("OnErrorListener-->go:" + what + "===" + extra + " this.videoUrl=" + this.videoUrl)
-        that.hideLoadIng();
 
-        if (StrUtil.isNotEmpty(this.videoUrl) && !FileUtil.accessSync(this.videoUrl)) {
-          ToastUtil.showToast(Utility.resourceToString(getContext(), $r('app.string.file_not_exist')))
-        } else {
-          ToastUtil.showToast(getContext()
-            .resourceManager
-            .getStringByNameSync("Honey_the_video_is_playing_errant_The_system_is_wandering"))
+        // 检查是否为WebDAV播放错误
+        let isWebDavError = false;
+        if (this.currentSong && this.currentSong.type === CommonConstants.TYPE_WEBDAV && this.currentSong.filePath) {
+          try {
+            const globalContext = GlobalContext.getContext();
+            const webDavAuthInfo = globalContext.getObject('webDavAuthInfo') as WebDavAuthItem;
+            if (webDavAuthInfo) {
+              isWebDavError = true;
+            }
+          } catch (error) {
+            console.error('OnErrorListener-->go: 获取WebDAV认证信息失败');
+          }
+        }
 
+        if (isWebDavError) {
+          Logger.error(`heanup WebDAV播放错误 - what: ${what}, extra: ${extra}, URL: ${this.videoUrl}`);
+          
+          // 播放失败时清空实例变量中的WebDAV认证信息,防止错误账户持续使用
+          // this.currentWebDavAuthInfo = null;
+          Logger.info('heanup WebDAV播放失败,保留认证信息用于后续歌曲切换');
+
+          // 根据错误代码提供更具体的错误信息
+          let errorMessage = "WebDAV播放失败";
+          if (what === -1) { // 网络错误
+            errorMessage = "网络连接失败,请检查WebDAV服务器连接";
+          } else if (what === -1004) { // HTTP 404
+            errorMessage = "文件未找到,请检查WebDAV服务器上的文件";
+          } else if (what === -1001) { // 超时
+            errorMessage = "连接超时,请检查网络或WebDAV服务器状态";
+          } else if (what === -1003) { // 无法解析主机
+            errorMessage = "无法连接到WebDAV服务器";
+          }
+
+          ToastUtil.showToast(errorMessage);
+        } else {
+          if (StrUtil.isNotEmpty(this.videoUrl) &&
+              !this.videoUrl.startsWith('http://') &&
+              !this.videoUrl.startsWith('https://') &&
+              !FileUtil.accessSync(this.videoUrl)) {
+            ToastUtil.showToast(Utility.resourceToString(getContext(), $r('app.string.file_not_exist')))
+          } else {
+            ToastUtil.showToast(getContext()
+              .resourceManager
+              .getStringByNameSync("Honey_the_video_is_playing_errant_The_system_is_wandering"))
+          }
         }
 
+        that.hideLoadIng();
       }
     }
 
@@ -12262,7 +12485,6 @@ export struct LocalMusic {
     this.durationTime = Math.floor(this.duration / 1000);
     this.durationStringTime = secondToTime((this.durationTime));
   }
-
   private playbackStateChangeListener = (playbackState: avSession.AVPlaybackState) => {
     const duration = playbackState?.extras?.duration;
     if (typeof duration === 'number') {
@@ -12628,7 +12850,12 @@ export struct LocalMusic {
       this.CONTROL_PlayStatus = PlayStatus.INIT;
       this.stop();
       this.currentSong = this.songList[this.curIndex]
-      this.videoUrl = this.songList[this.curIndex].filePath;
+      if(this.currentSong.type==CommonConstants.TYPE_WEBDAV){
+        this.videoUrl = this.currentSong.filePath.replace(/ /g, '%20');
+      }else{
+        this.videoUrl = this.currentSong.filePath
+      }
+
       this.artist = this.songList[this.curIndex].artist
       this.name = this.songList[this.curIndex].name
       //this.cover = this.songList[this.curIndex].pixelMapPath
@@ -12716,7 +12943,11 @@ export struct LocalMusic {
       this.CONTROL_PlayStatus = PlayStatus.INIT;
       this.stop();
       this.currentSong = this.songList[this.curIndex];
-      this.videoUrl = this.songList[this.curIndex].filePath;
+      if(this.currentSong.type==CommonConstants.TYPE_WEBDAV){
+        this.videoUrl = this.currentSong.filePath.replace(/ /g, '%20');
+      }else{
+        this.videoUrl = this.currentSong.filePath
+      }
       this.name = this.songList[this.curIndex].name;
       this.artist = this.songList[this.curIndex].artist
       // this.cover = this.songList[this.curIndex].pixelMapPath
@@ -12733,7 +12964,11 @@ export struct LocalMusic {
       this.CONTROL_PlayStatus = PlayStatus.INIT;
       this.stop();
       this.currentSong = this.songList[this.curIndex]
-      this.videoUrl = this.songList[index].filePath;
+      if(this.currentSong.type===CommonConstants.TYPE_WEBDAV){
+        this.videoUrl = this.songList[index].filePath.replace(/ /g, '%20');
+      }else{
+        this.videoUrl = this.songList[index].filePath
+      }
       this.name = this.songList[index].name
       this.artist = this.songList[this.curIndex].artist
       this.curIndex = index;
@@ -12761,7 +12996,11 @@ export struct LocalMusic {
     this.CONTROL_PlayStatus = PlayStatus.INIT;
     this.stop();
     this.currentSong = this.songList[this.curIndex]
-    this.videoUrl = this.songList[this.curIndex].filePath;
+    if(this.currentSong.type==CommonConstants.TYPE_WEBDAV){
+      this.videoUrl = this.songList[this.curIndex].filePath.replace(/ /g, '%20');
+    }else{
+      this.videoUrl = this.songList[this.curIndex].filePath
+    }
     this.name = this.songList[this.curIndex].name
     this.artist = this.songList[this.curIndex].artist
     this.changeImageAnimation()
@@ -12776,7 +13015,11 @@ export struct LocalMusic {
       this.CONTROL_PlayStatus = PlayStatus.INIT;
       this.stop();
       this.currentSong = prevSong;
-      this.videoUrl = prevSong.filePath;
+      if(this.currentSong.type==CommonConstants.TYPE_WEBDAV){
+        this.videoUrl = prevSong.filePath.replace(/ /g, '%20');
+      }else{
+        this.videoUrl = prevSong.filePath
+      }
       this.cover = prevSong.pixelMapPath
       this.artist = prevSong.artist
       this.name = prevSong.name;
@@ -12792,7 +13035,11 @@ export struct LocalMusic {
       this.CONTROL_PlayStatus = PlayStatus.INIT;
       this.stop();
       this.currentSong = this.songList[this.curIndex];
-      this.videoUrl = this.songList[this.curIndex].filePath;
+      if(this.currentSong.type==CommonConstants.TYPE_WEBDAV){
+        this.videoUrl = this.songList[this.curIndex].filePath.replace(/ /g, '%20');
+      }else{
+        this.videoUrl =  this.songList[this.curIndex].filePath
+      }
       this.artist = this.songList[this.curIndex].artist
       this.name = this.songList[this.curIndex].name;
       this.changeImageAnimation()
@@ -13206,10 +13453,33 @@ export struct LocalMusic {
   /**
    * 处理歌单播放请求
    */
-  private handlePlaylistPlayRequest(playlistId: string, playlistName: string, songFilePaths: string[], startIndex: number) {
+  private async handlePlaylistPlayRequest(playlistId: string, playlistName:
+    string, songFilePaths: string[], startIndex: number) {
     try {
       Logger.info(`heanup 处理歌单播放请求: ${playlistName}, 歌曲数量: ${songFilePaths.length}, 开始索引: ${startIndex}`)
 
+      // 检查是否为WebDAV播放请求
+      if (playlistId === 'webdav-playlist') {
+        Logger.info(`heanup 检测到WebDAV播放请求,从内存读取videoItems`)
+
+        // 从全局内存读取videoItems数据
+        const videoItems = getWebdavVideoItems();
+        const currentPlayIndex = getWebdavCurrentPlayIndex();
+
+        Logger.info(`heanup 从内存读取到WebDAV歌曲列表,长度:${videoItems.length},索引:${currentPlayIndex}`)
+
+        if (videoItems && videoItems.length > 0) {
+          // 验证webdav_account_id设置情况
+          const accountIds = videoItems.map(item => item.webdav_account_id).filter(id => id);
+          Logger.info(`heanup 有 ${accountIds.length}/${videoItems.length} 首歌曲设置了webdav_account_id`)
+
+          this.finishLoadingPlaylist(videoItems, currentPlayIndex, playlistName)
+          return
+        } else {
+          return
+        }
+      }
+
       // 从数据库重新加载这些歌曲,使用索引记录位置以保持顺序
       const songMap: Map<number, VideoItem> = new Map()
       let completedQueries = 0
@@ -13505,5 +13775,3 @@ async function scanCurrentDirectoryTask(context: Context, dirPath: string, lockP
     console.error(' 扫描当前目录失败:', error);
   }
 }
-
-

+ 27 - 0
entry/src/main/ets/viewmodel/FileInfo.ets

@@ -0,0 +1,27 @@
+// 文件信息
+@Observed
+export class FileInfo{
+  rootpath: string
+  name: string
+  totalSize: number
+  time: number
+  readOnly: boolean = false
+
+  // WebDAV相关属性
+  fileName: string = ''
+  href: string = ''
+  contentLength: number = 0
+  isDirectory: boolean = false  // 是否为文件夹
+
+  constructor(rootpath: string, name: string, totalSize: number, time: number) {
+    this.rootpath = rootpath
+    this.name = name
+    this.totalSize = totalSize
+    this.time = time
+    this.fileName = name
+  }
+
+  public setReadOnly(readOnly: boolean): void {
+    this.readOnly = readOnly
+  }
+}

+ 93 - 0
entry/src/main/ets/viewmodel/Song.ets

@@ -0,0 +1,93 @@
+import { image } from "@kit.ImageKit"
+import { SongType } from "../common/enums/SongType"
+import { Constants } from "../Constants"
+import { JSON } from "@kit.ArkTS"
+
+@Observed
+export class Song{
+  // 歌曲索引
+  public id: number = -1
+  // 云盘歌曲还是本地歌曲
+  public type: SongType = SongType.WebDav
+  public songType: SongType = SongType.WebDav
+  // WebDav所属账号
+  public WebDavAccountId: number = -1
+  public webDavAccountId: number = -1
+  // 图片路径
+  public img:ResourceStr | image.PixelMap
+  // 歌曲保存路径
+  public localFilePath: string = ''
+  // 网盘的路径,为空则说明在根目录下,否则表示在根目录下的路径
+  public webFilePath: string = ''
+  // 歌曲URL或路径
+  public src: string = ''
+  // 文件大小
+  public fileSize: number = 0
+
+  // 歌曲名
+  public title:string = Constants.UNKNOWN_TITLE
+  // 文件名称
+  public name:string = ''
+  // 艺术家
+  public artist:string = Constants.UNKNOWN_ARTIST
+  // 已下载的大小
+  public receivedSize: number = 0
+  // 总大小
+  public totalSize: number = 0
+  // 修改时间
+  public time : number = 0
+  // 偏好程度
+  public score: number = 0
+  // 歌单中的id
+  public songInPlayListId: number = 0
+  // 高级
+  // 采样率
+  public sampleRate: string = Constants.UNKNOWN_SAMPLE_RATE
+  // 轨道数量
+  public trackCount: string = Constants.UNKNOWN_TRACK_COUNT
+  // 类型
+  public mimeType: string = Constants.UNKNOWN_MIME_TYPE
+
+
+  constructor(id:number,img:string) {
+    this.id = id
+    if(img !== ''){
+      this.img = img
+    }else{
+      this.img = Constants.COMMON_SONG_DEFAULT_IMAGE
+    }
+  }
+
+  public static  equal(song1: Song,song2: Song):boolean{
+    if(song1.type === song2.type && song1.type === SongType.Local){
+      return song1.localFilePath === song2.localFilePath && song1.name === song2.name
+    }else if(song1.type === song2.type && song2.type === SongType.WebDav){
+      return song1.WebDavAccountId === song2.WebDavAccountId && song1.name === song2.name && song1.webFilePath === song2.webFilePath
+    }else{
+      return false
+    }
+  }
+
+  // 克隆方法
+  public static clone(song: Song): Song {
+    const clonedSong = new Song(song.id, '');
+    clonedSong.img = song.img
+    clonedSong.type = song.type;
+    clonedSong.WebDavAccountId = song.WebDavAccountId;
+    clonedSong.localFilePath = song.localFilePath;
+    clonedSong.webFilePath = song.webFilePath;
+    clonedSong.title = song.title;
+    clonedSong.name = song.name;
+    clonedSong.artist = song.artist;
+    clonedSong.receivedSize = song.receivedSize;
+    clonedSong.totalSize = song.totalSize;
+    clonedSong.time = song.time;
+    clonedSong.score = song.score;
+    clonedSong.songInPlayListId = song.songInPlayListId;
+    clonedSong.sampleRate = song.sampleRate;
+    clonedSong.trackCount = song.trackCount;
+    clonedSong.mimeType = song.mimeType;
+    return clonedSong;
+  }
+
+}

+ 2 - 0
entry/src/main/ets/viewmodel/VideoItem.ets

@@ -68,6 +68,8 @@ export class VideoItem  {
   COMMENT?:string//注释
   disc?:string//碟号
 
+  webdav_account_id?: string// WebDAV账号ID,用于获取认证信息
+
   constructor(name: string, id: string, filePath: string, type:number,videoSize:number,cTime: string,pixelMap?: image.PixelMap,
     size?: string,pixelMapPath?: string,artist?: string,album?: string,fileName?: string, lastPlayed?:Date) {
     this.name = name;

+ 30 - 0
entry/src/main/ets/viewmodel/WebDavAccount.ets

@@ -0,0 +1,30 @@
+import { Constants } from "../Constants";
+import { FileInfo } from "./FileInfo";
+
+@Observed
+export class WebDavAccount{
+  public name: string = Constants.UNKNOWN_NAME
+  // 网络配置
+  public id :number= 0;
+  public isActivate: boolean = true
+  public host = ''
+  public localHost = '' // 内网地址
+  public isUseLocalHost: boolean = false // 是否使用内网
+  public port : number = 80
+  // 当前所在的目录
+  public filepath: string = ''
+  public imageFilePath: string = ''
+  public lyricFilePath: string = ''
+  public uploadFilePath: string = ''
+  public account:string = ''
+  public password:string = ''
+  public enableHttps: boolean = false
+  public lyricFilePaths: string[] = []
+  public imageFilePaths: string[] = []
+  // 自定义封面路径
+  public coverPath?: string
+
+  public setIsUseLocalHost(isuse: boolean){
+    this.isUseLocalHost = isuse
+  }
+}

+ 4 - 0
entry/src/main/resources/base/element/string.json

@@ -626,6 +626,10 @@
     {
       "name": "playlist_play_started",
       "value": "开始播放歌单"
+    },
+    {
+      "name": "set_cover",
+      "value": "设为歌单封面"
     }
   ]
 }

+ 1 - 0
entry/src/main/resources/base/media/cloudDisk.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761291572100" class="icon" viewBox="0 0 1159 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1610" xmlns:xlink="http://www.w3.org/1999/xlink" width="226.3671875" height="200"><path d="M995.472141 845.325513c-22.521994 0-40.539589 18.017595-40.53959 40.53959 0 22.521994 18.017595 40.539589 40.53959 40.539589 22.521994 0 40.539589-18.017595 40.539589-40.539589 0-22.521994-18.017595-40.539589-40.539589-40.53959z m151.648094-9.008797l-135.131965-750.733138C1004.480938 37.536657 963.941349 4.504399 914.392962 3.002933H280.774194c-51.049853 1.501466-91.589443 36.035191-97.595308 85.583577l-135.131965 747.730206c-4.504399 15.014663-9.008798 31.530792-9.008798 49.548387 0 88.58651 72.070381 138.134897 159.155425 138.134897h798.780059c88.58651 0 159.155425-49.548387 159.155425-138.134897 0-18.017595-3.002933-34.533724-9.008797-49.548387zM493.982405 376.868035c0-66.064516 36.035191-120.117302 102.099706-120.117302s93.090909 63.061584 93.09091 129.1261c43.542522 0 106.604106 24.02346 106.604105 67.565982s-36.035191 75.073314-79.577713 75.073314H477.466276c-43.542522 0-79.577713-31.530792-79.577713-75.073314 0-45.043988 52.55132-76.57478 96.093842-76.57478z m507.495601 600.58651H172.668622c-43.542522 0-79.577713-52.55132-82.580645-85.583577 0-43.542522 27.026393-109.607038 70.568914-109.607038l830.310851 1.501466c43.542522 0 102.099707 24.02346 100.59824 117.114369-6.005865 55.554252-46.545455 76.57478-90.087976 76.57478z" fill="#1890ff" p-id="1611"></path></svg>

+ 1 - 0
entry/src/main/resources/base/media/folder.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761353872982" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4674" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M855.04 385.024q19.456 2.048 38.912 10.24t33.792 23.04 21.504 37.376 2.048 54.272q-2.048 8.192-8.192 40.448t-14.336 74.24-18.432 86.528-19.456 76.288q-5.12 18.432-14.848 37.888t-25.088 35.328-36.864 26.112-51.2 10.24l-567.296 0q-21.504 0-44.544-9.216t-42.496-26.112-31.744-40.96-12.288-53.76l0-439.296q0-62.464 33.792-97.792t95.232-35.328l503.808 0q22.528 0 46.592 8.704t43.52 24.064 31.744 35.84 12.288 44.032l0 11.264-53.248 0q-40.96 0-95.744-0.512t-116.736-0.512-115.712-0.512-92.672-0.512l-47.104 0q-26.624 0-41.472 16.896t-23.04 44.544q-8.192 29.696-18.432 62.976t-18.432 61.952q-10.24 33.792-20.48 65.536-2.048 8.192-2.048 13.312 0 17.408 11.776 29.184t29.184 11.776q31.744 0 43.008-39.936l54.272-198.656q133.12 1.024 243.712 1.024l286.72 0z" p-id="4675"></path></svg>

+ 8 - 0
entry/src/main/resources/dark/element/color.json

@@ -196,6 +196,14 @@
     {
       "name": "left_draw_bg",
       "value": "#000000"
+    },
+    {
+      "name": "cancel_button_background",
+      "value": "#666666"
+    },
+    {
+      "name": "cancel_button_text",
+      "value": "#F5F5F5"
     }
   ]
 }