Browse Source

ISO格式的可以分析出存在哪些音乐文件 但是还提取不了
实现播放页封面可以双指缩放大小
优化搜索功能
可以多选文件和文件夹删除
实现网盘文件可以重命名

onecold 4 tháng trước cách đây
mục cha
commit
257b72e5d9

+ 69 - 0
entry/src/main/cpp/napi_init.cpp

@@ -605,6 +605,73 @@ napi_value DeleteEntry(napi_env env, napi_callback_info info)
     }
 }
 
+napi_value CreateDirectory(napi_env env, napi_callback_info info)
+{
+    try {
+        size_t argc = 2;
+        napi_value args[2] = {nullptr};
+        NapiCheck(napi_get_cb_info(env, info, &argc, args, nullptr, nullptr), "Failed to get arguments for createDirectory");
+        if (argc < 2) {
+            throw std::runtime_error("createDirectory requires treeId and path");
+        }
+        int64_t treeId = ReadInt64(env, args[0], "treeId");
+        std::string path = ReadString(env, args[1], "path", false);
+        auto tree = RequireTree(treeId);
+        auto session = tree->session.lock();
+        auto client = session ? session->client.lock() : nullptr;
+        if (!client || client->ctx == nullptr) {
+            throw std::runtime_error("SMB client context is not available");
+        }
+        Ensure(tree->connected, "SMB tree is not connected");
+        std::string normalized = NormalizeRemotePath(path);
+        Ensure(!normalized.empty(), "path must not be empty");
+        smb2_context *ctx = client->ctx;
+        int rc = smb2_mkdir(ctx, normalized.c_str());
+        if (rc != 0) {
+            throw std::runtime_error(BuildSmbErrorMessage(ctx, "Failed to create remote directory"));
+        }
+        return CreateUndefined(env);
+    } catch (const std::exception &error) {
+        napi_throw_error(env, nullptr, error.what());
+        return nullptr;
+    }
+}
+
+napi_value RenameEntry(napi_env env, napi_callback_info info)
+{
+    try {
+        size_t argc = 3;
+        napi_value args[3] = {nullptr};
+        NapiCheck(napi_get_cb_info(env, info, &argc, args, nullptr, nullptr), "Failed to get arguments for renameEntry");
+        if (argc < 3) {
+            throw std::runtime_error("renameEntry requires treeId, oldPath and newPath");
+        }
+        int64_t treeId = ReadInt64(env, args[0], "treeId");
+        std::string oldPath = ReadString(env, args[1], "oldPath", false);
+        std::string newPath = ReadString(env, args[2], "newPath", false);
+        auto tree = RequireTree(treeId);
+        auto session = tree->session.lock();
+        auto client = session ? session->client.lock() : nullptr;
+        if (!client || client->ctx == nullptr) {
+            throw std::runtime_error("SMB client context is not available");
+        }
+        Ensure(tree->connected, "SMB tree is not connected");
+        std::string normalizedOld = NormalizeRemotePath(oldPath);
+        std::string normalizedNew = NormalizeRemotePath(newPath);
+        Ensure(!normalizedOld.empty(), "oldPath must not be empty");
+        Ensure(!normalizedNew.empty(), "newPath must not be empty");
+        smb2_context *ctx = client->ctx;
+        int rc = smb2_rename(ctx, normalizedOld.c_str(), normalizedNew.c_str());
+        if (rc != 0) {
+            throw std::runtime_error(BuildSmbErrorMessage(ctx, "Failed to rename remote entry"));
+        }
+        return CreateUndefined(env);
+    } catch (const std::exception &error) {
+        napi_throw_error(env, nullptr, error.what());
+        return nullptr;
+    }
+}
+
 napi_value DownloadSmbFile(napi_env env, napi_callback_info info)
 {
     try {
@@ -847,6 +914,8 @@ static napi_value Init(napi_env env, napi_value exports)
         {"disconnectTree", nullptr, DisconnectTree, nullptr, nullptr, nullptr, napi_default, nullptr},
         {"readDirectory", nullptr, ReadDirectory, nullptr, nullptr, nullptr, napi_default, nullptr},
         {"deleteEntry", nullptr, DeleteEntry, nullptr, nullptr, nullptr, napi_default, nullptr},
+        {"createDirectory", nullptr, CreateDirectory, nullptr, nullptr, nullptr, napi_default, nullptr},
+        {"renameEntry", nullptr, RenameEntry, nullptr, nullptr, nullptr, napi_default, nullptr},
         {"downloadSmbFile", nullptr, DownloadSmbFile, nullptr, nullptr, nullptr, napi_default, nullptr},
         {"readSmbFileRange", nullptr, ReadSmbFileRange, nullptr, nullptr, nullptr, napi_default, nullptr}
     };

+ 2 - 0
entry/src/main/cpp/types/libentry/Index.d.ts

@@ -16,6 +16,8 @@ export interface NativeModule {
   readDirectory(treeId: number, path: string): NativeDirectoryEntry[];
   downloadSmbFile(host: string, share: string, username: string, password: string, domain: string | undefined, remotePath: string, localPath: string): void;
   deleteEntry(treeId: number, path: string, isDirectory: boolean): void;
+  createDirectory(treeId: number, path: string): void;
+  renameEntry(treeId: number, oldPath: string, newPath: string): void;
   readSmbFileRange(host: string, share: string, username: string, password: string, domain: string | undefined, remotePath: string, offset: number, length: number): ArrayBuffer;
 }
 

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

@@ -74,7 +74,7 @@ export class CommonConstants {
     '.3gv','.m4v','.avi','.rmvb','.flv','.3g2','.rmvb','.mpg','.webm','.ogv','.f4v','.swf'
     ,'.srt','.dsf','.vob','.3gp','.mpeg','.ts','.ogg','.opus','.wv','.aiff','.amr','.aif','.dff'
     ,'.aif','.au','.eac3','.mlp','.tak','.thd','.tta','.wv','.ac3','.amr','.mka','.mpc','.ra','.dts'
-  , '.asf','.m2ts','.m4b','.mts','.rm','.wtv','.av3a','.dsd','.cue']
+  , '.asf','.m2ts','.m4b','.mts','.rm','.wtv','.av3a','.dsd','.cue','.iso']
 
   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'

+ 48 - 0
entry/src/main/ets/common/network/BaiduPanClient.ets

@@ -112,8 +112,15 @@ export interface BaiduFileMetaResponse {
   list: BaiduFileMeta[];
 }
 
+interface BaiduFileManagerInfo {
+  errno?: number;
+  path?: string;
+}
+
 interface BaiduFileManagerResponse {
   errno: number;
+  info?: BaiduFileManagerInfo[];
+  taskid?: number;
 }
 
 async function parseJson<T>(payload: string): Promise<T> {
@@ -245,6 +252,47 @@ export async function deleteFiles(accessToken: string, paths: string[]): Promise
   }
 }
 
