Prechádzať zdrojové kódy

删除逻辑优化
iso提取的优化 目前还是提取不成功

onecold 4 mesiacov pred
rodič
commit
8ecd6df166

+ 8 - 1
entry/src/main/cpp/CMakeLists.txt

@@ -13,6 +13,9 @@ endif()
 
 include_directories(${NATIVERENDER_ROOT_PATH}
                     ${NATIVERENDER_ROOT_PATH}/include
+                    ${NATIVERENDER_ROOT_PATH}/third_party/sacd/libdstdec
+                    ${NATIVERENDER_ROOT_PATH}/third_party/sacd/libdstdec/binding
+                    ${NATIVERENDER_ROOT_PATH}/third_party/sacd/libdstdec/decoder
                     ${CMAKE_CURRENT_SOURCE_DIR}/../../../../ijkplayer/src/main/cpp/third_party/ffmpeg/ffmpeg/${OHOS_ARCH}/include
                     ${CMAKE_CURRENT_SOURCE_DIR}/../../../../ijkplayer/src/main/cpp/third_party/openssl/${OHOS_ARCH}/include
                     ${CMAKE_CURRENT_SOURCE_DIR}/../../../../ijkplayer/src/main/cpp/third_party/soundtouch/${OHOS_ARCH}/include
@@ -29,5 +32,9 @@ else()
     message(FATAL_ERROR "${SMB2_LIB_NAME} not found for architecture ${OHOS_ARCH} at ${SMB2_LIB_PATH}")
 endif()
 
-add_library(entry SHARED napi_init.cpp)
+add_library(entry SHARED
+    napi_init.cpp
+    third_party/sacd/libdstdec/binding/dst_decoder_mt.cpp
+    third_party/sacd/libdstdec/decoder/decoder.cpp
+)
 target_link_libraries(entry PUBLIC libace_napi.z.so hilog_ndk.z smb2)

+ 185 - 1
entry/src/main/cpp/napi_init.cpp

@@ -17,6 +17,7 @@
 #include <vector>
 
 #include "hilog/log.h"
+#include "third_party/sacd/libdstdec/decoder/decoder.h"
 
 extern "C" {
 #include "smb2/smb2.h"
@@ -26,9 +27,13 @@ extern "C" {
 namespace {
 constexpr unsigned int SMB_LOG_DOMAIN = 0xD001780;
 constexpr const char *SMB_LOG_TAG = "libsmb2";
+constexpr unsigned int SACD_DST_LOG_DOMAIN = 0xD001780;
+constexpr const char *SACD_DST_LOG_TAG = "sacd_dst";
 
 #define SMB_LOGI(fmt, ...) OH_LOG_Print(LOG_APP, LOG_INFO, SMB_LOG_DOMAIN, SMB_LOG_TAG, fmt, ##__VA_ARGS__)
 #define SMB_LOGE(fmt, ...) OH_LOG_Print(LOG_APP, LOG_ERROR, SMB_LOG_DOMAIN, SMB_LOG_TAG, fmt, ##__VA_ARGS__)
+#define SACD_DST_LOGI(fmt, ...) OH_LOG_Print(LOG_APP, LOG_INFO, SACD_DST_LOG_DOMAIN, SACD_DST_LOG_TAG, fmt, ##__VA_ARGS__)
+#define SACD_DST_LOGE(fmt, ...) OH_LOG_Print(LOG_APP, LOG_ERROR, SACD_DST_LOG_DOMAIN, SACD_DST_LOG_TAG, fmt, ##__VA_ARGS__)
 inline void NapiCheck(napi_status status, const char *message)
 {
     if (status != napi_ok) {
@@ -60,11 +65,25 @@ struct SambaTree {
     bool connected = false;
 };
 
+struct SacdDstDecoderSession {
+    int64_t id = 0;
+    uint32_t channelCount = 0;
+    uint32_t samplerate = 0;
+    uint32_t framerate = 0;
+    uint32_t channelFrameSize = 0;
+    size_t dsdFrameSize = 0;
+    std::unique_ptr<dst::decoder_t> decoder;
+    std::vector<uint8_t> decodeBuffer;
+    std::mutex decodeMutex;
+};
+
 std::atomic<int64_t> g_nextHandle{1};
 std::mutex g_mutex;
+std::mutex g_sacdDstMutex;
 std::unordered_map<int64_t, std::shared_ptr<SambaClient>> g_clients;
 std::unordered_map<int64_t, std::shared_ptr<SambaSession>> g_sessions;
 std::unordered_map<int64_t, std::shared_ptr<SambaTree>> g_trees;
+std::unordered_map<int64_t, std::shared_ptr<SacdDstDecoderSession>> g_sacdDstSessions;
 
 int64_t GenerateHandle()
 {
@@ -170,6 +189,36 @@ bool ReadBool(napi_env env, napi_value value, const char *name)
     return result;
 }
 
+uint32_t ReadUint32(napi_env env, napi_value value, const char *name)
+{
+    int64_t result = ReadInt64(env, value, name);
+    if (result < 0 || result > UINT32_MAX) {
+        throw std::runtime_error(std::string(name) + " is out of range");
+    }
+    return static_cast<uint32_t>(result);
+}
+
+struct ArrayBufferInfo {
+    uint8_t *data = nullptr;
+    size_t length = 0;
+};
+
+ArrayBufferInfo ReadArrayBuffer(napi_env env, napi_value value, const char *name)
+{
+    bool isArrayBuffer = false;
+    NapiCheck(napi_is_arraybuffer(env, value, &isArrayBuffer), "Failed to read array buffer type");
+    if (!isArrayBuffer) {
+        throw std::runtime_error(std::string(name) + " must be an ArrayBuffer");
+    }
+    void *data = nullptr;
+    size_t length = 0;
+    NapiCheck(napi_get_arraybuffer_info(env, value, &data, &length), "Failed to read array buffer content");
+    return {
+        static_cast<uint8_t *>(data),
+        length
+    };
+}
+
 napi_value CreateInt64Value(napi_env env, int64_t value)
 {
     napi_value result = nullptr;
@@ -184,6 +233,17 @@ napi_value CreateUndefined(napi_env env)
     return result;
 }
 
+napi_value CreateArrayBufferCopy(napi_env env, const uint8_t *data, size_t length)
+{
+    napi_value result = nullptr;
+    void *output = nullptr;
+    NapiCheck(napi_create_arraybuffer(env, length, &output, &result), "Failed to create output array buffer");
+    if (length > 0 && data != nullptr) {
+        std::memcpy(output, data, length);
+    }
+    return result;
+}
+
 std::shared_ptr<SambaClient> RequireClient(int64_t clientId)
 {
     std::lock_guard<std::mutex> lock(g_mutex);
@@ -214,6 +274,16 @@ std::shared_ptr<SambaTree> RequireTree(int64_t treeId)
     return it->second;
 }
 
+std::shared_ptr<SacdDstDecoderSession> RequireSacdDstDecoderSession(int64_t decoderId)
+{
+    std::lock_guard<std::mutex> lock(g_sacdDstMutex);
+    auto it = g_sacdDstSessions.find(decoderId);
+    if (it == g_sacdDstSessions.end() || it->second == nullptr || it->second->decoder == nullptr) {
+        throw std::runtime_error("Invalid SACD DST decoder handle");
+    }
+    return it->second;
+}
+
 void RemoveTreeEntry(int64_t treeId)
 {
     std::lock_guard<std::mutex> lock(g_mutex);
@@ -900,6 +970,117 @@ napi_value ReadSmbFileRange(napi_env env, napi_callback_info info)
         return nullptr;
     }
 }
+
+napi_value CreateSacdDstDecoder(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 createSacdDstDecoder");
+        if (argc < 1) {
+            throw std::runtime_error("createSacdDstDecoder requires channelCount");
+        }
+
+        uint32_t channelCount = ReadUint32(env, args[0], "channelCount");
+        uint32_t samplerate = argc >= 2 ? ReadUint32(env, args[1], "samplerate") : 2822400;
+        uint32_t framerate = argc >= 3 ? ReadUint32(env, args[2], "framerate") : 75;
+        Ensure(channelCount > 0 && channelCount <= 6, "channelCount must be between 1 and 6");
+        Ensure(samplerate > 0, "samplerate must be greater than 0");
+        Ensure(framerate > 0, "framerate must be greater than 0");
+
+        auto session = std::make_shared<SacdDstDecoderSession>();
+        session->id = GenerateHandle();
+        session->channelCount = channelCount;
+        session->samplerate = samplerate;
+        session->framerate = framerate;
+        session->channelFrameSize = samplerate / 8 / framerate;
+        Ensure(session->channelFrameSize > 0, "Computed channel frame size is invalid");
+        session->dsdFrameSize = static_cast<size_t>(channelCount) *
+                                static_cast<size_t>(session->channelFrameSize);
+        Ensure(session->dsdFrameSize > 0, "Computed DSD frame size is invalid");
+        session->decodeBuffer.resize(session->dsdFrameSize);
+        session->decoder = std::make_unique<dst::decoder_t>();
+        if (!session->decoder || session->decoder->init(channelCount, session->channelFrameSize) != 0) {
+            throw std::runtime_error("Failed to initialize SACD DST decoder");
+        }
+
+        {
+            std::lock_guard<std::mutex> lock(g_sacdDstMutex);
+            g_sacdDstSessions[session->id] = session;
+        }
+        SACD_DST_LOGI("create decoder id=%{public}lld channels=%{public}u frameSize=%{public}u",
+            static_cast<long long>(session->id), channelCount, session->channelFrameSize);
+        return CreateInt64Value(env, session->id);
+    } catch (const std::exception &error) {
+        napi_throw_error(env, nullptr, error.what());
+        return nullptr;
+    }
+}
+
+napi_value DecodeSacdDstFrame(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 decodeSacdDstFrame");
+        if (argc < 2) {
+            throw std::runtime_error("decodeSacdDstFrame requires decoderId and dstFrame");
+        }
+
+        int64_t decoderId = ReadInt64(env, args[0], "decoderId");
+        ArrayBufferInfo dstFrame = ReadArrayBuffer(env, args[1], "dstFrame");
+        Ensure(dstFrame.data != nullptr && dstFrame.length > 0, "dstFrame must not be empty");
+
+        auto session = RequireSacdDstDecoderSession(decoderId);
+        std::lock_guard<std::mutex> decodeLock(session->decodeMutex);
+        uint8_t *decodedData = session->decodeBuffer.data();
+        Ensure(decodedData != nullptr && !session->decodeBuffer.empty(), "Decoded buffer is not initialized");
+
+        const int rc = session->decoder->decode(
+            dstFrame.data,
+            static_cast<unsigned int>(dstFrame.length * 8),
+            decodedData
+        );
+        if (rc != 0) {
+            SACD_DST_LOGE("decode failed id=%{public}lld rc=%{public}d frameSize=%{public}zu output=%{public}zu",
+                static_cast<long long>(decoderId), rc, dstFrame.length, session->dsdFrameSize);
+            throw std::runtime_error("Failed to decode SACD DST frame");
+        }
+        return CreateArrayBufferCopy(env, decodedData, session->dsdFrameSize);
+    } catch (const std::exception &error) {
+        napi_throw_error(env, nullptr, error.what());
+        return nullptr;
+    }
+}
+
+napi_value ReleaseSacdDstDecoder(napi_env env, napi_callback_info info)
+{
+    try {
+        size_t argc = 1;
+        napi_value args[1] = {nullptr};
+        NapiCheck(napi_get_cb_info(env, info, &argc, args, nullptr, nullptr), "Failed to get arguments for releaseSacdDstDecoder");
+        if (argc < 1) {
+            return CreateUndefined(env);
+        }
+        int64_t decoderId = ReadInt64(env, args[0], "decoderId");
+        std::shared_ptr<SacdDstDecoderSession> session;
+        {
+            std::lock_guard<std::mutex> lock(g_sacdDstMutex);
+            auto it = g_sacdDstSessions.find(decoderId);
+            if (it != g_sacdDstSessions.end()) {
+                session = it->second;
+                g_sacdDstSessions.erase(it);
+            }
+        }
+        if (session) {
+            SACD_DST_LOGI("release decoder id=%{public}lld", static_cast<long long>(decoderId));
+        }
+        return CreateUndefined(env);
+    } catch (const std::exception &error) {
+        napi_throw_error(env, nullptr, error.what());
+        return nullptr;
+    }
+}
 }
 
 EXTERN_C_START
@@ -917,7 +1098,10 @@ static napi_value Init(napi_env env, napi_value exports)
         {"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}
+        {"readSmbFileRange", nullptr, ReadSmbFileRange, nullptr, nullptr, nullptr, napi_default, nullptr},
+        {"createSacdDstDecoder", nullptr, CreateSacdDstDecoder, nullptr, nullptr, nullptr, napi_default, nullptr},
+        {"decodeSacdDstFrame", nullptr, DecodeSacdDstFrame, nullptr, nullptr, nullptr, napi_default, nullptr},
+        {"releaseSacdDstDecoder", nullptr, ReleaseSacdDstDecoder, nullptr, nullptr, nullptr, napi_default, nullptr}
     };
     NapiCheck(napi_define_properties(env, exports, sizeof(descriptors) / sizeof(descriptors[0]), descriptors), "Failed to define native exports");
     return exports;

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

@@ -19,6 +19,9 @@ export interface NativeModule {
   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;
+  createSacdDstDecoder(channelCount: number, samplerate?: number, framerate?: number): number;
+  decodeSacdDstFrame(decoderId: number, dstFrame: ArrayBuffer): ArrayBuffer;
+  releaseSacdDstDecoder(decoderId: number): void;
 }
 
 declare const libentry: NativeModule;

+ 232 - 55
entry/src/main/ets/common/util/IsoArchiveTaskHelper.ets

@@ -1,5 +1,7 @@
 import { taskpool } from '@kit.ArkTS';
 import fileIo from '@ohos.file.fs';
+import nativeBridge from 'libentry.so';
+import Logger from './Logger';
 
 const ISO_SECTOR_SIZE: number = 2048;
 const ISO_DESCRIPTOR_START_LSN: number = 16;
@@ -15,6 +17,7 @@ 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_FRAME_RATE: number = 75;
 const SACD_MAX_PACKET_COUNT: number = 7;
 const SACD_MAX_FRAME_BUFFER_SIZE: number = 64 * 1024;
 
@@ -115,6 +118,7 @@ interface IsoExtractTaskPayload {
   isoPath: string;
   destinationDir: string;
   entries: IsoArchiveHelperAudioEntry[];
+  outputPaths?: string[];
 }
 
 export interface IsoArchiveHelperAudioEntry {
@@ -136,15 +140,15 @@ export interface IsoArchiveHelperAudioEntry {
 export interface IsoArchiveHelperExtractTaskResult {
   extractedPaths: string[];
   failedEntries: string[];
+  failureDetails: IsoArchiveHelperExtractFailureDetail[];
 }
 
-interface IsoExtractProgressState {
-  totalUnits: number;
-  completedUnits: number;
-  lastProgress: number;
+export interface IsoArchiveHelperExtractFailureDetail {
+  path: string;
+  reason: string;
 }
 
-interface IsoExtractProgressMessage {
+export interface IsoArchiveHelperExtractProgressMessage {
   progress: number;
   current: number;
   total: number;
@@ -152,7 +156,29 @@ interface IsoExtractProgressMessage {
   stage?: string;
 }
 
+export type IsoArchiveExtractProgressReporter = (message: IsoArchiveHelperExtractProgressMessage) => void;
+
+interface SacdDstNativeModule {
+  createSacdDstDecoder(channelCount: number, samplerate: number, framerate: number): number;
+  decodeSacdDstFrame(decoderId: number, dstFrame: ArrayBuffer): ArrayBuffer;
+  releaseSacdDstDecoder(decoderId: number): void;
+}
+
+const sacdDstNativeBridge: SacdDstNativeModule = nativeBridge as SacdDstNativeModule;
+
+interface IsoExtractProgressState {
+  totalUnits: number;
+  completedUnits: number;
+  lastProgress: number;
+}
+
+interface IsoEntryExtractResult {
+  success: boolean;
+  reason?: string;
+}
+
 export function listIsoAudioEntriesCore(isoPath: string): IsoArchiveHelperAudioEntry[] {
+  Logger.info('IsoArchiveTaskHelper', `heanup ISO list start path=${isoPath}`);
   const standardEntries: IsoArchiveHelperAudioEntry[] = listStandardIsoAudioEntries(isoPath);
   if (standardEntries.length > 0) {
     return standardEntries;
@@ -160,10 +186,13 @@ export function listIsoAudioEntriesCore(isoPath: string): IsoArchiveHelperAudioE
   return listSacdAudioEntries(isoPath);
 }
 
-export function extractIsoAudioEntriesCore(payloadJson: string): IsoArchiveHelperExtractTaskResult {
+export function extractIsoAudioEntriesCore(payloadJson: string,
+  progressReporter?: IsoArchiveExtractProgressReporter): IsoArchiveHelperExtractTaskResult {
+  Logger.info('IsoArchiveTaskHelper', `heanup ISO extract task start payloadLength=${payloadJson ? payloadJson.length : 0}`);
   const result: IsoArchiveHelperExtractTaskResult = {
     extractedPaths: [],
-    failedEntries: []
+    failedEntries: [],
+    failureDetails: []
   };
 
   let payload: IsoExtractTaskPayload | null = null;
@@ -174,8 +203,10 @@ export function extractIsoAudioEntriesCore(payloadJson: string): IsoArchiveHelpe
   }
 
   if (!payload || !payload.isoPath || !payload.destinationDir || !Array.isArray(payload.entries)) {
+    Logger.error('IsoArchiveTaskHelper', 'heanup ISO extract payload invalid');
     return result;
   }
+  Logger.info('IsoArchiveTaskHelper', `heanup ISO extract parsed iso=${payload.isoPath}, dest=${payload.destinationDir}, entries=${payload.entries.length}`);
 
   let isoFile: fileIo.File | undefined = undefined;
   let progressState: IsoExtractProgressState | undefined = undefined;
@@ -183,7 +214,8 @@ export function extractIsoAudioEntriesCore(payloadJson: string): IsoArchiveHelpe
     isoFile = fileIo.openSync(payload.isoPath, fileIo.OpenMode.READ_ONLY);
     ensureDirectoryExists(payload.destinationDir);
     progressState = createIsoExtractProgressState(payload.entries);
-    reportIsoExtractProgress(progressState, '', 'prepare', true);
+    Logger.info('IsoArchiveTaskHelper',
+      `heanup ISO extract enter loop entries=${payload.entries.length}, hasOutputPaths=${payload.outputPaths ? payload.outputPaths.length : 0}`);
 
     for (let index: number = 0; index < payload.entries.length; index++) {
       const entry: IsoArchiveHelperAudioEntry = payload.entries[index];
@@ -191,16 +223,24 @@ export function extractIsoAudioEntriesCore(payloadJson: string): IsoArchiveHelpe
         continue;
       }
 
-      const outputPath: string = buildUniqueOutputPath(payload.destinationDir, entry.name || getLastPathSegment(entry.path));
-      let extracted: boolean = false;
+      const outputPath: string = payload.outputPaths && payload.outputPaths.length > index && payload.outputPaths[index] ?
+        payload.outputPaths[index] : buildUniqueOutputPath(payload.destinationDir, entry.name || getLastPathSegment(entry.path));
+      Logger.info('IsoArchiveTaskHelper',
+        `heanup ISO extract entry index=${index}, path=${entry.path}, type=${entry.entryType || ''}, ` +
+          `size=${entry.size || 0}, channelCount=${entry.channelCount || 0}, frameFormat=${entry.frameFormat ?? -1}, ` +
+          `output=${outputPath}`);
+      let extractResult: IsoEntryExtractResult = { success: false, reason: '提取失败' };
       if (entry.entryType === 'sacd_track') {
-        extracted = extractSacdTrackEntry(isoFile, entry, outputPath, progressState);
+        extractResult = extractSacdTrackEntry(isoFile, entry, outputPath, progressState, progressReporter);
       } else {
-        extracted = extractStandardIsoEntry(isoFile, entry, outputPath, progressState);
+        extractResult = extractStandardIsoEntry(isoFile, entry, outputPath, progressState, progressReporter);
       }
-      if (extracted) {
+      if (extractResult.success) {
+        Logger.info('IsoArchiveTaskHelper', `heanup ISO extract entry success path=${entry.path}, output=${outputPath}`);
         result.extractedPaths.push(outputPath);
       } else {
+        Logger.error('IsoArchiveTaskHelper',
+          `heanup ISO extract entry failed path=${entry.path}, output=${outputPath}, reason=${extractResult.reason || '提取失败'}`);
         if (pathExists(outputPath)) {
           try {
             fileIo.unlinkSync(outputPath);
@@ -208,21 +248,29 @@ export function extractIsoAudioEntriesCore(payloadJson: string): IsoArchiveHelpe
           }
         }
         result.failedEntries.push(entry.path);
+        result.failureDetails.push({
+          path: entry.path,
+          reason: extractResult.reason || '提取失败'
+        });
       }
     }
     if (progressState) {
       progressState.completedUnits = progressState.totalUnits;
-      reportIsoExtractProgress(progressState, '', 'done', true);
+      reportIsoExtractProgress(progressState, '', 'done', true, progressReporter);
     }
   } 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);
+        result.failureDetails.push({
+          path: entry.path,
+          reason: '提取任务执行异常'
+        });
       }
     }
     if (progressState) {
-      reportIsoExtractProgress(progressState, '', 'error', true);
+      reportIsoExtractProgress(progressState, '', 'error', true, progressReporter);
     }
   } finally {
     if (isoFile) {
@@ -265,7 +313,7 @@ function getIsoExtractEntryUnits(entry: IsoArchiveHelperAudioEntry): number {
 }
 
 function advanceIsoExtractProgress(progressState: IsoExtractProgressState | undefined, deltaUnits: number,
-  entryName: string, stage: string): void {
+  entryName: string, stage: string, progressReporter?: IsoArchiveExtractProgressReporter): void {
   if (!progressState || deltaUnits <= 0) {
     return;
   }
@@ -273,11 +321,11 @@ function advanceIsoExtractProgress(progressState: IsoExtractProgressState | unde
   if (progressState.completedUnits > progressState.totalUnits) {
     progressState.completedUnits = progressState.totalUnits;
   }
-  reportIsoExtractProgress(progressState, entryName, stage, false);
+  reportIsoExtractProgress(progressState, entryName, stage, false, progressReporter);
 }
 
 function reportIsoExtractProgress(progressState: IsoExtractProgressState | undefined, entryName: string,
-  stage: string, force: boolean): void {
+  stage: string, force: boolean, progressReporter?: IsoArchiveExtractProgressReporter): void {
   if (!progressState) {
     return;
   }
@@ -294,13 +342,17 @@ function reportIsoExtractProgress(progressState: IsoExtractProgressState | undef
   }
   progressState.lastProgress = progress;
   try {
-    const progressMessage: IsoExtractProgressMessage = {
+    const progressMessage: IsoArchiveHelperExtractProgressMessage = {
       progress,
       current: progressState.completedUnits,
       total: progressState.totalUnits,
       name: entryName,
       stage
     };
+    if (progressReporter) {
+      progressReporter(progressMessage);
+      return;
+    }
     taskpool.Task.sendData(progressMessage);
   } catch (_error) {
   }
@@ -552,9 +604,12 @@ function getFileExtension(fileName: string): string {
 }
 
 function extractStandardIsoEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudioEntry, outputPath: string,
-  progressState?: IsoExtractProgressState): boolean {
+  progressState?: IsoExtractProgressState, progressReporter?: IsoArchiveExtractProgressReporter): IsoEntryExtractResult {
   if (entry.extent <= 0 || entry.size <= 0) {
-    return false;
+    return {
+      success: false,
+      reason: 'ISO 条目元数据无效'
+    };
   }
 
   let targetFile: fileIo.File | undefined = undefined;
@@ -570,7 +625,10 @@ function extractStandardIsoEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAu
       const buffer: ArrayBuffer = new ArrayBuffer(currentChunkSize);
       const bytesRead: number = fileIo.readSync(isoFile.fd, buffer, { offset: readOffset, length: currentChunkSize });
       if (bytesRead <= 0) {
-        return false;
+        return {
+          success: false,
+          reason: `读取 ISO 条目失败: ${entry.name}`
+        };
       }
 
       if (bytesRead === currentChunkSize) {
@@ -580,11 +638,14 @@ function extractStandardIsoEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAu
       }
       remaining -= bytesRead;
       readOffset += bytesRead;
-      advanceIsoExtractProgress(progressState, bytesRead, entry.name, 'extract');
+      advanceIsoExtractProgress(progressState, bytesRead, entry.name, 'extract', progressReporter);
     }
-    return true;
-  } catch (_error) {
-    return false;
+    return { success: true };
+  } catch (error) {
+    return {
+      success: false,
+      reason: `写出文件失败: ${(error as Error).message}`
+    };
   } finally {
     if (targetFile) {
       try {
@@ -697,7 +758,14 @@ function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTo
   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);
+    const rawLengthLsn: number = readUInt32BE(offsetBlock, 8 + 255 * 4 + index * 4);
+    let lengthLsn: number = rawLengthLsn;
+    if (index + 1 < maxTrackCount) {
+      const nextStartLsn: number = readUInt32BE(offsetBlock, 8 + (index + 1) * 4);
+      if (nextStartLsn > startLsn) {
+        lengthLsn = nextStartLsn - startLsn;
+      }
+    }
     if (startLsn <= 0 || lengthLsn <= 0) {
       continue;
     }
@@ -728,6 +796,9 @@ function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTo
       channelCount: areaInfo.channelCount,
       frameFormat: areaInfo.frameFormat
     });
+    Logger.info('IsoArchiveTaskHelper',
+      `heanup SACD track meta index=${index}, startLsn=${startLsn}, rawLengthLsn=${rawLengthLsn}, ` +
+        `resolvedLengthLsn=${lengthLsn}, startFrames=${startFrames}, durationFrames=${durationFrames}`);
   }
 
   return result;
@@ -756,7 +827,7 @@ function buildUniqueOutputPath(destinationDir: string, fileName: string): string
   const baseName: string = extension.length > 0 ? cleanName.substring(0, cleanName.length - extension.length) : cleanName;
 
   let candidatePath: string = `${trimTrailingSeparator(destinationDir)}/${cleanName}`;
-  let suffix: number = 1;
+  let suffix: number = 0;
   while (pathExists(candidatePath)) {
     suffix++;
     candidatePath = `${trimTrailingSeparator(destinationDir)}/${baseName} (${suffix})${extension}`;
@@ -823,21 +894,47 @@ function pathExists(path: string): boolean {
 }
 
 function extractSacdTrackEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudioEntry, outputPath: string,
-  progressState?: IsoExtractProgressState): boolean {
+  progressState?: IsoExtractProgressState, progressReporter?: IsoArchiveExtractProgressReporter): IsoEntryExtractResult {
+  Logger.info('IsoArchiveTaskHelper', `heanup SACD extract start path=${entry.path}, output=${outputPath}, startLsn=${entry.startLsn || 0}, lengthLsn=${entry.lengthLsn || 0}, area=${entry.areaType || ''}`);
   if (!entry.startLsn || !entry.lengthLsn || !entry.areaType) {
-    return false;
+    return {
+      success: false,
+      reason: 'SACD 轨道元数据无效'
+    };
   }
 
-  reportIsoExtractProgress(progressState, entry.name, 'extract', true);
+  reportIsoExtractProgress(progressState, entry.name, 'extract', true, progressReporter);
 
   const areaInfo: SacdAreaExtractInfo | null = resolveSacdAreaExtractInfo(isoFile, entry.areaType);
   if (!areaInfo) {
-    return false;
+    return {
+      success: false,
+      reason: '无法解析 SACD 区域信息'
+    };
   }
   const channelCount: number = entry.channelCount && entry.channelCount > 0 ? entry.channelCount : areaInfo.channelCount;
   const frameFormat: number = entry.frameFormat !== undefined ? entry.frameFormat : areaInfo.frameFormat;
+  Logger.info('IsoArchiveTaskHelper', `heanup SACD area resolved channels=${channelCount}, frameFormat=${frameFormat}, areaFrameFormat=${areaInfo.frameFormat}`);
+  let dstDecoderId: number | undefined = undefined;
   if (frameFormat === SACD_FRAME_FORMAT_DST) {
-    return false;
+    try {
+      dstDecoderId = Number(
+        sacdDstNativeBridge.createSacdDstDecoder(channelCount, SACD_SAMPLING_FREQUENCY, SACD_FRAME_RATE)
+      );
+      Logger.info('IsoArchiveTaskHelper', `heanup DST decoder created id=${dstDecoderId || 0}, channels=${channelCount}`);
+      if (!dstDecoderId || dstDecoderId <= 0) {
+        return {
+          success: false,
+          reason: '初始化 DST 解码器失败'
+        };
+      }
+    } catch (error) {
+      Logger.error('IsoArchiveTaskHelper', `heanup DST decoder create failed: ${(error as Error).message}`);
+      return {
+        success: false,
+        reason: `初始化 DST 解码器失败: ${(error as Error).message}`
+      };
+    }
   }
 
   let outputFile: fileIo.File | undefined = undefined;
@@ -865,26 +962,46 @@ function extractSacdTrackEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudi
       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;
+        return {
+          success: false,
+          reason: '读取 SACD 扇区数据失败'
+        };
       }
       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);
+        const processed: boolean = processSacdSector(
+          sectorBuffer,
+          channelCount,
+          assembler,
+          writer,
+          dstDecoderId,
+          lastSector
+        );
         if (!processed) {
-          return false;
+          Logger.error('IsoArchiveTaskHelper', `heanup SACD process sector failed sectorIndex=${sectorIndex + batchIndex}, lastSector=${lastSector}`);
+          return {
+            success: false,
+            reason: 'SACD 音轨帧解析失败'
+          };
         }
       }
       sectorIndex += currentBatchSectors;
-      advanceIsoExtractProgress(progressState, currentBatchSectors * ISO_SECTOR_SIZE, entry.name, 'extract');
+      advanceIsoExtractProgress(progressState, currentBatchSectors * ISO_SECTOR_SIZE, entry.name, 'extract',
+        progressReporter);
     }
 
     finalizeDsfWriter(writer);
-    return true;
-  } catch (_error) {
-    return false;
+    Logger.info('IsoArchiveTaskHelper', `heanup SACD extract success path=${entry.path}, output=${outputPath}`);
+    return { success: true };
+  } catch (error) {
+    Logger.error('IsoArchiveTaskHelper', `heanup SACD extract exception: ${(error as Error).message}`);
+    return {
+      success: false,
+      reason: `写出 SACD 音轨失败: ${(error as Error).message}`
+    };
   } finally {
     if (outputFile) {
       try {
@@ -892,6 +1009,12 @@ function extractSacdTrackEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudi
       } catch (_error) {
       }
     }
+    if (dstDecoderId && dstDecoderId > 0) {
+      try {
+        sacdDstNativeBridge.releaseSacdDstDecoder(dstDecoderId);
+      } catch (_error) {
+      }
+    }
   }
 }
 
@@ -1031,7 +1154,7 @@ function writeSacdFrameToDsf(writer: DsfWriterState, frameData: Uint8Array, chan
 }
 
 function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, assembler: SacdFrameAssembler,
-  writer: DsfWriterState, lastSector: boolean): boolean {
+  writer: DsfWriterState, dstDecoderId: number | undefined, lastSector: boolean): boolean {
   let offset: number = 0;
   const sectorHeader: number = sectorBuffer[offset];
   offset++;
@@ -1093,11 +1216,16 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
 
     if (packet.dataType === SACD_PACKET_TYPE_AUDIO) {
       if (packet.frameStart) {
-        if (!finalizeSacdFrame(assembler, writer)) {
-          return false;
-        }
         if (assembler.started && assembler.size > 0) {
-          resetSacdFrameAssembler(assembler);
+          if (!isSacdFrameReady(assembler)) {
+            Logger.error('IsoArchiveTaskHelper',
+              `heanup SACD frameStart before previous frame ready: size=${assembler.size}, ` +
+                `dst=${assembler.dstEncoded}, remainingSectorCount=${assembler.sectorCount}`);
+            return false;
+          }
+          if (!finalizeSacdFrame(assembler, writer, dstDecoderId)) {
+            return false;
+          }
         }
 
         const frameInfo: SacdFrameInfo | undefined = frameInfoIndex < frameInfos.length ? frameInfos[frameInfoIndex] : undefined;
@@ -1110,34 +1238,76 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
       }
 
       if (assembler.started) {
-        if (assembler.dstEncoded) {
-          return false;
-        }
         if (!appendSacdFrameBytes(assembler, sectorBuffer.subarray(offset, offset + packet.packetLength))) {
           return false;
         }
+        if (assembler.dstEncoded) {
+          if (assembler.sectorCount <= 0) {
+            Logger.error('IsoArchiveTaskHelper',
+              `heanup SACD DST packet overflow size=${assembler.size}, packetLength=${packet.packetLength}, ` +
+                `remainingSectorCount=${assembler.sectorCount}`);
+            return false;
+          }
+          assembler.sectorCount--;
+        }
       }
     }
     offset += packet.packetLength;
   }
 
   if (lastSector) {
-    const finalized: boolean = finalizeSacdFrame(assembler, writer);
-    resetSacdFrameAssembler(assembler);
-    return finalized;
+    if (!assembler.started || assembler.size <= 0) {
+      resetSacdFrameAssembler(assembler);
+      return true;
+    }
+    if (!isSacdFrameReady(assembler)) {
+      Logger.error('IsoArchiveTaskHelper',
+        `heanup SACD last sector frame incomplete size=${assembler.size}, dst=${assembler.dstEncoded}, ` +
+          `remainingSectorCount=${assembler.sectorCount}`);
+      resetSacdFrameAssembler(assembler);
+      return false;
+    }
+    return finalizeSacdFrame(assembler, writer, dstDecoderId);
   }
   return true;
 }
 
-function finalizeSacdFrame(assembler: SacdFrameAssembler, writer: DsfWriterState): boolean {
-  if (!isSacdFrameReady(assembler)) {
+function finalizeSacdFrame(assembler: SacdFrameAssembler, writer: DsfWriterState,
+  dstDecoderId: number | undefined): boolean {
+  if (!assembler.started || assembler.size <= 0) {
+    resetSacdFrameAssembler(assembler);
     return true;
   }
-  if (assembler.dstEncoded || assembler.channelCount <= 0 || assembler.size % assembler.channelCount !== 0) {
+  if (!isSacdFrameReady(assembler)) {
+    return false;
+  }
+  if (assembler.channelCount <= 0) {
+    resetSacdFrameAssembler(assembler);
+    return false;
+  }
+  let frameData: Uint8Array = assembler.buffer.subarray(0, assembler.size);
+  if (assembler.dstEncoded) {
+    if (!dstDecoderId || dstDecoderId <= 0) {
+      resetSacdFrameAssembler(assembler);
+      return false;
+    }
+    try {
+      const dstFrameBuffer: ArrayBuffer = copySacdFrameToArrayBuffer(frameData);
+      const decodedBuffer: ArrayBuffer = sacdDstNativeBridge.decodeSacdDstFrame(dstDecoderId, dstFrameBuffer);
+      frameData = new Uint8Array(decodedBuffer);
+    } catch (error) {
+      Logger.error('IsoArchiveTaskHelper', `heanup DST decode throw: ${(error as Error).message}`);
+      resetSacdFrameAssembler(assembler);
+      return false;
+    }
+    if (frameData.length === 0 || frameData.length % assembler.channelCount !== 0) {
+      resetSacdFrameAssembler(assembler);
+      return false;
+    }
+  } else if (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;
@@ -1153,6 +1323,13 @@ function isSacdFrameReady(assembler: SacdFrameAssembler): boolean {
   return assembler.size % SACD_FRAME_SIZE_64 === 0;
 }
 
+function copySacdFrameToArrayBuffer(frameData: Uint8Array): ArrayBuffer {
+  const buffer: ArrayBuffer = new ArrayBuffer(frameData.length);
+  const copied: Uint8Array = new Uint8Array(buffer);
+  copied.set(frameData);
+  return buffer;
+}
+
 function appendSacdFrameBytes(assembler: SacdFrameAssembler, source: Uint8Array): boolean {
   if (assembler.size + source.length > assembler.buffer.length) {
     return false;

+ 11 - 1
entry/src/main/ets/common/util/IsoArchiveUtil.ets

@@ -1,4 +1,6 @@
+import { taskpool } from '@kit.ArkTS';
 import {
+  IsoArchiveHelperExtractProgressMessage,
   extractIsoAudioEntriesCore,
   listIsoAudioEntriesCore
 } from './IsoArchiveTaskHelper';
@@ -22,6 +24,12 @@ export interface IsoArchiveAudioEntry {
 export interface IsoExtractTaskResult {
   extractedPaths: string[];
   failedEntries: string[];
+  failureDetails?: IsoExtractFailureDetail[];
+}
+
+export interface IsoExtractFailureDetail {
+  path: string;
+  reason: string;
 }
 
 export interface IsoExtractProgressMessage {
@@ -39,5 +47,7 @@ export async function listIsoAudioEntriesTask(isoPath: string): Promise<IsoArchi
 
 @Concurrent
 export async function extractIsoAudioEntriesTask(payloadJson: string): Promise<IsoExtractTaskResult> {
-  return extractIsoAudioEntriesCore(payloadJson) as IsoExtractTaskResult;
+  return extractIsoAudioEntriesCore(payloadJson, (message: IsoArchiveHelperExtractProgressMessage) => {
+    taskpool.Task.sendData(message);
+  }) as IsoExtractTaskResult;
 }

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

@@ -325,7 +325,6 @@ export class RcpSocket {
     return new Promise(async (resolve, reject) => {
       const url = this.buildRequestUrl(host, port, path, enableHttps);
       const timeoutDuration: number = 10000;
-      console.info(UtilName, 'testTag', '发送PROPFIND请求的url:' + url)
       // 创建 RCP 会话配置
       let response: string = ''
       const customHttpEventsHandler: rcp.HttpEventsHandler = {
@@ -402,15 +401,10 @@ export class RcpSocket {
       try {
         await rcpSession.fetch(req)
           .finally(() => {
-            console.info(UtilName, 'testTag', 'PROPFIND执行完毕')
             if (response != '') {
-              console.info(UtilName, 'testTag', 'WebDAV响应内容长度:', response.length.toString());
-              console.info(UtilName, 'testTag', 'WebDAV响应前500字符:', response.substring(0, 500));
               // 提取文件信息
               const filesInfo = this.extractHrefContents(response, path, url);
-              console.info(UtilName, 'testTag', '解析出文件数量:', filesInfo.length.toString());
               if (filesInfo.length !== 0) {
-                console.info(UtilName, 'testTag', '请求成功')
                 rcpSession.close()
                 resolve(filesInfo);
               } else {
@@ -667,7 +661,6 @@ export class RcpSocket {
       if (displayNameMatch && displayNameMatch[1]) {
         // 如果有 displayname,使用它
         name = this.decodeXMLEntities(displayNameMatch[1]);
-        console.info(UtilName, 'testTag', '使用displayname作为文件名:', name);
       } else {
         // 否则从href中提取文件名(最后一个/后的部分)
         name = fullHref;

+ 2 - 31
entry/src/main/ets/pages/SettingPage.ets

@@ -37,7 +37,6 @@ export struct SettingPage {
   @State showFindLocation: boolean = true
   @State showZMIndex: boolean = false //是否右侧显示字母索引
   @State autoHideTitle: boolean = true //滚动自动隐藏标题栏
-  @State isNoJumpToHome: boolean = false //网盘播放不跳转首页
   @State iconCurrentID: string = 'default';//图标的id
   @State iconArray: Array<Icon> = []
   @State webdavUploadDuplicateAction: string = 'skip' // WebDAV上传重复文件处理方式
@@ -298,7 +297,7 @@ export struct SettingPage {
     this.bgBrightness = PreferencesUtil.getNumberSync(SettingPage.BG_BRIGHTNESS, 0)
     this.isStartAutoPlay = PreferencesUtil.getBooleanSync(SettingPage.iS_START_AUTO_PLAY, false)
     this.isMemoryLastPlay = PreferencesUtil.getBooleanSync(SettingPage.iS_MEMORY_LAST_PLAY, false)
-    this.twoFingerType = PreferencesUtil.getNumberSync(SettingPage.TWO_FINGER_TYPE, 2)
+    this.twoFingerType = PreferencesUtil.getNumberSync(SettingPage.TWO_FINGER_TYPE, 3)
     this.isCircleBtn = PreferencesUtil.getBooleanSync(SettingPage.IS_CIRCLE_BTN, true)
     this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, false)
     this.isShowAllBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_ALLBAR, true)
@@ -324,7 +323,6 @@ export struct SettingPage {
     this.bluetoothLyricEnabled = PreferencesUtil.getBooleanSync(SettingPage.BLUETOOTH_LYRIC_ENABLED, false)
     this.bluetoothLyricMode = PreferencesUtil.getNumberSync(SettingPage.BLUETOOTH_LYRIC_MODE, 0)
     this.isCopyFileToDownLoad = PreferencesUtil.getBooleanSync(SettingPage.IS_COPYFILE_TO_DOWNLOAD, false)
-    this.isNoJumpToHome = PreferencesUtil.getBooleanSync('isNoJumpToHome', true)
     this.webdavUploadDuplicateAction = PreferencesUtil.getStringSync(SettingPage.WEBDAV_UPLOAD_DUPLICATE_ACTION, 'skip')
     this.webdavUploadAutoClear = PreferencesUtil.getBooleanSync(SettingPage.WEBDAV_UPLOAD_AUTO_CLEAR, false)
     this.webdavUploadAllowMobile = PreferencesUtil.getBooleanSync(SettingPage.WEBDAV_UPLOAD_ALLOW_MOBILE, false)
@@ -1964,34 +1962,7 @@ export struct SettingPage {
 
 
             Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
-            // 网盘播放不跳转到首页
-            Row() {
-              SymbolGlyph($r('sys.symbol.close_sidebar'))
-                .fontSize(20)
-                .fontColor([this.themeColor])
-                .alignSelf(ItemAlign.Center)
-                .margin({ left: 15 })
-              Text('网盘播放不跳转到首页')
-                .margin({ left: 8 })
-                .fontSize(15)
-                .fontColor(Color.Gray)
-                .fontWeight(480)
-                .layoutWeight(1)
-              Toggle({ type: ToggleType.Switch, isOn: this.isNoJumpToHome })
-                .selectedColor(this.themeColor)
-                .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
-                .switchPointColor(Color.White)
-                .margin({ right: 18 })
-                .onChange((checked: boolean) => {
-                  this.isNoJumpToHome = checked;
-                  PreferencesUtil.put('isNoJumpToHome', this.isNoJumpToHome)
-                })
-                .width(50)
-                .height(30);
-            }
-            .height(55)
-            .clickEffect({ level: ClickEffectLevel.HEAVY })
-            Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
+
 
             // 备份与恢复
             Button({ type: ButtonType.Normal, stateEffect: true }) {

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

@@ -34,6 +34,7 @@ import MediaTable from '../common/util/MediaTable';
 import { DownloadCenterManager, DownloadCenterTask } from '../common/util/DownloadCenterManager';
 import { DownloadCenter } from '../view/DownloadCenter';
 import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil';
+import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
 
 /**
  * 歌单播放事件数据
@@ -115,7 +116,6 @@ export struct WebDavMainPage {
   searchController: SearchController = new SearchController()
   private searchTicket: number = 0;
   @StorageProp('animationState') animationState: AnimationStatus= AnimationStatus.Running
-  @State isNoJumpToHome: boolean = false //网盘播放不跳转首页
   @State isSearchMode: boolean = false
   @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined;
   @StorageProp('topSafeHeight') topSafeHeight: number = 0;
@@ -890,7 +890,8 @@ export struct WebDavMainPage {
       if (this.selectedFolders.length > 0) {
         await this.webdavManager.deleteRemoteFolders(this.selectedAccount, this.selectedFolders.slice());
       }
-      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
+      this.removeSongsLocally(this.selectedSongs.slice())
+      this.removeFoldersLocally(this.selectedFolders.slice())
       this.exitMultiSelect();
       this.getUIContext().getPromptAction().showToast({ message: '删除成功' });
     } catch (error) {
@@ -930,7 +931,7 @@ export struct WebDavMainPage {
     this.isDeletingSelection = true;
     try {
       await this.webdavManager.deleteRemoteSongs(this.selectedAccount, [song]);
-      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
+      this.removeSongsLocally([song])
       this.getUIContext().getPromptAction().showToast({ message: '删除成功' });
     } catch (error) {
       const err = error as Error;
@@ -973,7 +974,7 @@ export struct WebDavMainPage {
     this.isDeletingSelection = true;
     try {
       await this.webdavManager.deleteRemoteFolders(this.selectedAccount, [folder]);
-      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
+      this.removeFoldersLocally([folder])
       this.getUIContext().getPromptAction().showToast({ message: '删除成功' });
     } catch (error) {
       const err = error as Error;
@@ -983,6 +984,91 @@ export struct WebDavMainPage {
     }
   }
 
+  private removeDisplayedRemoteSongsByKeys(keys: Set<string>): void {
+    for (let index = this.dataSource.dataArray.length - 1; index >= 0; index--) {
+      const item = this.dataSource.dataArray[index]
+      if (item && keys.has(item.filePath)) {
+        this.dataSource.deleteData(index)
+      }
+    }
+  }
+
+  private syncGlobalWebdavQueueAfterSongRemoval(keys: Set<string>): void {
+    if (keys.size <= 0 || !ArrayUtil.isNotEmpty(globalWebdavVideoItems)) {
+      return
+    }
+    const nextQueue = globalWebdavVideoItems.filter((item: VideoItem) => !keys.has(item.filePath))
+    if (nextQueue.length === globalWebdavVideoItems.length) {
+      return
+    }
+    const currentPath = this.currentSong?.filePath ?? '';
+    let nextIndex = nextQueue.findIndex((item: VideoItem) => item.filePath === currentPath)
+    if (nextIndex < 0) {
+      nextIndex = nextQueue.length > 0 ? Math.min(globalWebdavCurrentPlayIndex, nextQueue.length - 1) : 0
+    }
+    globalWebdavVideoItems = nextQueue
+    globalWebdavCurrentPlayIndex = nextIndex
+    const eventQueueRefresh: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAY_QUEUE_REFRESH }
+    emitter.emit(eventQueueRefresh, { data: { source: 'webdav-delete' } })
+  }
+
+  private removeSongsLocally(songs: VideoItem[]): void {
+    if (ArrayUtil.isEmpty(songs)) {
+      return
+    }
+    const keys: Set<string> = new Set<string>(songs.map((item: VideoItem) => item.filePath))
+    this.songs = this.songs.filter((item: VideoItem) => !keys.has(item.filePath))
+    this.webdavManager.webDavSongs = this.songs.slice()
+    this.filteredList = this.filteredList.filter((item: VideoItem) => !keys.has(item.filePath))
+    this.removeDisplayedRemoteSongsByKeys(keys)
+    this.syncGlobalWebdavQueueAfterSongRemoval(keys)
+    if (this.isSearchMode && this.searchText.length > 0) {
+      this.syncSelectionAfterRefresh()
+    } else {
+      this.listRefreshKey += 1
+      this.syncSelectionAfterRefresh()
+    }
+    this.scheduleRemoteThumbPrefetch()
+  }
+
+  private removeFoldersLocally(folders: FileInfo[]): void {
+    if (ArrayUtil.isEmpty(folders)) {
+      return
+    }
+    const keys: Set<string> = new Set<string>(folders.map((item: FileInfo) => this.resolveFolderSelectionKey(item)))
+    this.webDavFiles = this.webDavFiles.filter((item: FileInfo) => !keys.has(this.resolveFolderSelectionKey(item)))
+    this.webdavManager.webDavFiles = this.webDavFiles.slice()
+    this.visibleFoldersState = this.visibleFoldersState.filter((item: FileInfo) =>
+      !keys.has(this.resolveFolderSelectionKey(item)))
+    this.filteredFolderList = this.filteredFolderList.filter((item: FileInfo) =>
+      !keys.has(this.resolveFolderSelectionKey(item)))
+    if (this.isSearchMode && this.searchText.length > 0) {
+      this.refreshDisplaySongs(this.filteredList)
+    } else {
+      this.updateVisibleFolders()
+    }
+    this.syncSelectionAfterRefresh()
+  }
+
+  @Builder
+  private buildSwipeDeleteAction(onDelete: () => void) {
+    Button() {
+      Image($r('app.media.delete2'))
+        .fillColor(Color.White)
+        .width(20)
+        .height(20)
+    }
+    .width(56)
+    .height(56)
+    .type(ButtonType.Circle)
+    .backgroundColor('#E53935')
+    .margin(8)
+    .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.86 })
+    .onClick(() => {
+      onDelete()
+    })
+  }
+
   private canRenameRemoteSong(song: VideoItem): boolean {
     return song.type === CommonConstants.TYPE_WEBDAV ||
       song.type === CommonConstants.TYPE_SMB ||
@@ -1922,7 +2008,6 @@ export struct WebDavMainPage {
     this.isShowFileName = PreferencesUtil.getBooleanSync('isShowFileName', false)
     this.isLongNameRoLL = PreferencesUtil.getBooleanSync('isLongNameRoLL', true)
     this.sortType = PreferencesUtil.getNumberSync('webDavSortType', 0)
-    this.isNoJumpToHome = PreferencesUtil.getBooleanSync('isNoJumpToHome', true)
     this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true)
     this.autoHideTitle = PreferencesUtil.getBooleanSync('autoHideTitle', true)
   }
@@ -2315,12 +2400,6 @@ export struct WebDavMainPage {
 
       Logger.info(TAG, `heanup 发送WebDAV播放事件,只传递索引: ${playIndex}`);
 
-      if(!this.isNoJumpToHome){
-        // 跳转到首页播放器
-        this.getUIContext()?.animateTo({ duration: 555 }, () => {
-          this.mType = 0
-        })
-      }
 
     } catch (error) {
       const err = error as Error;
@@ -2744,6 +2823,7 @@ export struct WebDavMainPage {
           .onSubmit((value: string) => {
             console.log('onecold onSubmit ='+value)
             this.searchController.stopEditing()
+            this.commitSearchHistory(value)
             this.onSearchInput(value);
 
           })
@@ -2768,6 +2848,7 @@ export struct WebDavMainPage {
           .zIndex(0)
           .onClick(()=>{
             this.isSearchMode = true
+            this.loadSearchHistory()
             this.restoreCurrentDirectorySearchView()
             void this.webdavManager.ensureGlobalSearchIndex(this.selectedAccount)
           })
@@ -2868,6 +2949,30 @@ export struct WebDavMainPage {
   @State searchText: string = ''; // 用户输入内容
   @State filteredList: Array<VideoItem> = []; // 过滤后的歌曲结果
   @State filteredFolderList: Array<FileInfo> = []; // 过滤后的文件夹结果
+  @State searchHistoryItems: string[] = [];
+  private readonly searchHistoryScope: string = 'webdav_music';
+
+  private loadSearchHistory(): void {
+    this.searchHistoryItems = SearchHistoryUtil.load(this.searchHistoryScope);
+  }
+
+  private saveSearchHistory(keyword: string): void {
+    this.searchHistoryItems = SearchHistoryUtil.save(this.searchHistoryScope, keyword);
+  }
+
+  private commitSearchHistory(keyword: string): void {
+    const normalized = keyword.trim();
+    if (normalized.length === 0) {
+      return;
+    }
+    this.saveSearchHistory(normalized);
+  }
+
+  private applySearchHistory(keyword: string): void {
+    this.searchText = keyword;
+    this.searchController.stopEditing();
+    this.onSearchInput(keyword);
+  }
 
   private restoreCurrentDirectorySearchView(): void {
     this.filteredList = this.sortSongsForType([...this.songs], this.sortType)
@@ -2933,6 +3038,7 @@ export struct WebDavMainPage {
     this.searchText = keyword;
     const ticket = ++this.searchTicket;
     if (keyword.length === 0) {
+      this.loadSearchHistory();
       this.restoreCurrentDirectorySearchView();
       return;
     }
@@ -2943,6 +3049,47 @@ export struct WebDavMainPage {
     void this.applyGlobalSearch(keyword, ticket);
   }
 
+  @Builder
+  private SearchHistoryView(): void {
+    Column({ space: 10 }) {
+      Row() {
+        Text('搜索历史')
+          .fontSize(14)
+          .fontWeight(FontWeight.Medium)
+          .fontColor($r('app.color.text_color'))
+        Blank()
+        Button('清空')
+          .fontSize(12)
+          .fontColor(this.themeColor)
+          .backgroundColor(Color.Transparent)
+          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
+          .onClick(() => {
+            SearchHistoryUtil.clear(this.searchHistoryScope);
+            this.searchHistoryItems = [];
+          })
+      }
+      .width('100%')
+
+      Flex({ wrap: FlexWrap.Wrap }) {
+        ForEach(this.searchHistoryItems, (keyword: string) => {
+          Button(keyword)
+            .fontSize(12)
+            .fontColor($r('app.color.text_color'))
+            .backgroundColor(this.isDarkMode ? '#222222' : '#F2F3F5')
+            .borderRadius(16)
+            .margin({ right: 8, bottom: 8 })
+            .padding({ left: 12, right: 12, top: 8, bottom: 8 })
+            .onClick(() => {
+              this.applySearchHistory(keyword);
+            })
+        })
+      }
+      .width('100%')
+    }
+    .width('100%')
+    .padding({ left: 20, right: 20, top: this.getSearchLoadingTopOffset(), bottom: 10 })
+  }
+
   private openSongPropertySheet(song: VideoItem): void {
     this.songForPropertySheet = song;
     this.isShowSongPropertySheet = true;
@@ -3153,6 +3300,8 @@ export struct WebDavMainPage {
     if (this.shouldShowSearchIndexingTip()) {
       return 8
     }
+    if(this.isLoading)
+      return 8
     return this.topSafeHeight + 85
   }
 
@@ -3284,18 +3433,44 @@ export struct WebDavMainPage {
         List({ scroller: this.listScroller ,space: 0 }) {
           // 显示文件夹 - 只显示当前目录下的直接子文件夹
           ForEach(this.isSearchMode ? this.filteredFolderList : this.visibleFoldersState, (folder: FileInfo) => {
-            ListItem() {
-              this.buildFolderItem(folder)
+            if (this.isMultiSelect) {
+              ListItem() {
+                this.buildFolderItem(folder)
+              }
+              .clickEffect({ level: ClickEffectLevel.MIDDLE })
+            } else {
+              ListItem() {
+                this.buildFolderItem(folder)
+              }
+              .clickEffect({ level: ClickEffectLevel.MIDDLE })
+              .swipeAction({
+                end: this.buildSwipeDeleteAction(() => {
+                  this.confirmDeleteSingleFolder(folder)
+                }),
+                edgeEffect: SwipeEdgeEffect.None
+              })
             }
-            .clickEffect({ level: ClickEffectLevel.MIDDLE })
           }, (folder: FileInfo) =>  folder.name+folder.fileName)
 
           // 显示歌曲
           LazyForEach(this.dataSource, (song: VideoItem, index: number) => {
-            ListItem() {
-              this.buildSongItem(song, index)
+            if (this.isMultiSelect) {
+              ListItem() {
+                this.buildSongItem(song, index)
+              }
+              .clickEffect({ level: ClickEffectLevel.MIDDLE })
+            } else {
+              ListItem() {
+                this.buildSongItem(song, index)
+              }
+              .clickEffect({ level: ClickEffectLevel.MIDDLE })
+              .swipeAction({
+                end: this.buildSwipeDeleteAction(() => {
+                  this.confirmDeleteSingleSong(song)
+                }),
+                edgeEffect: SwipeEdgeEffect.None
+              })
             }
-            .clickEffect({ level: ClickEffectLevel.MIDDLE })
           }, (item: VideoItem, index: number) =>  item.filePath + '_' + index+this.listRefreshKey)
         }
         .scrollBar(BarState.Off)
@@ -3319,6 +3494,8 @@ export struct WebDavMainPage {
         .contentEndOffset(this.bottomSafeHeight+70)
         .layoutWeight(1)
         .margin({ top: 4 })
+      } else if (this.isSearchMode && this.searchText.length === 0 && this.searchHistoryItems.length > 0) {
+        this.SearchHistoryView()
       } else if (!this.isLoading) {
         Column() {
           Text(this.isSearchMode && this.searchText.length > 0 ? '没有找到匹配结果' : '暂无内容')

+ 2 - 3
entry/src/main/ets/view/IsoExtractComptent.ets

@@ -80,12 +80,14 @@ export struct IsoExtractComptent {
                   .select(this.isSelected(item.path))
                   .selectedColor(this.themeColor)
                   .shape(CheckBoxShape.CIRCLE)
+                  .enabled(!this.isExtracting)
                   .onChange((checked: boolean) => {
                     this.onToggleSelection(item.path, checked)
                   })
                   .width(20)
                   .height(20)
                   .margin({ left: 18, right: 12 })
+                  .hitTestBehavior(HitTestMode.Block)
 
                 Column({ space: 4 }) {
                   Text(item.path)
@@ -105,9 +107,6 @@ export struct IsoExtractComptent {
               .width('100%')
               .backgroundColor(Color.Transparent)
             }
-            .onClick(() => {
-              this.onToggleSelection(item.path, !this.isSelected(item.path))
-            })
           }, (item: IsoArchiveAudioEntry) => item.path)
         }
         .layoutWeight(1)

+ 384 - 111
entry/src/main/ets/view/LocalMusic.ets

@@ -155,6 +155,7 @@ import {
   isCueSplitItem
 } from '../common/util/CueUtils';
 import { CueComptent } from '../view/CueComptent';
+import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
 import { IsoExtractComptent } from '../view/IsoExtractComptent';
 import PlaylistTable from '../common/util/PlaylistTable';
 import { showAddToPlaylistDialog } from '../dialog/AddToPlaylistDialog';
@@ -191,6 +192,7 @@ import { CoverThumbCache } from '../common/util/CoverThumbCache';
 import { LyricCopySheet } from './LyricCopySheet';
 import {
   IsoArchiveAudioEntry,
+  IsoExtractFailureDetail,
   IsoExtractProgressMessage,
   IsoExtractTaskResult,
   listIsoAudioEntriesTask,
@@ -439,6 +441,13 @@ export struct LocalMusic {
   private isoExtractProgressTimer: number = 0
   private isoExtractExpectedOutputPaths: Array<string> = []
   private isoExtractTotalBytes: number = 0
+  private isoExtractDisplayedProgress: number = 0
+  private isoExtractReceivedTaskProgress: boolean = false
+  private isoExtractTask: taskpool.Task | undefined = undefined
+  private isoExtractLastActivityTime: number = 0
+  private readonly isoExtractStallTimeoutMs: number = 15000
+  private isoExtractCanceled: boolean = false
+  private isoExtractCancelMessage: string = ''
   searchController: SearchController = new SearchController()
   @State rightTopImage:Resource = $r('sys.symbol.sort')
   @State isEditAudio:boolean = false
@@ -992,6 +1001,7 @@ export struct LocalMusic {
       this.tipPopup = !this.tipPopup
       PreferencesUtil.putSync('isFirstTiped',false)
     }
+    this.loadSearchHistory()
     this.initSetting()
     this.syncEqualizerUi();
     this.UNKONWN = Utility.resourceToString(this.context, $r('app.string.unknown'));
@@ -1426,7 +1436,7 @@ export struct LocalMusic {
     this.isMemoryLastPlay = PreferencesUtil.getBooleanSync(SettingPage.iS_MEMORY_LAST_PLAY, false)
     this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true)
     this.isShowAllBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_ALLBAR, true)
-    this.twoFingerType = PreferencesUtil.getNumberSync(SettingPage.TWO_FINGER_TYPE, 2)
+    this.twoFingerType = PreferencesUtil.getNumberSync(SettingPage.TWO_FINGER_TYPE, 3)
     this.isCircleBtn = PreferencesUtil.getBooleanSync(SettingPage.IS_CIRCLE_BTN, true)
     this.isShowPlayPageBack = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_PLAYPAGE_BACK, true)
     this.isCoverTopBig = PreferencesUtil.getBooleanSync(SettingPage.IS_COVER_TOP_BIG, false)
@@ -1522,16 +1532,16 @@ export struct LocalMusic {
   makeWorker() {
     setTimeout(() => {
       if (PreferencesUtil.getBooleanSync('isFirstApp', true)) {
-        ToastUtil.showToast('请到文件扫描页面导入音乐!')
+        // ToastUtil.showToast('请到文件扫描页面导入音乐!')
         //发送worker通知扫描文件入库,用户反馈4000多首用这个方法扫描会闪退。
-        // workerInstance.postMessage({
-        //   code: 1,
-        //   data1: this.context,
-        //   data2: this.rootPath,
-        //   data3: this.lockPath,
-        //   data4: PreferencesUtil.getStringSync('COVER_API', ''),
-        //   data5: PreferencesUtil.getBooleanSync('autoParseMusicName', true),
-        // });
+        workerInstance.postMessage({
+          code: 1,
+          data1: this.context,
+          data2: this.rootPath,
+          data3: this.lockPath,
+          data4: PreferencesUtil.getStringSync('COVER_API', ''),
+          data5: PreferencesUtil.getBooleanSync('autoParseMusicName', true),
+        });
         PreferencesUtil.putSync('isFirstApp', false)
       } else {
 
@@ -4449,6 +4459,7 @@ export struct LocalMusic {
             if(this.modeType==0){
               this.modeType =1//如果是首页 切换到媒体库搜索
             }
+            this.commitSearchHistory(value)
             this.onSearchInput(this.searchText);
 
           })
@@ -4475,6 +4486,7 @@ export struct LocalMusic {
           .zIndex(0)
           .onClick(()=>{
             this.isSearchMode = true
+            this.loadSearchHistory()
           })
           //排序按钮
           Button({ type: ButtonType.Circle, stateEffect: true }) {
@@ -5232,6 +5244,79 @@ export struct LocalMusic {
   }
 
   private deleteComponentId: number = 0
+
+  private closeDeleteDialog(afterClose?: () => void): void {
+    if (this.deleteComponentId > 0) {
+      try {
+        this.getUIContext().getPromptAction().closeCustomDialog(this.deleteComponentId)
+      } catch (_error) {
+      }
+      this.deleteComponentId = 0
+    }
+    if (afterClose) {
+      setTimeout(() => {
+        afterClose()
+      }, 16)
+    }
+  }
+
+  private resetDeleteSelectionState(): void {
+    this.isMultiSelect = false
+    this.selectedFiles = []
+    this.isAllSelected = false
+  }
+
+  private removeDisplayedLocalItemsByKeys(deletedKeys: Set<string>): void {
+    for (let index = this.dataSource.dataArray.length - 1; index >= 0; index--) {
+      const item = this.dataSource.dataArray[index]
+      if (item && deletedKeys.has(item.filePath)) {
+        this.dataSource.deleteData(index)
+      }
+    }
+  }
+
+  private async removeDeletedLocalItemsLocally(deletedItems: Array<VideoItem>): Promise<void> {
+    if (ArrayUtil.isEmpty(deletedItems) || this.modeType !== 0) {
+      return
+    }
+    const deletedKeys: Set<string> = new Set<string>()
+    for (let i = 0; i < deletedItems.length; i++) {
+      const item = deletedItems[i]
+      if (StrUtil.isNotEmpty(item?.filePath)) {
+        deletedKeys.add(item.filePath)
+      }
+    }
+    if (deletedKeys.size <= 0) {
+      return
+    }
+
+    const nextVideoLocalList = this.videoLocalList.filter((item: VideoItem) => !deletedKeys.has(item.filePath))
+    const nextDirList = this.dirList.filter((item: VideoItem) => !deletedKeys.has(item.filePath))
+    const nextFilteredList = this.filteredList.filter((item: VideoItem) => !deletedKeys.has(item.filePath))
+    const removedCount = this.videoLocalList.length - nextVideoLocalList.length
+
+    this.videoLocalList = nextVideoLocalList
+    this.dirList = nextDirList
+    this.filteredList = nextFilteredList
+    this.removeDisplayedLocalItemsByKeys(deletedKeys)
+    if (removedCount > 0 && this.totalCount > 0) {
+      this.totalCount = Math.max(0, this.totalCount - removedCount)
+    }
+    this.pageCache.clear()
+    this.pageHasMoreCache.clear()
+    this.pageTotalCountCache.clear()
+    this.setButtonStatus()
+    this.markAlphaBetDirty()
+    this.syncLocalSelectionState()
+
+    this.addCache(this.currentPath, nextVideoLocalList)
+    if (this.currentPath === this.rootPath) {
+      this.addCache(this.currentPath + '_dirList', nextDirList)
+    }
+    this.deleteStringCache(this.currentPath + '_hash')
+    await this.saveCacheToStorage()
+  }
+
   //多选删除文件
   showWarnIsDelete(){
     this.getUIContext().getPromptAction().openCustomDialog({
@@ -5242,12 +5327,13 @@ export struct LocalMusic {
       showInSubWindow:false,
       maskColor: Color.Transparent,
       dialogTransition: // 设置弹窗内容显示的过渡效果
-      TransitionEffect.translate({ x: 0, y: 290, z: 0 })
-        .animation({ duration: 600, curve: Curve.Smooth }),
+      TransitionEffect.translate({ x: 0, y: 120, z: 0 })
+        .combine(TransitionEffect.opacity(0.01))
+        .animation({ duration: 260, curve: Curve.EaseOut }),
 
       maskTransition: // 设置蒙层显示的过渡效果
       TransitionEffect.opacity(0)
-        .animation({ duration: 600, curve: Curve.Smooth })
+        .animation({ duration: 220, curve: Curve.EaseOut })
     }).then((dialogId: number) => {
       this.deleteComponentId = dialogId
     })
@@ -5264,24 +5350,22 @@ export struct LocalMusic {
       isPlaylistMode: this.modeType == 4,
       playlistId: this.currentSongListID,
       onCancel:()=>{
-        this.isMultiSelect = false
-        this.selectedFiles = []
-        this.isAllSelected = false
-        this.getUIContext().getPromptAction().closeCustomDialog(this.deleteComponentId)
+        this.closeDeleteDialog(() => {
+          this.resetDeleteSelectionState()
+        })
       },
       onDeleteResult:(result: boolean)=>{
-        this.isMultiSelect = false
-        this.getUIContext().getPromptAction().closeCustomDialog(this.deleteComponentId)
-        if(result){//如果删除成功,更新数据
-          this.selectedFiles = [];
-          this.isAllSelected = false
-          if (this.modeType == 4) {
-            this.doSongListTask()
-          } else {
-            this.cache.delete(this.currentPath);
-            this.getSortedFiles(this.currentPath, true);
+        const deletedItems = [...this.selectedFiles]
+        this.closeDeleteDialog(() => {
+          this.resetDeleteSelectionState()
+          if(result){//如果删除成功,更新数据
+            if (this.modeType == 4) {
+              this.doSongListTask()
+            } else {
+              void this.removeDeletedLocalItemsLocally(deletedItems)
+            }
           }
-        }
+        })
 
       }
     })
@@ -5895,6 +5979,30 @@ export struct LocalMusic {
   @State searchText: string = ''; // 用户输入内容
   @State filteredList: Array<VideoItem> = []; // 过滤后的结果
   @State isSearchMode: boolean = false
+  @State searchHistoryItems: string[] = []
+  private readonly searchHistoryScope: string = 'local_music'
+
+  private loadSearchHistory(): void {
+    this.searchHistoryItems = SearchHistoryUtil.load(this.searchHistoryScope)
+  }
+
+  private saveSearchHistory(keyword: string): void {
+    this.searchHistoryItems = SearchHistoryUtil.save(this.searchHistoryScope, keyword)
+  }
+
+  private commitSearchHistory(keyword: string): void {
+    const normalized = keyword.trim()
+    if (normalized.length === 0) {
+      return
+    }
+    this.saveSearchHistory(normalized)
+  }
+
+  private applySearchHistory(keyword: string): void {
+    this.searchText = keyword
+    this.searchController.stopEditing()
+    void this.onSearchInput(keyword)
+  }
 
   // 实时搜索逻辑(带防抖)
   private async onSearchInput(value: string): Promise<void> {
@@ -5904,6 +6012,7 @@ export struct LocalMusic {
 
     // 清空搜索文本时保持搜索态,只重置为不带关键词的结果列表
     if (this.searchText === '') {
+      this.loadSearchHistory()
       await this.resetAndLoadFirstPage();
       return;
     }
@@ -5917,6 +6026,47 @@ export struct LocalMusic {
     await this.resetAndLoadFirstPage();
   }
 
+  @Builder
+  private SearchHistoryView(): void {
+    Column({ space: 10 }) {
+      Row() {
+        Text('搜索历史')
+          .fontSize(14)
+          .fontWeight(FontWeight.Medium)
+          .fontColor($r('app.color.text_color'))
+        Blank()
+        Button('清空')
+          .fontSize(12)
+          .fontColor(this.themeColor)
+          .backgroundColor(Color.Transparent)
+          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
+          .onClick(() => {
+            SearchHistoryUtil.clear(this.searchHistoryScope)
+            this.searchHistoryItems = []
+          })
+      }
+      .width('100%')
+
+      Flex({ wrap: FlexWrap.Wrap }) {
+        ForEach(this.searchHistoryItems, (keyword: string) => {
+          Button(keyword)
+            .fontSize(12)
+            .fontColor($r('app.color.text_color'))
+            .backgroundColor(this.isDarkMode ? '#222222' : '#F2F3F5')
+            .borderRadius(16)
+            .margin({ right: 8, bottom: 8 })
+            .padding({ left: 12, right: 12, top: 8, bottom: 8 })
+            .onClick(() => {
+              this.applySearchHistory(keyword)
+            })
+        })
+      }
+      .width('100%')
+    }
+    .width('100%')
+    .padding({ left: 16, right: 16, top: 10, bottom: 10 })
+  }
+
   @State dragItem: number = -1
   @State scaleItem: number = -1
   @State neighborItem: number = -1
@@ -7093,7 +7243,7 @@ export struct LocalMusic {
             (this.modeType == 0 || this.modeType == 1 || ((this.modeType == 3 || this.modeType == 2) && this.isCanBack)))
             && item.type !== CommonConstants.TYPE_IS_CSJAD &&
             !this.isHistory) ? { end: this.DeleteButton(item, index, item.filePath),
-          start:null,edgeEffect: SwipeEdgeEffect.None } : {}) //左滑
+          edgeEffect: SwipeEdgeEffect.None } : {}) //左滑
         .onClick(() => {
           if (this.isMultiSelect) {
             if (this.isSelectableLocalItem(item)) {
@@ -7759,10 +7909,12 @@ export struct LocalMusic {
 
   private async openIsoArchiveDialog(item: VideoItem): Promise<void> {
     if (!item || StrUtil.isEmpty(item.filePath)) {
+      Logger.error(TAG, `heanup ISO open dialog invalid path=${item ? item.filePath : ''}`)
       ToastUtil.showToast('ISO 文件无效')
       return
     }
 
+    Logger.info(TAG, `heanup ISO open dialog path=${item.filePath}, name=${item.fileName || item.name}`)
     this.closeIsoDialog()
     this.isoSourceFilePath = item.filePath
     this.isoSourceFileName = item.fileName || item.name
@@ -7778,14 +7930,16 @@ export struct LocalMusic {
       const task = new taskpool.Task(listIsoAudioEntriesTask, item.filePath)
       const entries = await taskpool.execute(task, taskpool.Priority.HIGH) as IsoArchiveAudioEntry[]
       this.isoAudioEntries = Array.isArray(entries) ? entries : []
+      Logger.info(TAG, `heanup ISO list entries done count=${this.isoAudioEntries.length}, path=${item.filePath}`)
       if (ArrayUtil.isEmpty(this.isoAudioEntries)) {
+        Logger.error(TAG, `heanup ISO list entries empty path=${item.filePath}`)
         ToastUtil.showToast('ISO 内未找到音频文件')
         this.closeIsoDialog()
         return
       }
-      this.selectedIsoAudioPaths = this.isoAudioEntries.map((entry: IsoArchiveAudioEntry) => entry.path)
+      this.selectedIsoAudioPaths = []
     } catch (error) {
-      Logger.error(TAG, `解析 ISO 失败: ${(error as Error).message}`)
+      Logger.error(TAG, `heanup 解析 ISO 失败: ${(error as Error).message}`)
       ToastUtil.showToast('解析 ISO 失败')
       this.closeIsoDialog()
       return
@@ -7810,30 +7964,33 @@ export struct LocalMusic {
     }).then((dialogId: number) => {
       this.isoComponentId = dialogId
     }).catch((error: BusinessError) => {
-      Logger.error(TAG, `打开 ISO 弹窗失败: ${error.message}`)
+      Logger.error(TAG, `heanup 打开 ISO 弹窗失败: ${error.message}`)
       this.closeIsoDialog()
     })
   }
 
   private toggleIsoEntrySelection(path: string, checked: boolean): void {
-    const exists = this.selectedIsoAudioPaths.includes(path)
+    const selectedPathSet: Set<string> = new Set<string>(this.selectedIsoAudioPaths)
     if (checked) {
-      if (!exists) {
-        this.selectedIsoAudioPaths = [...this.selectedIsoAudioPaths, path]
-      }
-      return
-    }
-    if (exists) {
-      this.selectedIsoAudioPaths = this.selectedIsoAudioPaths.filter(itemPath => itemPath !== path)
+      selectedPathSet.add(path)
+    } else {
+      selectedPathSet.delete(path)
     }
+    this.selectedIsoAudioPaths = this.isoAudioEntries
+      .filter((entry: IsoArchiveAudioEntry) => selectedPathSet.has(entry.path))
+      .map((entry: IsoArchiveAudioEntry) => entry.path)
+    Logger.info(TAG,
+      `heanup ISO toggle selection path=${path}, checked=${checked}, total=${this.selectedIsoAudioPaths.length}`)
   }
 
   private toggleIsoEntrySelectAll(): void {
     if (this.selectedIsoAudioPaths.length === this.isoAudioEntries.length) {
       this.selectedIsoAudioPaths = []
+      Logger.info(TAG, 'heanup ISO toggle select all cleared')
       return
     }
     this.selectedIsoAudioPaths = this.isoAudioEntries.map((entry: IsoArchiveAudioEntry) => entry.path)
+    Logger.info(TAG, `heanup ISO toggle select all count=${this.selectedIsoAudioPaths.length}`)
   }
 
   private startIsoExtractProgressWatcher(entries: Array<IsoArchiveAudioEntry>): void {
@@ -7843,6 +8000,9 @@ export struct LocalMusic {
       const size = entry && entry.size > 0 ? entry.size : 0
       return total + size
     }, 0)
+    this.isoExtractReceivedTaskProgress = false
+    this.isoExtractDisplayedProgress = 0
+    this.isoExtractLastActivityTime = Date.now()
     if (this.isoExtractTotalBytes <= 0 || ArrayUtil.isEmpty(this.isoExtractExpectedOutputPaths)) {
       return
     }
@@ -7858,11 +8018,17 @@ export struct LocalMusic {
     }
     this.isoExtractExpectedOutputPaths = []
     this.isoExtractTotalBytes = 0
+    this.isoExtractReceivedTaskProgress = false
+    this.isoExtractLastActivityTime = 0
+  }
+
+  private markIsoExtractActivity(): void {
+    this.isoExtractLastActivityTime = Date.now()
   }
 
   private updateIsoExtractProgressFromFiles(): void {
     if (!this.loadingProgressDialogId || this.isoExtractTotalBytes <= 0 ||
-      ArrayUtil.isEmpty(this.isoExtractExpectedOutputPaths)) {
+      ArrayUtil.isEmpty(this.isoExtractExpectedOutputPaths) || this.isoExtractReceivedTaskProgress) {
       return
     }
 
@@ -7889,14 +8055,54 @@ export struct LocalMusic {
       if (progress > 99) {
         progress = 99
       }
+      this.markIsoExtractActivity()
     } else if (existingCount > 0) {
       progress = 1
+      this.markIsoExtractActivity()
     }
 
     if (progress > 0) {
-      DialogHelper.updateLoading(this.loadingProgressDialogId,
-        `正在提取 ${progress}% (${existingCount}/${this.isoExtractExpectedOutputPaths.length})`, progress)
+      this.updateIsoExtractProgressDialog(progress,
+        `正在提取 ${progress}% (${existingCount}/${this.isoExtractExpectedOutputPaths.length})`)
+      return
     }
+
+    if (this.isoExtractTask && this.isoExtractLastActivityTime > 0 &&
+      Date.now() - this.isoExtractLastActivityTime > this.isoExtractStallTimeoutMs) {
+      Logger.warn(TAG, `heanup ISO 提取进度长时间无变化: timeout=${this.isoExtractStallTimeoutMs}`)
+      this.markIsoExtractActivity()
+      this.updateIsoExtractProgressDialog(1, '准备提取 1%')
+    }
+  }
+
+  private updateIsoExtractProgressDialog(progress: number, message: string, force: boolean = false): void {
+    if (!this.loadingProgressDialogId) {
+      return
+    }
+    let safeProgress = progress
+    if (safeProgress < 0) {
+      safeProgress = 0
+    }
+    if (safeProgress > 100) {
+      safeProgress = 100
+    }
+    if (!force && safeProgress < this.isoExtractDisplayedProgress) {
+      return
+    }
+    this.isoExtractDisplayedProgress = safeProgress
+    DialogHelper.updateLoading(this.loadingProgressDialogId, message, safeProgress)
+  }
+
+  private closeIsoExtractLoadingDialog(): void {
+    if (!this.loadingProgressDialogId) {
+      return
+    }
+    try {
+      DialogHelper.closeDialog(this.loadingProgressDialogId)
+    } catch (_error) {
+    }
+    this.loadingProgressDialogId = ''
+    this.isoExtractDisplayedProgress = 0
   }
 
   private buildIsoExtractExpectedOutputPaths(entries: Array<IsoArchiveAudioEntry>): Array<string> {
@@ -7918,7 +8124,7 @@ export struct LocalMusic {
     const baseName = extension.length > 0 ? cleanName.substring(0, cleanName.length - extension.length) : cleanName
 
     let candidatePath = `${this.trimIsoOutputTrailingSeparator(destinationDir)}/${cleanName}`
-    let suffix = 1
+    let suffix = 0
     while (occupiedPaths.has(candidatePath) || FileUtil.accessSync(candidatePath)) {
       suffix++
       candidatePath = `${this.trimIsoOutputTrailingSeparator(destinationDir)}/${baseName} (${suffix})${extension}`
@@ -7968,6 +8174,27 @@ export struct LocalMusic {
     return path
   }
 
+  private cancelIsoExtraction(reason?: string, closeLoadingDialog: boolean = true): void {
+    const currentTask = this.isoExtractTask
+    Logger.info(TAG,
+      `heanup ISO cancel request reason=${reason || ''}, hasTask=${currentTask ? 'true' : 'false'}, ` +
+        `closeLoading=${closeLoadingDialog}`)
+    this.isoExtractCanceled = true
+    this.isoExtractCancelMessage = reason ? reason : ''
+    this.isoExtractTask = undefined
+    if (currentTask) {
+      try {
+        taskpool.terminateTask(currentTask)
+      } catch (error) {
+        Logger.error(TAG, `heanup 终止 ISO 提取任务失败: ${(error as Error).message}`)
+      }
+    }
+    if (closeLoadingDialog) {
+      this.closeIsoExtractLoadingDialog()
+    }
+    this.closeIsoDialog()
+  }
+
   private async extractSelectedIsoAudioFiles(): Promise<void> {
     if (this.isIsoExtracting) {
       return
@@ -7976,45 +8203,83 @@ export struct LocalMusic {
       ToastUtil.showToast('请先选择要提取的音频文件')
       return
     }
+    const selectedPathSet: Set<string> = new Set<string>(this.selectedIsoAudioPaths)
     const selectedEntries = this.isoAudioEntries
-      .filter((entry: IsoArchiveAudioEntry) => this.selectedIsoAudioPaths.includes(entry.path))
+      .filter((entry: IsoArchiveAudioEntry) => selectedPathSet.has(entry.path))
     if (ArrayUtil.isEmpty(selectedEntries)) {
       ToastUtil.showToast('未找到要提取的音频文件')
       return
     }
     const hasSacdTrack: boolean = selectedEntries.some((entry: IsoArchiveAudioEntry) => entry.entryType === 'sacd_track')
-    const hasDstTrack: boolean = selectedEntries.some((entry: IsoArchiveAudioEntry) =>
-      entry.entryType === 'sacd_track' && entry.frameFormat === 0)
 
     this.isIsoExtracting = true
+    this.isoExtractCanceled = false
+    this.isoExtractCancelMessage = ''
+    Logger.info(TAG,
+      `heanup ISO extract start file=${this.isoSourceFilePath}, selected=${selectedEntries.length}, hasSacd=${hasSacdTrack}`)
+    Logger.info(TAG,
+      `heanup ISO selected entries: ${selectedEntries.map((entry: IsoArchiveAudioEntry) => entry.path).join(', ')}`)
     this.loadingProgressDialogId = DialogHelper.showLoadingProgress({
       progress: 0,
-      backCancel: false,
+      backCancel: true,
       autoCancel: false,
       loadColor: this.themeColor,
-      fontColor: this.themeColor
+      fontColor: this.themeColor,
+      onWillDismiss: (dismissDialogAction: DismissDialogAction) => {
+        if (dismissDialogAction.reason == DismissReason.PRESS_BACK) {
+          this.cancelIsoExtraction('已取消 ISO 提取', false)
+          dismissDialogAction.dismiss()
+        }
+      }
     })
+    this.isoExtractDisplayedProgress = 0
+    this.updateIsoExtractProgressDialog(1, '准备提取 1%', true)
     this.startIsoExtractProgressWatcher(selectedEntries)
 
     try {
+      const outputPaths: string[] = this.buildIsoExtractExpectedOutputPaths(selectedEntries)
       const payloadJson = JSON.stringify({
         isoPath: this.isoSourceFilePath,
         destinationDir: this.currentPath,
-        entries: selectedEntries
+        entries: selectedEntries,
+        outputPaths
       })
       const task = new taskpool.Task(extractIsoAudioEntriesTask, payloadJson)
+      this.isoExtractTask = task
       task.onReceiveData((progressData: IsoExtractProgressMessage) => {
+        this.markIsoExtractActivity()
         const progress = typeof progressData.progress === 'number' ? Math.floor(progressData.progress) : 0
+        Logger.info(TAG,
+          `heanup ISO task receive stage=${progressData.stage || ''}, progress=${progress}, name=${progressData.name || ''}`)
+        if (progress > 1 && !this.isoExtractReceivedTaskProgress) {
+          this.isoExtractReceivedTaskProgress = true
+          if (this.isoExtractProgressTimer) {
+            clearInterval(this.isoExtractProgressTimer)
+            this.isoExtractProgressTimer = 0
+          }
+          Logger.info(TAG, `heanup ISO 提取切换为任务进度: progress=${progress}, stage=${progressData.stage || ''}`)
+        }
         const stageText = progressData.stage === 'prepare' ? '准备提取' :
           (progressData.stage === 'done' ? '提取完成' : (progressData.stage === 'error' ? '提取异常' : '正在提取'))
         const nameText = StrUtil.isNotEmpty(progressData.name) ? ` ${progressData.name}` : ''
-        if (this.loadingProgressDialogId) {
-          DialogHelper.updateLoading(this.loadingProgressDialogId, `${stageText} ${progress}%${nameText}`, progress)
+        if (progress > this.isoExtractDisplayedProgress || progressData.stage !== 'extract') {
+          Logger.info(TAG,
+            `heanup ISO 提取任务进度: stage=${progressData.stage || ''}, progress=${progress}, ` +
+              `name=${progressData.name || ''}`)
         }
+        this.updateIsoExtractProgressDialog(progress, `${stageText} ${progress}%${nameText}`)
       })
       const result = await taskpool.execute(task, taskpool.Priority.HIGH) as IsoExtractTaskResult
+      this.isoExtractTask = undefined
+      Logger.info(TAG, `heanup ISO extract task completed canceled=${this.isoExtractCanceled}`)
+      if (this.isoExtractCanceled) {
+        return
+      }
       const extractedPaths = result?.extractedPaths || []
       const failedEntries = result?.failedEntries || []
+      const failureDetails = result?.failureDetails || []
+      Logger.info(TAG,
+        `heanup ISO extract result extracted=${extractedPaths.length}, failed=${failedEntries.length}, details=${failureDetails.length}`)
       if (ArrayUtil.isNotEmpty(extractedPaths)) {
         await this.importExtractedFilesToCurrentPath(extractedPaths)
       }
@@ -8024,23 +8289,34 @@ export struct LocalMusic {
         ToastUtil.showToast(`提取完成,成功 ${extractedPaths.length} 个,失败 ${failedEntries.length} 个`)
       } else if (extractedPaths.length > 0) {
         ToastUtil.showToast(`提取完成,已导入 ${extractedPaths.length} 个音频文件`)
-      } else if (hasDstTrack) {
-        ToastUtil.showToast('当前 SACD ISO 为 DST 压缩轨道,暂不支持提取')
+      } else if (ArrayUtil.isNotEmpty(failureDetails)) {
+        const firstFailure: IsoExtractFailureDetail = failureDetails[0]
+        const reasonText = firstFailure.reason && firstFailure.reason.length > 0 ? firstFailure.reason : '提取失败'
+        ToastUtil.showToast(reasonText)
       } else if (hasSacdTrack) {
         ToastUtil.showToast('SACD ISO 轨道提取失败')
       } else {
         ToastUtil.showToast('提取失败')
       }
     } catch (error) {
-      Logger.error(TAG, `提取 ISO 音频失败: ${(error as Error).message}`)
+      this.isoExtractTask = undefined
+      if (this.isoExtractCanceled) {
+        return
+      }
+      Logger.error(TAG, `heanup 提取 ISO 音频失败: ${(error as Error).message}`)
       ToastUtil.showToast('提取 ISO 音频失败')
     } finally {
       this.stopIsoExtractProgressWatcher()
+      this.isoExtractTask = undefined
       this.isIsoExtracting = false
-      if (this.loadingProgressDialogId) {
-        DialogHelper.closeDialog(this.loadingProgressDialogId)
-        this.loadingProgressDialogId = ''
+      Logger.info(TAG,
+        `heanup ISO extract finally canceled=${this.isoExtractCanceled}, cancelMessage=${this.isoExtractCancelMessage}`)
+      if (this.isoExtractCanceled && this.isoExtractCancelMessage.length > 0) {
+        ToastUtil.showToast(this.isoExtractCancelMessage)
       }
+      this.isoExtractCanceled = false
+      this.isoExtractCancelMessage = ''
+      this.closeIsoExtractLoadingDialog()
     }
   }
 
@@ -8091,6 +8367,7 @@ export struct LocalMusic {
     this.isoSourceFilePath = ''
     this.isIsoExtracting = false
     this.stopIsoExtractProgressWatcher()
+    this.isoExtractTask = undefined
   }
 
   @Builder
@@ -8112,6 +8389,7 @@ export struct LocalMusic {
       },
       onCancel: () => {
         if (this.isIsoExtracting) {
+          this.cancelIsoExtraction('已取消 ISO 提取')
           return
         }
         this.closeIsoDialog()
@@ -19300,22 +19578,22 @@ export struct LocalMusic {
   DeleteButton(item: VideoItem, index: number, filePath: string) {
     Row() {
       //加入收藏
-      Button() {
-        Image(Utility.getIsFav(this.favList, item) ? $r('app.media.add_fac_light2') : $r('app.media.add_fac'))
-          .fillColor(Color.White)
-          .width(20)
-      }
-      .width(40)
-      .height(40)
-      .type(ButtonType.Circle)
-      .backgroundColor(this.themeColor)
-      .margin(5)
-      .visibility(this.supportsLocalAudioActions(item) ? Visibility.Visible : Visibility.None)
-      .onClick(() => {
-        if (item) {
-          this.doFav(item)
-        }
-      })
+      // Button() {
+      //   Image(Utility.getIsFav(this.favList, item) ? $r('app.media.add_fac_light2') : $r('app.media.add_fac'))
+      //     .fillColor(Color.White)
+      //     .width(20)
+      // }
+      // .width(40)
+      // .height(40)
+      // .type(ButtonType.Circle)
+      // .backgroundColor(this.themeColor)
+      // .margin(5)
+      // .visibility(this.supportsLocalAudioActions(item) ? Visibility.Visible : Visibility.None)
+      // .onClick(() => {
+      //   if (item) {
+      //     this.doFav(item)
+      //   }
+      // })
 
       //加入下一首播放
       Button() {
@@ -19386,23 +19664,23 @@ export struct LocalMusic {
       })
 
       //重命名
-      Button() {
-        Image($r('app.media.rename2'))
-          .fillColor(Color.White)
-          .width(20)
-      }
-      .width(40)
-      .height(40)
-      .type(ButtonType.Circle)
-      .backgroundColor(this.themeColor)
-      .visibility(item.type === CommonConstants.TYPE_IS_DIR ?
-        (item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO ||
-          item.name === LocalMusic.STR_HISTORY_MUSIC ? Visibility.None : Visibility.Visible) : Visibility.Visible)
-      .margin(5)
-      .onClick(() => {
-        this.showReNameDialog(item, index + '', filePath)
-
-      })
+      // Button() {
+      //   Image($r('app.media.rename2'))
+      //     .fillColor(Color.White)
+      //     .width(20)
+      // }
+      // .width(40)
+      // .height(40)
+      // .type(ButtonType.Circle)
+      // .backgroundColor(this.themeColor)
+      // .visibility(item.type === CommonConstants.TYPE_IS_DIR ?
+      //   (item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO ||
+      //     item.name === LocalMusic.STR_HISTORY_MUSIC ? Visibility.None : Visibility.Visible) : Visibility.Visible)
+      // .margin(5)
+      // .onClick(() => {
+      //   this.showReNameDialog(item, index + '', filePath)
+      //
+      // })
 
       Button() {
         Image($r('app.media.delete2'))
@@ -19416,27 +19694,27 @@ export struct LocalMusic {
       .visibility(item.type === CommonConstants.TYPE_IS_DIR ?
         (item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO ||
           item.name === LocalMusic.STR_HISTORY_MUSIC ? Visibility.None : Visibility.Visible) : Visibility.Visible)
-      .margin(5)
+      .margin({ left:5,top:5,bottom:5,right:20} )
       .onClick(() => {
         this.selectSingleItemForDelete(item)
         this.showWarnIsDelete()
         // this.showWarnIsDeleteFile(item, index + '', filePath)
       })
 
-      Button() {
-        Image($r('app.media.share2'))
-          .fillColor(Color.White)
-          .width(20)
-      }
-      .width(40)
-      .height(40)
-      .type(ButtonType.Circle)
-      .backgroundColor(this.themeColor)
-      .margin(5)
-      .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible)
-      .onClick(() => {
-        Utility.doShareMusic(item, getContext(this) as common.UIAbilityContext)
-      })
+      // Button() {
+      //   Image($r('app.media.share2'))
+      //     .fillColor(Color.White)
+      //     .width(20)
+      // }
+      // .width(40)
+      // .height(40)
+      // .type(ButtonType.Circle)
+      // .backgroundColor(this.themeColor)
+      // .margin(5)
+      // .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible)
+      // .onClick(() => {
+      //   Utility.doShareMusic(item, getContext(this) as common.UIAbilityContext)
+      // })
 
     }
 
@@ -19904,11 +20182,6 @@ function cutPopupBuilder(dataBu: BubbleBean) {
           .margin({ top: 7, bottom: 7 })
 
         }
-        .transition(TransitionEffect.asymmetric(TransitionEffect.move(TransitionEdge.BOTTOM)
-          .animation({ duration: 500 }),
-          TransitionEffect.scale({ x: 0, y: 0 })))
-        // .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 500 }),
-        //   TransitionEffect.scale({ x: 0, y: 0 })  ))
         .onClick(() => {
           dataBu.onItemClick?.(index)
         })

+ 72 - 1
entry/src/main/ets/view/RemoteMusicPage.ets

@@ -33,6 +33,7 @@ import { getRemoteDriveDisplayLabel } from '../common/util/RemoteDriveLabel';
 import NavidromeListCache, { AllCacheData } from '../common/util/NavidromeListCache';
 import { LazyDataSource } from '../common/util/LazyDataSource';
 import { taskpool } from '@kit.ArkTS';
+import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
 
 const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist';
 const NAVIDROME_SEARCH_LIMIT = 500;
@@ -337,6 +338,8 @@ export struct RemoteMusicPage {
   // 搜索和排序相关状态
   @State isSearchMode: boolean = false;
   @State searchText: string = ''; // 用户输入内容
+  @State searchHistoryItems: string[] = [];
+  private readonly searchHistoryScope: string = 'remote_music';
   @State filteredList: Array<VideoItem> = []; // 过滤后的歌曲结果
   @State sortType: number = 0; // 排序类型 0:名称升序 1:名称降序 2:时间升序 3:时间降序 4:大小升序 5:大小降序
   @State isSearchLoading: boolean = false;
@@ -505,6 +508,27 @@ export struct RemoteMusicPage {
     }
   }
 
+  private loadSearchHistory(): void {
+    this.searchHistoryItems = SearchHistoryUtil.load(this.searchHistoryScope);
+  }
+
+  private saveSearchHistory(keyword: string): void {
+    this.searchHistoryItems = SearchHistoryUtil.save(this.searchHistoryScope, keyword);
+  }
+
+  private commitSearchHistory(keyword: string): void {
+    const normalized = keyword.trim();
+    if (normalized.length === 0) {
+      return;
+    }
+    this.saveSearchHistory(normalized);
+  }
+
+  private applySearchHistory(keyword: string): void {
+    this.searchText = keyword;
+    void this.onSearchInput(keyword);
+  }
+
   private resolveActiveAccount(): WebDavAccount | undefined {
     if (!this.selectedAccount) {
       return undefined;
@@ -2670,6 +2694,7 @@ export struct RemoteMusicPage {
           .placeholderFont({ size: 14, weight: 400 })
           .textFont({ size: 14, weight: 400 })
           .onSubmit((value: string) => {
+            this.commitSearchHistory(value);
             void this.onSearchInput(value);
           })
           .onChange((value: string) => {
@@ -2693,6 +2718,7 @@ export struct RemoteMusicPage {
           .visibility(this.selectedTab==0?Visibility.Visible:Visibility.None)
           .onClick(() => {
             this.isSearchMode = true;
+            this.loadSearchHistory();
             if (!this.isSearchLoading && this.searchText.length === 0) {
               this.filteredList = [];
             }
@@ -2872,6 +2898,7 @@ export struct RemoteMusicPage {
     void ServerLogUtil.debug('NavidromeSearch', `搜索输入: "${value}" -> "${keyword}"`);
 
     if (keyword.length === 0) {
+      this.loadSearchHistory();
       this.filteredList = [];
       this.isSearchLoading = false;
       this.songDataSource.pushArrayData(this.getVisibleSongs());
@@ -2882,7 +2909,6 @@ export struct RemoteMusicPage {
     if (!this.isSearchMode) {
       this.isSearchMode = true;
     }
-
     const account = this.resolveActiveAccount();
     if (!account) {
       this.filteredList = [];
@@ -3258,6 +3284,47 @@ export struct RemoteMusicPage {
     })
   }
 
+  @Builder
+  private SearchHistoryView(): void {
+    Column({ space: 10 }) {
+      Row() {
+        Text('搜索历史')
+          .fontSize(14)
+          .fontWeight(FontWeight.Medium)
+          .fontColor($r('app.color.text_color'))
+        Blank()
+        Button('清空')
+          .fontSize(12)
+          .fontColor(this.themeColor)
+          .backgroundColor(Color.Transparent)
+          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
+          .onClick(() => {
+            SearchHistoryUtil.clear(this.searchHistoryScope);
+            this.searchHistoryItems = [];
+          })
+      }
+      .width('100%')
+
+      Flex({ wrap: FlexWrap.Wrap }) {
+        ForEach(this.searchHistoryItems, (keyword: string) => {
+          Button(keyword)
+            .fontSize(12)
+            .fontColor($r('app.color.text_color'))
+            .backgroundColor(this.isDarkMode ? '#222222' : '#F2F3F5')
+            .borderRadius(16)
+            .margin({ right: 8, bottom: 8 })
+            .padding({ left: 12, right: 12, top: 8, bottom: 8 })
+            .onClick(() => {
+              this.applySearchHistory(keyword);
+            })
+        })
+      }
+      .width('100%')
+    }
+    .width('100%')
+    .padding({ left: 20, right: 20, top: this.topSafeHeight + 110, bottom: 10 })
+  }
+
   private hydrateSongQuality(items: VideoItem[]): void {
     if (!items || items.length === 0) {
       return;
@@ -3646,6 +3713,10 @@ export struct RemoteMusicPage {
         }
       }
 
+      if (this.isSearchMode && this.searchText.length === 0 && this.searchHistoryItems.length > 0) {
+        this.SearchHistoryView()
+      }
+
       // 空状态
       if (this.getCurrentCount() === 0 && !(this.isDetailView && this.filterType !== NavFilterType.None && this.isFilterLoading) && !(this.isSearchMode && this.searchText.length > 0 && this.isSearchLoading)) {
         Column() {