Jelajahi Sumber

webdav集成

chendeben 9 bulan lalu
induk
melakukan
bb9d6ab3e8

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

@@ -1,2 +1,31 @@
 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', '.aac', '.m4a', '.wav', '.ogg', '.flac', '.ape', '.wma', '.alac', '.opus']
+
+  // 图片扩展名
+  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.icon')
+}

+ 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;

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

@@ -0,0 +1,680 @@
+// 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;
+  private backgroundManager = BackgroundManager.getInstance()
+  public ErrorMessage: string | BusinessError = ''
+  public filesInfo: FileInfo[] = []
+
+  //private rcpSession : rcp.Session | null = null
+
+  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 != '') {
+              // 提取文件信息
+              const filesInfo = this.extractHrefContents(response, path,url);
+              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)
+        })
+    });
+  }
+
+  // 判断文件是否属于文件夹
+  private isFileFolder(filename: string):boolean{
+    return filename.toLowerCase().endsWith('/')
+  }
+
+
+
+
+  //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;
+  }
+
+  // 将 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;', "'")
+
+
+    // 正则匹配替换
+    return str.replace(/&(amp|lt|gt|quot|apos);/g, (match:string, entity:string) => {
+      const decoded = entityMap.get(`&${entity};`);
+      return decoded ? decoded : match;
+    });
+  }
+
+  // 使用正则表达式提取所有 <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]));
+
+      // 从href中提取文件名(最后一个/后的部分)
+      let 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;
+  }
+
+  constructor() {
+    console.info(UtilName,'testTag','RcpSocketUtil单例已创建')
+  }
+
+  // 获取文件列表(简化版包装方法)
+  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数据传输事件(占位)');
+  }
+
+  static getInstance(): RcpSocket {
+    if (!RcpSocket.instance) {
+      RcpSocket.instance = new RcpSocket();
+    }
+    return RcpSocket.instance;
+  }
+}

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

@@ -0,0 +1,538 @@
+// WebdavManager - WebDAV管理器(简化版)
+import { Song } from '../../viewmodel/Song';
+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 { SongType } from '../enums/SongType';
+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';
+
+const TAG = 'heanup WebdavManager';
+
+export interface TransferTask {
+  song: Song;
+  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: Song[] = [];
+  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 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}`);
+    for (let i = 0; i < this.observers.length; i++) {
+      const observer = this.observers[i];
+      observer(event);
+    }
+  }
+
+  // ==================== 数据库操作 ====================
+
+  // 创建WebDAV账户表
+  public async createWebDavTableInDB(): Promise<void> {
+    const sql = `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
+    )`;
+
+    try {
+      await this.dataBaseUtil.executeSql(sql);
+      Logger.info(TAG, 'WebDAV账户表创建成功');
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, `创建WebDAV账户表失败: ${error.message}`);
+      throw error;
+    }
+  }
+
+  // 从数据库查询所有账户
+  public async queryWebDavAccountsFromDB(): Promise<void> {
+    try {
+      const predicates = new relationalStore.RdbPredicates(this.webDavTable);
+      const columns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
+                      'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
+                      'account', 'password', 'enableHttps'];
+
+      const resultSet = await this.dataBaseUtil.queryData(this.webDavTable, columns, 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;
+
+        this.webDavAccounts.push(account);
+      }
+      resultSet.close();
+
+      Logger.info(TAG, `从数据库加载了 ${this.webDavAccounts.length} 个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
+  ): 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
+      };
+
+      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
+      };
+
+      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 {
+    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
+      );
+
+      // 保存所有文件(包括文件夹)
+      this.webDavFiles = files;
+
+      // 调试:输出获取到的文件总数
+      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];
+        Logger.info(TAG, `文件 ${i}: ${file.fileName}, href: ${file.href}, isDirectory: ${file.isDirectory}`);
+
+        if (file.isDirectory) {
+          folderCount++;
+          Logger.info(TAG, `📁 文件夹: ${file.fileName}`);
+        } else if (this.isAudioFile(file.fileName)) {
+          audioCount++;
+          Logger.info(TAG, `🎵 音频文件: ${file.fileName}`);
+          const song = this.fileInfoToSong(file, account);
+          this.webDavSongs.push(song);
+        } else {
+          Logger.info(TAG, `📄 其他文件: ${file.fileName}`);
+        }
+      }
+
+      Logger.info(TAG, `从WebDAV加载了 ${folderCount} 个文件夹, ${audioCount} 首歌曲`);
+      this.notifyObservers(WebdavManagerStates.LoadFilesInfoSucceed);
+    } catch (error) {
+      Logger.error(TAG, `从WebDAV加载文件列表失败: ${error}`);
+      this.ErrorMessage = error as BusinessError;
+      this.notifyObservers(WebdavManagerStates.LoadFilesInfoFailed);
+    }
+  }
+
+  // 将FileInfo转换为Song
+  private fileInfoToSong(fileInfo: FileInfo, account: WebDavAccount): Song {
+    const song = new Song(-1, '');
+    song.title = this.getFileNameWithoutExtension(fileInfo.fileName);
+    song.artist = Constants.UNKNOWN_ARTIST;
+    song.name = fileInfo.fileName;
+
+    // 构建完整的WebDAV URL
+    const protocol = account.enableHttps ? 'https' : 'http';
+    const host = account.isUseLocalHost ? account.localHost : account.host;
+    const port = account.port;
+    // href已经包含完整路径,直接使用
+    song.src = `${protocol}://${host}:${port}${fileInfo.href}`;
+
+    song.songType = SongType.WebDav;
+    song.webDavAccountId = account.id;
+    song.webFilePath = fileInfo.href;
+    song.fileSize = fileInfo.contentLength;
+    song.time = fileInfo.time;
+    song.img = Constants.COMMON_SONG_DEFAULT_IMAGE;
+
+    return song;
+  }
+
+  // 判断是否为音频文件
+  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;
+    }
+
+    // 保存当前路径到历史记录
+    this.pathHistory.push(this.currentPath);
+
+    // 加载文件夹内容
+    await this.loadFilesInfoFromWebdav(folder.href);
+  }
+
+  // 返回上级目录
+  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}`);
+    }
+  }
+
+  // 保存配置到Preferences
+  public async saveInfo(): Promise<void> {
+    try {
+      // 这里可以添加保存配置到Preferences的逻辑
+      Logger.info(TAG, '保存配置到Preferences');
+    } catch (error) {
+      Logger.error(TAG, `保存配置到Preferences失败: ${error}`);
+    }
+  }
+
+  // ==================== 下载队列管理 ====================
+
+  // 添加到下载队列
+  public addToDownloadQueue(song: Song, account: WebDavAccount): void {
+    const task: TransferTask = { song, account };
+    this.downloadQueue.push(task);
+    Logger.info(TAG, `添加到下载队列: ${song.title}`);
+    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.title}`);
+      this.notifyObservers(WebdavManagerStates.DownloadQueueChanged);
+    }
+  }
+
+  // 清空下载队列
+  public clearDownloadQueue(): void {
+    this.downloadQueue = [];
+    Logger.info(TAG, '清空下载队列');
+    this.notifyObservers(WebdavManagerStates.DownloadQueueChanged);
+  }
+
+  // ==================== 上传队列管理 ====================
+
+  // 添加到上传队列
+  public addToUploadQueue(song: Song, account: WebDavAccount): void {
+    const task: TransferTask = { song, account };
+    this.uploadQueue.push(task);
+    Logger.info(TAG, `添加到上传队列: ${song.title}`);
+    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.title}`);
+      this.notifyObservers(WebdavManagerStates.UploadQueueChanged);
+    }
+  }
+
+  // 清空上传队列
+  public clearUploadQueue(): void {
+    this.uploadQueue = [];
+    Logger.info(TAG, '清空上传队列');
+    this.notifyObservers(WebdavManagerStates.UploadQueueChanged);
+  }
+}

+ 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);

+ 62 - 3
entry/src/main/ets/pages/NewIndex.ets

@@ -40,6 +40,7 @@ import { convertPlaylistSongsToVideoItems } from './PlaylistDetailPage';
 import MediaTable from '../common/util/MediaTable';
 import { showCreatePlaylistDialog } from '../dialog/PlaylistDialog';
 import { ImagePickerUtil } from '../common/util/ImagePickerUtil';
+import { WebDavMainPage } from './WebDavMainPage';
 
 // import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
 
@@ -143,7 +144,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'),
@@ -158,8 +159,10 @@ struct NewIndex {
   tabSelectedIndexesChanged() {
     if(this.tabSelectedIndexes[0]==1){
       this.mType = 0
+    } else if(this.tabSelectedIndexes[0]==2){
+      // 网盘选项卡
+      this.mType = 6
     }
-
   }
   // 歌单相关状态变量
   @State playlistList: Playlist[] = []
@@ -357,6 +360,9 @@ struct NewIndex {
             .visibility(this.mType === 4 ? Visibility.Visible : Visibility.None)
           AboutPage()
             .visibility(this.mType === 5 ? Visibility.Visible : Visibility.None)
+
+          WebDavMainPage()
+            .visibility(this.mType === 6 ? Visibility.Visible : Visibility.None)
           
           // 抽屉打开时的遮罩层,用于拦截点击事件  这个只能正常尺寸的手机竖屏的才能生效
           if (this.isShowDrawer&&this.isPhonePortrait()) {
@@ -641,8 +647,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.buildWebDavTab()
         }
 
       }
@@ -1115,6 +1123,57 @@ struct NewIndex {
     })
   }
 
+  /**
+   * 构建WebDAV网盘tab内容
+   */
+  @Builder
+  buildWebDavTab() {
+    ListItem() {
+      Button({ type: ButtonType.Capsule, stateEffect: true }) {
+        Row() {
+          Image($r('app.media.cloudDisk'))
+            .width(22)
+            .height(22)
+            .margin({ left: 25 })
+            .fillColor(this.themeColor)
+          
+          Text('WebDAV网盘')
+            .margin({ left: 10, right: 20 })
+            .fontSize(15)
+            .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.mType = 6
+        this.doShowDrawer()
+      })
+    }
+    
+    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(){

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

@@ -0,0 +1,788 @@
+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';
+
+const TAG = 'heanup WebDavMainPage';
+
+@Preview
+@Entry
+@Component
+export struct WebDavMainPage {
+  @State webdavManager: WebdavManager = WebdavManager.getInstance();
+  @State accounts: WebDavAccount[] = [];
+  @State selectedAccount: WebDavAccount | null = null;
+  @State songs: Song[] = [];
+  @State isLoading: boolean = false;
+  @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
+  @StorageProp('isDarkMode') isDarkMode: boolean = false;
+  @State topRectHeight: number = 0; // 顶部安全区高度
+
+  // 对话框控制器
+  private accountDialogController: CustomDialogController | null = null;
+  // 保存事件处理器引用,用于取消订阅
+  private eventHandler: (event: string) => void = (event: string) => {
+    this.handleWebdavEvent(event);
+  };
+
+  aboutToAppear(): void {
+    // 获取顶部安全区高度
+    this.getTopRectHeight();
+
+    // 加载账户列表
+    this.loadAccounts();
+
+    // 订阅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 {
+    Logger.info(TAG, `收到WebDAV事件: ${event}`);
+
+    switch (event) {
+      case WebdavManagerStates.LoadFilesInfoSucceed:
+        this.songs = this.webdavManager.webDavSongs;
+        this.isLoading = false;
+        promptAction.showToast({
+          message: `加载成功: ${this.webdavManager.webDavFiles.filter(f => f.isDirectory).length}个文件夹, ${this.songs.length}首歌曲`
+        });
+        break;
+      case WebdavManagerStates.LoadFilesInfoFailed:
+        this.isLoading = false;
+        promptAction.showToast({ message: '加载失败' });
+        break;
+      case WebdavManagerStates.InsertAccountSucceed:
+      case WebdavManagerStates.EditAccountSucceed:
+      case WebdavManagerStates.RemoveAccountSucceed:
+        this.loadAccounts();
+        break;
+      default:
+        break;
+    }
+  }
+
+  // 加载账户列表
+  private loadAccounts(): void {
+    this.accounts = this.webdavManager.getAllWebDavAccounts();
+    if (this.accounts.length > 0 && !this.selectedAccount) {
+      this.selectedAccount = this.accounts[0];
+    }
+  }
+
+  // 加载文件列表
+  private async loadFiles(): Promise<void> {
+    if (!this.selectedAccount) {
+      promptAction.showToast({ message: '请先选择账户' });
+      return;
+    }
+
+    this.isLoading = true;
+    try {
+      await this.webdavManager.loadFilesInfoFromWebdav();
+    } catch (error) {
+      Logger.error(TAG, `加载文件失败: ${error}`);
+      this.isLoading = false;
+    }
+  }
+
+  // 进入文件夹
+  private async enterFolder(folder: FileInfo): Promise<void> {
+    this.isLoading = true;
+    try {
+      await this.webdavManager.enterFolder(folder);
+    } catch (error) {
+      Logger.error(TAG, `进入文件夹失败: ${error}`);
+      this.isLoading = false;
+    }
+  }
+
+  // 返回上级目录
+  private async goBack(): Promise<void> {
+    this.isLoading = true;
+    try {
+      await this.webdavManager.goBack();
+    } catch (error) {
+      Logger.error(TAG, `返回失败: ${error}`);
+      this.isLoading = false;
+    }
+  }
+
+  // 切换账户
+  private switchAccount(account: WebDavAccount): void {
+    this.selectedAccount = account;
+    this.songs = [];
+  }
+
+  // 显示添加账户对话框
+  private showAddAccountDialog(): void {
+    this.accountDialogController = new CustomDialogController({
+      builder: WebDavAccountDialog({
+        isEditMode: false,
+        account: new WebDavAccount(),
+        onConfirm: async (account: WebDavAccount) => {
+          try {
+            await 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
+            );
+            promptAction.showToast({ message: '添加成功' });
+          } catch (error) {
+            Logger.error(TAG, `添加账户失败: ${error}`);
+            promptAction.showToast({ message: '添加失败' });
+          }
+        }
+      }),
+      autoCancel: true,
+      customStyle: true
+    });
+    this.accountDialogController.open();
+  }
+
+  // 显示编辑账户对话框
+  private showEditAccountDialog(account: WebDavAccount): void {
+    this.accountDialogController = new CustomDialogController({
+      builder: WebDavAccountDialog({
+        isEditMode: true,
+        account: account,
+        onConfirm: async (updatedAccount: WebDavAccount) => {
+          try {
+            await this.webdavManager.editAccount(updatedAccount);
+            promptAction.showToast({ message: '更新成功' });
+          } catch (error) {
+            Logger.error(TAG, `更新账户失败: ${error}`);
+            promptAction.showToast({ message: '更新失败' });
+          }
+        }
+      }),
+      autoCancel: true,
+      customStyle: true
+    });
+    this.accountDialogController.open();
+  }
+
+  // 显示账户选择对话框
+  private showAccountSelectDialog(): void {
+    if (this.accounts.length === 0) {
+      promptAction.showToast({ message: '没有可用的账户' });
+      return;
+    }
+
+    // 构建按钮数组,至少1个,最多6个
+    const button1: promptAction.Button = { text: this.accounts[0].name, color: '#000000' };
+    const button2: promptAction.Button | undefined = this.accounts.length > 1 ? { text: this.accounts[1].name, color: '#000000' } : undefined;
+    const button3: promptAction.Button | undefined = this.accounts.length > 2 ? { text: this.accounts[2].name, color: '#000000' } : undefined;
+    const button4: promptAction.Button | undefined = this.accounts.length > 3 ? { text: this.accounts[3].name, color: '#000000' } : undefined;
+    const button5: promptAction.Button | undefined = this.accounts.length > 4 ? { text: this.accounts[4].name, color: '#000000' } : undefined;
+    const button6: promptAction.Button | undefined = this.accounts.length > 5 ? { text: this.accounts[5].name, color: '#000000' } : undefined;
+
+    promptAction.showActionMenu({
+      title: '选择账户',
+      buttons: [button1, button2, button3, button4, button5, button6]
+    }).then((result) => {
+      if (result.index >= 0 && result.index < this.accounts.length) {
+        this.switchAccount(this.accounts[result.index]);
+        promptAction.showToast({ message: `已切换到: ${this.accounts[result.index].name}` });
+      }
+    });
+  }
+
+  // 将Song转换为VideoItem
+  private convertSongToVideoItem(song: Song, index: number): VideoItem {
+    const videoItem = new VideoItem(
+      song.title,
+      index.toString(),
+      song.src, // WebDAV URL作为文件路径
+      CommonConstants.TYPE_INTERNET, // 使用网络类型
+      song.fileSize,
+      song.time.toString(),
+      undefined, // pixelMap
+      undefined, // size
+      typeof song.img === 'string' ? song.img : undefined, // pixelMapPath
+      song.artist,
+      undefined, // album
+      song.name // fileName
+    );
+    return videoItem;
+  }
+
+  // 播放WebDAV歌曲
+  private playSong(song: Song, index: number): void {
+    try {
+      // 将当前歌曲列表转换为VideoItem数组
+      const videoItems: VideoItem[] = [];
+      for (let i = 0; i < this.songs.length; i++) {
+        const item = this.convertSongToVideoItem(this.songs[i], i);
+        videoItems.push(item);
+      }
+
+      // 保存到全局上下文
+      const globalContext = GlobalContext.getContext();
+      globalContext.setObject('videoItems', videoItems);
+      globalContext.setObject('currentPlayIndex', index);
+
+      // 跳转到播放器页面
+      router.pushUrl({
+        url: 'pages/MainIndex',
+        params: {
+          playIndex: index,
+          fromWebDAV: true
+        }
+      }).catch((error: Error) => {
+        Logger.error(TAG, `跳转播放器失败: ${error.message}`);
+        promptAction.showToast({ message: '播放失败' });
+      });
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `播放歌曲失败: ${err.message}`);
+      promptAction.showToast({ message: '播放失败' });
+    }
+  }
+
+  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.back'))
+            .width(24)
+            .height(24)
+            .margin({ left: 12, right: 8 })
+            .onClick(() => {
+              router.back();
+            })
+
+          Text('WebDAV网盘')
+            .fontSize(18)
+            .fontColor(Color.White)
+            .fontWeight(FontWeight.Medium)
+            .layoutWeight(1)
+            .textAlign(TextAlign.Center)
+
+          // 添加账户按钮
+          Image($r('app.media.add'))
+            .width(24)
+            .height(24)
+            .margin({ right: 12 })
+            .onClick(() => this.showAddAccountDialog())
+        }
+        .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)
+
+      Text('点击右上角 + 添加账户')
+        .fontSize(14)
+        .fontColor($r('app.color.index_tab_font_color'))
+        .opacity(0.4)
+    }
+    .justifyContent(FlexAlign.Center)
+    .width('100%')
+    .layoutWeight(1)
+  }
+
+  // 内容视图
+  @Builder
+  buildContentView() {
+    Column() {
+      // 账户选择器
+      if (this.accounts.length > 0) {
+        Row() {
+          Text('当前账户:')
+            .fontSize(14)
+            .fontColor($r('app.color.index_tab_font_color'))
+            .margin({ right: 8 })
+
+          Text(this.selectedAccount?.name || '未选择')
+            .fontSize(14)
+            .fontColor(this.themeColor)
+            .layoutWeight(1)
+
+          Button('切换', { type: ButtonType.Normal, stateEffect: true })
+            .fontSize(12)
+            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
+            .backgroundColor(this.themeColor)
+            .onClick(() => {
+              this.showAccountSelectDialog();
+            })
+
+          Button('编辑', { type: ButtonType.Normal, stateEffect: true })
+            .fontSize(12)
+            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
+            .backgroundColor($r('app.color.input_background'))
+            .fontColor($r('app.color.index_tab_font_color'))
+            .margin({ left: 8 })
+            .onClick(() => {
+              if (this.selectedAccount) {
+                this.showEditAccountDialog(this.selectedAccount);
+              }
+            })
+        }
+        .width('100%')
+        .padding(12)
+        .backgroundColor($r('app.color.input_background'))
+        .borderRadius(8)
+        .margin({ left: 12, right: 12, top: 8 })
+      }
+
+      // 加载按钮和面包屑导航
+      Column({ space: 8 }) {
+        Row({ space: 12 }) {
+          Button('加载文件列表', { type: ButtonType.Capsule, stateEffect: true })
+            .layoutWeight(1)
+            .backgroundColor(this.themeColor)
+            .enabled(!this.isLoading)
+            .onClick(() => this.loadFiles())
+
+          // 返回上级按钮
+          if (this.webdavManager.canGoBack()) {
+            Button({ type: ButtonType.Circle }) {
+              Image($r('app.media.back'))
+                .width(20)
+                .height(20)
+                .fillColor(Color.White)
+            }
+            .width(40)
+            .height(40)
+            .backgroundColor(this.themeColor)
+            .onClick(() => this.goBack())
+          }
+        }
+        .width('100%')
+
+        // 面包屑导航
+        if (this.webdavManager.currentPath !== '') {
+          Row({ space: 8 }) {
+            Image($r('app.media.cloudDisk'))
+              .width(16)
+              .height(16)
+              .fillColor($r('app.color.index_tab_font_color'))
+              .opacity(0.6)
+
+            Text(this.webdavManager.getBreadcrumbs().join(' / '))
+              .fontSize(13)
+              .fontColor($r('app.color.index_tab_font_color'))
+              .opacity(0.6)
+              .maxLines(1)
+              .textOverflow({ overflow: TextOverflow.Ellipsis })
+          }
+          .width('100%')
+          .padding({ left: 4, right: 4 })
+        }
+
+        // 统计信息
+        if (this.webdavManager.webDavFiles.length > 0) {
+          Text(`${this.webdavManager.webDavFiles.filter(f => f.isDirectory).length} 个文件夹, ${this.songs.length} 首歌曲`)
+            .fontSize(13)
+            .fontColor($r('app.color.index_tab_font_color'))
+            .opacity(0.6)
+        }
+      }
+      .width('100%')
+      .padding(12)
+      .margin({ top: 8 })
+
+      // 加载状态
+      if (this.isLoading) {
+        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)
+      }
+
+      // 文件列表(文件夹 + 歌曲)
+      if (this.webdavManager.webDavFiles.length > 0) {
+        List({ space: 0 }) {
+          // 显示文件夹
+          ForEach(this.webdavManager.webDavFiles.filter(f => f.isDirectory), (folder: FileInfo) => {
+            ListItem() {
+              this.buildFolderItem(folder)
+            }
+          })
+
+          // 显示歌曲
+          ForEach(this.songs, (song: Song, index: number) => {
+            ListItem() {
+              this.buildSongItem(song, index)
+            }
+          })
+        }
+        .layoutWeight(1)
+        .divider({ strokeWidth: 1, color: '#EEEEEE' })
+        .margin({ top: 8 })
+      } 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) {
+    Row({ space: 12 }) {
+      // 文件夹图标
+      Image($r('app.media.cloudDisk'))
+        .width(48)
+        .height(48)
+        .fillColor(this.themeColor)
+        .borderRadius(4)
+        .padding(8)
+        .backgroundColor($r('app.color.input_background'))
+
+      // 文件夹信息
+      Column({ space: 4 }) {
+        Text(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)
+
+      // 进入按钮
+      Button({ type: ButtonType.Circle }) {
+        Image($r('app.media.ic_play'))
+          .width(20)
+          .height(20)
+          .fillColor(Color.White)
+          .rotate({ angle: 180 }) // 旋转箭头
+      }
+      .width(36)
+      .height(36)
+      .backgroundColor(this.themeColor)
+      .onClick(() => {
+        this.enterFolder(folder);
+      })
+    }
+    .width('100%')
+    .padding(12)
+    .backgroundColor($r('app.color.start_window_background'))
+  }
+
+  // 歌曲列表项
+  @Builder
+  buildSongItem(song: Song, index: number) {
+    Row({ space: 12 }) {
+      // 序号
+      Text((index + 1).toString())
+        .fontSize(14)
+        .fontColor($r('app.color.index_tab_font_color'))
+        .opacity(0.6)
+        .width(30)
+        .textAlign(TextAlign.Center)
+
+      // 歌曲封面
+      Image(song.img)
+        .width(48)
+        .height(48)
+        .borderRadius(4)
+        .objectFit(ImageFit.Cover)
+
+      // 歌曲信息
+      Column({ space: 4 }) {
+        Text(song.title)
+          .fontSize(15)
+          .fontColor($r('app.color.index_tab_font_color'))
+          .maxLines(1)
+          .textOverflow({ overflow: TextOverflow.Ellipsis })
+
+        Text(song.artist)
+          .fontSize(13)
+          .fontColor($r('app.color.index_tab_font_color'))
+          .opacity(0.6)
+          .maxLines(1)
+          .textOverflow({ overflow: TextOverflow.Ellipsis })
+      }
+      .alignItems(HorizontalAlign.Start)
+      .layoutWeight(1)
+
+      // 播放按钮
+      Button({ type: ButtonType.Circle }) {
+        Image($r('app.media.ic_play'))
+          .width(20)
+          .height(20)
+          .fillColor(Color.White)
+      }
+      .width(36)
+      .height(36)
+      .backgroundColor(this.themeColor)
+      .onClick(() => {
+        this.playSong(song, index);
+      })
+    }
+    .width('100%')
+    .padding(12)
+    .backgroundColor($r('app.color.start_window_background'))
+  }
+}
+
+// WebDAV账户对话框
+@CustomDialog
+struct WebDavAccountDialog {
+  controller: CustomDialogController;
+  @Prop isEditMode: boolean = false;
+  @Prop account: WebDavAccount;
+  onConfirm?: (account: WebDavAccount) => void;
+
+  @State accountName: string = '';
+  @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;
+  @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
+
+  aboutToAppear(): void {
+    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;
+  }
+
+  build() {
+    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($r('app.color.index_tab_font_color'))
+          .width('100%')
+        TextInput({ placeholder: '请输入账户名称', text: this.accountName })
+          .onChange((value: string) => {
+            this.accountName = value;
+          })
+      }
+      .alignItems(HorizontalAlign.Start)
+
+      // 服务器地址
+      Column({ space: 8 }) {
+        Text('服务器地址')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'))
+          .width('100%')
+        TextInput({ placeholder: '例如: example.com', text: this.host })
+          .onChange((value: string) => {
+            this.host = value;
+          })
+      }
+      .alignItems(HorizontalAlign.Start)
+
+      // 端口
+      Column({ space: 8 }) {
+        Text('端口')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'))
+          .width('100%')
+        TextInput({ placeholder: '默认: 80', text: this.port.toString() })
+          .type(InputType.Number)
+          .onChange((value: string) => {
+            this.port = parseInt(value) || 80;
+          })
+      }
+      .alignItems(HorizontalAlign.Start)
+
+      // 文件目录
+      Column({ space: 8 }) {
+        Text('文件目录')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'))
+          .width('100%')
+        TextInput({ placeholder: '例如: /music', text: this.filepath })
+          .onChange((value: string) => {
+            this.filepath = value;
+          })
+      }
+      .alignItems(HorizontalAlign.Start)
+
+      // 用户名
+      Column({ space: 8 }) {
+        Text('用户名')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'))
+          .width('100%')
+        TextInput({ placeholder: '请输入用户名', text: this.username })
+          .onChange((value: string) => {
+            this.username = value;
+          })
+      }
+      .alignItems(HorizontalAlign.Start)
+
+      // 密码
+      Column({ space: 8 }) {
+        Text('密码')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'))
+          .width('100%')
+        TextInput({ placeholder: '请输入密码', text: this.password })
+          .type(InputType.Password)
+          .onChange((value: string) => {
+            this.password = value;
+          })
+      }
+      .alignItems(HorizontalAlign.Start)
+
+      // 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.input_background'))
+          .fontColor($r('app.color.index_tab_font_color'))
+          .layoutWeight(1)
+          .onClick(() => {
+            this.controller.close();
+          })
+
+        Button(this.isEditMode ? '保存' : '添加', { type: ButtonType.Capsule })
+          .backgroundColor(this.themeColor)
+          .layoutWeight(1)
+          .onClick(() => {
+            if (!this.accountName || !this.host) {
+              promptAction.showToast({ message: '请填写账户名称和服务器地址' });
+              return;
+            }
+
+            const updatedAccount = new WebDavAccount();
+            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.isActivate = true;
+            updatedAccount.localHost = '';
+            updatedAccount.isUseLocalHost = false;
+            updatedAccount.lyricFilePath = '';
+            updatedAccount.uploadFilePath = '';
+            updatedAccount.imageFilePath = '';
+
+            this.onConfirm?.(updatedAccount);
+            this.controller.close();
+          })
+      }
+      .width('100%')
+      .margin({ top: 8 })
+    }
+    .padding(24)
+    .backgroundColor($r('app.color.start_window_background'))
+    .borderRadius(16)
+  }
+}

+ 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;
+  }
+
+}

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

@@ -0,0 +1,28 @@
+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 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>