+export async function renameFile(accessToken: string, path: string, newName: string): Promise<void> {
+  if (!path || path.length === 0) {
+    throw new Error('缺少百度网盘文件路径');
+  }
+  if (!newName || newName.trim().length === 0) {
+    throw new Error('缺少百度网盘新文件名');
+  }
+  const requestUrl = `${BaiduConstants.PAN_BASE}/rest/2.0/xpan/file?method=filemanager&access_token=${accessToken}&opera=rename`;
+  const httpRequest = http.createHttp();
+  const filelist = JSON.stringify([{ path, newname: newName.trim() }]);
+  const body = `async=0&filelist=${encodeURIComponent(filelist)}&ondup=fail`;
+  Logger.info(TAG, `百度网盘重命名请求: ${requestUrl}`);
+  Logger.info(TAG, `百度网盘重命名body: ${body}`);
+  const options: http.HttpRequestOptions = {
+    method: http.RequestMethod.POST,
+    connectTimeout: 10000,
+    readTimeout: 10000,
+    expectDataType: http.HttpDataType.STRING,
+    header: {
+      'User-Agent': BaiduConstants.USER_AGENT,
+      'Content-Type': 'application/x-www-form-urlencoded'
+    },
+    extraData: body
+  };
+  try {
+    const response = await httpRequest.request(requestUrl, options);
+    const payload = parseResponseBody(response);
+    Logger.info(TAG, `百度网盘重命名响应: ${payload}`);
+    const parsed = await parseJson<BaiduFileManagerResponse>(payload);
+    if (parsed.errno !== 0) {
+      throw new Error(`重命名失败 errno=${parsed.errno}`);
+    }
+    const infoItem = parsed.info && parsed.info.length > 0 ? parsed.info[0] : undefined;
+    if (infoItem && infoItem.errno && infoItem.errno !== 0) {
+      throw new Error(`重命名失败 errno=${infoItem.errno}`);
+    }
+  } finally {
+    httpRequest.destroy();
+  }
+}
+
 export function appendAccessTokenToDlink(dlink: string, accessToken: string): string {
   if (!dlink) {
     return '';

+ 24 - 0
entry/src/main/ets/common/network/SmbBridge.ets

@@ -21,6 +21,11 @@ export interface SmbCreateOptions extends SmbConnectionOptions {
   path: string;
 }
 
+export interface SmbRenameOptions extends SmbConnectionOptions {
+  path: string;
+  newPath: string;
+}
+
 export interface SmbDirectoryEntry {
   name: string;
   isDirectory: boolean;
@@ -72,6 +77,13 @@ export class NativeSambaTree {
     bridge.createDirectory(this.treeId, normalized);
   }
 
+  renameEntry(path?: string, newPath?: string): void {
+    this.ensureOpen();
+    const normalized = this.normalizePath(path);
+    const normalizedNew = this.normalizePath(newPath);
+    bridge.renameEntry(this.treeId, normalized, normalizedNew);
+  }
+
   async close(): Promise<void> {
     if (this.closed) {
       return;
@@ -183,3 +195,15 @@ export async function createSmbDirectory(options: SmbCreateOptions): Promise<voi
     tree.createDirectory(options.path);
   });
 }
+
+export async function renameSmbEntry(options: SmbRenameOptions): Promise<void> {
+  if (!options.path || options.path.length === 0) {
+    throw new Error('Remote path must not be empty');
+  }
+  if (!options.newPath || options.newPath.length === 0) {
+    throw new Error('New remote path must not be empty');
+  }
+  await withSmbTree(options, async (tree: NativeSambaTree) => {
+    tree.renameEntry(options.path, options.newPath);
+  });
+}

+ 125 - 0
entry/src/main/ets/common/util/DownloadCenterManager.ets

@@ -6,6 +6,7 @@ import { rcp } from '@kit.RemoteCommunicationKit';
 
 const TAG: string = 'DownloadCenterManager';
 const HISTORY_KEY: string = 'download_center_completed_history_v1';
+const ACTIVE_SNAPSHOT_KEY: string = 'download_center_active_snapshot_v1';
 const MAX_HISTORY_COUNT: number = 300;
 const MAX_CONCURRENT_DOWNLOADS: number = 3;
 
@@ -70,6 +71,7 @@ export class DownloadCenterManager {
   private runningTaskIds: Set<string> = new Set<string>();
 
   private constructor() {
+    this.loadActiveSnapshots();
     this.loadCompletedHistory();
   }
 
@@ -164,6 +166,7 @@ export class DownloadCenterManager {
     this.activeTasks.unshift(task);
     this.runtimeMap.set(task.taskId, runtime);
     this.queue.push(runtime);
+    this.saveActiveSnapshots();
     this.notifyObservers();
     void this.processQueue();
 
@@ -185,12 +188,14 @@ export class DownloadCenterManager {
       active.status = 'paused';
       this.removeRuntimeFromQueue(taskId);
       runtime.isQueued = false;
+      this.saveActiveSnapshots();
       this.notifyObservers();
       return;
     }
 
     if (active.status === 'downloading') {
       active.status = 'paused';
+      this.saveActiveSnapshots();
       this.notifyObservers();
     }
   }
@@ -213,10 +218,47 @@ export class DownloadCenterManager {
     if (active.status !== 'downloading') {
       active.status = 'pending';
     }
+    this.saveActiveSnapshots();
     this.notifyObservers();
     void this.processQueue();
   }
 
+  public deleteTask(taskId: string): void {
+    const runtime = this.runtimeMap.get(taskId);
+    const active = this.findActiveTask(taskId);
+    if (active) {
+      if (runtime) {
+        runtime.pauseRequested = true;
+        runtime.isQueued = false;
+      }
+      this.removeRuntimeFromQueue(taskId);
+      this.runningTaskIds.delete(taskId);
+      this.runtimeMap.delete(taskId);
+      this.activeTasks = this.activeTasks.filter((task: DownloadCenterTask): boolean => {
+        return task.taskId !== taskId;
+      });
+      try {
+        if (StrUtil.isNotEmpty(active.targetPath) && FileUtil.accessSync(active.targetPath)) {
+          fileIo.unlinkSync(active.targetPath);
+        }
+      } catch (error) {
+        Logger.warn(TAG, `删除下载文件失败: ${(error as Error).message}`);
+      }
+      this.saveActiveSnapshots();
+      this.notifyObservers();
+      return;
+    }
+
+    const completedLength: number = this.completedTasks.length;
+    this.completedTasks = this.completedTasks.filter((task: DownloadCenterTask): boolean => {
+      return task.taskId !== taskId;
+    });
+    if (this.completedTasks.length !== completedLength) {
+      this.saveCompletedHistory();
+      this.notifyObservers();
+    }
+  }
+
   private removeRuntimeFromQueue(taskId: string): void {
     const queueIndex = this.queue.findIndex((runtime: DownloadRuntimeContext): boolean => {
       return runtime.task.taskId === taskId;
@@ -250,6 +292,7 @@ export class DownloadCenterManager {
         if (runtime.pauseRequested || active.status === 'paused') {
           active.status = 'paused';
           active.speedBytesPerSec = 0;
+          this.saveActiveSnapshots();
           this.notifyObservers();
           continue;
         }
@@ -258,6 +301,7 @@ export class DownloadCenterManager {
         active.status = 'downloading';
         active.errorMessage = '';
         active.finishedAt = 0;
+        this.saveActiveSnapshots();
         this.notifyObservers();
         void this.executeRuntime(runtime, active);
       }
@@ -272,6 +316,7 @@ export class DownloadCenterManager {
       if (runtime.pauseRequested) {
         active.status = 'paused';
         active.speedBytesPerSec = 0;
+        this.saveActiveSnapshots();
         this.notifyObservers();
         return;
       }
@@ -307,6 +352,7 @@ export class DownloadCenterManager {
         }
       }
 
+      this.saveActiveSnapshots();
       this.saveCompletedHistory();
       this.notifyObservers();
     } catch (error) {
@@ -314,6 +360,7 @@ export class DownloadCenterManager {
         active.status = 'paused';
         active.errorMessage = '';
         active.speedBytesPerSec = 0;
+        this.saveActiveSnapshots();
         this.notifyObservers();
         return;
       }
@@ -322,6 +369,7 @@ export class DownloadCenterManager {
       active.status = 'failed';
       active.speedBytesPerSec = 0;
       active.errorMessage = err.message;
+      this.saveActiveSnapshots();
       this.notifyObservers();
       Logger.error(TAG, `下载失败: ${active.fileName}, err=${err.message}`);
     } finally {
@@ -580,6 +628,7 @@ export class DownloadCenterManager {
     }
 
     // 立即通知观察者,确保实时更新
+    this.saveActiveSnapshots();
     this.notifyObservers();
   }
 
@@ -792,6 +841,82 @@ export class DownloadCenterManager {
     }
   }
 
+  private loadActiveSnapshots(): void {
+    try {
+      const snapshotJson: string = PreferencesUtil.getStringSync(ACTIVE_SNAPSHOT_KEY, '');
+      if (StrUtil.isEmpty(snapshotJson)) {
+        this.activeTasks = [];
+        return;
+      }
+      const parsed = JSON.parse(snapshotJson) as DownloadCenterTask[];
+      if (!parsed || !Array.isArray(parsed)) {
+        this.activeTasks = [];
+        return;
+      }
+
+      this.activeTasks = parsed.map((item: DownloadCenterTask): DownloadCenterTask => {
+        const restored: DownloadCenterTask = {
+          taskId: item.taskId,
+          title: item.title,
+          fileName: item.fileName,
+          coverPath: item.coverPath,
+          sizeText: item.sizeText,
+          sourceUrl: item.sourceUrl,
+          targetPath: item.targetPath,
+          downloadDir: item.downloadDir,
+          totalBytes: item.totalBytes,
+          downloadedBytes: item.downloadedBytes,
+          progress: item.progress,
+          speedBytesPerSec: 0,
+          status: item.status === 'completed' ? 'paused' : (item.status === 'failed' ? 'failed' : 'paused'),
+          errorMessage: item.status === 'failed' ? item.errorMessage : '',
+          createdAt: item.createdAt,
+          finishedAt: 0
+        };
+        if (restored.totalBytes > 0 && restored.downloadedBytes > 0) {
+          const percent = (restored.downloadedBytes / restored.totalBytes) * 100;
+          restored.progress = Math.min(99.9, Math.max(0.1, Math.round(percent * 10) / 10));
+        }
+        return restored;
+      }).filter((item: DownloadCenterTask): boolean => {
+        return StrUtil.isNotEmpty(item.targetPath)
+          && StrUtil.isNotEmpty(item.fileName)
+          && StrUtil.isNotEmpty(item.sourceUrl)
+          && item.status !== 'completed';
+      });
+
+      this.runtimeMap.clear();
+      this.queue = [];
+      for (let i: number = 0; i < this.activeTasks.length; i += 1) {
+        const task: DownloadCenterTask = this.activeTasks[i];
+        this.runtimeMap.set(task.taskId, {
+          task: task,
+          headers: new Map<string, string>(),
+          pauseRequested: true,
+          isQueued: false
+        });
+      }
+    } catch (error) {
+      this.activeTasks = [];
+      this.runtimeMap.clear();
+      this.queue = [];
+      Logger.warn(TAG, `加载未完成下载任务失败: ${(error as Error).message}`);
+    }
+  }
+
+  private saveActiveSnapshots(): void {
+    try {
+      const snapshots: DownloadCenterTask[] = this.activeTasks.filter((task: DownloadCenterTask): boolean => {
+        return task.status !== 'completed';
+      }).map((task: DownloadCenterTask): DownloadCenterTask => {
+        return this.cloneTask(task);
+      });
+      PreferencesUtil.putSync(ACTIVE_SNAPSHOT_KEY, JSON.stringify(snapshots));
+    } catch (error) {
+      Logger.warn(TAG, `保存未完成下载任务失败: ${(error as Error).message}`);
+    }
+  }
+
   private saveCompletedHistory(): void {
     try {
       const serialized = JSON.stringify(this.completedTasks);

+ 44 - 0
entry/src/main/ets/common/util/EqualizerManager.ets

@@ -272,6 +272,50 @@ export class EqualizerManager {
     return newPreset;
   }
 
+  // 导入外部预设为自定义预设,若同名则覆盖原自定义预设
+  importPreset(name: string, gains: number[]): EqualizerPreset {
+    const normalizedName = name.trim().length > 0 ? name.trim() : `导入预设_${Date.now()}`;
+    const normalizedGains: number[] = [];
+    for (let i = 0; i < 10; i++) {
+      let value = i < gains.length ? gains[i] : 0;
+      if (!Number.isFinite(value)) {
+        value = 0;
+      }
+      if (value < -12) {
+        value = -12;
+      }
+      if (value > 12) {
+        value = 12;
+      }
+      normalizedGains.push(Math.round(value * 10) / 10);
+    }
+
+    let existingIndex = -1;
+    for (let i = 0; i < this._customPresets.length; i++) {
+      if (this._customPresets[i].name === normalizedName) {
+        existingIndex = i;
+        break;
+      }
+    }
+
+    const presetId = existingIndex >= 0 ? this._customPresets[existingIndex].id : `custom_${Date.now()}`;
+    const preset: EqualizerPreset = {
+      id: presetId,
+      name: normalizedName,
+      gains: normalizedGains,
+      isCustom: true
+    };
+
+    if (existingIndex >= 0) {
+      this._customPresets.splice(existingIndex, 1, preset);
+    } else {
+      this._customPresets.push(preset);
+    }
+    this._currentPresetId = preset.id;
+    this.saveState();
+    return preset;
+  }
+
   // 删除自定义预设
   deleteCustomPreset(presetId: string): boolean {
     let index = -1;

+ 1278 - 0
entry/src/main/ets/common/util/IsoArchiveTaskHelper.ets

@@ -0,0 +1,1278 @@
+import { taskpool } from '@kit.ArkTS';
+import fileIo from '@ohos.file.fs';
+
+const ISO_SECTOR_SIZE: number = 2048;
+const ISO_DESCRIPTOR_START_LSN: number = 16;
+const ISO_DESCRIPTOR_END_LSN: number = 64;
+const ISO_PRIMARY_VOLUME_DESCRIPTOR: number = 1;
+const ISO_SUPPLEMENTARY_VOLUME_DESCRIPTOR: number = 2;
+const ISO_TERMINATOR_DESCRIPTOR: number = 255;
+const ISO_STANDARD_IDENTIFIER: string = 'CD001';
+
+const SACD_START_OF_MASTER_TOC: number = 510;
+const SACD_MASTER_TOC_LEN: number = 10;
+const SACD_SAMPLING_FREQUENCY: number = 2822400;
+const SACD_BLOCK_SIZE_PER_CHANNEL: number = 4096;
+const SACD_BITS_PER_SAMPLE: number = 1;
+const SACD_FRAME_SIZE_64: number = 4704;
+const SACD_MAX_PACKET_COUNT: number = 7;
+const SACD_MAX_FRAME_BUFFER_SIZE: number = 64 * 1024;
+
+const SACD_PACKET_TYPE_AUDIO: number = 2;
+
+const DSF_HEADER_SIZE: number = 92;
+const DSF_CHANNEL_TYPE_MONO: number = 1;
+const DSF_CHANNEL_TYPE_STEREO: number = 2;
+const DSF_CHANNEL_TYPE_3_CHANNELS: number = 3;
+const DSF_CHANNEL_TYPE_QUAD: number = 4;
+const DSF_CHANNEL_TYPE_4_CHANNELS: number = 5;
+const DSF_CHANNEL_TYPE_5_CHANNELS: number = 6;
+const DSF_CHANNEL_TYPE_5_1_CHANNELS: number = 7;
+
+const SACD_FRAME_FORMAT_DST: number = 0;
+const BIT_REVERSE_TABLE: Uint8Array = createBitReverseTable();
+
+const AUDIO_EXTENSIONS: string[] = [
+  '.mp3', '.wav', '.wma', '.mp2', '.mov', '.flac', '.midi', '.ra', '.aac', '.ape', '.cda', '.alac', '.m4a',
+  '.ogg', '.opus', '.wv', '.aiff', '.amr', '.aif', '.dff', '.au', '.eac3', '.mlp', '.tak', '.thd', '.tta',
+  '.ac3', '.mka', '.mpc', '.asf', '.m2ts', '.m4b', '.mts', '.rm', '.wtv', '.dts', '.av3a', '.dsd', '.dsf'
+];
+
+interface IsoDirectoryRootDescriptor {
+  extent: number;
+  size: number;
+  isJoliet: boolean;
+}
+
+interface IsoDirectoryTaskItem {
+  extent: number;
+  size: number;
+  isJoliet: boolean;
+  relativePath: string;
+}
+
+interface IsoDirectoryRecord {
+  extent: number;
+  size: number;
+  isDirectory: boolean;
+  name: string;
+}
+
+interface SacdMasterTocInfo {
+  area1Toc1Start: number;
+  area1TocSize: number;
+  area2Toc1Start: number;
+  area2TocSize: number;
+}
+
+interface SacdAreaTocInfo {
+  trackOffset: number;
+  trackCount: number;
+  channelCount?: number;
+  frameFormat?: number;
+}
+
+interface SacdAreaExtractInfo {
+  areaStartLsn: number;
+  areaTocSize: number;
+  channelCount: number;
+  frameFormat: number;
+}
+
+interface SacdPacketInfo {
+  frameStart: boolean;
+  dataType: number;
+  packetLength: number;
+}
+
+interface SacdFrameInfo {
+  sectorCount: number;
+  channelBit2?: boolean;
+  channelBit3?: boolean;
+}
+
+interface SacdFrameAssembler {
+  started: boolean;
+  size: number;
+  channelCount: number;
+  dstEncoded: boolean;
+  sectorCount: number;
+  buffer: Uint8Array;
+}
+
+interface DsfWriterState {
+  file: fileIo.File;
+  channelCount: number;
+  channelType: number;
+  channelBuffers: Uint8Array[];
+  blockFill: number;
+  audioDataSize: number;
+  sampleBytes: number;
+  outputOffset: number;
+}
+
+interface IsoExtractTaskPayload {
+  isoPath: string;
+  destinationDir: string;
+  entries: IsoArchiveHelperAudioEntry[];
+}
+
+export interface IsoArchiveHelperAudioEntry {
+  path: string;
+  name: string;
+  size: number;
+  extent: number;
+  entryType?: string;
+  trackNumber?: number;
+  startLsn?: number;
+  lengthLsn?: number;
+  startTimeFrames?: number;
+  durationFrames?: number;
+  areaType?: string;
+  channelCount?: number;
+  frameFormat?: number;
+}
+
+export interface IsoArchiveHelperExtractTaskResult {
+  extractedPaths: string[];
+  failedEntries: string[];
+}
+
+interface IsoExtractProgressState {
+  totalUnits: number;
+  completedUnits: number;
+  lastProgress: number;
+}
+
+interface IsoExtractProgressMessage {
+  progress: number;
+  current: number;
+  total: number;
+  name?: string;
+  stage?: string;
+}
+
+export function listIsoAudioEntriesCore(isoPath: string): IsoArchiveHelperAudioEntry[] {
+  const standardEntries: IsoArchiveHelperAudioEntry[] = listStandardIsoAudioEntries(isoPath);
+  if (standardEntries.length > 0) {
+    return standardEntries;
+  }
+  return listSacdAudioEntries(isoPath);
+}
+
+export function extractIsoAudioEntriesCore(payloadJson: string): IsoArchiveHelperExtractTaskResult {
+  const result: IsoArchiveHelperExtractTaskResult = {
+    extractedPaths: [],
+    failedEntries: []
+  };
+
+  let payload: IsoExtractTaskPayload | null = null;
+  try {
+    payload = JSON.parse(payloadJson) as IsoExtractTaskPayload;
+  } catch (_error) {
+    return result;
+  }
+
+  if (!payload || !payload.isoPath || !payload.destinationDir || !Array.isArray(payload.entries)) {
+    return result;
+  }
+
+  let isoFile: fileIo.File | undefined = undefined;
+  let progressState: IsoExtractProgressState | undefined = undefined;
+  try {
+    isoFile = fileIo.openSync(payload.isoPath, fileIo.OpenMode.READ_ONLY);
+    ensureDirectoryExists(payload.destinationDir);
+    progressState = createIsoExtractProgressState(payload.entries);
+    reportIsoExtractProgress(progressState, '', 'prepare', true);
+
+    for (let index: number = 0; index < payload.entries.length; index++) {
+      const entry: IsoArchiveHelperAudioEntry = payload.entries[index];
+      if (!entry || !entry.path) {
+        continue;
+      }
+
+      const outputPath: string = buildUniqueOutputPath(payload.destinationDir, entry.name || getLastPathSegment(entry.path));
+      let extracted: boolean = false;
+      if (entry.entryType === 'sacd_track') {
+        extracted = extractSacdTrackEntry(isoFile, entry, outputPath, progressState);
+      } else {
+        extracted = extractStandardIsoEntry(isoFile, entry, outputPath, progressState);
+      }
+      if (extracted) {
+        result.extractedPaths.push(outputPath);
+      } else {
+        if (pathExists(outputPath)) {
+          try {
+            fileIo.unlinkSync(outputPath);
+          } catch (_error) {
+          }
+        }
+        result.failedEntries.push(entry.path);
+      }
+    }
+    if (progressState) {
+      progressState.completedUnits = progressState.totalUnits;
+      reportIsoExtractProgress(progressState, '', 'done', true);
+    }
+  } catch (_error) {
+    for (let index: number = 0; index < payload.entries.length; index++) {
+      const entry: IsoArchiveHelperAudioEntry = payload.entries[index];
+      if (entry && entry.path) {
+        result.failedEntries.push(entry.path);
+      }
+    }
+    if (progressState) {
+      reportIsoExtractProgress(progressState, '', 'error', true);
+    }
+  } finally {
+    if (isoFile) {
+      try {
+        fileIo.closeSync(isoFile);
+      } catch (_error) {
+      }
+    }
+  }
+
+  return result;
+}
+
+function createIsoExtractProgressState(entries: IsoArchiveHelperAudioEntry[]): IsoExtractProgressState {
+  let totalUnits: number = 0;
+  for (let index: number = 0; index < entries.length; index++) {
+    totalUnits += getIsoExtractEntryUnits(entries[index]);
+  }
+  if (totalUnits <= 0) {
+    totalUnits = 1;
+  }
+  return {
+    totalUnits,
+    completedUnits: 0,
+    lastProgress: -1
+  };
+}
+
+function getIsoExtractEntryUnits(entry: IsoArchiveHelperAudioEntry): number {
+  if (!entry) {
+    return 0;
+  }
+  if (entry.entryType === 'sacd_track' && entry.lengthLsn && entry.lengthLsn > 0) {
+    return entry.lengthLsn * ISO_SECTOR_SIZE;
+  }
+  if (entry.size > 0) {
+    return entry.size;
+  }
+  return ISO_SECTOR_SIZE;
+}
+
+function advanceIsoExtractProgress(progressState: IsoExtractProgressState | undefined, deltaUnits: number,
+  entryName: string, stage: string): void {
+  if (!progressState || deltaUnits <= 0) {
+    return;
+  }
+  progressState.completedUnits += deltaUnits;
+  if (progressState.completedUnits > progressState.totalUnits) {
+    progressState.completedUnits = progressState.totalUnits;
+  }
+  reportIsoExtractProgress(progressState, entryName, stage, false);
+}
+
+function reportIsoExtractProgress(progressState: IsoExtractProgressState | undefined, entryName: string,
+  stage: string, force: boolean): void {
+  if (!progressState) {
+    return;
+  }
+  let progress: number = 0;
+  if (progressState.completedUnits <= 0) {
+    progress = 0;
+  } else if (progressState.completedUnits >= progressState.totalUnits) {
+    progress = 100;
+  } else {
+    progress = Math.ceil((progressState.completedUnits / progressState.totalUnits) * 100);
+  }
+  if (!force && progress <= progressState.lastProgress) {
+    return;
+  }
+  progressState.lastProgress = progress;
+  try {
+    const progressMessage: IsoExtractProgressMessage = {
+      progress,
+      current: progressState.completedUnits,
+      total: progressState.totalUnits,
+      name: entryName,
+      stage
+    };
+    taskpool.Task.sendData(progressMessage);
+  } catch (_error) {
+  }
+}
+
+function listStandardIsoAudioEntries(isoPath: string): IsoArchiveHelperAudioEntry[] {
+  let isoFile: fileIo.File | undefined = undefined;
+  try {
+    isoFile = fileIo.openSync(isoPath, fileIo.OpenMode.READ_ONLY);
+    const rootDescriptor: IsoDirectoryRootDescriptor | null = resolveIsoRootDescriptor(isoFile);
+    if (!rootDescriptor) {
+      return [];
+    }
+    return collectIsoAudioEntries(isoFile, rootDescriptor);
+  } catch (_error) {
+    return [];
+  } finally {
+    if (isoFile) {
+      try {
+        fileIo.closeSync(isoFile);
+      } catch (_error) {
+      }
+    }
+  }
+}
+
+function resolveIsoRootDescriptor(isoFile: fileIo.File): IsoDirectoryRootDescriptor | null {
+  let primaryRoot: IsoDirectoryRootDescriptor | null = null;
+  let jolietRoot: IsoDirectoryRootDescriptor | null = null;
+
+  for (let lsn: number = ISO_DESCRIPTOR_START_LSN; lsn < ISO_DESCRIPTOR_END_LSN; lsn++) {
+    const descriptor: Uint8Array = readBytes(isoFile, lsn * ISO_SECTOR_SIZE, ISO_SECTOR_SIZE);
+    if (descriptor.length < ISO_SECTOR_SIZE) {
+      break;
+    }
+    if (readAscii(descriptor, 1, 5) !== ISO_STANDARD_IDENTIFIER) {
+      if (descriptor[0] === ISO_TERMINATOR_DESCRIPTOR) {
+        break;
+      }
+      continue;
+    }
+
+    if (descriptor[0] === ISO_PRIMARY_VOLUME_DESCRIPTOR) {
+      primaryRoot = parseIsoRootFromVolumeDescriptor(descriptor, false);
+    } else if (descriptor[0] === ISO_SUPPLEMENTARY_VOLUME_DESCRIPTOR && isJolietDescriptor(descriptor)) {
+      jolietRoot = parseIsoRootFromVolumeDescriptor(descriptor, true);
+    } else if (descriptor[0] === ISO_TERMINATOR_DESCRIPTOR) {
+      break;
+    }
+
+    if (descriptor[0] === ISO_TERMINATOR_DESCRIPTOR) {
+      break;
+    }
+  }
+
+  if (jolietRoot) {
+    return jolietRoot;
+  }
+  return primaryRoot;
+}
+
+function isJolietDescriptor(descriptor: Uint8Array): boolean {
+  if (descriptor.length < 91) {
+    return false;
+  }
+  if (descriptor[88] !== 0x25 || descriptor[89] !== 0x2f) {
+    return false;
+  }
+  return descriptor[90] === 0x40 || descriptor[90] === 0x43 || descriptor[90] === 0x45;
+}
+
+function parseIsoRootFromVolumeDescriptor(descriptor: Uint8Array, isJoliet: boolean): IsoDirectoryRootDescriptor | null {
+  const recordLength: number = descriptor[156];
+  if (recordLength <= 0 || 156 + recordLength > descriptor.length) {
+    return null;
+  }
+
+  const extent: number = readUInt32LE(descriptor, 158);
+  const size: number = readUInt32LE(descriptor, 166);
+  if (extent <= 0 || size <= 0) {
+    return null;
+  }
+
+  return {
+    extent,
+    size,
+    isJoliet
+  };
+}
+
+function collectIsoAudioEntries(isoFile: fileIo.File, rootDescriptor: IsoDirectoryRootDescriptor): IsoArchiveHelperAudioEntry[] {
+  const result: IsoArchiveHelperAudioEntry[] = [];
+  const visitedKeys: Set<string> = new Set<string>();
+  const stack: IsoDirectoryTaskItem[] = [{
+    extent: rootDescriptor.extent,
+    size: rootDescriptor.size,
+    isJoliet: rootDescriptor.isJoliet,
+    relativePath: ''
+  }];
+
+  while (stack.length > 0) {
+    const current: IsoDirectoryTaskItem = stack.pop() as IsoDirectoryTaskItem;
+    const visitKey: string = `${current.extent}_${current.size}_${current.relativePath}_${current.isJoliet ? '1' : '0'}`;
+    if (visitedKeys.has(visitKey)) {
+      continue;
+    }
+    visitedKeys.add(visitKey);
+
+    const directoryData: Uint8Array = readBytes(isoFile, current.extent * ISO_SECTOR_SIZE, current.size);
+    let offset: number = 0;
+    while (offset < directoryData.length) {
+      const recordLength: number = directoryData[offset];
+      if (recordLength === 0) {
+        offset = moveToNextSector(offset);
+        continue;
+      }
+      if (offset + recordLength > directoryData.length) {
+        break;
+      }
+
+      const record: IsoDirectoryRecord | null = parseIsoDirectoryRecord(directoryData, offset, current.isJoliet);
+      offset += recordLength;
+      if (!record || !record.name) {
+        continue;
+      }
+
+      const relativePath: string = current.relativePath.length > 0 ? `${current.relativePath}/${record.name}` : record.name;
+      if (record.isDirectory) {
+        if (record.extent > 0 && record.size > 0) {
+          stack.push({
+            extent: record.extent,
+            size: record.size,
+            isJoliet: current.isJoliet,
+            relativePath
+          });
+        }
+        continue;
+      }
+
+      if (!isAudioFileName(record.name)) {
+        continue;
+      }
+
+      result.push({
+        path: relativePath,
+        name: record.name,
+        size: record.size,
+        extent: record.extent,
+        entryType: 'iso_file'
+      });
+    }
+  }
+
+  result.sort((left: IsoArchiveHelperAudioEntry, right: IsoArchiveHelperAudioEntry) => left.path.localeCompare(right.path));
+  return result;
+}
+
+function moveToNextSector(offset: number): number {
+  const sectorIndex: number = Math.floor(offset / ISO_SECTOR_SIZE);
+  return (sectorIndex + 1) * ISO_SECTOR_SIZE;
+}
+
+function parseIsoDirectoryRecord(buffer: Uint8Array, offset: number, isJoliet: boolean): IsoDirectoryRecord | null {
+  const recordLength: number = buffer[offset];
+  if (recordLength < 34) {
+    return null;
+  }
+
+  const nameLength: number = buffer[offset + 32];
+  const nameStart: number = offset + 33;
+  if (nameStart + nameLength > offset + recordLength) {
+    return null;
+  }
+
+  if (nameLength === 1) {
+    const specialName: number = buffer[nameStart];
+    if (specialName === 0 || specialName === 1) {
+      return null;
+    }
+  }
+
+  const nameBytes: Uint8Array = buffer.subarray(nameStart, nameStart + nameLength);
+  let decodedName: string = isJoliet ? decodeJolietString(nameBytes) : decodeAsciiString(nameBytes);
+  decodedName = sanitizeIsoEntryName(decodedName);
+  if (!decodedName) {
+    return null;
+  }
+
+  return {
+    extent: readUInt32LE(buffer, offset + 2),
+    size: readUInt32LE(buffer, offset + 10),
+    isDirectory: (buffer[offset + 25] & 0x02) !== 0,
+    name: decodedName
+  };
+}
+
+function sanitizeIsoEntryName(name: string): string {
+  let sanitized: string = name;
+  const versionIndex: number = sanitized.indexOf(';');
+  if (versionIndex >= 0) {
+    sanitized = sanitized.substring(0, versionIndex);
+  }
+  if (sanitized.endsWith('.')) {
+    sanitized = sanitized.substring(0, sanitized.length - 1);
+  }
+  return sanitized.trim();
+}
+
+function decodeAsciiString(bytes: Uint8Array): string {
+  let result: string = '';
+  for (let index: number = 0; index < bytes.length; index++) {
+    const value: number = bytes[index];
+    if (value === 0) {
+      continue;
+    }
+    result += String.fromCharCode(value);
+  }
+  return result;
+}
+
+function decodeJolietString(bytes: Uint8Array): string {
+  let result: string = '';
+  for (let index: number = 0; index + 1 < bytes.length; index += 2) {
+    const codePoint: number = (bytes[index] << 8) | bytes[index + 1];
+    if (codePoint === 0) {
+      continue;
+    }
+    result += String.fromCharCode(codePoint);
+  }
+  return result;
+}
+
+function isAudioFileName(fileName: string): boolean {
+  const extension: string = getFileExtension(fileName).toLowerCase();
+  for (let index: number = 0; index < AUDIO_EXTENSIONS.length; index++) {
+    if (extension === AUDIO_EXTENSIONS[index]) {
+      return true;
+    }
+  }
+  return false;
+}
+
+function getFileExtension(fileName: string): string {
+  const lastDot: number = fileName.lastIndexOf('.');
+  if (lastDot < 0) {
+    return '';
+  }
+  return fileName.substring(lastDot);
+}
+
+function extractStandardIsoEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudioEntry, outputPath: string,
+  progressState?: IsoExtractProgressState): boolean {
+  if (entry.extent <= 0 || entry.size <= 0) {
+    return false;
+  }
+
+  let targetFile: fileIo.File | undefined = undefined;
+  try {
+    targetFile = fileIo.openSync(outputPath, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
+
+    let remaining: number = entry.size;
+    let readOffset: number = entry.extent * ISO_SECTOR_SIZE;
+    const chunkSize: number = 256 * 1024;
+
+    while (remaining > 0) {
+      const currentChunkSize: number = remaining > chunkSize ? chunkSize : remaining;
+      const buffer: ArrayBuffer = new ArrayBuffer(currentChunkSize);
+      const bytesRead: number = fileIo.readSync(isoFile.fd, buffer, { offset: readOffset, length: currentChunkSize });
+      if (bytesRead <= 0) {
+        return false;
+      }
+
+      if (bytesRead === currentChunkSize) {
+        fileIo.writeSync(targetFile.fd, buffer);
+      } else {
+        fileIo.writeSync(targetFile.fd, buffer, { length: bytesRead });
+      }
+      remaining -= bytesRead;
+      readOffset += bytesRead;
+      advanceIsoExtractProgress(progressState, bytesRead, entry.name, 'extract');
+    }
+    return true;
+  } catch (_error) {
+    return false;
+  } finally {
+    if (targetFile) {
+      try {
+        fileIo.closeSync(targetFile);
+      } catch (_error) {
+      }
+    }
+  }
+}
+
+function readSectors(file: fileIo.File, startLsn: number, sectorCount: number): Uint8Array {
+  return readBytes(file, startLsn * ISO_SECTOR_SIZE, sectorCount * ISO_SECTOR_SIZE);
+}
+
+function listSacdAudioEntries(isoPath: string): IsoArchiveHelperAudioEntry[] {
+  let isoFile: fileIo.File | undefined = undefined;
+  try {
+    isoFile = fileIo.openSync(isoPath, fileIo.OpenMode.READ_ONLY);
+    const masterToc: SacdMasterTocInfo | null = parseSacdMasterToc(isoFile);
+    if (!masterToc) {
+      return [];
+    }
+
+    const stereoEntries: IsoArchiveHelperAudioEntry[] = parseSacdAreaEntries(
+      isoFile,
+      masterToc.area1Toc1Start,
+      masterToc.area1TocSize,
+      '2CH'
+    );
+    if (stereoEntries.length > 0) {
+      return stereoEntries;
+    }
+
+    return parseSacdAreaEntries(isoFile, masterToc.area2Toc1Start, masterToc.area2TocSize, 'MCH');
+  } catch (_error) {
+    return [];
+  } finally {
+    if (isoFile) {
+      try {
+        fileIo.closeSync(isoFile);
+      } catch (_error) {
+      }
+    }
+  }
+}
+
+function parseSacdMasterToc(isoFile: fileIo.File): SacdMasterTocInfo | null {
+  const masterData: Uint8Array = readBytes(isoFile, SACD_START_OF_MASTER_TOC * ISO_SECTOR_SIZE,
+    SACD_MASTER_TOC_LEN * ISO_SECTOR_SIZE);
+  if (masterData.length < ISO_SECTOR_SIZE) {
+    return null;
+  }
+  if (readAscii(masterData, 0, 8) !== 'SACDMTOC') {
+    return null;
+  }
+
+  return {
+    area1Toc1Start: readUInt32BE(masterData, 64),
+    area1TocSize: readUInt16BE(masterData, 84),
+    area2Toc1Start: readUInt32BE(masterData, 72),
+    area2TocSize: readUInt16BE(masterData, 86)
+  };
+}
+
+function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTocSize: number,
+  areaType: string): IsoArchiveHelperAudioEntry[] {
+  if (areaStartLsn <= 0 || areaTocSize <= 0) {
+    return [];
+  }
+
+  const areaData: Uint8Array = readBytes(isoFile, areaStartLsn * ISO_SECTOR_SIZE, areaTocSize * ISO_SECTOR_SIZE);
+  if (areaData.length < ISO_SECTOR_SIZE) {
+    return [];
+  }
+
+  const areaId: string = readAscii(areaData, 0, 8);
+  if (areaId !== 'TWOCHTOC' && areaId !== 'MULCHTOC') {
+    return [];
+  }
+
+  const areaInfo: SacdAreaTocInfo = {
+    trackOffset: areaData[68],
+    trackCount: areaData[69],
+    channelCount: areaData[32],
+    frameFormat: areaData[21] & 0x0f
+  };
+  if (areaInfo.trackCount <= 0) {
+    return [];
+  }
+
+  let trackListOffsetBlock: Uint8Array | null = null;
+  let trackListTimeBlock: Uint8Array | null = null;
+  const sectorCount: number = Math.floor(areaData.length / ISO_SECTOR_SIZE);
+  for (let sectorIndex: number = 0; sectorIndex < sectorCount; sectorIndex++) {
+    const offset: number = sectorIndex * ISO_SECTOR_SIZE;
+    const blockId: string = readAscii(areaData, offset, 8);
+    if (blockId === 'SACDTRL1') {
+      trackListOffsetBlock = areaData.subarray(offset, offset + ISO_SECTOR_SIZE);
+    } else if (blockId === 'SACDTRL2') {
+      trackListTimeBlock = areaData.subarray(offset, offset + ISO_SECTOR_SIZE);
+    }
+  }
+
+  if (!trackListOffsetBlock) {
+    return [];
+  }
+  const offsetBlock: Uint8Array = trackListOffsetBlock;
+
+  const result: IsoArchiveHelperAudioEntry[] = [];
+  const maxTrackCount: number = areaInfo.trackCount > 255 ? 255 : areaInfo.trackCount;
+  for (let index: number = 0; index < maxTrackCount; index++) {
+    const startLsn: number = readUInt32BE(offsetBlock, 8 + index * 4);
+    const lengthLsn: number = readUInt32BE(offsetBlock, 8 + 255 * 4 + index * 4);
+    if (startLsn <= 0 || lengthLsn <= 0) {
+      continue;
+    }
+
+    let startFrames: number = 0;
+    let durationFrames: number = 0;
+    if (trackListTimeBlock) {
+      startFrames = readSacdTrackTimeFrames(trackListTimeBlock, 8 + index * 4);
+      durationFrames = readSacdTrackTimeFrames(trackListTimeBlock, 8 + 255 * 4 + index * 4);
+    }
+
+    const trackNumber: number = areaInfo.trackOffset > 0 ? areaInfo.trackOffset + index : index + 1;
+    const trackLabel: string = areaType === 'MCH' ? `MCH ${padTrackNumber(trackNumber)}. Track ${padTrackNumber(trackNumber)}.dsf`
+      : `${padTrackNumber(trackNumber)}. Track ${padTrackNumber(trackNumber)}.dsf`;
+
+    result.push({
+      path: trackLabel,
+      name: trackLabel,
+      size: lengthLsn * ISO_SECTOR_SIZE,
+      extent: startLsn,
+      entryType: 'sacd_track',
+      trackNumber,
+      startLsn,
+      lengthLsn,
+      startTimeFrames: startFrames,
+      durationFrames,
+      areaType,
+      channelCount: areaInfo.channelCount,
+      frameFormat: areaInfo.frameFormat
+    });
+  }
+
+  return result;
+}
+
+function readSacdTrackTimeFrames(trackListTimeBlock: Uint8Array, offset: number): number {
+  if (offset + 3 >= trackListTimeBlock.length) {
+    return 0;
+  }
+  const minutes: number = trackListTimeBlock[offset];
+  const seconds: number = trackListTimeBlock[offset + 1];
+  const frames: number = trackListTimeBlock[offset + 2];
+  return minutes * 60 * 75 + seconds * 75 + frames;
+}
+
+function padTrackNumber(trackNumber: number): string {
+  if (trackNumber < 10) {
+    return `0${trackNumber}`;
+  }
+  return `${trackNumber}`;
+}
+
+function buildUniqueOutputPath(destinationDir: string, fileName: string): string {
+  const cleanName: string = sanitizeOutputFileName(fileName);
+  const extension: string = getFileExtension(cleanName);
+  const baseName: string = extension.length > 0 ? cleanName.substring(0, cleanName.length - extension.length) : cleanName;
+
+  let candidatePath: string = `${trimTrailingSeparator(destinationDir)}/${cleanName}`;
+  let suffix: number = 1;
+  while (pathExists(candidatePath)) {
+    suffix++;
+    candidatePath = `${trimTrailingSeparator(destinationDir)}/${baseName} (${suffix})${extension}`;
+  }
+  return candidatePath;
+}
+
+function sanitizeOutputFileName(fileName: string): string {
+  const lastSegment: string = getLastPathSegment(fileName);
+  let sanitized: string = '';
+  for (let index: number = 0; index < lastSegment.length; index++) {
+    const char: string = lastSegment[index];
+    if (char === '/' || char === '\\' || char === ':' || char === '*' || char === '?' || char === '"' ||
+      char === '<' || char === '>' || char === '|') {
+      sanitized += '_';
+    } else {
+      sanitized += char;
+    }
+  }
+  if (sanitized.trim().length === 0) {
+    return 'iso_audio.bin';
+  }
+  return sanitized;
+}
+
+function getLastPathSegment(path: string): string {
+  const normalizedPath: string = path.replace(/\\/g, '/');
+  const lastSeparator: number = normalizedPath.lastIndexOf('/');
+  if (lastSeparator < 0) {
+    return normalizedPath;
+  }
+  return normalizedPath.substring(lastSeparator + 1);
+}
+
+function trimTrailingSeparator(path: string): string {
+  if (path.endsWith('/')) {
+    return path.substring(0, path.length - 1);
+  }
+  return path;
+}
+
+function ensureDirectoryExists(dirPath: string): void {
+  if (!dirPath || pathExists(dirPath)) {
+    return;
+  }
+  const normalizedPath: string = trimTrailingSeparator(dirPath);
+  const lastSeparator: number = normalizedPath.lastIndexOf('/');
+  if (lastSeparator > 0) {
+    ensureDirectoryExists(normalizedPath.substring(0, lastSeparator));
+  }
+  try {
+    fileIo.mkdirSync(normalizedPath);
+  } catch (_error) {
+  }
+}
+
+function pathExists(path: string): boolean {
+  try {
+    fileIo.accessSync(path);
+    return true;
+  } catch (_error) {
+    return false;
+  }
+}
+
+function extractSacdTrackEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudioEntry, outputPath: string,
+  progressState?: IsoExtractProgressState): boolean {
+  if (!entry.startLsn || !entry.lengthLsn || !entry.areaType) {
+    return false;
+  }
+
+  reportIsoExtractProgress(progressState, entry.name, 'extract', true);
+
+  const areaInfo: SacdAreaExtractInfo | null = resolveSacdAreaExtractInfo(isoFile, entry.areaType);
+  if (!areaInfo) {
+    return false;
+  }
+  const channelCount: number = entry.channelCount && entry.channelCount > 0 ? entry.channelCount : areaInfo.channelCount;
+  const frameFormat: number = entry.frameFormat !== undefined ? entry.frameFormat : areaInfo.frameFormat;
+  if (frameFormat === SACD_FRAME_FORMAT_DST) {
+    return false;
+  }
+
+  let outputFile: fileIo.File | undefined = undefined;
+  try {
+    outputFile = fileIo.openSync(outputPath,
+      fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
+    const writer: DsfWriterState = createDsfWriter(outputFile, channelCount);
+
+    // 先写占位头,导出完成后再回填真实大小和样本数。
+    writeDsfHeader(writer);
+
+    const assembler: SacdFrameAssembler = {
+      started: false,
+      size: 0,
+      channelCount,
+      dstEncoded: false,
+      sectorCount: 0,
+      buffer: new Uint8Array(SACD_MAX_FRAME_BUFFER_SIZE)
+    };
+
+    const sectorsPerBatch: number = 128;
+    let sectorIndex: number = 0;
+    while (sectorIndex < entry.lengthLsn) {
+      const remainingSectors: number = entry.lengthLsn - sectorIndex;
+      const currentBatchSectors: number = remainingSectors > sectorsPerBatch ? sectorsPerBatch : remainingSectors;
+      const sectorBatch: Uint8Array = readSectors(isoFile, entry.startLsn + sectorIndex, currentBatchSectors);
+      if (sectorBatch.length < currentBatchSectors * ISO_SECTOR_SIZE) {
+        return false;
+      }
+      for (let batchIndex: number = 0; batchIndex < currentBatchSectors; batchIndex++) {
+        const start: number = batchIndex * ISO_SECTOR_SIZE;
+        const end: number = start + ISO_SECTOR_SIZE;
+        const sectorBuffer: Uint8Array = sectorBatch.subarray(start, end);
+        const lastSector: boolean = sectorIndex + batchIndex === entry.lengthLsn - 1;
+        const processed: boolean = processSacdSector(sectorBuffer, channelCount, assembler, writer, lastSector);
+        if (!processed) {
+          return false;
+        }
+      }
+      sectorIndex += currentBatchSectors;
+      advanceIsoExtractProgress(progressState, currentBatchSectors * ISO_SECTOR_SIZE, entry.name, 'extract');
+    }
+
+    finalizeDsfWriter(writer);
+    return true;
+  } catch (_error) {
+    return false;
+  } finally {
+    if (outputFile) {
+      try {
+        fileIo.closeSync(outputFile);
+      } catch (_error) {
+      }
+    }
+  }
+}
+
+function resolveSacdAreaExtractInfo(isoFile: fileIo.File, areaType: string): SacdAreaExtractInfo | null {
+  const masterToc: SacdMasterTocInfo | null = parseSacdMasterToc(isoFile);
+  if (!masterToc) {
+    return null;
+  }
+
+  let areaStartLsn: number = 0;
+  let areaTocSize: number = 0;
+  if (areaType === 'MCH') {
+    areaStartLsn = masterToc.area2Toc1Start;
+    areaTocSize = masterToc.area2TocSize;
+  } else {
+    areaStartLsn = masterToc.area1Toc1Start;
+    areaTocSize = masterToc.area1TocSize;
+  }
+  if (areaStartLsn <= 0 || areaTocSize <= 0) {
+    return null;
+  }
+
+  const areaData: Uint8Array = readBytes(isoFile, areaStartLsn * ISO_SECTOR_SIZE, areaTocSize * ISO_SECTOR_SIZE);
+  if (areaData.length < ISO_SECTOR_SIZE) {
+    return null;
+  }
+  const areaId: string = readAscii(areaData, 0, 8);
+  if (areaId !== 'TWOCHTOC' && areaId !== 'MULCHTOC') {
+    return null;
+  }
+
+  const channelCount: number = areaData[32];
+  const frameFormat: number = areaData[21] & 0x0f;
+  if (channelCount <= 0) {
+    return null;
+  }
+
+  return {
+    areaStartLsn,
+    areaTocSize,
+    channelCount,
+    frameFormat
+  };
+}
+
+function createDsfWriter(file: fileIo.File, channelCount: number): DsfWriterState {
+  const channelBuffers: Uint8Array[] = [];
+  for (let channelIndex: number = 0; channelIndex < channelCount; channelIndex++) {
+    channelBuffers.push(new Uint8Array(new ArrayBuffer(SACD_BLOCK_SIZE_PER_CHANNEL)));
+  }
+
+  return {
+    file,
+    channelCount,
+    channelType: mapDsfChannelType(channelCount),
+    channelBuffers,
+    blockFill: 0,
+    audioDataSize: 0,
+    sampleBytes: 0,
+    outputOffset: DSF_HEADER_SIZE
+  };
+}
+
+function writeDsfHeader(writer: DsfWriterState): void {
+  const headerBuffer: ArrayBuffer = new ArrayBuffer(DSF_HEADER_SIZE);
+  const bytes: Uint8Array = new Uint8Array(headerBuffer);
+
+  writeAscii(bytes, 0, 'DSD ');
+  writeUInt64LE(bytes, 4, 28);
+  writeUInt64LE(bytes, 12, DSF_HEADER_SIZE + writer.audioDataSize);
+  writeUInt64LE(bytes, 20, 0);
+
+  writeAscii(bytes, 28, 'fmt ');
+  writeUInt64LE(bytes, 32, 52);
+  writeUInt32LE(bytes, 40, 1);
+  writeUInt32LE(bytes, 44, 0);
+  writeUInt32LE(bytes, 48, writer.channelType);
+  writeUInt32LE(bytes, 52, writer.channelCount);
+  writeUInt32LE(bytes, 56, SACD_SAMPLING_FREQUENCY);
+  writeUInt32LE(bytes, 60, SACD_BITS_PER_SAMPLE);
+  writeUInt64LE(bytes, 64, Math.floor(writer.sampleBytes / writer.channelCount) * 8);
+  writeUInt32LE(bytes, 72, SACD_BLOCK_SIZE_PER_CHANNEL);
+  writeUInt32LE(bytes, 76, 0);
+
+  writeAscii(bytes, 80, 'data');
+  writeUInt64LE(bytes, 84, 12 + writer.audioDataSize);
+
+  fileIo.writeSync(writer.file.fd, headerBuffer, { offset: 0, length: DSF_HEADER_SIZE });
+}
+
+function finalizeDsfWriter(writer: DsfWriterState): void {
+  if (writer.blockFill > 0) {
+    flushDsfBlocks(writer, writer.blockFill);
+  }
+  writeDsfHeader(writer);
+}
+
+function flushDsfBlocks(writer: DsfWriterState, actualBytesPerChannel: number): void {
+  for (let channelIndex: number = 0; channelIndex < writer.channelCount; channelIndex++) {
+    const channelBuffer: Uint8Array = writer.channelBuffers[channelIndex];
+    if (actualBytesPerChannel < SACD_BLOCK_SIZE_PER_CHANNEL) {
+      channelBuffer.fill(0, actualBytesPerChannel);
+    }
+    fileIo.writeSync(writer.file.fd, channelBuffer.buffer, {
+      offset: writer.outputOffset,
+      length: SACD_BLOCK_SIZE_PER_CHANNEL
+    });
+    writer.outputOffset += SACD_BLOCK_SIZE_PER_CHANNEL;
+  }
+  writer.sampleBytes += actualBytesPerChannel * writer.channelCount;
+  writer.audioDataSize += SACD_BLOCK_SIZE_PER_CHANNEL * writer.channelCount;
+  writer.blockFill = 0;
+  for (let channelIndex: number = 0; channelIndex < writer.channelCount; channelIndex++) {
+    writer.channelBuffers[channelIndex].fill(0);
+  }
+}
+
+function writeSacdFrameToDsf(writer: DsfWriterState, frameData: Uint8Array, channelCount: number): boolean {
+  if (channelCount !== writer.channelCount || channelCount <= 0) {
+    return false;
+  }
+  let offset: number = 0;
+  while (offset < frameData.length) {
+    if (writer.blockFill >= SACD_BLOCK_SIZE_PER_CHANNEL) {
+      flushDsfBlocks(writer, SACD_BLOCK_SIZE_PER_CHANNEL);
+    }
+    for (let channelIndex: number = 0; channelIndex < writer.channelCount; channelIndex++) {
+      if (offset >= frameData.length) {
+        break;
+      }
+      writer.channelBuffers[channelIndex][writer.blockFill] = reverseBits(frameData[offset]);
+      offset++;
+    }
+    writer.blockFill++;
+  }
+  return true;
+}
+
+function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, assembler: SacdFrameAssembler,
+  writer: DsfWriterState, lastSector: boolean): boolean {
+  let offset: number = 0;
+  const sectorHeader: number = sectorBuffer[offset];
+  offset++;
+
+  const dstEncoded: boolean = (sectorHeader & 0x01) !== 0;
+  const frameInfoCount: number = (sectorHeader >> 2) & 0x07;
+  const packetInfoCount: number = (sectorHeader >> 5) & 0x07;
+  if (packetInfoCount > SACD_MAX_PACKET_COUNT || frameInfoCount > SACD_MAX_PACKET_COUNT) {
+    return false;
+  }
+
+  const packets: SacdPacketInfo[] = [];
+  for (let packetIndex: number = 0; packetIndex < packetInfoCount; packetIndex++) {
+    if (offset + 1 >= sectorBuffer.length) {
+      return false;
+    }
+    const byte0: number = sectorBuffer[offset];
+    const byte1: number = sectorBuffer[offset + 1];
+    packets.push({
+      frameStart: ((byte0 >> 7) & 0x01) === 1,
+      dataType: (byte0 >> 3) & 0x07,
+      packetLength: ((byte0 & 0x07) << 8) | byte1
+    });
+    offset += 2;
+  }
+
+  const frameInfos: SacdFrameInfo[] = [];
+  for (let frameIndex: number = 0; frameIndex < frameInfoCount; frameIndex++) {
+    if (offset + 2 >= sectorBuffer.length) {
+      return false;
+    }
+    if (dstEncoded) {
+      if (offset + 3 >= sectorBuffer.length) {
+        return false;
+      }
+      const extraByte: number = sectorBuffer[offset + 3];
+      frameInfos.push({
+        sectorCount: (extraByte >> 2) & 0x1f,
+        channelBit2: ((extraByte >> 1) & 0x01) === 1,
+        channelBit3: (extraByte & 0x01) === 1
+      });
+      offset += 4;
+    } else {
+      frameInfos.push({
+        sectorCount: 0,
+        channelBit2: false,
+        channelBit3: false
+      });
+      offset += 3;
+    }
+  }
+
+  let frameInfoIndex: number = 0;
+  for (let packetIndex: number = 0; packetIndex < packets.length; packetIndex++) {
+    const packet: SacdPacketInfo = packets[packetIndex];
+    if (offset + packet.packetLength > sectorBuffer.length) {
+      return false;
+    }
+
+    if (packet.dataType === SACD_PACKET_TYPE_AUDIO) {
+      if (packet.frameStart) {
+        if (!finalizeSacdFrame(assembler, writer)) {
+          return false;
+        }
+        if (assembler.started && assembler.size > 0) {
+          resetSacdFrameAssembler(assembler);
+        }
+
+        const frameInfo: SacdFrameInfo | undefined = frameInfoIndex < frameInfos.length ? frameInfos[frameInfoIndex] : undefined;
+        assembler.started = true;
+        assembler.size = 0;
+        assembler.dstEncoded = dstEncoded;
+        assembler.sectorCount = frameInfo ? frameInfo.sectorCount : 0;
+        assembler.channelCount = resolveSacdFrameChannelCount(frameInfo, areaChannelCount);
+        frameInfoIndex++;
+      }
+
+      if (assembler.started) {
+        if (assembler.dstEncoded) {
+          return false;
+        }
+        if (!appendSacdFrameBytes(assembler, sectorBuffer.subarray(offset, offset + packet.packetLength))) {
+          return false;
+        }
+      }
+    }
+    offset += packet.packetLength;
+  }
+
+  if (lastSector) {
+    const finalized: boolean = finalizeSacdFrame(assembler, writer);
+    resetSacdFrameAssembler(assembler);
+    return finalized;
+  }
+  return true;
+}
+
+function finalizeSacdFrame(assembler: SacdFrameAssembler, writer: DsfWriterState): boolean {
+  if (!isSacdFrameReady(assembler)) {
+    return true;
+  }
+  if (assembler.dstEncoded || assembler.channelCount <= 0 || assembler.size % assembler.channelCount !== 0) {
+    resetSacdFrameAssembler(assembler);
+    return false;
+  }
+  const frameData: Uint8Array = assembler.buffer.subarray(0, assembler.size);
+  const written: boolean = writeSacdFrameToDsf(writer, frameData, assembler.channelCount);
+  resetSacdFrameAssembler(assembler);
+  return written;
+}
+
+function isSacdFrameReady(assembler: SacdFrameAssembler): boolean {
+  if (!assembler.started || assembler.size <= 0) {
+    return false;
+  }
+  if (assembler.dstEncoded) {
+    return assembler.sectorCount === 0;
+  }
+  return assembler.size % SACD_FRAME_SIZE_64 === 0;
+}
+
+function appendSacdFrameBytes(assembler: SacdFrameAssembler, source: Uint8Array): boolean {
+  if (assembler.size + source.length > assembler.buffer.length) {
+    return false;
+  }
+  assembler.buffer.set(source, assembler.size);
+  assembler.size += source.length;
+  return true;
+}
+
+function resetSacdFrameAssembler(assembler: SacdFrameAssembler): void {
+  assembler.started = false;
+  assembler.size = 0;
+  assembler.dstEncoded = false;
+  assembler.sectorCount = 0;
+}
+
+function resolveSacdFrameChannelCount(frameInfo: SacdFrameInfo | undefined, defaultChannelCount: number): number {
+  if (!frameInfo) {
+    return defaultChannelCount > 0 ? defaultChannelCount : 2;
+  }
+  if (frameInfo.channelBit2 === true && frameInfo.channelBit3 !== true) {
+    return 6;
+  }
+  if (frameInfo.channelBit2 !== true && frameInfo.channelBit3 === true) {
+    return 5;
+  }
+  return defaultChannelCount > 0 ? defaultChannelCount : 2;
+}
+
+function mapDsfChannelType(channelCount: number): number {
+  switch (channelCount) {
+    case 1:
+      return DSF_CHANNEL_TYPE_MONO;
+    case 2:
+      return DSF_CHANNEL_TYPE_STEREO;
+    case 3:
+      return DSF_CHANNEL_TYPE_3_CHANNELS;
+    case 4:
+      return DSF_CHANNEL_TYPE_4_CHANNELS;
+    case 5:
+      return DSF_CHANNEL_TYPE_5_CHANNELS;
+    case 6:
+      return DSF_CHANNEL_TYPE_5_1_CHANNELS;
+    default:
+      return DSF_CHANNEL_TYPE_STEREO;
+  }
+}
+
+function createBitReverseTable(): Uint8Array {
+  const table: Uint8Array = new Uint8Array(256);
+  for (let value: number = 0; value < 256; value++) {
+    let source: number = value;
+    let reversed: number = 0;
+    for (let bitIndex: number = 0; bitIndex < 8; bitIndex++) {
+      reversed = (reversed << 1) | (source & 0x01);
+      source >>= 1;
+    }
+    table[value] = reversed;
+  }
+  return table;
+}
+
+function reverseBits(value: number): number {
+  return BIT_REVERSE_TABLE[value & 0xff];
+}
+
+function readBytes(file: fileIo.File, offset: number, length: number): Uint8Array {
+  if (length <= 0) {
+    return new Uint8Array(0);
+  }
+  const buffer: ArrayBuffer = new ArrayBuffer(length);
+  const bytesRead: number = fileIo.readSync(file.fd, buffer, { offset, length });
+  if (bytesRead <= 0) {
+    return new Uint8Array(0);
+  }
+  return new Uint8Array(buffer, 0, bytesRead);
+}
+
+function readAscii(buffer: Uint8Array, offset: number, length: number): string {
+  let result: string = '';
+  const maxLength: number = offset + length;
+  for (let index: number = offset; index < maxLength && index < buffer.length; index++) {
+    const value: number = buffer[index];
+    if (value === 0) {
+      continue;
+    }
+    result += String.fromCharCode(value);
+  }
+  return result;
+}
+
+function writeAscii(buffer: Uint8Array, offset: number, text: string): void {
+  for (let index: number = 0; index < text.length && offset + index < buffer.length; index++) {
+    buffer[offset + index] = text.charCodeAt(index);
+  }
+}
+
+function readUInt16BE(buffer: Uint8Array, offset: number): number {
+  return (buffer[offset] << 8) | buffer[offset + 1];
+}
+
+function readUInt32BE(buffer: Uint8Array, offset: number): number {
+  return buffer[offset] * 16777216 + (buffer[offset + 1] << 16) + (buffer[offset + 2] << 8) + buffer[offset + 3];
+}
+
+function readUInt32LE(buffer: Uint8Array, offset: number): number {
+  return buffer[offset] + (buffer[offset + 1] << 8) + (buffer[offset + 2] << 16) + buffer[offset + 3] * 16777216;
+}
+
+function writeUInt32LE(buffer: Uint8Array, offset: number, value: number): void {
+  buffer[offset] = value & 0xff;
+  buffer[offset + 1] = (value >> 8) & 0xff;
+  buffer[offset + 2] = (value >> 16) & 0xff;
+  buffer[offset + 3] = (value >> 24) & 0xff;
+}
+
+function writeUInt64LE(buffer: Uint8Array, offset: number, value: number): void {
+  let current: number = value;
+  for (let index: number = 0; index < 8; index++) {
+    buffer[offset + index] = current & 0xff;
+    current = Math.floor(current / 256);
+  }
+}

+ 43 - 0
entry/src/main/ets/common/util/IsoArchiveUtil.ets

@@ -0,0 +1,43 @@
+import {
+  extractIsoAudioEntriesCore,
+  listIsoAudioEntriesCore
+} from './IsoArchiveTaskHelper';
+
+export interface IsoArchiveAudioEntry {
+  path: string;
+  name: string;
+  size: number;
+  extent: number;
+  entryType?: string;
+  trackNumber?: number;
+  startLsn?: number;
+  lengthLsn?: number;
+  startTimeFrames?: number;
+  durationFrames?: number;
+  areaType?: string;
+  channelCount?: number;
+  frameFormat?: number;
+}
+
+export interface IsoExtractTaskResult {
+  extractedPaths: string[];
+  failedEntries: string[];
+}
+
+export interface IsoExtractProgressMessage {
+  progress: number;
+  current: number;
+  total: number;
+  name?: string;
+  stage?: string;
+}
+
+@Concurrent
+export async function listIsoAudioEntriesTask(isoPath: string): Promise<IsoArchiveAudioEntry[]> {
+  return listIsoAudioEntriesCore(isoPath) as IsoArchiveAudioEntry[];
+}
+
+@Concurrent
+export async function extractIsoAudioEntriesTask(payloadJson: string): Promise<IsoExtractTaskResult> {
+  return extractIsoAudioEntriesCore(payloadJson) as IsoExtractTaskResult;
+}

+ 441 - 2
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -17,13 +17,13 @@ import { CommonConstants } from '../constants/CommonConstants';
 import { GlobalContext, PreferencesUtil } from '@pura/harmony-utils';
 import { MusicInfo, parseMusicFileName, resolveAudioQualityTag, Utility } from './Utility';
 import { RemoteDriveType } from '../enums/RemoteDriveType';
-import { createSmbDirectory, deleteSmbEntry, listSmbDirectory, SmbDirectoryEntry } from '../network/SmbBridge';
+import { createSmbDirectory, deleteSmbEntry, listSmbDirectory, renameSmbEntry, SmbDirectoryEntry } from '../network/SmbBridge';
 import { NavidromeApi, NavidromeAlbumDetail, NavidromeArtist, NavidromeArtistDetail, NavidromeSong } from '../network/NavidromeApi';
 import { WebDavUrlUtil } from './WebDavUrlUtil';
 import MediaTable from './MediaTable';
 import { FtpClient, FileInfo as FtpEntryInfo, StringEncoding, AccessOptions } from '@liuzhosoft/ftp4h';
 import { BaiduConstants } from '../constants/BaiduConstants';
-import { appendAccessTokenToDlink, BaiduListEntry, BaiduFileMeta, buildAudioStreamingUrl, calculateMD5, createFolder, deleteFiles as deleteBaiduFiles, ensureAudioStreamReady, fetchFileMetas as fetchBaiduFileMetas, getUploadDomain, listDirectory as listBaiduDirectory, precreateFile, refreshAccessToken as refreshBaiduAccessToken, uploadFilePart } from '../network/BaiduPanClient';
+import { appendAccessTokenToDlink, BaiduListEntry, BaiduFileMeta, buildAudioStreamingUrl, calculateMD5, createFolder, deleteFiles as deleteBaiduFiles, ensureAudioStreamReady, fetchFileMetas as fetchBaiduFileMetas, getUploadDomain, listDirectory as listBaiduDirectory, precreateFile, refreshAccessToken as refreshBaiduAccessToken, renameFile as renameBaiduFile, uploadFilePart } from '../network/BaiduPanClient';
 import { ServerLogUtil } from './ServerLogUtil';
 import { taskpool } from '@kit.ArkTS';
 import { fileIo } from '@kit.CoreFileKit';
@@ -1571,6 +1571,89 @@ export class RemoteDriveManager {
     this.invalidateGlobalSearchIndex(account);
   }
 
+  public async deleteRemoteFolders(account: WebDavAccount, folders: FileInfo[]): Promise<void> {
+    if (!account) {
+      throw new Error('请先选择账户');
+    }
+    if (!folders || folders.length === 0) {
+      return;
+    }
+    const deletable = folders.filter(item => item && item.isDirectory && item.href && item.href.length > 0);
+    if (deletable.length === 0) {
+      return;
+    }
+    if (account.webType === RemoteDriveType.WebDav) {
+      await this.deleteWebDavFolders(account, deletable);
+    } else if (account.webType === RemoteDriveType.Smb) {
+      await this.deleteSmbFolders(account, deletable);
+    } else if (account.webType === RemoteDriveType.Ftp) {
+      await this.deleteFtpFolders(account, deletable);
+    } else if (account.webType === RemoteDriveType.Baidu) {
+      await this.deleteBaiduFolders(account, deletable);
+    } else {
+      throw new Error('当前账户类型暂不支持删除文件夹');
+    }
+    this.invalidateGlobalSearchIndex(account);
+  }
+
+  public async renameRemoteSong(account: WebDavAccount, song: VideoItem, newName: string): Promise<void> {
+    if (!account) {
+      throw new Error('请先选择账户');
+    }
+    if (!song) {
+      throw new Error('文件不存在');
+    }
+    const targetFileName = this.buildRemoteRenameTargetFileName(song, newName);
+    const currentFileName = this.resolveRemoteSongFileName(song);
+    if (targetFileName === currentFileName) {
+      return;
+    }
+
+    if (account.webType === RemoteDriveType.WebDav) {
+      await this.renameWebDavSong(account, song, targetFileName);
+    } else if (account.webType === RemoteDriveType.Smb) {
+      await this.renameSmbSong(account, song, targetFileName);
+    } else if (account.webType === RemoteDriveType.Ftp) {
+      await this.renameFtpSong(account, song, targetFileName);
+    } else if (account.webType === RemoteDriveType.Baidu) {
+      await this.renameBaiduSong(account, song, targetFileName);
+    } else {
+      throw new Error('当前账户类型暂不支持重命名');
+    }
+
+    this.invalidateGlobalSearchIndex(account);
+    this.notifyObservers(RemoteDriveManagerStates.RenameWebDavSong);
+  }
+
+  public async renameRemoteFolder(account: WebDavAccount, folder: FileInfo, newName: string): Promise<void> {
+    if (!account) {
+      throw new Error('请先选择账户');
+    }
+    if (!folder || !folder.isDirectory) {
+      throw new Error('文件夹不存在');
+    }
+    const targetFolderName = this.buildRemoteRenameTargetFolderName(newName);
+    const currentFolderName = this.resolveRemoteFolderDisplayName(folder);
+    if (targetFolderName === currentFolderName) {
+      return;
+    }
+
+    if (account.webType === RemoteDriveType.WebDav) {
+      await this.renameWebDavFolder(account, folder, targetFolderName);
+    } else if (account.webType === RemoteDriveType.Smb) {
+      await this.renameSmbFolder(account, folder, targetFolderName);
+    } else if (account.webType === RemoteDriveType.Ftp) {
+      await this.renameFtpFolder(account, folder, targetFolderName);
+    } else if (account.webType === RemoteDriveType.Baidu) {
+      await this.renameBaiduFolder(account, folder, targetFolderName);
+    } else {
+      throw new Error('当前账户类型暂不支持重命名文件夹');
+    }
+
+    this.invalidateGlobalSearchIndex(account);
+    this.notifyObservers(RemoteDriveManagerStates.RenameWebDavSong);
+  }
+
   private resolveWebDavDeletePath(song: VideoItem): string {
     if (song.remote_rel_path && song.remote_rel_path.length > 0) {
       return this.normalizeRemoteHref(song.remote_rel_path);
@@ -1584,6 +1667,15 @@ export class RemoteDriveManager {
     throw new Error(`无法确定 WebDAV 文件路径: ${song.name}`);
   }
 
+  private resolveWebDavFolderPath(folder: FileInfo): string {
+    const href = folder.href && folder.href.length > 0 ? folder.href : `/${folder.fileName}`;
+    const normalizedPath = this.normalizeRemoteHref(href);
+    if (!normalizedPath || normalizedPath === '/') {
+      throw new Error(`无法确定 WebDAV 文件夹路径: ${folder.fileName}`);
+    }
+    return normalizedPath;
+  }
+
   private async deleteWebDavSongs(account: WebDavAccount, songs: VideoItem[]): Promise<void> {
     const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
     if (!host || host.length === 0) {
@@ -1608,6 +1700,82 @@ export class RemoteDriveManager {
     }
   }
 
+  private async deleteWebDavFolders(account: WebDavAccount, folders: FileInfo[]): Promise<void> {
+    const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+    if (!host || host.length === 0) {
+      throw new Error('WebDAV账户缺少服务器地址');
+    }
+    for (let i = 0; i < folders.length; i++) {
+      const folder = folders[i];
+      const relativePath = this.resolveWebDavFolderPath(folder);
+      try {
+        await this.rcpSocket.RcpSendDelete(
+          host,
+          account.port,
+          account.account,
+          account.password,
+          relativePath,
+          account.enableHttps
+        );
+      } catch (error) {
+        const err = error as Error;
+        throw new Error(`删除文件夹 ${this.resolveRemoteFolderDisplayName(folder)} 失败: ${err.message}`);
+      }
+    }
+  }
+
+  private async renameWebDavSong(account: WebDavAccount, song: VideoItem, targetFileName: string): Promise<void> {
+    const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+    if (!host || host.length === 0) {
+      throw new Error('WebDAV账户缺少服务器地址');
+    }
+    const relativePath = this.resolveWebDavDeletePath(song);
+    const targetPath = this.buildRenameTargetPath(relativePath, targetFileName);
+    if (relativePath === targetPath) {
+      return;
+    }
+    try {
+      await this.rcpSocket.RcpSendMove(
+        host,
+        account.port,
+        account.account,
+        account.password,
+        relativePath,
+        account.enableHttps,
+        targetPath
+      );
+    } catch (error) {
+      const err = error as Error;
+      throw new Error(`重命名 ${song.name} 失败: ${err.message}`);
+    }
+  }
+
+  private async renameWebDavFolder(account: WebDavAccount, folder: FileInfo, targetFolderName: string): Promise<void> {
+    const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+    if (!host || host.length === 0) {
+      throw new Error('WebDAV账户缺少服务器地址');
+    }
+    const relativePath = this.resolveWebDavFolderPath(folder);
+    const targetPath = this.buildRenameTargetPath(relativePath, targetFolderName);
+    if (relativePath === targetPath) {
+      return;
+    }
+    try {
+      await this.rcpSocket.RcpSendMove(
+        host,
+        account.port,
+        account.account,
+        account.password,
+        relativePath,
+        account.enableHttps,
+        targetPath
+      );
+    } catch (error) {
+      const err = error as Error;
+      throw new Error(`重命名文件夹 ${this.resolveRemoteFolderDisplayName(folder)} 失败: ${err.message}`);
+    }
+  }
+
   private resolveSmbSongPath(song: VideoItem, account: WebDavAccount): string {
     if (song.remote_rel_path && song.remote_rel_path.length > 0) {
       return this.normalizeSmbRelativePath(song.remote_rel_path, account.smbShare);
@@ -1619,6 +1787,15 @@ export class RemoteDriveManager {
     throw new Error(`无法确定SMB路径: ${song.name}`);
   }
 
+  private resolveSmbFolderPath(folder: FileInfo, account: WebDavAccount): string {
+    const target = folder.href && folder.href.length > 0 ? folder.href : `/${folder.fileName}`;
+    const relativePath = this.normalizeSmbRelativePath(target, account.smbShare);
+    if (!relativePath || relativePath === '/') {
+      throw new Error(`无法确定SMB文件夹路径: ${folder.fileName}`);
+    }
+    return relativePath;
+  }
+
   private async deleteSmbSongs(account: WebDavAccount, songs: VideoItem[]): Promise<void> {
     if (!account.smbShare || account.smbShare.length === 0) {
       throw new Error('SMB账户缺少共享名称');
@@ -1650,6 +1827,92 @@ export class RemoteDriveManager {
     }
   }
 
+  private async deleteSmbFolders(account: WebDavAccount, folders: FileInfo[]): Promise<void> {
+    if (!account.smbShare || account.smbShare.length === 0) {
+      throw new Error('SMB账户缺少共享名称');
+    }
+    const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+    if (!host || host.length === 0) {
+      throw new Error('SMB账户缺少服务器地址');
+    }
+    for (let i = 0; i < folders.length; i++) {
+      const folder = folders[i];
+      const relativePath = this.resolveSmbFolderPath(folder, account);
+      try {
+        await deleteSmbEntry({
+          host,
+          share: account.smbShare,
+          username: account.account,
+          password: account.password,
+          domain: account.smbDomain,
+          path: relativePath,
+          isDirectory: true
+        });
+      } catch (error) {
+        const err = error as Error;
+        throw new Error(`删除文件夹 ${this.resolveRemoteFolderDisplayName(folder)} 失败: ${err.message}`);
+      }
+    }
+  }
+
+  private async renameSmbSong(account: WebDavAccount, song: VideoItem, targetFileName: string): Promise<void> {
+    if (!account.smbShare || account.smbShare.length === 0) {
+      throw new Error('SMB账户缺少共享名称');
+    }
+    const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+    if (!host || host.length === 0) {
+      throw new Error('SMB账户缺少服务器地址');
+    }
+    const relativePath = this.resolveSmbSongPath(song, account);
+    const targetPath = this.buildRenameTargetPath(relativePath, targetFileName);
+    if (relativePath === targetPath) {
+      return;
+    }
+    try {
+      await renameSmbEntry({
+        host,
+        share: account.smbShare,
+        username: account.account,
+        password: account.password,
+        domain: account.smbDomain,
+        path: relativePath,
+        newPath: targetPath
+      });
+    } catch (error) {
+      const err = error as Error;
+      throw new Error(`重命名 ${song.name} 失败: ${err.message}`);
+    }
+  }
+
+  private async renameSmbFolder(account: WebDavAccount, folder: FileInfo, targetFolderName: string): Promise<void> {
+    if (!account.smbShare || account.smbShare.length === 0) {
+      throw new Error('SMB账户缺少共享名称');
+    }
+    const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+    if (!host || host.length === 0) {
+      throw new Error('SMB账户缺少服务器地址');
+    }
+    const relativePath = this.resolveSmbFolderPath(folder, account);
+    const targetPath = this.buildRenameTargetPath(relativePath, targetFolderName);
+    if (relativePath === targetPath) {
+      return;
+    }
+    try {
+      await renameSmbEntry({
+        host,
+        share: account.smbShare,
+        username: account.account,
+        password: account.password,
+        domain: account.smbDomain,
+        path: relativePath,
+        newPath: targetPath
+      });
+    } catch (error) {
+      const err = error as Error;
+      throw new Error(`重命名文件夹 ${this.resolveRemoteFolderDisplayName(folder)} 失败: ${err.message}`);
+    }
+  }
+
   private resolveFtpSongPath(song: VideoItem): string {
     const target = song.remote_rel_path && song.remote_rel_path.length > 0 ? song.remote_rel_path : song.filePath;
     if (!target || target.length === 0) {
@@ -1658,6 +1921,15 @@ export class RemoteDriveManager {
     return this.normalizeFullPath(target);
   }
 
+  private resolveFtpFolderPath(folder: FileInfo): string {
+    const target = folder.href && folder.href.length > 0 ? folder.href : `/${folder.fileName}`;
+    const remotePath = this.normalizeFullPath(target);
+    if (!remotePath || remotePath === '/') {
+      throw new Error(`无法确定FTP文件夹路径: ${folder.fileName}`);
+    }
+    return remotePath;
+  }
+
   private async deleteFtpSongs(account: WebDavAccount, songs: VideoItem[]): Promise<void> {
     const options = this.buildFtpAccessOptions(account);
     const client = new FtpClient();
@@ -1676,6 +1948,62 @@ export class RemoteDriveManager {
     }
   }
 
+  private async deleteFtpFolders(account: WebDavAccount, folders: FileInfo[]): Promise<void> {
+    const options = this.buildFtpAccessOptions(account);
+    const client = new FtpClient();
+    try {
+      await client.access(options);
+      for (let i = 0; i < folders.length; i++) {
+        const folder = folders[i];
+        const remotePath = this.resolveFtpFolderPath(folder);
+        const success = await client.removeDir(remotePath);
+        if (!success) {
+          throw new Error(`删除文件夹 ${this.resolveRemoteFolderDisplayName(folder)} 失败`);
+        }
+      }
+    } finally {
+      await client.close();
+    }
+  }
+
+  private async renameFtpSong(account: WebDavAccount, song: VideoItem, targetFileName: string): Promise<void> {
+    const options = this.buildFtpAccessOptions(account);
+    const client = new FtpClient();
+    try {
+      await client.access(options);
+      const remotePath = this.resolveFtpSongPath(song);
+      const targetPath = this.buildRenameTargetPath(remotePath, targetFileName);
+      if (remotePath === targetPath) {
+        return;
+      }
+      const success = await client.rename(remotePath, targetPath);
+      if (!success) {
+        throw new Error(`重命名 ${song.name} 失败`);
+      }
+    } finally {
+      await client.close();
+    }
+  }
+
+  private async renameFtpFolder(account: WebDavAccount, folder: FileInfo, targetFolderName: string): Promise<void> {
+    const options = this.buildFtpAccessOptions(account);
+    const client = new FtpClient();
+    try {
+      await client.access(options);
+      const remotePath = this.resolveFtpFolderPath(folder);
+      const targetPath = this.buildRenameTargetPath(remotePath, targetFolderName);
+      if (remotePath === targetPath) {
+        return;
+      }
+      const success = await client.rename(remotePath, targetPath);
+      if (!success) {
+        throw new Error(`重命名文件夹 ${this.resolveRemoteFolderDisplayName(folder)} 失败`);
+      }
+    } finally {
+      await client.close();
+    }
+  }
+
   private async deleteBaiduSongs(account: WebDavAccount, songs: VideoItem[]): Promise<void> {
     const accessToken = await this.ensureBaiduAccessToken(account);
     const paths: string[] = [];
@@ -1698,6 +2026,117 @@ export class RemoteDriveManager {
     }
   }
 
+  private async deleteBaiduFolders(account: WebDavAccount, folders: FileInfo[]): Promise<void> {
+    const accessToken = await this.ensureBaiduAccessToken(account);
+    const paths: string[] = [];
+    for (let i = 0; i < folders.length; i++) {
+      const path = this.resolveBaiduFolderPath(folders[i]);
+      paths.push(path);
+    }
+    const batchSize = 20;
+    for (let i = 0; i < paths.length; i += batchSize) {
+      const batch = paths.slice(i, i + batchSize);
+      try {
+        await deleteBaiduFiles(accessToken, batch);
+      } catch (error) {
+        const err = error as Error;
+        throw new Error(`删除文件夹失败: ${err.message}`);
+      }
+    }
+  }
+
+  private async renameBaiduSong(account: WebDavAccount, song: VideoItem, targetFileName: string): Promise<void> {
+    const accessToken = await this.ensureBaiduAccessToken(account);
+    const path = song.remote_rel_path ? this.normalizeFullPath(song.remote_rel_path) : '';
+    if (!path || path.length === 0 || path === '/') {
+      throw new Error(`无法确定百度网盘路径: ${song.name}`);
+    }
+    try {
+      await renameBaiduFile(accessToken, path, targetFileName);
+    } catch (error) {
+      const err = error as Error;
+      throw new Error(`重命名 ${song.name} 失败: ${err.message}`);
+    }
+  }
+
+  private async renameBaiduFolder(account: WebDavAccount, folder: FileInfo, targetFolderName: string): Promise<void> {
+    const accessToken = await this.ensureBaiduAccessToken(account);
+    const path = this.resolveBaiduFolderPath(folder);
+    try {
+      await renameBaiduFile(accessToken, path, targetFolderName);
+    } catch (error) {
+      const err = error as Error;
+      throw new Error(`重命名文件夹 ${this.resolveRemoteFolderDisplayName(folder)} 失败: ${err.message}`);
+    }
+  }
+
+  private resolveRemoteSongFileName(song: VideoItem): string {
+    const fileName = song.fileName && song.fileName.length > 0 ? song.fileName : song.name;
+    if (!fileName || fileName.length === 0) {
+      throw new Error('无法确定文件名称');
+    }
+    return fileName;
+  }
+
+  private resolveRemoteFolderDisplayName(folder: FileInfo): string {
+    const fileName = folder.fileName && folder.fileName.length > 0 ? folder.fileName : folder.name;
+    const normalizedName = fileName.replace(/\/+$/, '');
+    if (!normalizedName || normalizedName.length === 0) {
+      throw new Error('无法确定文件夹名称');
+    }
+    return normalizedName;
+  }
+
+  private buildRemoteRenameTargetFolderName(newName: string): string {
+    const trimmed = newName ? newName.trim() : '';
+    const nextName = trimmed.replace(/\/+$/, '');
+    if (!nextName) {
+      throw new Error('文件夹名称不能为空');
+    }
+    const invalidChars = /[\\/:*?"<>|]/;
+    if (invalidChars.test(nextName)) {
+      throw new Error('文件夹名称不能包含 \\ / : * ? " < > | 字符');
+    }
+    return nextName;
+  }
+
+  private resolveBaiduFolderPath(folder: FileInfo): string {
+    const path = folder.href ? this.normalizeFullPath(folder.href) : '';
+    if (!path || path.length === 0 || path === '/') {
+      throw new Error(`无法确定百度网盘路径: ${folder.fileName}`);
+    }
+    return path;
+  }
+
+  private buildRemoteRenameTargetFileName(song: VideoItem, newName: string): string {
+    const trimmed = newName ? newName.trim() : '';
+    if (!trimmed) {
+      throw new Error('文件名称不能为空');
+    }
+    if (trimmed === '.' || trimmed === '..') {
+      throw new Error('文件名称不合法');
+    }
+    const invalidChars = /[\\/:*?"<>|]/;
+    if (invalidChars.test(trimmed)) {
+      throw new Error('文件名称不能包含 \\ / : * ? " < > | 字符');
+    }
+    const currentFileName = this.resolveRemoteSongFileName(song);
+    const extension = this.getFileExtension(currentFileName);
+    let nextName = trimmed;
+    if (extension.length > 0 && nextName.toLowerCase().endsWith(extension.toLowerCase())) {
+      nextName = nextName.substring(0, nextName.length - extension.length).trim();
+    }
+    if (!nextName) {
+      throw new Error('文件名称不能为空');
+    }
+    return extension.length > 0 ? `${nextName}${extension}` : nextName;
+  }
+
+  private buildRenameTargetPath(sourcePath: string, targetFileName: string): string {
+    const parentPath = this.getDirectoryFromRemotePath(sourcePath);
+    return this.normalizeFullPath(`${parentPath === '/' ? '' : parentPath}/${targetFileName}`);
+  }
+
   private scheduleBaiduPrefetch(account: WebDavAccount, accessToken: string, entries: BaiduListEntry[]): void {
     const fileEntries = entries.filter(entry => entry.isdir === 0 && this.isAudioFile(entry.server_filename));
     if (fileEntries.length === 0) {

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

@@ -466,6 +466,15 @@ export class Utility {
     return false;
   }
 
+  static isIsoByExtension(filename: string) {
+    const lastIndex = filename.lastIndexOf('.')
+    if (lastIndex !== -1) {
+      const fileExtension = filename.slice(lastIndex).toLowerCase()
+      return fileExtension === '.iso'
+    }
+    return false
+  }
+
   //判断是否是媒体文件  音乐和视频都可以
   static isMeidaByExtension(filename:string) {
     const extensions = CommonConstants.MEDIA_FORMAT
@@ -715,7 +724,8 @@ export class Utility {
     ,autoParseMusicName?:boolean,originFileName?:string): Promise<VideoItem> {
     await ReqPermissionUtil.persistPermission(uri);
 
-    if(StrUtil.isNotEmpty(uri)&&uri.toLowerCase().endsWith('.cue')){
+    if(StrUtil.isNotEmpty(uri)&&
+      (uri.toLowerCase().endsWith('.iso')||uri.toLowerCase().endsWith('.cue'))){
       const cueItem = new VideoItem(FileUtil.getFileName(uri),uri,uri,type,0,'')
       cueItem.fileName = FileUtil.getFileName(uri)
       return  cueItem
@@ -1160,7 +1170,8 @@ export class Utility {
     let list: Array<VideoItem> = [];
     for(let i=0;i<localList.length;i++){
       //播放列表过滤.cue文件
-      if(localList[i].type === type&&!localList[i].filePath.toLowerCase().endsWith('.cue') ){
+      if(localList[i].type === type&&!localList[i].filePath.toLowerCase().endsWith('.cue')
+        && !Utility.isIsoByExtension(localList[i].filePath)){
         list.push(localList[i]);
       }
 

+ 8 - 4
entry/src/main/ets/dialog/CreateFolderDialog.ets

@@ -6,6 +6,10 @@ export struct CreateFolderDialog {
   onConfirm?: (folderName: string) => void;
   onCancel?: () => void;
   initialName: string = '';
+  title: string = '新建文件夹';
+  placeholder: string = '请输入文件夹名称';
+  confirmText: string = '创建';
+  cancelText: string = '取消';
   themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
 
   @State folderName: string = '';
@@ -30,12 +34,12 @@ export struct CreateFolderDialog {
 
   build() {
     Column() {
-      Text('新建文件夹')
+      Text(this.title)
         .fontSize(20)
         .fontWeight(FontWeight.Medium)
         .margin({ bottom: 12 });
 
-      TextInput({ text: this.folderName, placeholder: '请输入文件夹名称' })
+      TextInput({ text: this.folderName, placeholder: this.placeholder })
         .width('100%')
         .height(48)
         .backgroundColor(Color.White)
@@ -46,7 +50,7 @@ export struct CreateFolderDialog {
         });
 
       Row() {
-        Button('取消')
+        Button(this.cancelText)
           .layoutWeight(1)
           .height(44)
           .backgroundColor(Color.Transparent)
@@ -55,7 +59,7 @@ export struct CreateFolderDialog {
           .onClick(() => this.handleCancel())
           .margin({ right: 8 });
 
-        Button('创建')
+        Button(this.confirmText)
           .layoutWeight(1)
           .height(44)
           .backgroundColor(this.themeColor || CommonConstants.DEFAULT_THEME_COLOR)

+ 1 - 0
entry/src/main/ets/pages/SettingPage.ets

@@ -96,6 +96,7 @@ export struct SettingPage {
   static readonly IS_COVER_TOP: string = 'IS_COVER_TOP';
   static readonly IS_SHOW_HEADER: string = 'IS_SHOW_HEADER';
   static readonly IS_COVER_TOP_BIG: string = 'IS_COVER_TOP_BIG';
+  static readonly RECTANGLE_COVER_SCALE: string = 'rectangle_cover_scale';
   static readonly LOG_UPLOAD_ENABLED: string = 'logUploadEnabled';
   static readonly MEITU_UPLOAD_TOKEN_KEY: string = 'meituUploadToken';
   static readonly MEITU_UPLOAD_DOMAIN_KEY: string = 'meituUploadDomain';

+ 487 - 25
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -12,7 +12,6 @@ import { emitter } from '@kit.BasicServicesKit';
 import { EventConstants } from '../common/constants/EventConstants';
 import { LazyDataSource } from '../common/util/LazyDataSource';
 import { ArrayUtil, FileUtil, MD5, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
-import { DialogHelper } from '@pura/harmony-dialog';
 import { ButtonFancyModifier,
   MenuModifier,
   ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
@@ -141,6 +140,7 @@ export struct WebDavMainPage {
   @State listRefreshKey: number = 0 // 列表刷新标识
   @Consume isMultiSelect: boolean
   @State selectedSongs: VideoItem[] = []
+  @State selectedFolders: FileInfo[] = []
   @State isAllSelected: boolean = false
   @State isDeletingSelection: boolean = false
   @State ignoreTapAfterExitMultiSelect: boolean = false
@@ -668,11 +668,47 @@ export struct WebDavMainPage {
     return this.selectedSongs.some(item => item.filePath === song.filePath);
   }
 
+  private resolveFolderSelectionKey(folder: FileInfo): string {
+    return folder.href;
+  }
+
+  private isFolderSelected(folder: FileInfo): boolean {
+    const key = this.resolveFolderSelectionKey(folder);
+    return this.selectedFolders.some(item => this.resolveFolderSelectionKey(item) === key);
+  }
+
+  private getSelectableSongs(): VideoItem[] {
+    return this.isSearchMode ? this.filteredList : this.songs;
+  }
+
+  private getSelectableFolders(): FileInfo[] {
+    return this.isSearchMode ? this.filteredFolderList : this.visibleFoldersState;
+  }
+
+  private getSelectedEntryCount(): number {
+    return this.selectedSongs.length + this.selectedFolders.length;
+  }
+
+  private hasSelectedFolders(): boolean {
+    return this.selectedFolders.length > 0;
+  }
+
+  private canOperateOnSelectedSongs(): boolean {
+    return this.selectedSongs.length > 0 && !this.hasSelectedFolders() && !this.isDeletingSelection;
+  }
+
+  private updateAllSelectedState(): void {
+    const totalSelectable = this.getSelectableSongs().length + this.getSelectableFolders().length;
+    const totalSelected = this.getSelectedEntryCount();
+    this.isAllSelected = totalSelected > 0 && totalSelected === totalSelectable;
+  }
+
   private enterMultiSelect(song: VideoItem): void {
     if (!this.isMultiSelect) {
       this.isMultiSelect = true;
       this.selectedSongs = [song];
-      this.isAllSelected = this.selectedSongs.length === this.songs.length && this.songs.length > 0;
+      this.selectedFolders = [];
+      this.updateAllSelectedState();
       return;
     }
     if (!this.isSongSelected(song)) {
@@ -688,14 +724,16 @@ export struct WebDavMainPage {
     if (exists) {
       const next = this.selectedSongs.filter(item => item.filePath !== song.filePath);
       this.selectedSongs = next;
-      this.isAllSelected = next.length > 0 && next.length === this.songs.length;
+      this.updateAllSelectedState();
       if (next.length === 0) {
-        this.exitMultiSelect();
+        if (this.selectedFolders.length === 0) {
+          this.exitMultiSelect();
+        }
       }
     } else {
       const next = [...this.selectedSongs, song];
       this.selectedSongs = next;
-      this.isAllSelected = next.length === this.songs.length && this.songs.length > 0;
+      this.updateAllSelectedState();
     }
   }
 
@@ -714,23 +752,73 @@ export struct WebDavMainPage {
     }
   }
 
+  private enterFolderMultiSelect(folder: FileInfo): void {
+    if (!this.isMultiSelect) {
+      this.isMultiSelect = true;
+      this.selectedFolders = [folder];
+      this.selectedSongs = [];
+      this.updateAllSelectedState();
+      return;
+    }
+    if (!this.isFolderSelected(folder)) {
+      this.toggleFolderSelection(folder);
+    }
+  }
+
+  private toggleFolderSelection(folder: FileInfo): void {
+    if (!this.isMultiSelect) {
+      this.isMultiSelect = true;
+    }
+    const key = this.resolveFolderSelectionKey(folder);
+    const exists = this.selectedFolders.some(item => this.resolveFolderSelectionKey(item) === key);
+    if (exists) {
+      const next = this.selectedFolders.filter(item => this.resolveFolderSelectionKey(item) !== key);
+      this.selectedFolders = next;
+      this.updateAllSelectedState();
+      if (next.length === 0 && this.selectedSongs.length === 0) {
+        this.exitMultiSelect();
+      }
+    } else {
+      this.selectedFolders = [...this.selectedFolders, folder];
+      this.updateAllSelectedState();
+    }
+  }
+
+  private handleFolderCheckboxSelection(folder: FileInfo, checked: boolean): void {
+    if (this.ignoreTapAfterExitMultiSelect) {
+      return;
+    }
+    if (!this.isMultiSelect) {
+      this.isMultiSelect = true;
+    }
+    const exists = this.isFolderSelected(folder);
+    if (checked && !exists) {
+      this.toggleFolderSelection(folder);
+    } else if (!checked && exists) {
+      this.toggleFolderSelection(folder);
+    }
+  }
+
   private toggleSelectAll(): void {
     if (this.isAllSelected) {
       this.selectedSongs = [];
+      this.selectedFolders = [];
       this.isAllSelected = false;
       return;
     }
     if (!this.isMultiSelect) {
       this.isMultiSelect = true;
     }
-    this.selectedSongs = this.songs.slice();
-    this.isAllSelected = this.selectedSongs.length > 0 && this.selectedSongs.length === this.songs.length;
+    this.selectedSongs = this.getSelectableSongs().slice();
+    this.selectedFolders = this.getSelectableFolders().slice();
+    this.updateAllSelectedState();
   }
 
   private exitMultiSelect(): void {
     this.ignoreTapAfterExitMultiSelect = true;
     this.isMultiSelect = false;
     this.selectedSongs = [];
+    this.selectedFolders = [];
     this.isAllSelected = false;
     setTimeout(() => {
       this.isMultiSelect = false;
@@ -743,13 +831,22 @@ export struct WebDavMainPage {
     if (!this.isMultiSelect) {
       return;
     }
-    const currentKeys: Set<string> = new Set(this.songs.map(item => item.filePath));
-    const filtered = this.selectedSongs.filter(item => currentKeys.has(item.filePath));
-    if (filtered.length !== this.selectedSongs.length) {
-      this.selectedSongs = filtered;
+    const currentSongKeys: Set<string> = new Set(this.getSelectableSongs().map(item => item.filePath));
+    const filteredSongs = this.selectedSongs.filter(item => currentSongKeys.has(item.filePath));
+    if (filteredSongs.length !== this.selectedSongs.length) {
+      this.selectedSongs = filteredSongs;
+    }
+    const currentFolderKeys: Set<string> = new Set(
+      this.getSelectableFolders().map(item => this.resolveFolderSelectionKey(item))
+    );
+    const filteredFolders = this.selectedFolders.filter(item =>
+      currentFolderKeys.has(this.resolveFolderSelectionKey(item))
+    );
+    if (filteredFolders.length !== this.selectedFolders.length) {
+      this.selectedFolders = filteredFolders;
     }
-    this.isAllSelected = filtered.length > 0 && filtered.length === this.songs.length;
-    if (filtered.length === 0) {
+    this.updateAllSelectedState();
+    if (filteredSongs.length === 0 && filteredFolders.length === 0) {
       this.exitMultiSelect();
     }
   }
@@ -759,13 +856,14 @@ export struct WebDavMainPage {
       this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
       return;
     }
-    if (!this.isMultiSelect || this.selectedSongs.length === 0) {
-      this.getUIContext().getPromptAction().showToast({ message: '请先选择要删除的文件' });
+    const totalSelected = this.getSelectedEntryCount();
+    if (!this.isMultiSelect || totalSelected === 0) {
+      this.getUIContext().getPromptAction().showToast({ message: '请先选择要删除的文件或文件夹' });
       return;
     }
     this.getUIContext().showAlertDialog({
-      title: '删除文件',
-      message: `确定删除选中的${this.selectedSongs.length}个文件吗?`,
+      title: '删除项目',
+      message: `确定删除选中的${totalSelected}项吗?`,
       primaryButton: {
         value: '取消',
         action: () => {}
@@ -781,16 +879,20 @@ export struct WebDavMainPage {
   }
 
   private async performDeleteSelected(): Promise<void> {
-    if (!this.selectedAccount || this.selectedSongs.length === 0) {
+    if (!this.selectedAccount || this.getSelectedEntryCount() === 0) {
       return;
     }
     this.isDeletingSelection = true;
     try {
-      await this.webdavManager.deleteRemoteSongs(this.selectedAccount, this.selectedSongs.slice());
+      if (this.selectedSongs.length > 0) {
+        await this.webdavManager.deleteRemoteSongs(this.selectedAccount, this.selectedSongs.slice());
+      }
+      if (this.selectedFolders.length > 0) {
+        await this.webdavManager.deleteRemoteFolders(this.selectedAccount, this.selectedFolders.slice());
+      }
       await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
       this.exitMultiSelect();
       this.getUIContext().getPromptAction().showToast({ message: '删除成功' });
-      this.exitMultiSelect()
     } catch (error) {
       const err = error as Error;
       this.getUIContext().getPromptAction().showToast({ message: `删除失败: ${err.message}` });
@@ -838,6 +940,206 @@ export struct WebDavMainPage {
     }
   }
 
+  private getRemoteFolderDisplayName(folder: FileInfo): string {
+    return decodeUrlEncodedString((folder.fileName ?? folder.name ?? '').replace(/\/+$/, ''));
+  }
+
+  private confirmDeleteSingleFolder(folder: FileInfo): void {
+    if (!this.selectedAccount) {
+      this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
+      return;
+    }
+    this.getUIContext().showAlertDialog({
+      title: '删除文件夹',
+      message: `确定删除 "${this.getRemoteFolderDisplayName(folder)}" 吗?`,
+      primaryButton: {
+        value: '取消',
+        action: () => {}
+      },
+      secondaryButton: {
+        value: '删除',
+        fontColor: Color.Red,
+        action: () => {
+          void this.performDeleteSingleFolder(folder);
+        }
+      }
+    });
+  }
+
+  private async performDeleteSingleFolder(folder: FileInfo): Promise<void> {
+    if (!this.selectedAccount) {
+      return;
+    }
+    this.isDeletingSelection = true;
+    try {
+      await this.webdavManager.deleteRemoteFolders(this.selectedAccount, [folder]);
+      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
+      this.getUIContext().getPromptAction().showToast({ message: '删除成功' });
+    } catch (error) {
+      const err = error as Error;
+      this.getUIContext().getPromptAction().showToast({ message: `删除失败: ${err.message}` });
+    } finally {
+      this.isDeletingSelection = false;
+    }
+  }
+
+  private canRenameRemoteSong(song: VideoItem): boolean {
+    return song.type === CommonConstants.TYPE_WEBDAV ||
+      song.type === CommonConstants.TYPE_SMB ||
+      song.type === CommonConstants.TYPE_FTP ||
+      song.type === CommonConstants.TYPE_BAIDU;
+  }
+
+  private canRenameRemoteFolder(): boolean {
+    return !!this.selectedAccount &&
+      (this.selectedAccount.webType === RemoteDriveType.WebDav ||
+        this.selectedAccount.webType === RemoteDriveType.Smb ||
+        this.selectedAccount.webType === RemoteDriveType.Ftp ||
+        this.selectedAccount.webType === RemoteDriveType.Baidu);
+  }
+
+  private getRemoteSongRenameFileName(song: VideoItem): string {
+    const rawName = StrUtil.isNotEmpty(song.fileName) ? song.fileName as string : song.name;
+    return decodeUrlEncodedString(rawName ?? '');
+  }
+
+  private getRemoteSongRenameExtension(song: VideoItem): string {
+    const fileName = this.getRemoteSongRenameFileName(song);
+    const lastDotIndex = fileName.lastIndexOf('.');
+    if (lastDotIndex <= 0) {
+      return '';
+    }
+    return fileName.substring(lastDotIndex);
+  }
+
+  private async resolveSongAccountForAction(song: VideoItem): Promise<WebDavAccount | null> {
+    if (song.webdav_account_id) {
+      const account = await this.webdavManager.getWebDavAccountById(song.webdav_account_id);
+      if (account) {
+        return account;
+      }
+    }
+    return this.selectedAccount ?? null;
+  }
+
+  private showRenameSongDialog(song: VideoItem): void {
+    if (!this.canRenameRemoteSong(song)) {
+      this.getUIContext().getPromptAction().showToast({ message: '当前文件暂不支持重命名' });
+      return;
+    }
+    const extension = this.getRemoteSongRenameExtension(song);
+    const currentThemeColor = this.themeColor;
+    this.renameSongDialogController = new CustomDialogController({
+      builder: CreateFolderDialog({
+        onConfirm: (fileName: string) => {
+          if (fileName.trim().length > 0) {
+            void this.performRenameRemoteSong(song, fileName.trim());
+          } else {
+            this.getUIContext().getPromptAction().showToast({ message: '文件名称不能为空' });
+          }
+        },
+        onCancel: () => {
+        },
+        initialName: this.getRemoteSongRenameFileName(song),
+        title: '重命名',
+        placeholder: extension.length > 0 ? `请输入文件名称,保留 ${extension} 格式` : '请输入文件名称',
+        confirmText: '确定',
+        cancelText: '取消',
+        themeColor: currentThemeColor
+      }),
+      autoCancel: true,
+      alignment: DialogAlignment.Center,
+      customStyle: false
+    });
+    this.renameSongDialogController.open();
+  }
+
+  private showRenameFolderDialog(folder: FileInfo): void {
+    if (!this.canRenameRemoteFolder()) {
+      this.getUIContext().getPromptAction().showToast({ message: '当前文件夹暂不支持重命名' });
+      return;
+    }
+    const currentThemeColor = this.themeColor;
+    this.renameSongDialogController = new CustomDialogController({
+      builder: CreateFolderDialog({
+        onConfirm: (folderName: string) => {
+          if (folderName.trim().length > 0) {
+            void this.performRenameRemoteFolder(folder, folderName.trim());
+          } else {
+            this.getUIContext().getPromptAction().showToast({ message: '文件夹名称不能为空' });
+          }
+        },
+        onCancel: () => {
+        },
+        initialName: this.getRemoteFolderDisplayName(folder),
+        title: '重命名',
+        placeholder: '请输入文件夹名称',
+        confirmText: '确定',
+        cancelText: '取消',
+        themeColor: currentThemeColor
+      }),
+      autoCancel: true,
+      alignment: DialogAlignment.Center,
+      customStyle: false
+    });
+    this.renameSongDialogController.open();
+  }
+
+  private async performRenameRemoteSong(song: VideoItem, content: string): Promise<void> {
+    const nextName = content ? content.trim() : '';
+    if (!nextName) {
+      this.getUIContext().getPromptAction().showToast({ message: '文件名称不能为空' });
+      return;
+    }
+    const currentFileName = this.getRemoteSongRenameFileName(song);
+    if (nextName === currentFileName) {
+      return;
+    }
+    const account = await this.resolveSongAccountForAction(song);
+    if (!account) {
+      this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
+      return;
+    }
+    this.isLoading = true;
+    try {
+      await this.webdavManager.renameRemoteSong(account, song, nextName);
+      await this.webdavManager.loadFilesInfoFromAccount(account, this.webdavManager.currentPath);
+      this.getUIContext().getPromptAction().showToast({ message: '重命名成功' });
+    } catch (error) {
+      const err = error as Error;
+      this.getUIContext().getPromptAction().showToast({ message: `重命名失败: ${err.message}` });
+    } finally {
+      this.isLoading = false;
+    }
+  }
+
+  private async performRenameRemoteFolder(folder: FileInfo, content: string): Promise<void> {
+    const nextName = content ? content.trim().replace(/\/+$/, '') : '';
+    if (!nextName) {
+      this.getUIContext().getPromptAction().showToast({ message: '文件夹名称不能为空' });
+      return;
+    }
+    const currentFolderName = this.getRemoteFolderDisplayName(folder);
+    if (nextName === currentFolderName) {
+      return;
+    }
+    if (!this.selectedAccount) {
+      this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
+      return;
+    }
+    this.isLoading = true;
+    try {
+      await this.webdavManager.renameRemoteFolder(this.selectedAccount, folder, nextName);
+      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
+      this.getUIContext().getPromptAction().showToast({ message: '重命名成功' });
+    } catch (error) {
+      const err = error as Error;
+      this.getUIContext().getPromptAction().showToast({ message: `重命名失败: ${err.message}` });
+    } finally {
+      this.isLoading = false;
+    }
+  }
+
   private addSongToNextPlay(song: VideoItem): void {
     if (!song) {
       return;
@@ -1342,6 +1644,81 @@ export struct WebDavMainPage {
     this.downloadCenterManager.resumeTask(taskId);
   }
 
+  private deleteDownloadTask(taskId: string): void {
+    if (StrUtil.isEmpty(taskId)) {
+      return;
+    }
+    this.downloadCenterManager.deleteTask(taskId);
+    this.syncDownloadCenterTasks();
+    this.downloadCenterRefreshVersion += 1;
+  }
+
+  private buildDownloadedVideoItem(task: DownloadCenterTask): VideoItem {
+    const fileName = StrUtil.isNotEmpty(task.fileName) ? task.fileName : FileUtil.getFileName(task.targetPath);
+    const title = StrUtil.isNotEmpty(task.title) ? task.title : fileName;
+    const item = new VideoItem(
+      title,
+      task.targetPath,
+      task.targetPath,
+      CommonConstants.TYPE_LOCAL,
+      task.totalBytes,
+      '',
+      task.sizeText,
+      task.coverPath,
+      undefined,
+      undefined,
+      fileName
+    );
+    item.parentPath = StrUtil.isNotEmpty(task.downloadDir) ? task.downloadDir : FileUtil.getParentPath(task.targetPath);
+    item.pixelMapPath = task.coverPath;
+    item.size = task.sizeText;
+    item.videoSize = task.totalBytes;
+    item.type = CommonConstants.TYPE_LOCAL;
+    return item;
+  }
+
+  private playCompletedDownloadTask(taskId: string): void {
+    if (StrUtil.isEmpty(taskId)) {
+      return;
+    }
+    const playableTasks = this.downloadCenterCompletedTasks.filter((task: DownloadCenterTask): boolean => {
+      return StrUtil.isNotEmpty(task.targetPath)
+    });
+    const playlist: VideoItem[] = playableTasks.map((task: DownloadCenterTask): VideoItem => {
+      return this.buildDownloadedVideoItem(task)
+    });
+
+    if (playlist.length <= 0) {
+      this.getUIContext().getPromptAction().showToast({ message: '没有可播放的下载文件' });
+      return;
+    }
+
+    const startIndex = playableTasks.findIndex((task: DownloadCenterTask): boolean => {
+      return task.taskId === taskId;
+    });
+
+    if (startIndex < 0 || startIndex >= playlist.length) {
+      this.getUIContext().getPromptAction().showToast({ message: '未找到对应下载任务' });
+      return;
+    }
+
+    globalWebdavVideoItems = playlist;
+    globalWebdavCurrentPlayIndex = startIndex;
+
+    const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
+    const playlistData: PlaylistEventData = {
+      playlistId: 'webdav-playlist',
+      playlistName: '下载中心',
+      songCount: playlist.length,
+      startIndex,
+      isJump: true,
+      songFilePaths: playlist.map((item: VideoItem): string => item.filePath)
+    };
+    emitter.emit(eventPlaylistPlay, { data: playlistData });
+
+
+  }
+
   private async resolveSongDownloadSourceUrl(song: VideoItem): Promise<string> {
     if (song.type === CommonConstants.TYPE_BAIDU && song.webdav_account_id) {
       try {
@@ -2473,6 +2850,12 @@ export struct WebDavMainPage {
       onResumeTask: (taskId: string) => {
         this.resumeDownloadTask(taskId);
       },
+      onDeleteTask: (taskId: string) => {
+        this.deleteDownloadTask(taskId);
+      },
+      onPlayCompletedTask: (taskId: string) => {
+        this.playCompletedDownloadTask(taskId);
+      },
       onClose: () => {
         this.isShowDownloadCenter = false;
       }
@@ -2491,6 +2874,7 @@ export struct WebDavMainPage {
     this.filteredFolderList = this.sortFoldersForType([...this.visibleFoldersState], this.sortType)
     this.isSearchLoading = false
     this.refreshDisplaySongs(this.filteredList)
+    this.syncSelectionAfterRefresh()
   }
 
   private shouldShowSearchLocation(): boolean {
@@ -2530,6 +2914,7 @@ export struct WebDavMainPage {
       this.filteredFolderList = this.sortFoldersForType([...result.folders], this.sortType)
       this.isSearchLoading = result.isIndexing
       this.refreshDisplaySongs(this.filteredList)
+      this.syncSelectionAfterRefresh()
     } catch (error) {
       if (ticket !== this.searchTicket) {
         return
@@ -2538,6 +2923,7 @@ export struct WebDavMainPage {
       this.filteredList = []
       this.filteredFolderList = []
       this.refreshDisplaySongs([])
+      this.syncSelectionAfterRefresh()
       ToastUtil.showToast(`搜索失败: ${(error as Error).message}`)
     }
   }
@@ -2972,7 +3358,7 @@ export struct WebDavMainPage {
         .fontSize(13)
         .backgroundColor(this.themeColor)
         .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
-        .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection)
+        .enabled(this.getSelectedEntryCount() > 0 && !this.isDeletingSelection)
         .onClick(() => this.confirmDeleteSelected())
 
       Button('+歌单', { type: ButtonType.Circle, stateEffect: true })
@@ -2981,7 +3367,7 @@ export struct WebDavMainPage {
         .fontSize(13)
         .backgroundColor(this.themeColor)
         .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
-        .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection)
+        .enabled(this.canOperateOnSelectedSongs())
         .onClick(() => {
           void this.openSelectedSongsAddToPlaylistDialog();
         })
@@ -2992,7 +3378,7 @@ export struct WebDavMainPage {
         .fontSize(13)
         .backgroundColor(this.themeColor)
         .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
-        .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection)
+        .enabled(this.canOperateOnSelectedSongs())
         .onClick(() => {
           void this.enqueueSelectedSongsDownload();
         })
@@ -3048,7 +3434,7 @@ export struct WebDavMainPage {
 
         // 文件夹信息
         Column({ space: 4 }) {
-          Text(decodeUrlEncodedString(folder.fileName.replace('/', '')))
+          Text(decodeUrlEncodedString(folder.fileName.replace(/\/+$/, '')))
             .fontSize(15)
             .fontColor($r('app.color.index_tab_font_color'))
             .maxLines(1)
@@ -3069,15 +3455,35 @@ export struct WebDavMainPage {
         }
         .alignItems(HorizontalAlign.Start)
         .layoutWeight(1)
+
+        Checkbox({ name: `folder_checkbox_${this.resolveFolderSelectionKey(folder)}` })
+          .select(this.isFolderSelected(folder))
+          .selectedColor(this.themeColor)
+          .opacity(this.isMultiSelect ? 1 : 0)
+          .animation({
+            duration: 300,
+            curve: 'Smooth'
+          })
+          .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None)
+          .onChange((checked: boolean) => this.handleFolderCheckboxSelection(folder, checked))
+          .margin({ left: 10, top: 8, bottom: 8, right: 12 })
+          .width(22)
+          .height(22)
       }
     }
     .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(() => {
+      if (this.ignoreTapAfterExitMultiSelect) {
+        return;
+      }
+      if (this.isMultiSelect) {
+        this.toggleFolderSelection(folder);
+        return;
+      }
       if (this.isSearchMode && this.searchText.length > 0) {
         this.isSearchMode = false
         this.searchController.stopEditing()
@@ -3086,6 +3492,14 @@ export struct WebDavMainPage {
       this.enterFolder(folder);
       this.breadcrumbs = this.webdavManager.getBreadcrumbs();
     })
+    .bindContextMenu(this.FolderLongPressMenuBuilder(folder), ResponseType.LongPress,
+      {
+        preview: MenuPreviewMode.IMAGE
+      })
+    .bindContextMenu(this.FolderLongPressMenuBuilder(folder), ResponseType.RightClick,
+      {
+        preview: MenuPreviewMode.IMAGE
+      })
   }
 
   // 歌曲列表项
@@ -3263,6 +3677,14 @@ export struct WebDavMainPage {
             .onClick(async () => {
               await this.enqueueSongDownload(song);
             })
+          MenuItem({
+            symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.rename')),
+            content: '重命名'
+          })
+            .visibility(this.canRenameRemoteSong(song) ? Visibility.Visible : Visibility.None)
+            .onClick(() => {
+              this.showRenameSongDialog(song);
+            })
           //属性
           MenuItem({
             symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music_note_list')),
@@ -3292,10 +3714,50 @@ export struct WebDavMainPage {
 
   }
 
+  @Builder
+  FolderLongPressMenuBuilder(folder: FileInfo) {
+    Scroll() {
+      Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
+        Menu() {
+          MenuItem({
+            symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.checkmark_square_on_square')),
+            content: $r('app.string.select_all')
+          })
+            .onClick(() => {
+              this.enterFolderMultiSelect(folder);
+            })
+
+          MenuItem({
+            symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.rename')),
+            content: '重命名'
+          })
+            .visibility(this.canRenameRemoteFolder() ? Visibility.Visible : Visibility.None)
+            .onClick(() => {
+              this.showRenameFolderDialog(folder);
+            })
+
+          MenuItem({
+            symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.trash')),
+            content: $r('app.string.delete')
+          })
+            .onClick(() => {
+              this.confirmDeleteSingleFolder(folder);
+            })
+        }
+        .font({ size: 14, weight: FontWeight.Normal })
+      }
+      .width(180)
+    }
+    .height('auto')
+    .enableScrollInteraction(true)
+    .scrollBar(BarState.Off)
+  }
+
   
   
   // 对话框控制器
   private createFolderDialogController: CustomDialogController | null = null;
+  private renameSongDialogController: CustomDialogController | null = null;
 
   /**
    * 显示创建文件夹对话框

+ 6 - 2
entry/src/main/ets/view/DeleteComptent.ets

@@ -42,6 +42,11 @@ export struct DeleteComptent {
 
   }
 
+  private hasOnlyDirectories(): boolean {
+    return this.selectedFiles.length > 0 &&
+      this.selectedFiles.every(item => item.type === CommonConstants.TYPE_IS_DIR)
+  }
+
   build() {
     Column() {
       if (this.isDeleting) {
@@ -249,8 +254,7 @@ export struct DeleteComptent {
             .visibility(this.onlyRemoveFromPlaylist ? Visibility.None : Visibility.Visible)
 
           }
-          .visibility(this.selectedFiles.length==1&&this.selectedFiles[0].type==CommonConstants.TYPE_IS_DIR?
-          Visibility.None:Visibility.Visible)
+          .visibility(this.hasOnlyDirectories() ? Visibility.None : Visibility.Visible)
 
           Flex({ justifyContent: FlexAlign.SpaceAround }) {
             Button($r('app.string.cancel'))

+ 340 - 84
entry/src/main/ets/view/DownloadCenter.ets

@@ -3,6 +3,7 @@ import { DownloadCenterTask } from '../common/util/DownloadCenterManager';
 import { CommonConstants } from '../common/constants/CommonConstants';
 import { StrUtil } from '@pura/harmony-utils';
 import { PointLightDefaultButton } from './PointLight/PointLightDeFaultButton';
+import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
 
 @Observed
 class DownloadCenterTaskState implements DownloadCenterTask {
@@ -53,16 +54,29 @@ struct DownloadCenterTaskRow {
   @ObjectLink task: DownloadCenterTaskState;
   @Prop isDownloading: boolean = true;
   @Prop refreshVersion: number = 0;
+  @Prop isSelectionMode: boolean = false;
+  @Prop isSelected: boolean = false;
+  onSelectionChange: (taskId: string, selected: boolean) => void = (_taskId: string, _selected: boolean): void => {};
   onPauseTask: (taskId: string) => void = (_taskId: string): void => {};
   onResumeTask: (taskId: string) => void = (_taskId: string): void => {};
+  onDeleteTask: (taskId: string) => void = (_taskId: string): void => {};
+  onRowClick: (taskId: string) => void = (_taskId: string): void => {};
 
   build() {
     ListItem() {
       Column({ space: 8 }) {
         Row({ space: 10 }) {
+          if (this.isSelectionMode && this.isDownloading) {
+            Checkbox({ name: this.task.taskId })
+              .select(this.isSelected)
+              .onChange((value: boolean) => {
+                this.onSelectionChange(this.task.taskId, value)
+              })
+          }
+
           Image(StrUtil.isNotEmpty(this.task.coverPath) ? this.task.coverPath : $r('app.media.alt'))
-            .width(48)
-            .height(48)
+            .width(54)
+            .height(54)
             .borderRadius(9)
             .sourceSize({ width: 38, height: 38 })
             .alt($r('app.media.alt'))
@@ -75,29 +89,54 @@ struct DownloadCenterTaskRow {
               .fontWeight(FontWeight.Medium)
               .fontColor($r('app.color.text_color'))
               .maxLines(1)
+              .padding({bottom:4})
               .textOverflow({ overflow: TextOverflow.Ellipsis })
 
-            Row() {
-              Text(this.getTaskTotalSizeText())
-                .fontSize(12)
-                .fontColor($r('app.color.text_color'))
-                .opacity(0.6)
-              Text('  ·  ')
-                .fontSize(12)
-                .fontColor($r('app.color.text_color'))
-                .opacity(0.35)
-              Text(this.isDownloading ?'' : '已完成')
-                .fontSize(12)
-                .fontColor($r('app.color.text_color'))
-                .opacity(0.85)
-
-              Text(this.getTaskSpeedText())
-                .fontSize(12)
-                .fontColor($r('app.color.text_color'))
-                .opacity(0.75)
-                .margin({ right: 6 })
+            if (this.isDownloading) {
+              Progress({ value: this.task.progress, total: 100, type: ProgressType.Linear })
+                .width('100%')
+                .color(this.getTaskStatusColor())
+                .padding({bottom:4})
+                .backgroundColor($r('app.color.track_color'))
+                .style({ strokeWidth: 5 })
+
+              Row() {
+                Text(this.getTaskProgressLabelText())
+                  .fontSize(12)
+                  .fontColor(this.getTaskStatusColor())
+                  .maxLines(1)
+                  .textOverflow({ overflow: TextOverflow.Ellipsis })
+                  .layoutWeight(1)
+
+                Text(this.getTaskSpeedText())
+                  .fontSize(12)
+                  .fontColor($r('app.color.text_color'))
+                  .opacity(0.72)
+                  .margin({ right: 8 })
+
+                Text(this.getTaskProgressInfo())
+                  .fontSize(12)
+                  .fontColor($r('app.color.text_color'))
+                  .opacity(0.6)
+              }
+              .width('100%')
+            } else {
+              Row() {
+                Text(this.getTaskTotalSizeText())
+                  .fontSize(12)
+                  .fontColor($r('app.color.text_color'))
+                  .opacity(0.6)
+                Text('  ·  ')
+                  .fontSize(12)
+                  .fontColor($r('app.color.text_color'))
+                  .opacity(0.35)
+                Text('已完成')
+                  .fontSize(12)
+                  .fontColor($r('app.color.text_color'))
+                  .opacity(0.85)
+              }
+              .width('100%')
             }
-            .width('100%')
           }
           .alignItems(HorizontalAlign.Start)
           .layoutWeight(1)
@@ -108,28 +147,6 @@ struct DownloadCenterTaskRow {
         }
         .width('100%')
 
-        if (this.isDownloading) {
-          Progress({ value: this.task.progress, total: 100, type: ProgressType.Linear })
-            .width('100%')
-            .color(this.getTaskStatusColor())
-            .backgroundColor($r('app.color.track_color'))
-            .style({ strokeWidth: 5 })
-
-          Row() {
-            Text(this.getTaskProgressLabelText())
-              .fontSize(12)
-              .fontColor(this.getTaskStatusColor())
-              .maxLines(1)
-              .textOverflow({ overflow: TextOverflow.Ellipsis })
-              .layoutWeight(1)
-            Text(this.getTaskProgressInfo())
-              .fontSize(12)
-              .fontColor($r('app.color.text_color'))
-              .opacity(0.6)
-          }
-          .width('100%')
-        }
-
         Text(`${this.refreshVersion}`)
           .fontSize(0.1)
           .fontColor(Color.Transparent)
@@ -138,24 +155,44 @@ struct DownloadCenterTaskRow {
           .height(0)
       }
       .width('100%')
-      .padding(12)
-      .backgroundColor($r('app.color.start_window_background'))
-      .borderRadius(10)
+      .padding(10)
+      .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
+      .border({
+        color: $r('app.color.index_background'),
+        width: 1.8
+      })
+      .borderRadius(15)
     }
+    .swipeAction({ end: this.deleteActionBuilder(), edgeEffect: SwipeEdgeEffect.None })
+    .onClick(() => {
+      if (this.isSelectionMode && this.isDownloading) {
+        this.onSelectionChange(this.task.taskId, !this.isSelected)
+        return
+      }
+      this.onRowClick(this.task.taskId)
+    })
   }
 
   @Builder
   private taskActionButtonBuilder() {
-    PointLightDefaultButton({
-      isPx: false,
-      isSysBol: true,
-      pointColor: this.isTaskRunning() ? $r('app.color.text_color') : this.themeColor,
-      imageResource: this.isTaskRunning() ? $r('sys.symbol.pause') : $r('sys.symbol.play_fill'),
-      builderHeight: 30,
-      builderWidth: 30,
-      buttonScale: 1,
-      canShadow: false,
-    })
+    Button() {
+      PointLightDefaultButton({
+        isPx: false,
+        isSysBol: true,
+        pointColor: this.isTaskRunning() ? $r('app.color.text_color') : this.themeColor,
+        imageResource: this.isTaskRunning() ? $r('sys.symbol.pause') : $r('sys.symbol.play_fill'),
+        builderHeight: 36,
+        builderWidth: 36,
+        buttonScale: 1,
+        canShadow: true,
+      })
+    }
+      .width(38)
+      .height(38)
+      .backgroundColor(Color.Transparent)
+      .borderRadius(16)
+      .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
+      .stateEffect(false)
       .onClick(() => {
         if (this.isTaskRunning()) {
           this.onPauseTask(this.task.taskId);
@@ -165,6 +202,25 @@ struct DownloadCenterTaskRow {
       })
   }
 
+  @Builder
+  private deleteActionBuilder() {
+    Row() {
+      Button('删除')
+        .width(72)
+        .height(52)
+        .fontSize(14)
+        .fontColor(Color.White)
+        .backgroundColor($r('app.color.btn_red'))
+        .borderRadius(14)
+        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
+        .onClick(() => {
+          this.onDeleteTask(this.task.taskId)
+        })
+    }
+    .padding({ left: 8, right: 4 })
+    .justifyContent(FlexAlign.Center)
+  }
+
   private isTaskRunning(): boolean {
     return this.task.status === 'downloading';
   }
@@ -189,28 +245,44 @@ struct DownloadCenterTaskRow {
     return isInt ? `${Math.round(rounded)}%` : `${rounded.toFixed(1)}%`;
   }
 
+  private getEffectiveTaskStatus(): 'pending' | 'downloading' | 'paused' | 'completed' | 'failed' {
+    if (this.shouldUsePauseStyleForFailure()) {
+      return 'paused';
+    }
+    return this.task.status;
+  }
+
+  private shouldUsePauseStyleForFailure(): boolean {
+    if (this.task.status !== 'failed') {
+      return false;
+    }
+    const message = StrUtil.isNotEmpty(this.task.errorMessage) ? this.task.errorMessage.toLowerCase() : '';
+    return message.includes('failed writing received') ||
+      message.includes('failed wwiting received') ||
+      message.includes('writing received');
+  }
+
   private getTaskProgressLabelText(): string {
-    if (this.task.status === 'failed') {
+    const status = this.getEffectiveTaskStatus();
+    if (status === 'failed') {
       return StrUtil.isNotEmpty(this.task.errorMessage) ? this.task.errorMessage : '下载失败';
     }
-    if (this.task.status === 'paused') {
+    if (status === 'paused') {
       return `已暂停 ${this.getTaskProgressLabel()}`;
     }
-    if (this.task.status === 'pending') {
+    if (status === 'pending') {
       return `等待中 ${this.getTaskProgressLabel()}`;
     }
     return this.getTaskProgressLabel();
   }
 
   private getTaskSpeedText(): string {
-    if (this.task.status === 'failed') {
+    const status = this.getEffectiveTaskStatus();
+    if (status === 'failed') {
       return '--';
     }
-    if (this.task.status === 'paused') {
-      return '已暂停';
-    }
-    if (this.task.status === 'pending') {
-      return '等待中';
+    if (status === 'paused' || status === 'pending') {
+      return '0 B/s';
     }
     if (this.task.speedBytesPerSec > 0) {
       return `${this.formatBytes(this.task.speedBytesPerSec)}/s`;
@@ -219,7 +291,7 @@ struct DownloadCenterTaskRow {
   }
 
   private getTaskStatusText(): string {
-    switch (this.task.status) {
+    switch (this.getEffectiveTaskStatus()) {
       case 'downloading':
         return '下载中';
       case 'paused':
@@ -235,10 +307,11 @@ struct DownloadCenterTaskRow {
   }
 
   private getTaskStatusColor() {
-    if (this.task.status === 'failed') {
+    const status = this.getEffectiveTaskStatus();
+    if (status === 'failed') {
       return $r('app.color.btn_red');
     }
-    if (this.task.status === 'paused' || this.task.status === 'pending') {
+    if (status === 'paused' || status === 'pending') {
       return $r('app.color.text_color');
     }
     return this.themeColor;
@@ -274,9 +347,13 @@ export struct DownloadCenter {
   @State activeTasks: DownloadCenterTaskState[] = [];
   @State completedTasks: DownloadCenterTaskState[] = [];
   @State renderVersion: number = 0;
+  @State isSelectionMode: boolean = false;
+  @State selectedTaskIds: string[] = [];
   onClose: () => void = () => {};
   onPauseTask: (taskId: string) => void = (_taskId: string): void => {};
   onResumeTask: (taskId: string) => void = (_taskId: string): void => {};
+  onDeleteTask: (taskId: string) => void = (_taskId: string): void => {};
+  onPlayCompletedTask: (taskId: string) => void = (_taskId: string): void => {};
 
   aboutToAppear(): void {
     this.syncTasksFromProps();
@@ -298,12 +375,10 @@ export struct DownloadCenter {
       Row({ space: 10 }) {
         Button({ type: ButtonType.Circle, stateEffect: true }) {
           SymbolGlyph($r('sys.symbol.chevron_left'))
-            .fontSize(22)
-            .fontColor([this.themeColor])
+            .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
         }
-        .width(38)
-        .height(38)
-        .backgroundColor($r('app.color.bg_card'))
+        .attributeModifier(new ButtonFancyModifier(40, 40))
+        .attributeModifier(new ShadowModifier())
         .onClick(() => this.onClose())
 
         Text('下载中心')
@@ -312,15 +387,27 @@ export struct DownloadCenter {
           .fontColor($r('app.color.text_color'))
           .layoutWeight(1)
 
+        if (this.getCurrentTabIndex() === 0 && this.activeTasks.length >= 2) {
+          Button({ type: ButtonType.Circle, stateEffect: true }) {
+            SymbolGlyph(this.isSelectionMode ? $r('sys.symbol.checkmark_circle') : $r('sys.symbol.checkmark_square_on_square'))
+              .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
+          }
+          .attributeModifier(new ButtonFancyModifier(40, 40))
+          .attributeModifier(new ShadowModifier())
+            .onClick(() => {
+              this.toggleSelectionMode()
+            })
+        }
+
         Button({ type: ButtonType.Circle, stateEffect: true }) {
-          SymbolGlyph($r('sys.symbol.folder'))
-            .fontSize(20)
-            .fontColor([this.themeColor])
+          SymbolGlyph($r('sys.symbol.exclamationmark'))
+            .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
         }
-          .width(38)
-          .height(38)
-          .backgroundColor($r('app.color.bg_card'))
-          .onClick(() => {
+        .attributeModifier(new ButtonFancyModifier(40, 40))
+        .attributeModifier(new ShadowModifier())
+        .zIndex(0)
+
+        .onClick(() => {
             this.showDownloadDirectoryDialog();
           })
 
@@ -345,16 +432,21 @@ export struct DownloadCenter {
       }
       .width('100%')
       .layoutWeight(1)
+
+      if (this.isSelectionMode && this.getCurrentTabIndex() === 0) {
+        this.selectionActionBarBuilder()
+      }
     }
     .width('100%')
     .height('100%')
     .padding({ top: this.topSafeHeight + 10, left: 12, right: 12, bottom: this.bottomSafeHeight + 12 })
-    .backgroundColor($r('app.color.index_background'))
+    .backgroundColor($r('app.color.start_window_background'))
   }
 
   private syncTasksFromProps(): void {
     this.activeTasks = this.mergeTaskStates(this.activeTasks, this.activeTasksProp);
     this.completedTasks = this.mergeTaskStates(this.completedTasks, this.completedTasksProp);
+    this.selectedTaskIds = this.filterExistingSelectedTaskIds();
     this.renderVersion = this.tasksVersion;
   }
 
@@ -379,8 +471,15 @@ export struct DownloadCenter {
               task: task,
               isDownloading: true,
               refreshVersion: this.renderVersion,
+              isSelectionMode: this.isSelectionMode,
+              isSelected: this.isTaskSelected(task.taskId),
+              onSelectionChange: (taskId: string, selected: boolean) => {
+                this.setTaskSelection(taskId, selected)
+              },
               onPauseTask: this.onPauseTask,
-              onResumeTask: this.onResumeTask
+              onResumeTask: this.onResumeTask,
+              onDeleteTask: this.handleDeleteTask.bind(this),
+              onRowClick: (_taskId: string): void => {}
             })
           }, (task: DownloadCenterTaskState): string => {
             return this.getTaskRenderKey(task);
@@ -408,7 +507,9 @@ export struct DownloadCenter {
               themeColor: this.themeColor,
               task: task,
               isDownloading: false,
-              refreshVersion: this.renderVersion
+              refreshVersion: this.renderVersion,
+              onDeleteTask: this.handleDeleteTask.bind(this),
+              onRowClick: this.handleCompletedTaskClick.bind(this)
             })
           }, (task: DownloadCenterTaskState): string => {
             return this.getTaskRenderKey(task);
@@ -421,6 +522,56 @@ export struct DownloadCenter {
     }
   }
 
+  @Builder
+  private selectionActionBarBuilder() {
+    Row({ space: 10 }) {
+      Button(this.isAllActiveTasksSelected() ? '反选' : '全选')
+        .layoutWeight(1)
+        .height(38)
+        .fontSize(13)
+        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
+        .backgroundColor($r('app.color.bg_card'))
+        .fontColor($r('app.color.text_color'))
+        .onClick(() => {
+          this.toggleSelectAllActiveTasks()
+        })
+
+      Button('开始')
+        .layoutWeight(1)
+        .height(38)
+        .fontSize(13)
+        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
+        .backgroundColor(this.themeColor)
+        .fontColor(Color.White)
+        .onClick(() => {
+          this.resumeSelectedTasks()
+        })
+
+      Button('暂停')
+        .layoutWeight(1)
+        .height(38)
+        .fontSize(13)
+        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
+        .backgroundColor($r('app.color.bg_card'))
+        .fontColor($r('app.color.text_color'))
+        .onClick(() => {
+          this.pauseSelectedTasks()
+        })
+
+      Button('删除')
+        .layoutWeight(1)
+        .height(38)
+        .fontSize(13)
+        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
+        .backgroundColor($r('app.color.btn_red'))
+        .fontColor(Color.White)
+        .onClick(() => {
+          this.deleteSelectedTasks()
+        })
+    }
+    .width('100%')
+  }
+
   private showDownloadDirectoryDialog(): void {
     this.getUIContext().showAlertDialog({
       title: '下载目录说明',
@@ -479,10 +630,115 @@ export struct DownloadCenter {
     return mergedTasks;
   }
 
+  private toggleSelectionMode(): void {
+    this.isSelectionMode = !this.isSelectionMode;
+    if (!this.isSelectionMode) {
+      this.selectedTaskIds = [];
+    }
+  }
+
+  private setTaskSelection(taskId: string, selected: boolean): void {
+    const index: number = this.selectedTaskIds.indexOf(taskId);
+    if (selected) {
+      if (index < 0) {
+        this.selectedTaskIds = [...this.selectedTaskIds, taskId];
+      }
+      return;
+    }
+    if (index >= 0) {
+      const nextSelected: string[] = this.selectedTaskIds.slice();
+      nextSelected.splice(index, 1);
+      this.selectedTaskIds = nextSelected;
+    }
+  }
+
+  private isTaskSelected(taskId: string): boolean {
+    return this.selectedTaskIds.indexOf(taskId) >= 0;
+  }
+
+  private isAllActiveTasksSelected(): boolean {
+    return this.activeTasks.length > 0 && this.selectedTaskIds.length === this.activeTasks.length;
+  }
+
+  private toggleSelectAllActiveTasks(): void {
+    if (this.isAllActiveTasksSelected()) {
+      this.selectedTaskIds = [];
+      return;
+    }
+    this.selectedTaskIds = this.activeTasks.map((task: DownloadCenterTaskState): string => task.taskId);
+  }
+
+  private pauseSelectedTasks(): void {
+    for (let i = 0; i < this.selectedTaskIds.length; i += 1) {
+      this.onPauseTask(this.selectedTaskIds[i]);
+    }
+  }
+
+  private resumeSelectedTasks(): void {
+    for (let i = 0; i < this.selectedTaskIds.length; i += 1) {
+      this.onResumeTask(this.selectedTaskIds[i]);
+    }
+  }
+
+  private deleteSelectedTasks(): void {
+    const taskIds: string[] = [...this.selectedTaskIds];
+    this.removeTasksLocally(taskIds);
+    for (let i = 0; i < taskIds.length; i += 1) {
+      this.onDeleteTask(taskIds[i]);
+    }
+    this.selectedTaskIds = [];
+    this.isSelectionMode = false;
+  }
+
+  private handleDeleteTask(taskId: string): void {
+    if (StrUtil.isEmpty(taskId)) {
+      return
+    }
+    this.removeTasksLocally([taskId])
+    this.onDeleteTask(taskId)
+  }
+
+  private handleCompletedTaskClick(taskId: string): void {
+    if (StrUtil.isEmpty(taskId)) {
+      return
+    }
+    this.onPlayCompletedTask(taskId)
+  }
+
   private getTaskRenderKey(task: DownloadCenterTaskState): string {
     return task.taskId;
   }
 
+  private filterExistingSelectedTaskIds(): string[] {
+    if (this.selectedTaskIds.length <= 0) {
+      return [];
+    }
+    const activeTaskIds: Set<string> = new Set<string>();
+    for (let i = 0; i < this.activeTasks.length; i += 1) {
+      activeTaskIds.add(this.activeTasks[i].taskId);
+    }
+    const nextSelected: string[] = [];
+    for (let i = 0; i < this.selectedTaskIds.length; i += 1) {
+      const taskId: string = this.selectedTaskIds[i];
+      if (activeTaskIds.has(taskId)) {
+        nextSelected.push(taskId);
+      }
+    }
+    return nextSelected;
+  }
+
+  private removeTasksLocally(taskIds: string[]): void {
+    if (taskIds.length <= 0) {
+      return;
+    }
+    this.activeTasks = this.activeTasks.filter((task: DownloadCenterTaskState): boolean => {
+      return taskIds.indexOf(task.taskId) < 0;
+    });
+    this.completedTasks = this.completedTasks.filter((task: DownloadCenterTaskState): boolean => {
+      return taskIds.indexOf(task.taskId) < 0;
+    });
+  }
+
   private getCurrentTabIndex(): number {
     if (!this.selectedIndexes || this.selectedIndexes.length <= 0) {
       return 0;

+ 222 - 0
entry/src/main/ets/view/EqualizerView.ets

@@ -10,6 +10,122 @@ import { AnimationHelper, DialogAction, DialogHelper } from '@pura/harmony-dialo
 import Logger from '../common/util/Logger';
 import { CommonConstants } from '../common/constants/CommonConstants';
 import { Utility } from '../common/util/Utility';
+import { fileIo, fileUri, picker, ReadTextOptions } from '@kit.CoreFileKit';
+import ReqPermissionUtil from '../common/util/ReqPermissionUtil';
+
+const EQUALIZER_IMPORT_EXTENSIONS: string[] = ['.feq', '.fpp', '.fcp'];
+const EQ_IMPORT_FREQUENCIES: number[] = [32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
+
+function buildImportedPresetName(fileName: string): string {
+  if (!fileName || fileName.length === 0) {
+    return `导入预设_${Date.now()}`;
+  }
+  const lastDotIndex = fileName.lastIndexOf('.');
+  if (lastDotIndex <= 0) {
+    return fileName;
+  }
+  return fileName.substring(0, lastDotIndex);
+}
+
+function clampEqGain(value: number): number {
+  if (!Number.isFinite(value)) {
+    return 0;
+  }
+  if (value < -12) {
+    return -12;
+  }
+  if (value > 12) {
+    return 12;
+  }
+  return Math.round(value * 10) / 10;
+}
+
+function parseEqNumberList(content: string): number[] {
+  const matches = content.match(/[-+]?\d+(?:\.\d+)?/g);
+  if (!matches || matches.length < 10) {
+    return [];
+  }
+  const values: number[] = [];
+  for (let i = 0; i < matches.length; i++) {
+    const value = Number(matches[i]);
+    if (Number.isFinite(value)) {
+      values.push(value);
+    }
+  }
+  return values;
+}
+
+function parseEqFrequencyMap(content: string): Map<number, number> {
+  const lines = content.split(/\r?\n/);
+  const result = new Map<number, number>();
+  for (let i = 0; i < lines.length; i++) {
+    const line = lines[i].trim();
+    if (line.length === 0) {
+      continue;
+    }
+    const match = line.match(/(\d+(?:\.\d+)?)\s*(?:hz|khz)?[^-+\d]*([-+]?\d+(?:\.\d+)?)/i);
+    if (!match) {
+      continue;
+    }
+    let frequency = Number(match[1]);
+    const gain = Number(match[2]);
+    if (!Number.isFinite(frequency) || !Number.isFinite(gain)) {
+      continue;
+    }
+    if (frequency > 0 && frequency < 20) {
+      frequency *= 1000;
+    }
+    result.set(Math.round(frequency), gain);
+  }
+  return result;
+}
+
+function convertFrequencyMapToGains(freqMap: Map<number, number>): number[] {
+  if (freqMap.size === 0) {
+    return [];
+  }
+  const gains: number[] = [];
+  for (let i = 0; i < EQ_IMPORT_FREQUENCIES.length; i++) {
+    const target = EQ_IMPORT_FREQUENCIES[i];
+    let matchedGain: number | undefined = freqMap.get(target);
+    if (matchedGain === undefined) {
+      let nearestFreq = -1;
+      let nearestDiff = Number.MAX_SAFE_INTEGER;
+      freqMap.forEach((gain, freq) => {
+        const diff = Math.abs(freq - target);
+        if (diff < nearestDiff) {
+          nearestDiff = diff;
+          nearestFreq = freq;
+          matchedGain = gain;
+        }
+      });
+      if (nearestFreq >= 0 && nearestDiff > target * 0.45) {
+        matchedGain = 0;
+      }
+    }
+    gains.push(clampEqGain(matchedGain ?? 0));
+  }
+  return gains;
+}
+
+function parseExternalEqualizerPreset(content: string): number[] {
+  const normalized = content.replace(/\u0000/g, ' ').trim();
+  if (normalized.length === 0) {
+    throw new Error('预设文件内容为空');
+  }
+
+  const frequencyMap = parseEqFrequencyMap(normalized);
+  if (frequencyMap.size >= 5) {
+    return convertFrequencyMapToGains(frequencyMap);
+  }
+
+  const values = parseEqNumberList(normalized);
+  if (values.length >= 10) {
+    return values.slice(0, 10).map((value: number) => clampEqGain(value));
+  }
+
+  throw new Error('暂不支持该预设文件格式');
+}
 
 @Component
 export struct EqualizerView {
@@ -125,6 +241,46 @@ export struct EqualizerView {
     });
   }
 
+  private readPresetTextFromUri(uri: string): string {
+    const resolvedPath = new fileUri.FileUri(uri).path;
+    const stat = fileIo.statSync(resolvedPath);
+    const options: ReadTextOptions = {
+      offset: 0,
+      length: stat.size,
+      encoding: 'utf-8'
+    };
+    return fileIo.readTextSync(resolvedPath, options);
+  }
+
+  private async importPresetFromFile(): Promise<void> {
+    try {
+      const documentSelectOptions = new picker.DocumentSelectOptions();
+      documentSelectOptions.fileSuffixFilters = EQUALIZER_IMPORT_EXTENSIONS;
+      documentSelectOptions.maxSelectNumber = 1;
+      const documentPicker = new picker.DocumentViewPicker();
+      const result = await documentPicker.select(documentSelectOptions);
+      if (!result || result.length <= 0) {
+        return;
+      }
+      await ReqPermissionUtil.persistPermission(result[0]);
+      const fileUriObj = new fileUri.FileUri(result[0]);
+      const fileName = fileUriObj.name;
+      const content = this.readPresetTextFromUri(result[0]);
+      const gains = parseExternalEqualizerPreset(content);
+      const preset = this.equalizerManager.importPreset(buildImportedPresetName(fileName), gains);
+      this.presets = this.equalizerManager.getAllPresets();
+      this.currentPresetId = preset.id;
+      this.gains = this.equalizerManager.getCurrentGains();
+      this.updateSelectedPresetIndex();
+      this.notifyEqualizerChanged();
+      ToastUtil.showToast(`已导入预设: ${preset.name}`);
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(EqualizerView.TAG, `导入预设失败: ${err.message}`);
+      ToastUtil.showToast(`导入预设失败: ${err.message}`);
+    }
+  }
+
   private getPresetSelectOptions(): SelectOption[] {
     const options: SelectOption[] = [];
     for (let i = 0; i < this.presets.length; i++) {
@@ -261,6 +417,19 @@ export struct EqualizerView {
 
       Blank().width(20)
 
+      Button('导入预设')
+        .fontSize(14)
+        .fontColor(this.themeColor)
+        .backgroundColor(Color.Transparent)
+        .border({ width: 1, color: this.themeColor, radius: 20 })
+        .height(36)
+        .padding({ left: 20, right: 20 })
+        .onClick(() => {
+          void this.importPresetFromFile()
+        })
+
+      Blank().width(20)
+
       Button('保存预设')
         .fontSize(14)
         .fontColor(Color.White)
@@ -457,6 +626,46 @@ export struct EqualizerViewWithCallback {
     });
   }
 
+  private readPresetTextFromUri(uri: string): string {
+    const resolvedPath = new fileUri.FileUri(uri).path;
+    const stat = fileIo.statSync(resolvedPath);
+    const options: ReadTextOptions = {
+      offset: 0,
+      length: stat.size,
+      encoding: 'utf-8'
+    };
+    return fileIo.readTextSync(resolvedPath, options);
+  }
+
+  private async importPresetFromFile(): Promise<void> {
+    try {
+      const documentSelectOptions = new picker.DocumentSelectOptions();
+      documentSelectOptions.fileSuffixFilters = EQUALIZER_IMPORT_EXTENSIONS;
+      documentSelectOptions.maxSelectNumber = 1;
+      const documentPicker = new picker.DocumentViewPicker();
+      const result = await documentPicker.select(documentSelectOptions);
+      if (!result || result.length <= 0) {
+        return;
+      }
+      await ReqPermissionUtil.persistPermission(result[0]);
+      const fileUriObj = new fileUri.FileUri(result[0]);
+      const fileName = fileUriObj.name;
+      const content = this.readPresetTextFromUri(result[0]);
+      const gains = parseExternalEqualizerPreset(content);
+      const preset = this.equalizerManager.importPreset(buildImportedPresetName(fileName), gains);
+      this.presets = this.equalizerManager.getAllPresets();
+      this.currentPresetId = preset.id;
+      this.gains = this.equalizerManager.getCurrentGains();
+      this.updateSelectedPresetIndex();
+      this.notifyChange();
+      ToastUtil.showToast(`已导入预设: ${preset.name}`);
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(EqualizerViewWithCallback.TAG, `导入预设失败: ${err.message}`);
+      ToastUtil.showToast(`导入预设失败: ${err.message}`);
+    }
+  }
+
   private getPresetSelectOptions(): SelectOption[] {
     const options: SelectOption[] = [];
     for (let i = 0; i < this.presets.length; i++) {
@@ -857,6 +1066,19 @@ export struct EqualizerViewWithCallback {
 
       Blank().width(20)
 
+      Button('导入预设')
+        .fontSize(14)
+        .fontColor(this.themeColor)
+        .backgroundColor(Color.Transparent)
+        .border({ width: 1, color: this.themeColor, radius: 20 })
+        .height(36)
+        .padding({ left: 20, right: 20 })
+        .onClick(() => {
+          void this.importPresetFromFile()
+        })
+
+      Blank().width(20)
+
       Button('保存预设')
         .fontSize(14)
         .fontColor(Color.White)

+ 157 - 0
entry/src/main/ets/view/IsoExtractComptent.ets

@@ -0,0 +1,157 @@
+import { CommonConstants } from '../common/constants/CommonConstants';
+import { IsoArchiveAudioEntry } from '../common/util/IsoArchiveUtil';
+import { Utility } from '../common/util/Utility';
+
+@Component
+export struct IsoExtractComptent {
+  @Prop isoFileName: string = ''
+  @Prop isLongNameRoLL: boolean = true
+  @Prop entries: IsoArchiveAudioEntry[] = []
+  @Prop selectedPaths: string[] = []
+  @Prop isExtracting: boolean = false
+  onToggleSelection = (path: string, checked: boolean) => {
+  }
+  onToggleSelectAll = () => {
+  }
+  onExtract = () => {
+  }
+  onCancel = () => {
+  }
+  @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR
+
+  private isSelected(path: string): boolean {
+    return this.selectedPaths.includes(path)
+  }
+
+  build() {
+    Column() {
+      Row() {
+        Column({ space: 4 }) {
+          Text(this.isoFileName)
+            .fontSize(16)
+            .fontWeight(FontWeight.Medium)
+            .maxLines(1)
+            .textOverflow({ overflow: this.isLongNameRoLL ? TextOverflow.MARQUEE : TextOverflow.Ellipsis })
+
+          Text(`ISO 音频 ${this.entries.length} 项,已选 ${this.selectedPaths.length} 项`)
+            .fontSize(12)
+            .fontColor(Color.Gray)
+        }
+        .layoutWeight(1)
+        .alignItems(HorizontalAlign.Start)
+        .margin({ left: 24 })
+
+        Button({ type: ButtonType.Circle, stateEffect: true }) {
+          Image($r('app.media.close'))
+            .fillColor($r('app.color.text_color'))
+            .width(26)
+            .height(26)
+        }
+        .width(34)
+        .height(34)
+        .margin({ right: 18 })
+        .backgroundColor(Color.Transparent)
+        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
+        .enabled(!this.isExtracting)
+        .onClick(() => {
+          this.onCancel()
+        })
+      }
+      .margin({ top: 20, bottom: 8 })
+
+      if (this.entries.length === 0) {
+        Column({ space: 8 }) {
+          SymbolGlyph($r('sys.symbol.music_note_list'))
+            .fontSize(34)
+            .fontColor([Color.Gray])
+
+          Text('ISO 内未发现可提取的音频文件')
+            .fontSize(14)
+            .fontColor(Color.Gray)
+        }
+        .layoutWeight(1)
+        .justifyContent(FlexAlign.Center)
+      } else {
+        List() {
+          ForEach(this.entries, (item: IsoArchiveAudioEntry, index: number) => {
+            ListItem() {
+              Row() {
+                Checkbox({ name: `iso_${index}` })
+                  .select(this.isSelected(item.path))
+                  .selectedColor(this.themeColor)
+                  .shape(CheckBoxShape.CIRCLE)
+                  .onChange((checked: boolean) => {
+                    this.onToggleSelection(item.path, checked)
+                  })
+                  .width(20)
+                  .height(20)
+                  .margin({ left: 18, right: 12 })
+
+                Column({ space: 4 }) {
+                  Text(item.path)
+                    .fontSize(14)
+                    .maxLines(1)
+                    .textOverflow({ overflow: this.isLongNameRoLL ? TextOverflow.MARQUEE : TextOverflow.Ellipsis })
+                    .fontColor($r('app.color.text_color'))
+
+                  Text(Utility.formatFSize(item.size))
+                    .fontSize(11)
+                    .fontColor(Color.Gray)
+                }
+                .layoutWeight(1)
+                .alignItems(HorizontalAlign.Start)
+                .padding({ top: 14, bottom: 14, right: 18 })
+              }
+              .width('100%')
+              .backgroundColor(Color.Transparent)
+            }
+            .onClick(() => {
+              this.onToggleSelection(item.path, !this.isSelected(item.path))
+            })
+          }, (item: IsoArchiveAudioEntry) => item.path)
+        }
+        .layoutWeight(1)
+        .scrollBar(BarState.Off)
+        .divider({
+          strokeWidth: 0.2,
+          color: Color.Gray,
+          startMargin: 50,
+          endMargin: 18
+        })
+      }
+
+      Row() {
+        Button(this.selectedPaths.length === this.entries.length && this.entries.length > 0 ? '取消全选' : '全选')
+          .layoutWeight(1)
+          .height(44)
+          .borderRadius(22)
+          .fontSize(14)
+          .fontColor(this.themeColor)
+          .backgroundColor('#1A000000')
+          .enabled(!this.isExtracting && this.entries.length > 0)
+          .onClick(() => {
+            this.onToggleSelectAll()
+          })
+
+        Button(this.isExtracting ? '提取中...' : '提取文件')
+          .layoutWeight(1)
+          .height(44)
+          .borderRadius(22)
+          .fontSize(14)
+          .fontColor(Color.White)
+          .backgroundColor(this.themeColor)
+          .enabled(!this.isExtracting && this.selectedPaths.length > 0)
+          .onClick(() => {
+            this.onExtract()
+          })
+      }
+      .padding({ left: 24, right: 24, top: 12, bottom: 20 })
+      .margin({ left: 6, right: 6 })
+      .constraintSize({ minHeight: 44 })
+    }
+    .backgroundColor($r('app.color.start_window_background'))
+    .backgroundBlurStyle(BlurStyle.Regular)
+    .height('90%')
+    .width('100%')
+  }
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 697 - 112
entry/src/main/ets/view/LocalMusic.ets


+ 46 - 17
entry/src/main/ets/view/RemoteMusicPage.ets

@@ -201,6 +201,10 @@ export struct RemoteMusicPage {
   @State scaleValue: number = 1 //缩放值
   @State columns: number = 4 //瀑布流列数
   @State isWaterFlowScrolling: boolean = false //WaterFlow是否正在滚动
+  @State refreshPullRatio: number = 1
+  @State isRefreshing: boolean = false
+  @State maxRefreshingHeight: number = 100.0
+  private contentNode?: ComponentContent<Object> = undefined
 
   @State isShowTitleBar: boolean = true //是否显示分类导航条
   private scroller: Scroller = new Scroller()
@@ -487,6 +491,20 @@ export struct RemoteMusicPage {
     this.doSortType(this.sortType)
   }
 
+  private async handlePullRefresh(): Promise<void> {
+    try {
+      await this.refreshNavidromeData(true)
+      if (this.isDetailView && this.filterType !== NavFilterType.None) {
+        await this.applyFilter(this.filterType, this.filterId, this.filterLabel)
+      }
+      if (this.isSearchMode && this.selectedTab === 0 && this.searchText.length > 0) {
+        await this.onSearchInput(this.searchText)
+      }
+    } finally {
+      this.isRefreshing = false
+    }
+  }
+
   private resolveActiveAccount(): WebDavAccount | undefined {
     if (!this.selectedAccount) {
       return undefined;
@@ -4306,26 +4324,37 @@ export struct RemoteMusicPage {
   }
 
   build() {
+    Refresh({ refreshing: $$this.isRefreshing, refreshingContent: this.contentNode }) {
+      Stack() {
+        Column() {
+          this.buildContentView()
+        }
+        .height('100%')
+        Column() {
+          this.topTitleBar()
+        }
 
-    Stack() {
-      Column() {
-        this.buildContentView()
       }
+      .alignContent(Alignment.Top)
+      .width('100%')
       .height('100%')
-      Column() {
-        this.topTitleBar()
-      }
-
-    }
-    .alignContent(Alignment.Top)
-    .width('100%')
-    .height('100%')
-    .backgroundImage(this.isCustomizeBg?this.customizeBgPath:$r('app.color.start_window_background'))
-    .backgroundImageSize(this.isLandscape?{width:'100%'}:{ height: '100%'})
-    .backgroundImagePosition(Alignment.Center)
-    .backdropBlur(this.blurValue)
-    .backgroundBrightness({rate:this.isCustomizeBg?0.1:0,lightUpDegree:this.bgBrightness})
-    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
+      .backgroundImage(this.isCustomizeBg?this.customizeBgPath:$r('app.color.start_window_background'))
+      .backgroundImageSize(this.isLandscape?{width:'100%'}:{ height: '100%'})
+      .backgroundImagePosition(Alignment.Center)
+      .backdropBlur(this.blurValue)
+      .backgroundBrightness({rate:this.isCustomizeBg?0.1:0,lightUpDegree:this.bgBrightness})
+      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
+
+    }
+    .pullDownRatio(this.refreshPullRatio)
+    .pullToRefresh(true)
+    .refreshOffset(0)
+    .onOffsetChange((offset: number) => {
+      this.refreshPullRatio = 1 - Math.pow((offset / this.maxRefreshingHeight), 3)
+    })
+    .onRefreshing(async () => {
+      await this.handlePullRefresh()
+    })
 
   }
 

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác