Эх сурвалжийг харах

音量增强优化
歌词设置增加逐字歌词开关
网盘搜索点击播放问题

onecold 5 сар өмнө
parent
commit
e0d231ffea

+ 3 - 0
entry/src/main/ets/common/constants/EventConstants.ets

@@ -55,6 +55,9 @@ export class EventConstants {
   // 播放暂停事件(比如hicar断开后,手机不播放了)
   static readonly EVENT_PLAY_PAUSE: number = 2005;
 
+  // 播放队列刷新事件(仅刷新列表,不触发重新播放)
+  static readonly EVENT_PLAY_QUEUE_REFRESH: number = 2006;
+
   /**
    * WebDAV 元数据同步事件
    */

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

@@ -1963,10 +1963,20 @@ export class RemoteDriveManager {
     }
     if (item.type === CommonConstants.TYPE_SMB || item.type === CommonConstants.TYPE_NAVIDROME ||
       item.type === CommonConstants.TYPE_FTP || item.type === CommonConstants.TYPE_BAIDU ||
-      item.type === CommonConstants.TYPE_JELLYFIN) {
+      item.type === CommonConstants.TYPE_JELLYFIN || item.type === CommonConstants.TYPE_EMBY ||
+      item.type === CommonConstants.TYPE_AUDIOSTATION || item.type === CommonConstants.TYPE_PLEX ||
+      item.type === CommonConstants.TYPE_DAOLIYU) {
       if (item.remote_rel_path) {
         return item.remote_rel_path;
       }
+      const stablePath = item.id || item.webdav_id || item.baiduFsId;
+      if (stablePath) {
+        item.remote_rel_path = stablePath;
+        return stablePath;
+      }
+      if (item.filePath && item.filePath.length > 0) {
+        return item.filePath;
+      }
       return null;
     }
     return null;

+ 22 - 12
entry/src/main/ets/dialog/AddToPlaylistDialog.ets

@@ -25,6 +25,7 @@ function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: bool
 struct AddToPlaylistDialogContent {
   @State playlists: Playlist[] = []
   @State selectedPlaylistId: string = ''
+  @State isSubmitting: boolean = false
   private currentSongs: VideoItem[] = []
   @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
   @State isDarkMode: boolean = false
@@ -36,7 +37,7 @@ struct AddToPlaylistDialogContent {
   }
 
   // 回调函数
-  onConfirm?: (playlistId: string) => void
+  onConfirm?: (playlistId: string) => void | Promise<void>
   onCancel?: () => void
   onCreateNew?: () => void
 
@@ -253,17 +254,17 @@ struct AddToPlaylistDialogContent {
             DialogHelper.closeDialog('addToPlaylistDialog')
           })
 
-        Button('添加')
+        Button(this.isSubmitting ? '添加中...' : '添加')
           .width('45%')
           .height(40)
           .backgroundColor(this.isDarkMode ? $r('app.color.silvery'):this.themeColor)
           .borderRadius(8)
           .fontSize(14)
           .fontColor(Color.White)
-          .enabled(this.selectedPlaylistId !== '')
-          .opacity(this.selectedPlaylistId !== '' ? 1 : 0.5)
+          .enabled(this.selectedPlaylistId !== '' && !this.isSubmitting)
+          .opacity(this.selectedPlaylistId !== '' && !this.isSubmitting ? 1 : 0.5)
           .onClick(() => {
-            this.handleConfirm()
+            void this.handleConfirm()
           })
       }
       .width('100%')
@@ -280,14 +281,23 @@ struct AddToPlaylistDialogContent {
   /**
    * 处理确认操作
    */
-  private handleConfirm() {
+  private async handleConfirm(): Promise<void> {
     if (!this.selectedPlaylistId) {
       ToastUtil.showToast('请选择一个歌单')
       return
     }
-
-    this.onConfirm?.(this.selectedPlaylistId)
-    DialogHelper.closeDialog('addToPlaylistDialog')
+    if (this.isSubmitting) {
+      return
+    }
+    this.isSubmitting = true
+    try {
+      await this.onConfirm?.(this.selectedPlaylistId)
+      DialogHelper.closeDialog('addToPlaylistDialog')
+    } catch (error) {
+      ToastUtil.showToast(`添加失败: ${(error as Error).message}`)
+    } finally {
+      this.isSubmitting = false
+    }
   }
 }
 
@@ -303,7 +313,7 @@ export struct AddToPlaylistDialogManager {
   buildAddToPlaylistDialog(
     songs: VideoItem[],
     playlists: Playlist[],
-    onConfirm: (playlistId: string) => void,
+    onConfirm: (playlistId: string) => void | Promise<void>,
     onCancel?: () => void,
     onCreateNew?: () => void
   ) {
@@ -322,7 +332,7 @@ export struct AddToPlaylistDialogManager {
   showAddToPlaylistDialog(
     songs: VideoItem[],
     playlists: Playlist[],
-    onConfirm: (playlistId: string) => void,
+    onConfirm: (playlistId: string) => void | Promise<void>,
     onCancel?: () => void,
     onCreateNew?: () => void
   ) {
@@ -350,7 +360,7 @@ const dialogManager = new AddToPlaylistDialogManager()
 export function showAddToPlaylistDialog(
   songs: VideoItem[],
   playlists: Playlist[],
-  onConfirm: (playlistId: string) => void,
+  onConfirm: (playlistId: string) => void | Promise<void>,
   onCancel?: () => void,
   onCreateNew?: () => void
 ) {

+ 30 - 30
entry/src/main/ets/pages/UploadMusicPage.ets

@@ -1214,7 +1214,7 @@ export struct UploadMusicPage {
           Progress({ value: this.uploadProgress, total: 100, type: ProgressType.Linear })
             .width('100%')
             .color(this.themeColor)
-            .backgroundColor(this.isDarkMode ? '#2E3238' : '#E5E5EA')
+            .backgroundColor($r('app.color.track_color'))
             .style({ strokeWidth: 6 })
 
           // 进度信息行
@@ -1252,7 +1252,7 @@ export struct UploadMusicPage {
         }
         .width('100%')
         .padding(14)
-        .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
+        .backgroundColor($r('app.color.bg_card'))
         .borderRadius(10)
 
         // 控制按钮
@@ -1303,7 +1303,7 @@ export struct UploadMusicPage {
       }
       .width('100%')
       .padding(16)
-      .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+      .backgroundColor($r('app.color.start_window_background'))
       .borderRadius(12)
       .shadow({
         radius: this.isDarkMode ? 8 : 12,
@@ -1364,7 +1364,7 @@ export struct UploadMusicPage {
     }
     .width('100%')
     .padding(16)
-    .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+    .backgroundColor($r('app.color.start_window_background'))
     .borderRadius(12)
     .shadow({
       radius: this.isDarkMode ? 8 : 12,
@@ -1459,7 +1459,7 @@ export struct UploadMusicPage {
             }
             .width('100%')
             .padding(12)
-            .backgroundColor(this.isDarkMode ? '#2E3033' : '#F1F3F5')
+            .backgroundColor($r('app.color.bg_card'))
             .borderRadius(8)
           }
         }, (item: VideoItem, index: number) => `${type}-${index}-${item.name}`)
@@ -1595,7 +1595,7 @@ export struct UploadMusicPage {
       }
       .width('100%')
       .padding(14)
-      .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
+      .backgroundColor($r('app.color.bg_card'))
       .borderRadius(10)
       .border({
         width: 1,
@@ -1623,12 +1623,12 @@ export struct UploadMusicPage {
       }
       .width('100%')
       .padding(14)
-      .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
+      .backgroundColor($r('app.color.bg_card'))
       .borderRadius(10)
     }
     .width('100%')
     .padding(16)
-    .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+    .backgroundColor($r('app.color.start_window_background'))
     .borderRadius(12)
     .shadow({
       radius: this.isDarkMode ? 8 : 12,
@@ -1676,7 +1676,7 @@ export struct UploadMusicPage {
       }
       .width('100%')
       .padding(16)
-      .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5')
+      .backgroundColor($r('app.color.index_background'))
 
       // 文件夹列表
       if (this.isLoadingFolders) {
@@ -1693,7 +1693,7 @@ export struct UploadMusicPage {
         .width('100%')
         .height(300)
         .justifyContent(FlexAlign.Center)
-        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+        .backgroundColor($r('app.color.start_window_background'))
       } else if (this.browseFolders.length === 0) {
         Column() {
           Text('📂')
@@ -1707,7 +1707,7 @@ export struct UploadMusicPage {
         .width('100%')
         .height(300)
         .justifyContent(FlexAlign.Center)
-        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+        .backgroundColor($r('app.color.start_window_background'))
       } else {
         List({ space: 8 }) {
           ForEach(this.browseFolders, (folder: FileInfo, index: number) => {
@@ -1730,7 +1730,7 @@ export struct UploadMusicPage {
               }
               .width('100%')
               .padding(16)
-              .backgroundColor(this.isDarkMode ? '#2E3033' : '#F1F3F5')
+              .backgroundColor($r('app.color.bg_card'))
               .borderRadius(8)
               .onClick(() => {
                 this.enterBrowseFolder(folder);
@@ -1741,7 +1741,7 @@ export struct UploadMusicPage {
         .width('100%')
         .layoutWeight(1)
         .padding(16)
-        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+        .backgroundColor($r('app.color.start_window_background'))
         .divider({
           strokeWidth: 1,
           color: this.isDarkMode ? '#19FFFFFF' : '#0C000000'
@@ -1754,7 +1754,7 @@ export struct UploadMusicPage {
           .fontSize(14)
           .height(48)
           .layoutWeight(1)
-          .backgroundColor(this.isDarkMode ? '#2E3033' : '#E5E5EA')
+          .backgroundColor($r('app.color.bg_card'))
           .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
           .borderRadius(8)
           .onClick(() => {
@@ -1842,8 +1842,8 @@ export struct UploadMusicPage {
               .width('100%')
               .padding(8)
               .backgroundColor(this.selectedAccount && this.selectedAccount.id === account.id
-                ? (this.isDarkMode ? '#2E3238' : '#FFFFFF')
-                : (this.isDarkMode ? '#2E3238' : '#F5F7FA'))
+                ? $r('app.color.start_window_background')
+                : $r('app.color.bg_card'))
               .borderRadius(10)
               .border({
                 width: this.selectedAccount && this.selectedAccount.id === account.id ? 2 : 1,
@@ -1862,7 +1862,7 @@ export struct UploadMusicPage {
     }
     .width('100%')
     .padding(10)
-    .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+    .backgroundColor($r('app.color.start_window_background'))
     .borderRadius(12)
     .shadow({
       radius: this.isDarkMode ? 8 : 12,
@@ -1975,7 +1975,7 @@ export struct UploadMusicPage {
         }
         .width('100%')
         .padding(12)
-        .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
+        .backgroundColor($r('app.color.bg_card'))
         .borderRadius(8)
         .border({
           width: 1,
@@ -1985,7 +1985,7 @@ export struct UploadMusicPage {
     }
     .width('100%')
     .padding(16)
-    .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+    .backgroundColor($r('app.color.start_window_background'))
     .borderRadius(12)
     .shadow({
       radius: this.isDarkMode ? 8 : 12,
@@ -2074,7 +2074,7 @@ export struct UploadMusicPage {
             }
             .width('100%')
             .padding(12)
-            .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
+            .backgroundColor($r('app.color.bg_card'))
             .borderRadius(8)
             .border({
               width: 1,
@@ -2088,7 +2088,7 @@ export struct UploadMusicPage {
     }
     .width('100%')
     .padding(16)
-    .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+    .backgroundColor($r('app.color.start_window_background'))
     .borderRadius(12)
     .shadow({
       radius: this.isDarkMode ? 8 : 12,
@@ -2145,11 +2145,11 @@ export struct UploadMusicPage {
       // 底部安全区
       Row()
         .height(this.bottomSafeHeight)
-        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+        .backgroundColor($r('app.color.start_window_background'))
     }
     .width('100%')
     .height('100%')
-    .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5')
+    .backgroundColor($r('app.color.index_background'))
     .bindSheet($$this.isShowFileBrowser, this.FileBrowserDialog(), {
       height: '95%',
       dragBar: true,
@@ -2242,7 +2242,7 @@ export struct UploadMusicPage {
       }
       .width('100%')
       .padding(16)
-      .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5')
+      .backgroundColor($r('app.color.index_background'))
 
       // 文件列表
       if (this.isLoadingFiles) {
@@ -2259,7 +2259,7 @@ export struct UploadMusicPage {
         .width('100%')
         .height(400)
         .justifyContent(FlexAlign.Center)
-        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+        .backgroundColor($r('app.color.start_window_background'))
       } else if (this.browserFiles.length === 0) {
         Column({ space: 12 }) {
           SymbolGlyph($r('sys.symbol.folder'))
@@ -2272,7 +2272,7 @@ export struct UploadMusicPage {
         .width('100%')
         .height(400)
         .justifyContent(FlexAlign.Center)
-        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+        .backgroundColor($r('app.color.start_window_background'))
       } else {
         List({ space: 8 }) {
           ForEach(this.browserFiles, (item: LocalFileItem, index: number) => {
@@ -2322,8 +2322,8 @@ export struct UploadMusicPage {
               .width('100%')
               .padding(12)
               .backgroundColor(this.selectedBrowserItems.has(item.path) ?
-                (this.isDarkMode ? '#2E3238' : '#FFFFFF') :
-                (this.isDarkMode ? '#2E3238' : '#F5F7FA'))
+                $r('app.color.start_window_background') :
+                $r('app.color.bg_card'))
               .borderRadius(8)
               .border({
                 width: this.selectedBrowserItems.has(item.path) ? 2 : 1,
@@ -2342,7 +2342,7 @@ export struct UploadMusicPage {
         .width('100%')
         .layoutWeight(1)
         .padding(16)
-        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+        .backgroundColor($r('app.color.start_window_background'))
       }
 
       // 底部按钮
@@ -2356,7 +2356,7 @@ export struct UploadMusicPage {
           .fontSize(14)
           .height(48)
           .padding({ left: 30, right: 30 })
-          .backgroundColor(this.isDarkMode ? '#2E3238' : '#E5E5EA')
+          .backgroundColor($r('app.color.bg_card'))
           .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
           .borderRadius(8)
           .onClick(() => {

+ 872 - 28
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -3,11 +3,10 @@ import { WebDavAccount } from '../viewmodel/WebDavAccount';
 import { Song } from '../viewmodel/Song';
 import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates';
 import Logger from '../common/util/Logger';
-import { promptAction, SymbolGlyphModifier, router, window, curves } from '@kit.ArkUI';
+import { SymbolGlyphModifier, router, curves } from '@kit.ArkUI';
 import { CommonConstants } from '../common/constants/CommonConstants';
 import { VideoItem } from '../viewmodel/VideoItem';
 import { GlobalContext } from '../common/util/GlobalContext';
-import { display } from '@kit.ArkUI';
 import { FileInfo } from '../viewmodel/FileInfo';
 import { emitter } from '@kit.BasicServicesKit';
 import { EventConstants } from '../common/constants/EventConstants';
@@ -26,8 +25,15 @@ import { CreateFolderDialog } from '../dialog/CreateFolderDialog';
 import { UploadMusicPage } from './UploadMusicPage';
 import { FFmpeg } from '@sj/ffmpeg';
 import FileManager from '../common/util/FileManager';
-import { fileIo, fileUri } from '@kit.CoreFileKit';
+import { fileIo, fileUri, picker } from '@kit.CoreFileKit';
 import { setVideoUrlForSong } from '../common/util/RemotePlayerUtil';
+import { showAddToPlaylistDialog } from '../dialog/AddToPlaylistDialog';
+import { showCreatePlaylistDialog } from '../dialog/PlaylistDialog';
+import { Playlist } from '../viewmodel/Playlist';
+import PlaylistTable from '../common/util/PlaylistTable';
+import MediaTable from '../common/util/MediaTable';
+import { DownloadCenterManager } from '../common/util/DownloadCenterManager';
+import { DownloadCenter } from '../view/DownloadCenter';
 
 /**
  * 歌单播放事件数据
@@ -133,9 +139,15 @@ export struct WebDavMainPage {
   @State selectedSongs: VideoItem[] = []
   @State isAllSelected: boolean = false
   @State isDeletingSelection: boolean = false
+  @State ignoreTapAfterExitMultiSelect: boolean = false
+  @State rootPath: string = ''
+  @State isShowDownloadCenter: boolean = false
+  @State downloadCenterTabIndex: number[] = [0]
+  private readonly downloadFolderName: string = '下载'
   private readonly metadataUpdateEvent: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED };
   private thumbnailTaskToken: number = 0;
   private thumbnailRunningKeys: Set<string> = new Set<string>();
+  private readonly downloadCenterManager: DownloadCenterManager = DownloadCenterManager.getInstance();
 
   private async runSerializedThumbnailFfmpeg(task: () => Promise<void>): Promise<void> {
     const nextTask: Promise<void> = globalRemoteThumbFfmpegQueue.then(async (): Promise<void> => {
@@ -659,6 +671,9 @@ export struct WebDavMainPage {
   }
 
   private handleCheckboxSelection(song: VideoItem, checked: boolean): void {
+    if (this.ignoreTapAfterExitMultiSelect) {
+      return;
+    }
     if (!this.isMultiSelect) {
       this.isMultiSelect = true;
     }
@@ -684,10 +699,15 @@ export struct WebDavMainPage {
   }
 
   private exitMultiSelect(): void {
+    this.ignoreTapAfterExitMultiSelect = true;
     this.isMultiSelect = false;
     this.selectedSongs = [];
     this.isAllSelected = false;
-    console.info('onecold this.isMultiSelect '+this.isMultiSelect)
+    setTimeout(() => {
+      this.isMultiSelect = false;
+      this.ignoreTapAfterExitMultiSelect = false;
+    }, 180);
+    console.info('onecold this.isMultiSelect ' + this.isMultiSelect)
   }
 
   private syncSelectionAfterRefresh(): void {
@@ -750,6 +770,669 @@ export struct WebDavMainPage {
     }
   }
 
+  private confirmDeleteSingleSong(song: VideoItem): void {
+    if (!this.selectedAccount) {
+      this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
+      return;
+    }
+    this.getUIContext().showAlertDialog({
+      title: '删除文件',
+      message: `确定删除 "${song.name}" 吗?`,
+      primaryButton: {
+        value: '取消',
+        action: () => {}
+      },
+      secondaryButton: {
+        value: '删除',
+        fontColor: Color.Red,
+        action: () => {
+          void this.performDeleteSingleSong(song);
+        }
+      }
+    });
+  }
+
+  private async performDeleteSingleSong(song: VideoItem): Promise<void> {
+    if (!this.selectedAccount) {
+      return;
+    }
+    this.isDeletingSelection = true;
+    try {
+      await this.webdavManager.deleteRemoteSongs(this.selectedAccount, [song]);
+      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
+      this.getUIContext().getPromptAction().showToast({ message: '删除成功' });
+    } catch (error) {
+      const err = error as Error;
+      this.getUIContext().getPromptAction().showToast({ message: `删除失败: ${err.message}` });
+    } finally {
+      this.isDeletingSelection = false;
+    }
+  }
+
+  private addSongToNextPlay(song: VideoItem): void {
+    if (!song) {
+      return;
+    }
+    const queue: VideoItem[] = globalWebdavVideoItems && globalWebdavVideoItems.length > 0 ?
+      globalWebdavVideoItems : this.songs;
+    if (!queue || queue.length === 0) {
+      this.getUIContext().getPromptAction().showToast({ message: '当前列表为空' });
+      return;
+    }
+
+    const currentPath = this.currentSong?.filePath;
+    if (StrUtil.isEmpty(currentPath)) {
+      const targetIndex = this.songs.findIndex((item: VideoItem) => item.filePath === song.filePath);
+      if (targetIndex >= 0) {
+        this.playSong(song, targetIndex, false);
+        this.getUIContext().getPromptAction().showToast({ message: '已开始播放' });
+      }
+      return;
+    }
+
+    if (currentPath === song.filePath) {
+      this.getUIContext().getPromptAction().showToast({ message: '当前正在播放这首歌' });
+      return;
+    }
+
+    let currentIndex = queue.findIndex((item: VideoItem) => item.filePath === currentPath);
+    if (currentIndex < 0 && globalWebdavCurrentPlayIndex >= 0 && globalWebdavCurrentPlayIndex < queue.length) {
+      currentIndex = globalWebdavCurrentPlayIndex;
+    }
+    if (currentIndex < 0) {
+      currentIndex = 0;
+    }
+
+    const existIndex = queue.findIndex((item: VideoItem) => item.filePath === song.filePath);
+    if (existIndex >= 0) {
+      const movedSong = queue.splice(existIndex, 1)[0];
+      if (existIndex < currentIndex) {
+        currentIndex -= 1;
+      }
+      queue.splice(currentIndex + 1, 0, movedSong);
+    } else {
+      queue.splice(currentIndex + 1, 0, song);
+    }
+
+    globalWebdavCurrentPlayIndex = currentIndex;
+    globalWebdavVideoItems = queue;
+    this.songs = [...queue];
+    this.dataSource.pushArrayData(this.songs);
+    const eventQueueRefresh: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAY_QUEUE_REFRESH };
+    emitter.emit(eventQueueRefresh, { data: { source: 'webdav' } });
+    this.getUIContext().getPromptAction().showToast({ message: '已添加到下一首播放' });
+  }
+
+  private cloneSongForDb(song: VideoItem): VideoItem {
+    const dbSong = new VideoItem(
+      song.name || '',
+      song.id || song.filePath,
+      song.filePath,
+      song.type,
+      song.videoSize || 0,
+      song.cTime || Utility.getFormatDateStr(new Date(), 'yyyy-MM-dd HH:mm')
+    );
+    dbSong.parentPath = song.parentPath;
+    dbSong.isFav = song.isFav;
+    dbSong.size = song.size;
+    dbSong.pixelMapPath = song.pixelMapPath;
+    dbSong.artist = song.artist;
+    dbSong.album = song.album;
+    dbSong.fileName = song.fileName;
+    dbSong.duration = song.duration;
+    dbSong.mimeType = song.mimeType;
+    dbSong.sampleRate = song.sampleRate;
+    dbSong.trackCount = song.trackCount;
+    dbSong.lastPlayedStr = song.lastPlayedStr;
+    dbSong.playCount = song.playCount;
+    dbSong.lyricContent = song.lyricContent;
+    dbSong.md5Str = song.md5Str;
+    dbSong.extra_json = song.extra_json;
+    dbSong.pyStr = song.pyStr;
+    dbSong.bit_rate = song.bit_rate;
+    dbSong.probe_score = song.probe_score;
+    dbSong.year = song.year;
+    dbSong.nb_streams = song.nb_streams;
+    dbSong.nb_programs = song.nb_programs;
+    dbSong.genre = song.genre;
+    dbSong.track = song.track;
+    dbSong.bits_per_raw_sample = song.bits_per_raw_sample;
+    dbSong.channels = song.channels;
+    dbSong.channel_layout = song.channel_layout;
+    dbSong.start_time = song.start_time;
+    dbSong.ALBUMARTIST = song.ALBUMARTIST;
+    dbSong.COMPOSER = song.COMPOSER;
+    dbSong.LYRICIST = song.LYRICIST;
+    dbSong.COMMENT = song.COMMENT;
+    dbSong.disc = song.disc;
+    dbSong.webdav_account_id = song.webdav_account_id;
+    dbSong.remote_rel_path = song.remote_rel_path;
+    dbSong.navArtistId = song.navArtistId;
+    dbSong.navAlbumId = song.navAlbumId;
+    dbSong.baiduFsId = song.baiduFsId;
+    dbSong.webdav_id = song.webdav_id;
+    dbSong.lyricIndex = song.lyricIndex;
+    return dbSong;
+  }
+
+  private emitPlaylistRefresh(): void {
+    emitter.emit({ eventId: EventConstants.EVENT_PLAYLIST_REFRESH }, {});
+  }
+
+  private buildPlaylistSongCandidates(songs: VideoItem[]): VideoItem[] {
+    if (!songs || songs.length <= 0) {
+      return [];
+    }
+    const result: VideoItem[] = [];
+    const existedPaths: Set<string> = new Set<string>();
+    for (let index: number = 0; index < songs.length; index += 1) {
+      const song = songs[index];
+      if (!song || StrUtil.isEmpty(song.filePath)) {
+        continue;
+      }
+      if (existedPaths.has(song.filePath)) {
+        continue;
+      }
+      existedPaths.add(song.filePath);
+      result.push(song);
+    }
+    return result;
+  }
+
+  private async resolvePlaylistSongPaths(mediaTable: MediaTable, songs: VideoItem[]): Promise<string[]> {
+    const paths: string[] = [];
+    for (let index: number = 0; index < songs.length; index += 1) {
+      const song: VideoItem = songs[index];
+      const songForDb: VideoItem = this.cloneSongForDb(song);
+      if (!songForDb.webdav_account_id && this.selectedAccount?.id) {
+        songForDb.webdav_account_id = this.selectedAccount.id.toString();
+      }
+      if (StrUtil.isEmpty(songForDb.id)) {
+        songForDb.id = songForDb.remote_rel_path || songForDb.filePath;
+      }
+      if (StrUtil.isEmpty(songForDb.remote_rel_path)) {
+        songForDb.remote_rel_path = songForDb.id || songForDb.filePath;
+      }
+      if (StrUtil.isEmpty(songForDb.parentPath) && songForDb.filePath.includes('/')) {
+        const pathIndex = songForDb.filePath.lastIndexOf('/');
+        if (pathIndex > 0) {
+          songForDb.parentPath = songForDb.filePath.substring(0, pathIndex);
+        }
+      }
+
+      const saved: boolean = await mediaTable.saveOrUpdateWebDavItem(songForDb);
+      if (!saved) {
+        Logger.warn(TAG, `歌曲入库失败,跳过加入歌单: ${songForDb.filePath}`);
+        continue;
+      }
+      const storedSong: VideoItem | null = await mediaTable.queryVideoByFilePath(songForDb.filePath);
+      if (storedSong && StrUtil.isNotEmpty(storedSong.filePath)) {
+        paths.push(storedSong.filePath);
+      }
+    }
+
+    const uniquePaths: string[] = [];
+    const pathSet: Set<string> = new Set<string>();
+    for (let index: number = 0; index < paths.length; index += 1) {
+      const path = paths[index];
+      if (StrUtil.isEmpty(path) || pathSet.has(path)) {
+        continue;
+      }
+      pathSet.add(path);
+      uniquePaths.push(path);
+    }
+    return uniquePaths;
+  }
+
+  private async openSongsAddToPlaylistDialog(songs: VideoItem[]): Promise<void> {
+    const candidateSongs: VideoItem[] = this.buildPlaylistSongCandidates(songs);
+    if (candidateSongs.length <= 0) {
+      this.getUIContext().getPromptAction().showToast({ message: '请选择要加入歌单的歌曲' });
+      return;
+    }
+
+    try {
+      const uiContext = this.getUIContext();
+      const hostCtx = uiContext ? uiContext.getHostContext() : undefined;
+      if (!hostCtx) {
+        this.getUIContext().getPromptAction().showToast({ message: '无法获取上下文' });
+        return;
+      }
+
+      const playlistTable: PlaylistTable = new PlaylistTable(hostCtx);
+      const mediaTable: MediaTable = new MediaTable(hostCtx);
+
+      await new Promise<void>((resolve) => {
+        mediaTable.getRdbStore(hostCtx, () => resolve());
+      });
+
+      const playlistSongPaths: string[] = await this.resolvePlaylistSongPaths(mediaTable, candidateSongs);
+      if (playlistSongPaths.length <= 0) {
+        this.getUIContext().getPromptAction().showToast({ message: '歌曲入库失败,无法添加到歌单' });
+        return;
+      }
+
+      const resolveSuccessToast = (): string => {
+        if (playlistSongPaths.length > 1) {
+          return `已添加${playlistSongPaths.length}首到歌单`;
+        }
+        return '已添加到歌单';
+      };
+
+      const openDialog = async (): Promise<void> => {
+        const playlists: Playlist[] = await playlistTable.queryAllPlaylists();
+        showAddToPlaylistDialog(
+          candidateSongs,
+          playlists,
+          async (playlistId: string) => {
+            const success = await playlistTable.addSongsToPlaylist(playlistId, playlistSongPaths);
+            if (success) {
+              this.emitPlaylistRefresh();
+            }
+            this.getUIContext().getPromptAction().showToast({ message: success ? resolveSuccessToast() : '歌曲已在该歌单中' });
+          },
+          () => {},
+          () => {
+            showCreatePlaylistDialog(
+              async (name: string, description: string, coverPath?: string) => {
+                const created = await playlistTable.createPlaylist(name, description, coverPath);
+                if (!created) {
+                  this.getUIContext().getPromptAction().showToast({ message: '歌单创建失败' });
+                  return;
+                }
+                const target = await playlistTable.queryPlaylistByName(name);
+                if (!target) {
+                  this.getUIContext().getPromptAction().showToast({ message: '未找到新建歌单' });
+                  return;
+                }
+                const addSuccess = await playlistTable.addSongsToPlaylist(target.id, playlistSongPaths);
+                if (addSuccess) {
+                  this.emitPlaylistRefresh();
+                }
+                this.getUIContext().getPromptAction().showToast({ message: addSuccess ? resolveSuccessToast() : '歌曲已在该歌单中' });
+              },
+              () => {}
+            );
+          }
+        );
+      };
+
+      await openDialog();
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `打开添加到歌单失败: ${err.message}`);
+      this.getUIContext().getPromptAction().showToast({ message: '添加到歌单失败' });
+    }
+  }
+
+  private async openSelectedSongsAddToPlaylistDialog(): Promise<void> {
+    if (!this.isMultiSelect || this.selectedSongs.length <= 0) {
+      this.getUIContext().getPromptAction().showToast({ message: '请先选择歌曲' });
+      return;
+    }
+    await this.openSongsAddToPlaylistDialog(this.selectedSongs.slice());
+  }
+
+  private async openSongAddToPlaylistDialog(song: VideoItem): Promise<void> {
+    if (!song) {
+      return;
+    }
+    await this.openSongsAddToPlaylistDialog([song]);
+  }
+
+  private sanitizeDownloadFileName(fileName: string): string {
+    const trimmed = (fileName || '').trim();
+    const sanitized = trimmed.replace(/[\\/:*?"<>|]/g, '_');
+    return sanitized.length > 0 ? sanitized : '未知歌曲.mp3';
+  }
+
+  private resolveDownloadFileName(song: VideoItem, sourceUrlOrPath: string): string {
+    if (StrUtil.isNotEmpty(song.fileName)) {
+      return this.sanitizeDownloadFileName(song.fileName as string);
+    }
+    if (StrUtil.isNotEmpty(song.name) && (song.name as string).includes('.')) {
+      return this.sanitizeDownloadFileName(song.name as string);
+    }
+    if (StrUtil.isNotEmpty(sourceUrlOrPath)) {
+      const purePath = sourceUrlOrPath.split('?')[0];
+      const lastSlash = purePath.lastIndexOf('/');
+      if (lastSlash >= 0 && lastSlash < purePath.length - 1) {
+        const urlName = purePath.substring(lastSlash + 1);
+        if (StrUtil.isNotEmpty(urlName)) {
+          return this.sanitizeDownloadFileName(urlName);
+        }
+      }
+    }
+    const baseName = StrUtil.isNotEmpty(song.name) ? song.name as string : '未知歌曲';
+    return this.sanitizeDownloadFileName(`${baseName}.mp3`);
+  }
+
+  private buildUniqueDownloadPath(downloadDir: string, fileName: string): string {
+    const safeName = this.sanitizeDownloadFileName(fileName);
+    const dotIndex = safeName.lastIndexOf('.');
+    const hasExt = dotIndex > 0;
+    const namePart = hasExt ? safeName.substring(0, dotIndex) : safeName;
+    const extPart = hasExt ? safeName.substring(dotIndex) : '';
+    let targetPath = `${downloadDir}/${safeName}`;
+    let suffix = 1;
+    while (FileUtil.accessSync(targetPath)) {
+      targetPath = `${downloadDir}/${namePart}(${suffix})${extPart}`;
+      suffix += 1;
+    }
+    return targetPath;
+  }
+
+  private async pickDownloadRootPath(): Promise<string> {
+    try {
+      const documentViewPicker = new picker.DocumentViewPicker();
+      const documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD });
+      if (!documentSaveResult || documentSaveResult.length === 0) {
+        return '';
+      }
+      const resolvedPath = new fileUri.FileUri(documentSaveResult[0]).path;
+      this.rootPath = resolvedPath;
+      return resolvedPath;
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `选择下载目录失败: ${err.message}`);
+      return '';
+    }
+  }
+
+  private async ensureDownloadDirectory(): Promise<string> {
+    let basePath = this.rootPath;
+    if (StrUtil.isEmpty(basePath) || !FileUtil.accessSync(basePath)) {
+      basePath = await this.pickDownloadRootPath();
+    }
+    if (StrUtil.isEmpty(basePath)) {
+      return '';
+    }
+    const downloadDir = `${basePath}/${this.downloadFolderName}`;
+    if (!FileUtil.accessSync(downloadDir)) {
+      await fileIo.mkdir(downloadDir);
+    }
+    return downloadDir;
+  }
+
+  private async upsertDownloadedSongToLibrary(targetPath: string, sourceSong: VideoItem): Promise<void> {
+    if (StrUtil.isEmpty(targetPath) || !FileUtil.accessSync(targetPath)) {
+      return;
+    }
+    try {
+      const pageContext = getContext(this);
+      const mediaModule = await import('../common/util/MediaTable');
+      const mediaTable = new mediaModule.default(pageContext);
+      await new Promise<void>((resolve) => {
+        mediaTable.getRdbStore(pageContext, () => resolve());
+      });
+
+      const exists = await mediaTable.isRecordExists(targetPath);
+      if (exists) {
+        return;
+      }
+
+      let mediaItem: VideoItem;
+      try {
+        mediaItem = await Utility.readMetaInfoFFmpeg(pageContext, targetPath, CommonConstants.TYPE_LOCAL, false);
+      } catch (error) {
+        Logger.warn(TAG, `下载歌曲读取元数据失败,使用兜底入库: ${(error as Error).message}`);
+        mediaItem = new VideoItem(
+          sourceSong.name || FileUtil.getFileName(targetPath),
+          targetPath,
+          targetPath,
+          CommonConstants.TYPE_LOCAL,
+          0,
+          Utility.getFormatDateStr(new Date(), 'yyyy-MM-dd HH:mm')
+        );
+        mediaItem.fileName = FileUtil.getFileName(targetPath);
+        mediaItem.artist = sourceSong.artist;
+        mediaItem.album = sourceSong.album;
+      }
+
+      mediaItem.id = targetPath;
+      mediaItem.filePath = targetPath;
+      mediaItem.type = CommonConstants.TYPE_LOCAL;
+      if (StrUtil.isEmpty(mediaItem.fileName)) {
+        mediaItem.fileName = FileUtil.getFileName(targetPath);
+      }
+      if (StrUtil.isEmpty(mediaItem.name)) {
+        mediaItem.name = mediaItem.fileName as string;
+      }
+      if (StrUtil.isEmpty(mediaItem.parentPath)) {
+        mediaItem.parentPath = FileUtil.getParentPath(targetPath);
+      }
+
+      await new Promise<void>((resolve) => {
+        mediaTable.insert(mediaItem, () => resolve());
+      });
+      emitter.emit({ eventId: EventConstants.EVENT_SCAN_UPDATE }, {});
+    } catch (error) {
+      Logger.error(TAG, `下载歌曲入库失败: ${(error as Error).message}`);
+    }
+  }
+
+  private openDownloadCenter(): void {
+    this.isShowDownloadCenter = true;
+  }
+
+  private resolveSongDownloadSizeText(song: VideoItem): string {
+    if (StrUtil.isNotEmpty(song.size)) {
+      return decodeUrlEncodedString(song.size as string);
+    }
+    if (song.videoSize > 0) {
+      return this.formatDownloadBytes(song.videoSize);
+    }
+    return '';
+  }
+
+  private parseSizeTextToBytes(sizeText: string): number {
+    if (StrUtil.isEmpty(sizeText)) {
+      return 0;
+    }
+    const normalized = sizeText.trim();
+    const match = normalized.match(/([0-9]+(?:\.[0-9]+)?)\s*(B|KB|MB|GB|TB)/i);
+    if (!match) {
+      return 0;
+    }
+    const value = parseFloat(match[1]);
+    if (!Number.isFinite(value) || value <= 0) {
+      return 0;
+    }
+    const unit = match[2].toUpperCase();
+    const factor = this.resolveUnitFactor(unit);
+    return Math.floor(value * factor);
+  }
+
+  private resolveUnitFactor(unit: string): number {
+    switch (unit) {
+      case 'KB':
+        return 1024;
+      case 'MB':
+        return 1024 * 1024;
+      case 'GB':
+        return 1024 * 1024 * 1024;
+      case 'TB':
+        return 1024 * 1024 * 1024 * 1024;
+      case 'B':
+      default:
+        return 1;
+    }
+  }
+
+  private formatDownloadBytes(bytes: number): string {
+    if (!Number.isFinite(bytes) || bytes <= 0) {
+      return '';
+    }
+    const units: string[] = ['B', 'KB', 'MB', 'GB', 'TB'];
+    let size: number = bytes;
+    let index: number = 0;
+    while (size >= 1024 && index < units.length - 1) {
+      size /= 1024;
+      index += 1;
+    }
+    const precision: number = index === 0 ? 0 : 2;
+    return `${size.toFixed(precision)} ${units[index]}`;
+  }
+
+  private pauseDownloadTask(taskId: string): void {
+    if (StrUtil.isEmpty(taskId)) {
+      return;
+    }
+    this.downloadCenterManager.pauseTask(taskId);
+  }
+
+  private resumeDownloadTask(taskId: string): void {
+    if (StrUtil.isEmpty(taskId)) {
+      return;
+    }
+    this.downloadCenterManager.resumeTask(taskId);
+  }
+
+  private async resolveSongDownloadSourceUrl(song: VideoItem): Promise<string> {
+    if (song.type === CommonConstants.TYPE_BAIDU && song.webdav_account_id) {
+      try {
+        const account = await this.webdavManager.getWebDavAccountById(song.webdav_account_id);
+        if (account) {
+          const dlink = await this.webdavManager.getBaiduDownloadUrl(account, song);
+          if (StrUtil.isNotEmpty(dlink)) {
+            Logger.info(TAG, `下载中心使用百度直链下载: ${song.name}`);
+            return dlink;
+          }
+        }
+      } catch (error) {
+        Logger.warn(TAG, `百度下载直链获取失败,回退播放地址: ${(error as Error).message}`);
+      }
+    }
+
+    return setVideoUrlForSong(song, {
+      context: getContext(this),
+      autoParseMusicName: false,
+      extractCover: false,
+      extractLyric: false,
+      extractAudioInfo: false
+    });
+  }
+
+  private async buildDownloadRequestHeaders(song: VideoItem, sourceUrl: string): Promise<Map<string, string>> {
+    if (song.type === CommonConstants.TYPE_BAIDU) {
+      const headers = new Map<string, string>();
+      // 参考 BaiduFileCache 下载链路,避免复用播放头导致 Range 行为异常
+      headers.set('User-Agent', 'pan.baidu.com');
+      headers.set('Accept', '*/*');
+      headers.set('Connection', 'Keep-Alive');
+      return headers;
+    }
+    return this.webdavManager.buildHttpHeadersWithAccountId(song, sourceUrl);
+  }
+
+  private async enqueueSongDownloadWithDirectory(song: VideoItem, downloadDir: string): Promise<string> {
+    if (!song) {
+      throw new Error('歌曲不存在');
+    }
+    if (StrUtil.isEmpty(downloadDir)) {
+      throw new Error('未选择下载目录');
+    }
+    const sourceUrl = await this.resolveSongDownloadSourceUrl(song);
+    const normalizedSource = sourceUrl.startsWith('file://') ? new fileUri.FileUri(sourceUrl).path : sourceUrl;
+    const fileName = this.resolveDownloadFileName(song, normalizedSource);
+    const targetPath = this.buildUniqueDownloadPath(downloadDir, fileName);
+    const sizeText = this.resolveSongDownloadSizeText(song);
+    const expectedBytes = this.parseSizeTextToBytes(sizeText);
+    const requestHeaders = await this.buildDownloadRequestHeaders(song, sourceUrl);
+    const coverPath = song.pixelMapPath ?? '';
+
+    this.downloadCenterManager.enqueueDownload({
+      title: song.name || fileName,
+      fileName: fileName,
+      coverPath: coverPath,
+      sizeText: sizeText,
+      expectedBytes: expectedBytes,
+      sourceUrl: sourceUrl,
+      targetPath: targetPath,
+      downloadDir: downloadDir,
+      headers: requestHeaders,
+      onCompleted: async () => {
+        await this.upsertDownloadedSongToLibrary(targetPath, song);
+      }
+    });
+    return fileName;
+  }
+
+  private async enqueueSongDownload(song: VideoItem): Promise<void> {
+    if (!song) {
+      return;
+    }
+    try {
+      const downloadDir = await this.ensureDownloadDirectory();
+      if (StrUtil.isEmpty(downloadDir)) {
+        this.getUIContext().getPromptAction().showToast({ message: '未选择下载目录' });
+        return;
+      }
+      const fileName: string = await this.enqueueSongDownloadWithDirectory(song, downloadDir);
+      this.openDownloadCenter();
+      this.getUIContext().getPromptAction().showToast({ message: `已加入下载队列: ${fileName}` });
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `加入下载队列失败: ${err.message}`);
+      this.getUIContext().getPromptAction().showToast({ message: `下载任务创建失败: ${err.message}` });
+    }
+  }
+
+  private async enqueueSelectedSongsDownload(): Promise<void> {
+    if (!this.isMultiSelect || this.selectedSongs.length <= 0) {
+      this.getUIContext().getPromptAction().showToast({ message: '请先选择要下载的歌曲' });
+      return;
+    }
+
+    const candidates: VideoItem[] = this.buildPlaylistSongCandidates(this.selectedSongs.slice());
+    if (candidates.length <= 0) {
+      this.getUIContext().getPromptAction().showToast({ message: '没有可下载的歌曲' });
+      return;
+    }
+
+    try {
+      const downloadDir = await this.ensureDownloadDirectory();
+      if (StrUtil.isEmpty(downloadDir)) {
+        this.getUIContext().getPromptAction().showToast({ message: '未选择下载目录' });
+        return;
+      }
+
+      let successCount: number = 0;
+      let failedCount: number = 0;
+
+      for (let index: number = 0; index < candidates.length; index += 1) {
+        const song: VideoItem = candidates[index];
+        try {
+          await this.enqueueSongDownloadWithDirectory(song, downloadDir);
+          successCount += 1;
+        } catch (error) {
+          failedCount += 1;
+          const err = error as Error;
+          Logger.error(TAG, `批量下载入队失败: ${(song.name || song.fileName || song.filePath)}, ${err.message}`);
+        }
+      }
+
+      if (successCount > 0) {
+        this.openDownloadCenter();
+        this.exitMultiSelect();
+      }
+
+      if (successCount > 0 && failedCount <= 0) {
+        this.getUIContext().getPromptAction().showToast({ message: `已加入下载队列: ${successCount}首` });
+        return;
+      }
+      if (successCount > 0) {
+        this.getUIContext().getPromptAction().showToast({ message: `已加入下载队列${successCount}首,失败${failedCount}首` });
+        return;
+      }
+      this.getUIContext().getPromptAction().showToast({ message: '下载任务创建失败' });
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `批量下载失败: ${err.message}`);
+      this.getUIContext().getPromptAction().showToast({ message: `批量下载失败: ${err.message}` });
+    }
+  }
+
   private buildSongMetaLine(song: VideoItem): string {
     const parts: string[] = [];
     if (StrUtil.isNotEmpty(song.duration)) {
@@ -1067,12 +1750,38 @@ export struct WebDavMainPage {
     this.updateListData(this.songs)
   }
 
+  private resolveSongPlayIndex(song: VideoItem, fallbackIndex: number): number {
+    if (this.songs.length <= 0) {
+      return -1;
+    }
+    if (song && StrUtil.isNotEmpty(song.filePath)) {
+      for (let i = 0; i < this.songs.length; i++) {
+        if (this.songs[i].filePath === song.filePath) {
+          return i;
+        }
+      }
+    }
+    if (fallbackIndex >= 0 && fallbackIndex < this.songs.length) {
+      return fallbackIndex;
+    }
+    return 0;
+  }
+
   // 播放WebDAV歌曲
   private playSong(song: VideoItem, index: number,isJump:boolean=false): void {
     try {
       Logger.info(TAG, `heanup === WebDAV playSong 方法被调用 ===`);
       Logger.info(TAG, 'heanup 播放指定歌曲: ' + song.name + ', 索引: ' + index);
       Logger.info(TAG, 'heanup 歌曲列表长度: ' + this.songs.length);
+      const playIndex = this.resolveSongPlayIndex(song, index);
+      if (playIndex < 0 || playIndex >= this.songs.length) {
+        this.getUIContext().getPromptAction().showToast({ message: '播放失败,未找到歌曲' });
+        Logger.warn(TAG, `heanup 无法定位播放索引: fallback=${index}, songPath=${song.filePath}`);
+        return;
+      }
+      if (playIndex !== index) {
+        Logger.info(TAG, `heanup 修正播放索引: fallback=${index}, real=${playIndex}, song=${song.name}`);
+      }
 
       // 检查歌曲是否有webdav_account_id
       Logger.info(TAG, `heanup 播放歌曲的webdav_account_id: ${song.webdav_account_id || '未设置'}`);
@@ -1107,21 +1816,21 @@ export struct WebDavMainPage {
         playlistId: 'webdav-playlist', // 使用特殊的ID标识网盘播放列表
         playlistName: `${getRemoteDriveDisplayLabel(this.selectedAccount?.webType)} - ${this.selectedAccount?.name || '未知账户'}`,
         songCount: this.songs.length,
-        startIndex: index,
+        startIndex: playIndex,
         isJump: isJump,//设置true会弹出播放页
         songFilePaths: songFilePaths
       };
 
       // 保存videoItems到全局内存
       globalWebdavVideoItems = videoItems;
-      globalWebdavCurrentPlayIndex = index;
+      globalWebdavCurrentPlayIndex = playIndex;
 
-      Logger.info(TAG, `heanup 保存WebDAV歌曲到全局内存,长度: ${videoItems.length}, 索引: ${index}`);
+      Logger.info(TAG, `heanup 保存WebDAV歌曲到全局内存,长度: ${videoItems.length}, 索引: ${playIndex}`);
 
       // 发送播放请求事件,只传递索引信息
       emitter.emit(eventPlaylistPlay, { data: playlistData });
 
-      Logger.info(TAG, `heanup 发送WebDAV播放事件,只传递索引: ${index}`);
+      Logger.info(TAG, `heanup 发送WebDAV播放事件,只传递索引: ${playIndex}`);
 
       if(!this.isNoJumpToHome){
         // 跳转到首页播放器
@@ -1291,6 +2000,14 @@ export struct WebDavMainPage {
           this.isShowUploadFile = !this.isShowUploadFile
         })
 
+      MenuItem({
+        symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.download')),
+        content: '下载中心'
+      })
+        .onClick(() => {
+          this.openDownloadCenter();
+        })
+
       MenuItem({
         symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.folder_badge_plus')),
         content: $r('app.string.create_foder')
@@ -1415,7 +2132,7 @@ export struct WebDavMainPage {
   topTitleBar(){
     Column() {
       Row({ space: 15 }) {
-        if (!this.isSearchMode ) {
+        if (!this.isSearchMode &&!(this.webdavManager.canGoBack())) {
           //左侧滑动按钮
           Button({ type: ButtonType.Circle, stateEffect: true }) {
             SymbolGlyph($r('sys.symbol.sort'))
@@ -1435,14 +2152,6 @@ export struct WebDavMainPage {
           .attributeModifier(new ShadowModifier())
           .zIndex(0)
 
-          Text(this.selectedAccount.name)
-            .margin({left:3,right:10})
-            .fontColor($r('app.color.text_color'))
-            .fontSize(19)
-            .maxLines(1)
-            .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动
-            .layoutWeight(1)
-            .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
         }else{
 
           //左侧搜索返回按钮
@@ -1454,14 +2163,32 @@ export struct WebDavMainPage {
           .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
           .animation({ duration: 300, curve: Curve.Ease })
           .onClick(() => {
-            this.isSearchMode = false
-            this.onSearchInput('')
+            if(this.isSearchMode){
+              this.isSearchMode = false
+              this.onSearchInput('')
+            }else{
+              if(this.webdavManager.canGoBack()){
+                this.goBack()
+              }
+            }
+
           })
           .attributeModifier(new ShadowModifier())
           .zIndex(0)
 
         }
 
+        if(!this.isSearchMode){
+          Text(this.selectedAccount.name)
+            .margin({left:3,right:10})
+            .fontColor($r('app.color.text_color'))
+            .fontSize(19)
+            .maxLines(1)
+            .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动
+            .layoutWeight(1)
+            .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
+        }
+
         //搜索框
         Search({ controller: this.searchController,value: this.searchText, placeholder: '搜索标题、艺术家...' })
           .searchButton('搜索',{fontColor:this.themeColor})
@@ -1570,6 +2297,30 @@ export struct WebDavMainPage {
     .width('100%')
     .height('100%')
   }
+
+  @Builder
+  DownloadCenterBuilder() {
+    DownloadCenter({
+      themeColor: this.themeColor,
+      isDarkMode: this.isDarkMode,
+      appName: this.appName,
+      topSafeHeight: this.topSafeHeight,
+      bottomSafeHeight: this.bottomSafeHeight,
+      selectedIndexes: $downloadCenterTabIndex,
+      onPauseTask: (taskId: string) => {
+        this.pauseDownloadTask(taskId);
+      },
+      onResumeTask: (taskId: string) => {
+        this.resumeDownloadTask(taskId);
+      },
+      onClose: () => {
+        this.isShowDownloadCenter = false;
+      }
+    })
+      .width('100%')
+      .height('100%')
+  }
+
   //搜索功能的实现
   @State searchText: string = ''; // 用户输入内容
   @State filteredList: Array<VideoItem> = []; // 过滤后的歌曲结果
@@ -1643,6 +2394,9 @@ export struct WebDavMainPage {
     .backgroundImagePosition(Alignment.Center)
     .backdropBlur(this.blurValue)
     .backgroundBrightness({rate:this.isCustomizeBg?0.1:0,lightUpDegree:this.bgBrightness})
+    .bindContentCover($$this.isShowDownloadCenter, this.DownloadCenterBuilder(), {
+      transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
+    })
     .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
   }
 
@@ -1654,8 +2408,9 @@ export struct WebDavMainPage {
       if (this.webdavManager.currentPath !== '') {
         Row({ space: 8 }) {
           Button({ type: ButtonType.Circle }) {
-            Image(this.webdavManager.canGoBack()?$r('app.media.back'):
-              this.selectedAccount.coverPath?this.selectedAccount.coverPath:getCloudDiskIcon(this.selectedAccount.webType))
+            // Image(this.webdavManager.canGoBack()?$r('app.media.back'):
+            //   this.selectedAccount.coverPath?this.selectedAccount.coverPath:getCloudDiskIcon(this.selectedAccount.webType))
+            Image(this.selectedAccount.coverPath)
               .width(15)
               .height(15)
               .borderRadius(10)
@@ -1757,8 +2512,6 @@ export struct WebDavMainPage {
             ListItem() {
               this.buildFolderItem(folder)
             }
-            // .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 600 }),
-            //   TransitionEffect.scale({ x: 0, y: 0 })))
             .clickEffect({ level: ClickEffectLevel.MIDDLE })
           }, (folder: FileInfo) =>  folder.name+folder.fileName)
 
@@ -1767,8 +2520,6 @@ export struct WebDavMainPage {
             ListItem() {
               this.buildSongItem(song, index)
             }
-            // .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 }).animation({ duration: 600 }),
-            //   TransitionEffect.scale({ x: 0, y: 0 })))
             .clickEffect({ level: ClickEffectLevel.MIDDLE })
           }, (item: VideoItem, index: number) =>  item.filePath + '_' + index+this.listRefreshKey)
         }
@@ -1833,6 +2584,28 @@ export struct WebDavMainPage {
         .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection)
         .onClick(() => this.confirmDeleteSelected())
 
+      Button('+歌单', { type: ButtonType.Circle, stateEffect: true })
+        .width(55)
+        .height(55)
+        .fontSize(13)
+        .backgroundColor(this.themeColor)
+        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
+        .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection)
+        .onClick(() => {
+          void this.openSelectedSongsAddToPlaylistDialog();
+        })
+
+      Button('下载', { type: ButtonType.Circle, stateEffect: true })
+        .width(55)
+        .height(55)
+        .fontSize(13)
+        .backgroundColor(this.themeColor)
+        .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
+        .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection)
+        .onClick(() => {
+          void this.enqueueSelectedSongsDownload();
+        })
+
       Button('取消', { type: ButtonType.Circle, stateEffect: true })
         .width(55)
         .height(55)
@@ -2006,15 +2779,86 @@ export struct WebDavMainPage {
     .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
     .backgroundColor(Color.Transparent)
     .onClick(() => {
+      if (this.ignoreTapAfterExitMultiSelect) {
+        return;
+      }
       if (this.isMultiSelect) {
         this.toggleSongSelection(song);
       } else {
         this.playSong(song, index);
       }
     })
-    .gesture(LongPressGesture().onAction(() => {
-      this.enterMultiSelect(song);
-    }))
+    // .gesture(LongPressGesture().onAction(() => {
+    //   this.enterMultiSelect(song);
+    // }))
+    .bindContextMenu(this.LongPressMenuBuilder(song), ResponseType.LongPress,
+      {
+        preview: MenuPreviewMode.IMAGE
+      })
+    .bindContextMenu(this.LongPressMenuBuilder(song), ResponseType.RightClick,
+      {
+        preview: MenuPreviewMode.IMAGE
+      })
+  }
+
+
+  @Builder
+  LongPressMenuBuilder(song: VideoItem) {
+
+    Scroll() {
+      Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
+        Menu() {
+
+          MenuItem({
+            symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.forward_end_fill')),
+            content: '下一首播放'
+          })
+            .onClick(() => {
+              this.addSongToNextPlay(song);
+            })
+
+          MenuItem({
+            symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music_note_list')),
+            content: '添加到歌单'
+          })
+            .onClick(async () => {
+              await this.openSongAddToPlaylistDialog(song);
+            })
+          //多选
+          MenuItem({
+            symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.checkmark_square_on_square')),
+            content: $r('app.string.select_all')
+          })
+            .onClick(() => {
+              this.enterMultiSelect(song);
+            })
+          //下载
+          MenuItem({
+            symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.download')),
+            content:  '下载'
+          })
+            .onClick(async () => {
+              await this.enqueueSongDownload(song);
+            })
+          //删除
+          MenuItem({
+            symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.trash')),
+            content: $r('app.string.delete')
+          })
+            .onClick(() => {
+              this.confirmDeleteSingleSong(song);
+            })
+
+
+        }
+        .font({ size: 14, weight: FontWeight.Normal })
+      }
+      .width(180)
+    }
+    .height('auto')
+    .enableScrollInteraction(true)
+    .scrollBar(BarState.Off)
+
   }
 
   

+ 500 - 0
entry/src/main/ets/view/DownloadCenter.ets

@@ -0,0 +1,500 @@
+import { SegmentButton, SegmentButtonItemTuple, SegmentButtonOptions } from '@kit.ArkUI';
+import { DownloadCenterManager, DownloadCenterTask } from '../common/util/DownloadCenterManager';
+import { CommonConstants } from '../common/constants/CommonConstants';
+import { StrUtil } from '@pura/harmony-utils';
+import { LazyDataSource } from '../common/util/LazyDataSource';
+
+@Component
+export struct DownloadCenter {
+  @Prop themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
+  @Prop isDarkMode: boolean = false;
+  @Prop appName: string = '';
+  @Prop topSafeHeight: number = 0;
+  @Prop bottomSafeHeight: number = 0;
+  @Link selectedIndexes: number[];
+  @State activeTaskCount: number = 0;
+  @State completedTaskCount: number = 0;
+  onClose: () => void = () => {};
+  onPauseTask: (taskId: string) => void = (_taskId: string): void => {};
+  onResumeTask: (taskId: string) => void = (_taskId: string): void => {};
+  private readonly downloadCenterManager: DownloadCenterManager = DownloadCenterManager.getInstance();
+  private readonly activeTaskDataSource: LazyDataSource<DownloadCenterTask> = new LazyDataSource<DownloadCenterTask>([]);
+  private readonly completedTaskDataSource: LazyDataSource<DownloadCenterTask> = new LazyDataSource<DownloadCenterTask>([]);
+  private refreshTimer: number = -1;
+  private readonly downloadCenterListener: () => void = (): void => {
+    this.syncTasksFromManager();
+  };
+
+  aboutToAppear(): void {
+    this.downloadCenterManager.subscribe(this.downloadCenterListener);
+    this.syncTasksFromManager();
+    this.startAutoRefresh();
+  }
+
+  aboutToDisappear(): void {
+    this.downloadCenterManager.unsubscribe(this.downloadCenterListener);
+    this.stopAutoRefresh();
+  }
+
+  build() {
+    Column({ space: 12 }) {
+      Row({ space: 10 }) {
+        Button({ type: ButtonType.Circle, stateEffect: true }) {
+          SymbolGlyph($r('sys.symbol.chevron_left'))
+            .fontSize(22)
+            .fontColor([this.themeColor])
+        }
+        .width(38)
+        .height(38)
+        .backgroundColor($r('app.color.bg_card'))
+        .onClick(() => this.onClose())
+
+        Text('下载中心')
+          .fontSize(20)
+          .fontWeight(FontWeight.Medium)
+          .fontColor($r('app.color.text_color'))
+          .layoutWeight(1)
+
+        Button({ type: ButtonType.Circle, stateEffect: true }) {
+          SymbolGlyph($r('sys.symbol.folder'))
+            .fontSize(20)
+            .fontColor([this.themeColor])
+        }
+          .width(38)
+          .height(38)
+          .backgroundColor($r('app.color.bg_card'))
+          .onClick(() => {
+            this.showDownloadDirectoryDialog();
+          })
+
+      }
+      .width('100%')
+
+      SegmentButton({
+        options: SegmentButtonOptions.capsule({
+          buttons: [{ text: '下载中' }, { text: '已完成' }] as SegmentButtonItemTuple,
+          backgroundColor: $r('app.color.index_background'),
+          selectedBackgroundColor: $r('app.color.start_window_background'),
+          selectedFontColor: $r('app.color.text_color'),
+          buttonPadding: { top: 10, bottom: 10 },
+          multiply: false
+        }),
+        selectedIndexes: $selectedIndexes
+      })
+        .width('100%')
+
+      Column() {
+        this.taskListBuilder()
+      }
+      .width('100%')
+      .layoutWeight(1)
+    }
+    .width('100%')
+    .height('100%')
+    .padding({ top: this.topSafeHeight + 10, left: 12, right: 12, bottom: this.bottomSafeHeight + 12 })
+    .backgroundColor($r('app.color.index_background'))
+  }
+
+  private syncTasksFromManager(): void {
+    this.updateActiveTasks(this.downloadCenterManager.getActiveTasks());
+    this.updateCompletedTasks(this.downloadCenterManager.getCompletedTasks());
+  }
+
+  private updateActiveTasks(tasks: DownloadCenterTask[]): void {
+    this.updateTaskDataSourceByDiff(this.activeTaskDataSource, tasks);
+    this.activeTaskCount = this.activeTaskDataSource.totalCount();
+  }
+
+  private updateCompletedTasks(tasks: DownloadCenterTask[]): void {
+    this.updateTaskDataSourceByDiff(this.completedTaskDataSource, tasks);
+    this.completedTaskCount = this.completedTaskDataSource.totalCount();
+  }
+
+  private updateTaskDataSourceByDiff(dataSource: LazyDataSource<DownloadCenterTask>, tasks: DownloadCenterTask[]): void {
+    const nextTasks: DownloadCenterTask[] = tasks ? tasks : [];
+    const currentTasks: DownloadCenterTask[] = dataSource.dataArray;
+
+    let structureChanged: boolean = currentTasks.length !== nextTasks.length;
+    if (!structureChanged) {
+      for (let index: number = 0; index < nextTasks.length; index += 1) {
+        if (currentTasks[index].taskId !== nextTasks[index].taskId) {
+          structureChanged = true;
+          break;
+        }
+      }
+    }
+
+    if (structureChanged) {
+      const replacedTasks: DownloadCenterTask[] = nextTasks.map((task: DownloadCenterTask): DownloadCenterTask => {
+        return this.cloneTask(task);
+      });
+      dataSource.pushArrayData(replacedTasks);
+      return;
+    }
+
+    for (let index: number = 0; index < nextTasks.length; index += 1) {
+      const currentTask: DownloadCenterTask = currentTasks[index];
+      const nextTask: DownloadCenterTask = nextTasks[index];
+      if (this.isTaskEqual(currentTask, nextTask)) {
+        continue;
+      }
+      dataSource.dataArray[index] = this.cloneTask(nextTask);
+      dataSource.notifyDataChange(index);
+    }
+  }
+
+  @Builder
+  private taskListBuilder() {
+    if (this.getCurrentTabIndex() === 0) {
+      if (this.activeTaskCount <= 0) {
+        Column() {
+          Text('暂无下载任务')
+            .fontSize(14)
+            .fontColor($r('app.color.text_color'))
+            .opacity(0.55)
+        }
+        .width('100%')
+        .height('100%')
+        .justifyContent(FlexAlign.Center)
+      } else {
+        List({ space: 10 }) {
+          LazyForEach(this.activeTaskDataSource, (task: DownloadCenterTask) => {
+            this.taskListItemBuilder(task, true)
+          }, (task: DownloadCenterTask): string => {
+            return task.taskId;
+          })
+        }
+        .scrollBar(BarState.Off)
+        .height('100%')
+        .width('100%')
+      }
+    } else {
+      if (this.completedTaskCount <= 0) {
+        Column() {
+          Text('暂无历史下载记录')
+            .fontSize(14)
+            .fontColor($r('app.color.text_color'))
+            .opacity(0.55)
+        }
+        .width('100%')
+        .height('100%')
+        .justifyContent(FlexAlign.Center)
+      } else {
+        List({ space: 10 }) {
+          LazyForEach(this.completedTaskDataSource, (task: DownloadCenterTask) => {
+            this.taskListItemBuilder(task, false)
+          }, (task: DownloadCenterTask): string => {
+            return task.taskId;
+          })
+        }
+        .scrollBar(BarState.Off)
+        .height('100%')
+        .width('100%')
+      }
+    }
+  }
+
+  @Builder
+  private taskListItemBuilder(task: DownloadCenterTask, isDownloading: boolean) {
+    ListItem() {
+      Column({ space: 8 }) {
+        Row({ space: 10 }) {
+          Image(StrUtil.isNotEmpty(task.coverPath) ? task.coverPath : $r('app.media.alt'))
+            .width(54)
+            .height(54)
+            .borderRadius(9)
+            .sourceSize({ width: 38, height: 38 })
+            .alt($r('app.media.alt'))
+            .fillColor(this.themeColor)
+            .objectFit(ImageFit.Cover)
+
+          Column({ space: 4 }) {
+            Text(task.title)
+              .fontSize(14)
+              .fontWeight(FontWeight.Medium)
+              .fontColor($r('app.color.text_color'))
+              .maxLines(1)
+              .textOverflow({ overflow: TextOverflow.Ellipsis })
+
+            Row() {
+            Text(this.getTaskTotalSizeText(task))
+                .fontSize(12)
+                .fontColor($r('app.color.text_color'))
+                .opacity(0.6)
+              Text('  ·  ')
+                .fontSize(12)
+                .fontColor($r('app.color.text_color'))
+                .opacity(0.35)
+              Text(isDownloading ? this.getTaskStatusText(task) : '已完成')
+                .fontSize(12)
+                .fontColor(this.getTaskStatusColor(task))
+                .opacity(0.85)
+            }
+            .width('100%')
+          }
+          .alignItems(HorizontalAlign.Start)
+          .layoutWeight(1)
+
+          if (isDownloading) {
+            this.taskActionButtonBuilder(task)
+          }
+        }
+        .width('100%')
+
+        if (isDownloading) {
+          Progress({ value: task.progress, total: 100, type: ProgressType.Linear })
+            .width('100%')
+            .color(this.getTaskStatusColor(task))
+            .backgroundColor($r('app.color.track_color'))
+            .style({ strokeWidth: 5 })
+
+          Row() {
+            Text(this.getTaskProgressLabelText(task))
+              .fontSize(12)
+              .fontColor(this.getTaskStatusColor(task))
+              .maxLines(1)
+              .textOverflow({ overflow: TextOverflow.Ellipsis })
+              .layoutWeight(1)
+            Text(this.getTaskSpeedText(task))
+              .fontSize(12)
+              .fontColor($r('app.color.text_color'))
+              .opacity(0.75)
+              .margin({ right: 6 })
+            Text(this.getTaskProgressInfo(task))
+              .fontSize(12)
+              .fontColor($r('app.color.text_color'))
+              .opacity(0.6)
+          }
+          .width('100%')
+        }
+        // else {
+        //   Text(this.getTaskProgressInfo(task))
+        //     .fontSize(12)
+        //     .fontColor($r('app.color.text_color'))
+        //     .opacity(0.6)
+        //     .width('100%')
+        // }
+      }
+      .width('100%')
+      .padding(12)
+      .backgroundColor($r('app.color.start_window_background'))
+      .borderRadius(10)
+    }
+  }
+
+  private startAutoRefresh(): void {
+    this.stopAutoRefresh();
+    this.refreshTimer = setInterval(() => {
+      this.syncTasksFromManager();
+    }, 120);
+  }
+
+  private stopAutoRefresh(): void {
+    if (this.refreshTimer >= 0) {
+      clearInterval(this.refreshTimer);
+      this.refreshTimer = -1;
+    }
+  }
+
+  private showDownloadDirectoryDialog(): void {
+    this.getUIContext().showAlertDialog({
+      title: '下载目录说明',
+      message: this.buildDownloadDirectoryMessage(),
+      primaryButton: {
+        value: '知道了',
+        action: () => {}
+      }
+    });
+  }
+
+  private buildDownloadDirectoryMessage(): string {
+    const appNameText: string = StrUtil.isNotEmpty(this.appName) ? this.appName : '本应用';
+    const expectedPath: string = `DownLoad/${appNameText}/下载`;
+    const currentDir: string = this.resolveCurrentDownloadDirectory();
+    if (StrUtil.isNotEmpty(currentDir)) {
+      return `下载完成后会保存到系统DownLoad目录下的应用文件夹,再进入下载文件夹。\n默认结构:${expectedPath}\n\n当前下载文件夹路径:\n${currentDir}`;
+    }
+    return `下载完成后会保存到系统DownLoad目录下的应用文件夹,再进入下载文件夹。\n默认结构:${expectedPath}\n\n当前暂无下载任务,开始下载后会显示实际下载路径。`;
+  }
+
+  private resolveCurrentDownloadDirectory(): string {
+    const activeTasks: DownloadCenterTask[] = this.activeTaskDataSource.dataArray;
+    for (let i = 0; i < activeTasks.length; i += 1) {
+      const task: DownloadCenterTask = activeTasks[i];
+      if (StrUtil.isNotEmpty(task.downloadDir)) {
+        return task.downloadDir;
+      }
+    }
+    const completedTasks: DownloadCenterTask[] = this.completedTaskDataSource.dataArray;
+    for (let i = 0; i < completedTasks.length; i += 1) {
+      const task: DownloadCenterTask = completedTasks[i];
+      if (StrUtil.isNotEmpty(task.downloadDir)) {
+        return task.downloadDir;
+      }
+    }
+    return '';
+  }
+
+  @Builder
+  private taskActionButtonBuilder(task: DownloadCenterTask) {
+    Button(this.getTaskActionText(task))
+      .fontSize(12)
+      .fontColor(this.isTaskRunning(task) ? $r('app.color.text_color') : $r('app.color.start_window_background'))
+      .height(30)
+      .padding({ left: 14, right: 14, top: 0, bottom: 0 })
+      .backgroundColor(this.isTaskRunning(task) ? $r('app.color.bg_card') : this.themeColor)
+      .borderRadius(15)
+      .onClick(() => {
+        if (this.isTaskRunning(task)) {
+          this.onPauseTask(task.taskId);
+          return;
+        }
+        this.onResumeTask(task.taskId);
+      })
+  }
+
+  private isTaskRunning(task: DownloadCenterTask): boolean {
+    return task.status === 'downloading';
+  }
+
+  private getTaskActionText(task: DownloadCenterTask): string {
+    return this.isTaskRunning(task) ? '暂停' : '开始';
+  }
+
+  private getTaskTotalSizeText(task: DownloadCenterTask): string {
+    if (task.totalBytes > 0) {
+      return this.formatBytes(task.totalBytes);
+    }
+    return StrUtil.isNotEmpty(task.sizeText) ? task.sizeText : '--';
+  }
+
+  private getTaskProgressInfo(task: DownloadCenterTask): string {
+    const downloadedText: string = task.downloadedBytes > 0 ? this.formatBytes(task.downloadedBytes) : '0 B';
+    const totalText: string = this.getTaskTotalSizeText(task);
+    return `${downloadedText} / ${totalText}`;
+  }
+
+  private getTaskProgressLabel(task: DownloadCenterTask): string {
+    const value = task.progress;
+    const rounded = Math.round(value * 10) / 10;
+    const isInt = Math.abs(rounded - Math.round(rounded)) < 0.001;
+    return isInt ? `${Math.round(rounded)}%` : `${rounded.toFixed(1)}%`;
+  }
+
+  private getTaskProgressLabelText(task: DownloadCenterTask): string {
+    if (task.status === 'failed') {
+      return StrUtil.isNotEmpty(task.errorMessage) ? task.errorMessage : '下载失败';
+    }
+    if (task.status === 'paused') {
+      return `已暂停 ${this.getTaskProgressLabel(task)}`;
+    }
+    if (task.status === 'pending') {
+      return `等待中 ${this.getTaskProgressLabel(task)}`;
+    }
+    return this.getTaskProgressLabel(task);
+  }
+
+  private getTaskSpeedText(task: DownloadCenterTask): string {
+    if (task.status === 'failed') {
+      return '--';
+    }
+    if (task.status === 'paused') {
+      return '已暂停';
+    }
+    if (task.status === 'pending') {
+      return '等待中';
+    }
+    if (task.speedBytesPerSec > 0) {
+      return `${this.formatBytes(task.speedBytesPerSec)}/s`;
+    }
+    return '0 B/s';
+  }
+
+  private getTaskStatusText(task: DownloadCenterTask): string {
+    switch (task.status) {
+      case 'downloading':
+        return '下载中';
+      case 'paused':
+        return '已暂停';
+      case 'failed':
+        return '下载失败';
+      case 'pending':
+        return '等待中';
+      case 'completed':
+      default:
+        return '已完成';
+    }
+  }
+
+  private getTaskStatusColor(task: DownloadCenterTask) {
+    if (task.status === 'failed') {
+      return $r('app.color.btn_red');
+    }
+    if (task.status === 'paused' || task.status === 'pending') {
+      return $r('app.color.text_color');
+    }
+    return this.themeColor;
+  }
+
+  private isTaskEqual(left: DownloadCenterTask, right: DownloadCenterTask): boolean {
+    return left.taskId === right.taskId
+      && left.title === right.title
+      && left.fileName === right.fileName
+      && left.coverPath === right.coverPath
+      && left.sizeText === right.sizeText
+      && left.sourceUrl === right.sourceUrl
+      && left.targetPath === right.targetPath
+      && left.downloadDir === right.downloadDir
+      && left.totalBytes === right.totalBytes
+      && left.downloadedBytes === right.downloadedBytes
+      && left.progress === right.progress
+      && left.speedBytesPerSec === right.speedBytesPerSec
+      && left.status === right.status
+      && left.errorMessage === right.errorMessage
+      && left.createdAt === right.createdAt
+      && left.finishedAt === right.finishedAt;
+  }
+
+  private cloneTask(task: DownloadCenterTask): DownloadCenterTask {
+    return {
+      taskId: task.taskId,
+      title: task.title,
+      fileName: task.fileName,
+      coverPath: task.coverPath,
+      sizeText: task.sizeText,
+      sourceUrl: task.sourceUrl,
+      targetPath: task.targetPath,
+      downloadDir: task.downloadDir,
+      totalBytes: task.totalBytes,
+      downloadedBytes: task.downloadedBytes,
+      progress: task.progress,
+      speedBytesPerSec: task.speedBytesPerSec,
+      status: task.status,
+      errorMessage: task.errorMessage,
+      createdAt: task.createdAt,
+      finishedAt: task.finishedAt
+    };
+  }
+
+  private formatBytes(bytes: number): string {
+    if (!Number.isFinite(bytes) || bytes <= 0) {
+      return '';
+    }
+    const units: string[] = ['B', 'KB', 'MB', 'GB', 'TB'];
+    let size: number = bytes;
+    let index: number = 0;
+    while (size >= 1024 && index < units.length - 1) {
+      size /= 1024;
+      index += 1;
+    }
+    const precision: number = index === 0 ? 0 : 2;
+    return `${size.toFixed(precision)} ${units[index]}`;
+  }
+
+  private getCurrentTabIndex(): number {
+    if (!this.selectedIndexes || this.selectedIndexes.length <= 0) {
+      return 0;
+    }
+    return this.selectedIndexes[0];
+  }
+}

+ 213 - 64
entry/src/main/ets/view/LocalMusic.ets

@@ -489,6 +489,7 @@ export struct LocalMusic {
   @State isShowTitleBar: boolean = true //是否显示分类导航条
   @State isShowAllBar: boolean = true //是否显示播放全部条
   @State isShowSingleLineLyric: boolean = false //是否显示单行歌词
+  @State isEnableWordByWordLyric: boolean = true //是否开启逐字歌词
   @State isShowHistory: boolean = true //是否显示最近播放
   @State @Watch('onColorModeChange') isCustomizeBg: boolean = false //自定义背景界面
   @State isGridMusic: boolean = false //是否网格布局
@@ -1045,6 +1046,11 @@ export struct LocalMusic {
 
     });
 
+    let eventQueueRefresh: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAY_QUEUE_REFRESH }
+    emitter.on(eventQueueRefresh, () => {
+      this.syncWebdavQueueWithoutRestart();
+    });
+
     let eventSetting: emitter.InnerEvent = { eventId: EventConstants.EVENT_SETTING_UPDATE }
     // 监听广播事件(通用设置配置更新)
     emitter.on(eventSetting, (eventData: emitter.EventData) => {
@@ -1379,6 +1385,8 @@ export struct LocalMusic {
     this.isShowPlayPageBack = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_PLAYPAGE_BACK, true)
     this.isCoverTopBig = PreferencesUtil.getBooleanSync(SettingPage.IS_COVER_TOP_BIG, false)
     this.isShowSingleLineLyric = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_SLLYRIC, false)
+    this.isEnableWordByWordLyric = PreferencesUtil.getBooleanSync(SettingPage.IS_ENABLE_WORD_BY_WORD_LYRIC, true)
+    this.syncWordByWordLyricMode()
     this.longPressSpeed = PreferencesUtil.getNumberSync(SettingPage.LONG_PRESS_SPEED, 3)
     this.isPlayListBgGrass = PreferencesUtil.getBooleanSync(SettingPage.IS_PLAYLIST_BG_GRASS, true)
     this.isShowHeader = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_HEADER, true)
@@ -2098,6 +2106,7 @@ export struct LocalMusic {
     emitter.off(EventConstants.EVENT_SETTING_UPDATE);
     emitter.off(EventConstants.EVENT_WEBDAV_METADATA_UPDATED);
     emitter.off(EventConstants.EVENT_EQUALIZER_CHANGED);
+    emitter.off(EventConstants.EVENT_PLAY_QUEUE_REFRESH);
     this.mDestroyPage = true;
     this.mIjkMediaPlayer.setScreenOnWhilePlaying(false);
     if (this.CONTROL_PlayStatus != PlayStatus.INIT) {
@@ -6099,7 +6108,7 @@ export struct LocalMusic {
       LazyForEach(this.dataSource, (item: VideoItem, index: number) => {
 
         GridItem() {
-          Stack({ alignContent: Alignment.Center }) {
+          Stack() {
             this.MusicItemGrid(item, index)
           }
         }
@@ -10728,7 +10737,7 @@ export struct LocalMusic {
                 duration: 500,
                 curve: Curve.Sharp
               }, () => {
-                this.scaleValueImage = Math.min(1, Math.max(0.74, 1 - this.translateY / 880));
+                this.scaleValueImage = Math.min(1, Math.max(0.5, 1 - this.translateY / 880));
                 console.info('onecold scaleValueImage:', this.scaleValueImage)
                 this.scaleValueText = Math.min(1, Math.max(0.88, 1 - this.translateY / 2000));
                 this.playDragUiOpacity = Math.min(1, Math.max(0, 1 - this.translateY / 300));
@@ -11006,7 +11015,7 @@ export struct LocalMusic {
             this.PlayTitle()
           }
           .opacity(this.playDragUiOpacity)
-          .position({ x: 0, y: this.isCoverOpacity() ? 15 : 40 })
+          .position({ x: 0, y: this.isCoverOpacity() ? 10 : 30 })
           .width('100%')
           .margin({ top: 0 })
           .height(PlayConstants.HEIGHT)
@@ -11097,7 +11106,7 @@ export struct LocalMusic {
         .justifyContent(FlexAlign.End)
       }
       .justifyContent(FlexAlign.End)
-      .margin({ bottom: 8 })
+      .margin({ bottom: 5 })
 
     }
     .backgroundImage(!this.isMusicBGCover||StrUtil.isEmpty(this.cover)?null: this.cover)
@@ -11225,6 +11234,12 @@ export struct LocalMusic {
   @State isDebug: boolean = false
   @State isHightLightCenter: boolean = true
 
+  private syncWordByWordLyricMode(): void {
+    this.lyricController.setEnableWordByWordLyric(this.isEnableWordByWordLyric)
+    this.lyricControllerXF.setEnableWordByWordLyric(this.isEnableWordByWordLyric)
+    this.lyricControllerSingle.setEnableWordByWordLyric(this.isEnableWordByWordLyric)
+  }
+
   /**
    * 初始化歌词加载与展示逻辑
    * @param lyricPath 歌词文件路径(支持普通.lrc和加密.lrcc)
@@ -11249,6 +11264,7 @@ export struct LocalMusic {
       return
     }
 
+    this.syncWordByWordLyricMode()
     this.initPipLyricSetting()
     this.lyricControllerSingle
       .setTextSize(15)
@@ -11813,7 +11829,7 @@ export struct LocalMusic {
   private LyricsTopItem() {
 
     Row() {
-      Stack() {
+      Stack({ alignContent: Alignment.Center }) {
 
         Image( StrUtil.isNotEmpty(this.cover)?
           this.cover:$r('app.media.alt'))
@@ -11824,7 +11840,7 @@ export struct LocalMusic {
           .alt($r('app.media.alt'))
           .borderRadius(8)
           .clickEffect({ level: ClickEffectLevel.HEAVY })
-          .margin({ left: this.currentLyricAlignMode === 0 ? 28 : -15 })
+          .margin({ left: this.currentLyricAlignMode === 0 ? 8 : 0 })
           .shadow({
             radius: 22,
             type: ShadowType.BLUR,
@@ -11832,7 +11848,8 @@ export struct LocalMusic {
           })
 
       }
-      .width('18%')
+      .width(62)
+      .height('100%')
 
       Column() {
         Column() {
@@ -11841,14 +11858,14 @@ export struct LocalMusic {
             .maxLines(1)
             .fontWeight(FontWeight.Bolder)
             .fontColor(Color.White)
-            .margin({ left: this.currentLyricAlignMode === 0 ? 24 : 0 })
+            .margin({ left: this.currentLyricAlignMode === 0 ? 8 : 0 })
           Row() {
             Text(this.currentSong?.artist !== undefined ? this.currentSong?.artist : '')
               .fontSize(13)
               .fontWeight(FontWeight.Bolder)
               .padding({ top: 8 })
               .fontColor(Color.White)
-              .margin({ left: this.currentLyricAlignMode === 0 ? 24 : 0 })
+              .margin({ left: this.currentLyricAlignMode === 0 ? 8 : 0 })
             Blank()
 
           }
@@ -11861,7 +11878,8 @@ export struct LocalMusic {
 
       }
       .height('100%')
-      .width('65%')
+      .layoutWeight(1)
+      .margin({ right: 8 })
 
       Column() {
         PointLightDefaultButton({
@@ -11869,8 +11887,8 @@ export struct LocalMusic {
           pointColor:Color.White,
           imageResource:$r('app.media.lyric'),
           isPx: false,
-          builderHeight: 27,
-          builderWidth: 27,
+          builderHeight: 23,
+          builderWidth: 23,
         })
           .onClick(() => {
             this.isLyricSetting = !this.isLyricSetting;
@@ -11886,12 +11904,16 @@ export struct LocalMusic {
           })
 
       }
-      .width('18%')
+      .width(44)
+      .height('100%')
+      .justifyContent(FlexAlign.Center)
+      .alignItems(HorizontalAlign.End)
 
     }
-    .width('82%')
+    .width('100%')
+    .padding({ left: 8, right: 8 })
     .height(58)
-    .justifyContent(FlexAlign.SpaceBetween)
+    .alignItems(VerticalAlign.Center)
   }
 
   //歌词显示
@@ -12060,8 +12082,8 @@ export struct LocalMusic {
           this.playMenuBuilder()
         },
         isPx: false,
-        builderHeight: 32,
-        builderWidth: 32,
+        builderHeight: 26,
+        builderWidth: 26,
       })
         .onClick(async () =>{
           this.isShowMoreView = !this.isShowMoreView;
@@ -12073,8 +12095,8 @@ export struct LocalMusic {
           this.playPreviousBuilder()
         },
         isPx: false,
-        builderHeight: 32,
-        builderWidth: 32,
+        builderHeight: 26,
+        builderWidth: 26,
       })
         .onClick(async () =>{
           await this.playPrevious()
@@ -12087,8 +12109,8 @@ export struct LocalMusic {
           this.PlayOrPauseButton()
         },
         isPx: false,
-        builderHeight: this.isPhoneLan()?42:55,
-        builderWidth: this.isPhoneLan()?42:55,
+        builderHeight: this.isPhoneLan()?38:42,
+        builderWidth: this.isPhoneLan()?38:42,
       })
         .onClick(() =>{
           this.playOrPause()
@@ -12099,8 +12121,8 @@ export struct LocalMusic {
           this.playNextBuilder()
         },
         isPx: false,
-        builderHeight: 32,
-        builderWidth: 32,
+        builderHeight: 26,
+        builderWidth: 26,
       })
         .onClick(async () =>{
           await this.playNext()
@@ -12137,7 +12159,7 @@ export struct LocalMusic {
         Image(this.CONTROL_PlayStatus === PlayStatus.PLAY ?
           (this.isCircleBtn?$r('app.media.hm_pause'):$r('app.media.ic_public_play'))
           : (this.isCircleBtn?$r('app.media.hm_play2'):$r('app.media.ic_public_pause')))
-          .width(this.isPhoneLan()?43:53)
+          .width(this.isPhoneLan()?38:42)
           .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
           .fillColor(Color.White)
           .aspectRatio(CommonConstants.ASPECT_RATIO)
@@ -12146,14 +12168,14 @@ export struct LocalMusic {
       // 加载进度圈
       if (this.isPlayerLoading) {
         Progress({value:0,total:100,type:ProgressType.Ring })
-          .width(this.isPhoneLan()?46:55)
-          .height(this.isPhoneLan()?46:55)
+          .width(this.isPhoneLan()?40:44)
+          .height(this.isPhoneLan()?40:44)
           .color(Color.White)
-          .style({ strokeWidth: 5, status: ProgressStatus.LOADING })
+          .style({ strokeWidth: 4, status: ProgressStatus.LOADING })
       }
     }
-    .width(this.isPhoneLan()?43:53)
-    .height(this.isPhoneLan()?43:53)
+    .width(this.isPhoneLan()?39:43)
+    .height(this.isPhoneLan()?39:43)
   }
 
 
@@ -12261,8 +12283,8 @@ export struct LocalMusic {
         this.playModeBuilder()
       },
       isPx: false,
-      builderHeight: 32,
-      builderWidth: 32,
+      builderHeight: 26,
+      builderWidth: 26,
     })
       .onClick(async () =>{
         this.setLoopMode()
@@ -12275,27 +12297,27 @@ export struct LocalMusic {
     Column() {
       if (this.playType === 0) {
         Image($r('app.media.loop'))
-          .width(26)
+          .width(24)
           .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
           .aspectRatio(CommonConstants.ASPECT_RATIO)
       } else if (this.playType === 1) {
         Image($r('app.media.single'))
-          .width(26)
+          .width(24)
           .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
           .aspectRatio(CommonConstants.ASPECT_RATIO)
       } else if (this.playType === 2) {
         Image($r('app.media.normal_play'))
           .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
-          .width(26)
+          .width(24)
           .aspectRatio(CommonConstants.ASPECT_RATIO)
       } else if (this.playType === 3) {
         Image($r('app.media.random'))
-          .width(26)
+          .width(24)
           .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
           .aspectRatio(CommonConstants.ASPECT_RATIO)
       } else if (this.playType === 4) {
         Image($r('app.media.noloop'))
-          .width(26)
+          .width(24)
           .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
           .aspectRatio(CommonConstants.ASPECT_RATIO)
       }
@@ -12311,8 +12333,8 @@ export struct LocalMusic {
         pointColor:Color.White,
         imageResource:$r('sys.symbol.rectangle_portrait_rotate'),
         isPx: false,
-        builderHeight: 26,
-        builderWidth: 26,
+        builderHeight: 22,
+        builderWidth: 22,
       })
         .onClick(async () =>{
           if (this.isLandscape) {
@@ -12329,8 +12351,8 @@ export struct LocalMusic {
           pointColor:Color.White,
           imageResource:$r('sys.symbol.rename'),
           isPx: false,
-          builderHeight: 26,
-          builderWidth: 26,
+          builderHeight: 22,
+          builderWidth: 22,
         })
           .bindSheet($$this.isShowEdit, this.editSheet(this.currentSong), {
             height:  '99%',
@@ -12355,8 +12377,8 @@ export struct LocalMusic {
         pointColor:Color.White,
         imageResource:$r('sys.symbol.slider_vertical_3'),
         isPx: false,
-        builderHeight: 26,
-        builderWidth: 26,
+        builderHeight: 22,
+        builderWidth: 22,
       })
         .onClick(async () => {
           this.isEqualizerSheet = !this.isEqualizerSheet;
@@ -12369,8 +12391,8 @@ export struct LocalMusic {
         imageResource:Utility.getIsFav(this.favList,this.currentSong)?
           $r('sys.symbol.heart_fill'):$r('sys.symbol.heart'),
         isPx: false,
-        builderHeight: 26,
-        builderWidth: 26,
+        builderHeight: 22,
+        builderWidth: 22,
       })
         .onClick(async () => {
           if(this.currentSong){
@@ -12630,7 +12652,7 @@ export struct LocalMusic {
         Column() {
           this.playCenterView()
         }
-        .position({ left: 28, bottom: 38 })
+        .position({ left: 28, bottom: 35 })
 
       } else {
         this.BottomControl()
@@ -12650,12 +12672,12 @@ export struct LocalMusic {
     Stack({alignContent:Alignment.Center}){
       Image( StrUtil.isNotEmpty(this.cover)?
         this.cover:$r('app.media.alt'))
-        .height(this.isHiCar()?(px2vp(this.windowHeight)*0.43):
+        .height(this.isHiCar()?(px2vp(this.windowHeight)*0.4):
           this.isCoverOpacity()?(px2vp(this.windowHeight)*0.54):'auto')
         .width(this.isHiCar()||this.isCoverOpacity()?'auto':this.isBigScreen() ? '72%' : this.isCoverTopBig ? '100%' : '88%')
         .objectFit(this.isCoverRectangle ? ImageFit.Contain : ImageFit.Auto)
         .alt($r('app.media.alt'))
-        .margin({ right: 8, left: 8, top: 8 })
+        .margin({ right: 8, left: 8, top: 5 })
         .aspectRatio(1)
         .geometryTransition('cover') // 绑定标识符
         .opacity(this.opacityValueImage)
@@ -12696,7 +12718,7 @@ export struct LocalMusic {
     .scale({ x: this.scaleValueImage, y: this.scaleValueImage,
       // 设置左下角为缩放中心点
       centerX: '0%',
-      centerY: '100%' })
+      centerY: '50%' })
   }
   @Builder
   CicleCoverView() {
@@ -12705,10 +12727,10 @@ export struct LocalMusic {
       Image($r('app.media.ic_music_disc'))
         .width(245)
         .height(245)
-        .margin({ right: 20, left: 20 ,top:this.isCoverRectangle||this.isLandscape?0:50 })
+        .margin({ right: 20, left: 20 ,top:this.isLandscape?0:50 })
         .aspectRatio(1)
-        .opacity(this.isCoverOpacity() || this.isCoverRectangle ? 0 : 1)
-        .visibility(this.isCoverOpacity() || this.isCoverRectangle ? Visibility.None : Visibility.Visible)
+        .opacity(this.isCoverOpacity()  ? 0 : 1)
+        .visibility(this.isCoverOpacity() ? Visibility.None : Visibility.Visible)
         .borderRadius('100%')
         .align(Alignment.Center)
         .clip(true)
@@ -13051,6 +13073,38 @@ export struct LocalMusic {
         })
 
 
+      }
+      if (!isPip) {
+        Row() {
+          Text(`逐字歌词:`)
+            .fontSize(14)
+            .fontColor(Color.White)
+
+          Column() {
+            Toggle({ type: ToggleType.Switch, isOn: this.isEnableWordByWordLyric })
+              .selectedColor(this.themeColor)
+              .switchPointColor(Color.White)
+              .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 })
+              .onChange((checked: boolean) => {
+                this.isEnableWordByWordLyric = checked;
+                PreferencesUtil.putSync(SettingPage.IS_ENABLE_WORD_BY_WORD_LYRIC, this.isEnableWordByWordLyric)
+                this.syncWordByWordLyricMode()
+                this.nodeController.updateBgColor(this.pipBg, this.lyricControllerXF)
+              })
+              .width(50)
+              .height(30)
+              .alignSelf(ItemAlign.Start)
+              .margin({ left: 12 })
+            Blank()
+          }
+          .width('76%')
+        }
+        .margin({
+          left: 20,
+          right: 15,
+          top: 10,
+          bottom: 10
+        })
       }
       Row() {
         Text(`歌词居中:`)
@@ -13397,7 +13451,7 @@ export struct LocalMusic {
         Text(`歌词字号:`)
           .fontSize(14)
           .fontColor(Color.White)
-
+          .margin({ right: 12 })
 
         Slider({
           value: isPip ? this.currentLyricSizePip : this.currentLyricSize,
@@ -13413,12 +13467,19 @@ export struct LocalMusic {
           .onChange((value: number) => {
             this.setLyricTextSize(value, isPip)
           })
-          .width('76%')
+          .layoutWeight(1)
+          .margin({ right: 3 })
+        Text(this.getLyricSizeDisplayValue(isPip))
+          .fontSize(13)
+          .fontColor(Color.White)
+          .width(44)
+          .textAlign(TextAlign.End)
 
       }
+      .width('76%')
       .margin({
         left: 20,
-        right: 15,
+        right: 45,
         top: 5,
         bottom: 5
       })
@@ -13427,6 +13488,7 @@ export struct LocalMusic {
         Text(`高亮倍数:`)
           .fontSize(14)
           .fontColor(Color.White)
+          .margin({ right: 12 })
 
         Slider({
           value: isPip ? this.currentHightLyricSizePip : this.currentHightLyricSize,
@@ -13442,12 +13504,19 @@ export struct LocalMusic {
           .onChange((value: number) => {
             this.setHighLyricTextSize(value, isPip)
           })
-          .width('76%')
+          .layoutWeight(1)
+          .margin({ right: 3 })
+        Text(this.getHighLyricSizeDisplayValue(isPip))
+          .fontSize(13)
+          .fontColor(Color.White)
+          .width(44)
+          .textAlign(TextAlign.End)
 
       }
+      .width('76%')
       .margin({
         left: 20,
-        right: 15,
+        right: 45,
         top: 5,
         bottom: 5
       })
@@ -13457,6 +13526,7 @@ export struct LocalMusic {
         Text(`歌词间隙:`)
           .fontSize(14)
           .fontColor(Color.White)
+          .margin({ right: 12 })
         Slider({
           value: isPip ? this.currentLyricLineSpacePip : this.currentLyricLineSpace,
           min: 0,
@@ -13471,13 +13541,20 @@ export struct LocalMusic {
           .onChange((value: number) => {
             this.setLyricLineSpace(value, isPip)
           })
-          .width('76%')
+          .layoutWeight(1)
+          .margin({ right: 3 })
+        Text(this.getLyricLineSpaceDisplayValue(isPip))
+          .fontSize(13)
+          .fontColor(Color.White)
+          .width(44)
+          .textAlign(TextAlign.End)
 
 
       }
+      .width('76%')
       .margin({
         left: 20,
-        right: 15,
+        right: 45,
         top: 5,
         bottom: 5
       })
@@ -13487,6 +13564,7 @@ export struct LocalMusic {
         Text(`歌词字重:`)
           .fontSize(14)
           .fontColor(Color.White)
+          .margin({ right: 12 })
         Slider({
           value: isPip ? this.lyricTextWeightPip : this.lyricTextWeight,
           min: 100,
@@ -13509,13 +13587,20 @@ export struct LocalMusic {
               PreferencesUtil.putSync('lyricTextWeight', this.lyricTextWeight)
             }
           })
-          .width('76%')
+          .layoutWeight(1)
+          .margin({ right: 3 })
+        Text(this.getLyricTextWeightDisplayValue(isPip))
+          .fontSize(13)
+          .fontColor(Color.White)
+          .width(44)
+          .textAlign(TextAlign.End)
 
 
       }
+      .width('76%')
       .margin({
         left: 20,
-        right: 15,
+        right: 45,
         top: 5,
         bottom: 5
       })
@@ -13533,6 +13618,26 @@ export struct LocalMusic {
 
   }
 
+  private getLyricSizeDisplayValue(isPip: boolean): string {
+    const lyricSize = isPip ? this.currentLyricSizePip : this.currentLyricSize;
+    return lyricSize.toFixed(1);
+  }
+
+  private getHighLyricSizeDisplayValue(isPip: boolean): string {
+    const highLyricSize = isPip ? this.currentHightLyricSizePip : this.currentHightLyricSize;
+    return highLyricSize.toFixed(1);
+  }
+
+  private getLyricLineSpaceDisplayValue(isPip: boolean): string {
+    const lyricLineSpace = isPip ? this.currentLyricLineSpacePip : this.currentLyricLineSpace;
+    return Math.round(lyricLineSpace).toString();
+  }
+
+  private getLyricTextWeightDisplayValue(isPip: boolean): string {
+    const lyricTextWeightValue = isPip ? this.lyricTextWeightPip : this.lyricTextWeight;
+    return Math.round(lyricTextWeightValue).toString();
+  }
+
   private setBlurDegree(index: number, isPip: boolean) {
     if (isPip) {
       this.blurDegreePip = index
@@ -14412,7 +14517,7 @@ export struct LocalMusic {
   private moreItems: MoreItem[] = [
     { id: 1, image: $r('app.media.speed'), title: '倍速' },
 
-    { id: 2, image: $r('app.media.lyric'), title: '歌词' },
+    { id: 30, image: $r('sys.symbol.lyrics_square'), title: '歌词' },
 
     { id: 24, image: $r('sys.symbol.slider_vertical_3'), title: '均衡器' },
 
@@ -14476,14 +14581,25 @@ export struct LocalMusic {
   @State isShowSpectrum: boolean = false//是否显示频谱
   @State spectrumModeIndex: number = 0 //选中的频谱特效索引
 
+  private getSafeBoostPercent(): number {
+    const rawBoostPercent = Math.max(0, Math.min(this.volumeBoostPercent, this.volumeBoostMaxPercent));
+    if (rawBoostPercent <= 150) {
+      return rawBoostPercent;
+    }
+    if (rawBoostPercent <= 300) {
+      return 150 + (rawBoostPercent - 150) * 0.45;
+    }
+    return 217.5 + (rawBoostPercent - 300) * 0.15;
+  }
+
   private getVolumeBoostFactor(): number {
-    return 1 + this.volumeBoostPercent / 100;
+    return 1 + this.getSafeBoostPercent() / 100;
   }
 
   private getCurrentOutputVolume(): number {
     const baseVolume = Math.max(0, Math.min(this.volume, 1));
     const boostedVolume = baseVolume * this.getVolumeBoostFactor();
-    const maxOutputVolume = 1 + this.volumeBoostMaxPercent / 100;
+    const maxOutputVolume = this.getVolumeBoostFactor();
     return Math.max(0, Math.min(boostedVolume, maxOutputVolume));
   }
 
@@ -14520,7 +14636,7 @@ export struct LocalMusic {
 
   doMore(moreId: number) {
     switch (moreId) {
-      case 2: //歌词
+      case 30: //歌词
         this.isLyricSetting = !this.isLyricSetting;
         this.isShowMoreView = false
         break;
@@ -18350,6 +18466,39 @@ export struct LocalMusic {
   /**
    * 处理歌单播放请求
    */
+  private syncWebdavQueueWithoutRestart(): void {
+    try {
+      if (!this.currentSong || !isWebDavType(this.currentSong.type)) {
+        return;
+      }
+      const webdavList = getWebdavVideoItems();
+      if (!ArrayUtil.isNotEmpty(webdavList)) {
+        return;
+      }
+      const currentPath = this.currentSong.filePath;
+      let targetIndex = webdavList.findIndex((item: VideoItem) => item.filePath === currentPath);
+      if (targetIndex < 0) {
+        const memoryIndex = getWebdavCurrentPlayIndex();
+        if (memoryIndex >= 0 && memoryIndex < webdavList.length) {
+          targetIndex = memoryIndex;
+        }
+      }
+      if (targetIndex < 0) {
+        targetIndex = 0;
+      }
+
+      this.songList = [...webdavList];
+      this.currentSongList = [...webdavList];
+      this.sonDataSource.pushArrayData(this.songList);
+      this.currentPlaylistSongFilePaths = this.songList.map(song => song.filePath);
+      this.curIndex = targetIndex;
+      PreferencesUtil.putSync('LastMusicList', this.songList);
+      Logger.info(TAG, `WebDAV播放队列已刷新,当前索引=${this.curIndex}, 队列长度=${this.songList.length}`);
+    } catch (error) {
+      Logger.error(TAG, `刷新WebDAV播放队列失败: ${(error as Error).message}`);
+    }
+  }
+
   private async handlePlaylistPlayRequest(playlistId: string, playlistName:
     string, songFilePaths: string[], startIndex: number, isJump: boolean, songCount?: number) {
     try {

BIN
entry/src/main/resources/base/media/lyric.png


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

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1773710895341" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6173" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M336.096 466.464H248.992a16 16 0 1 1 0-32h103.104a16 16 0 0 1 16 16v290.688L419.2 701.12a16 16 0 1 1 19.744 25.184l-76.928 60.32a16 16 0 0 1-25.888-12.608v-307.552zM480 304a16 16 0 1 1 0-32h256a48 48 0 0 1 48 48v448a48 48 0 0 1-48 48 16 16 0 1 1 0-32 16 16 0 0 0 16-16V320a16 16 0 0 0-16-16h-256z m19.744 142.016a16 16 0 0 1 0-32h192a16 16 0 0 1 0 32h-192zM512 512h160a32 32 0 0 1 32 32v96a32 32 0 0 1-32 32h-160a32 32 0 0 1-32-32v-96a32 32 0 0 1 32-32z m0 32v96h160v-96h-160zM291.968 279.744a16 16 0 1 1 6.848-31.264c20.256 4.416 40 16 59.232 34.432 18.784 17.92 32.064 35.968 39.744 54.336a16 16 0 1 1-29.536 12.352c-5.792-13.92-16.512-28.48-32.32-43.552-15.264-14.624-29.952-23.264-43.968-26.304z" p-id="6174"></path></svg>

+ 17 - 9
ijkplayer/src/main/cpp/ijkplayer/ff_ffplay.c

@@ -3499,17 +3499,25 @@ static int read_thread(void *arg)
                                (long long)io_seek_ret);
                         ret = av_seek_frame(is->ic, -1, byte_pos, AVSEEK_FLAG_BYTE | AVSEEK_FLAG_BACKWARD);
                     }
+                } else if (bounded_target <= 0 && ic->pb && (ic->pb->seekable & AVIO_SEEKABLE_NORMAL)) {
+                    // 对于未知时长网络流,仅允许回到起点,避免走 timestamp seek 的脆弱路径
+                    int64_t io_seek_ret = avio_seek(ic->pb, 0, SEEK_SET);
+                    if (io_seek_ret >= 0) {
+                        avformat_flush(is->ic);
+                        ret = 0;
+                        av_log(ffp, AV_LOG_WARNING, "heanup: network seek fallback to start success, io_pos=%lld\n",
+                               (long long)io_seek_ret);
+                    } else {
+                        av_log(ffp, AV_LOG_WARNING, "heanup: network seek fallback to start failed(%lld)\n",
+                               (long long)io_seek_ret);
+                        ret = -1;
+                    }
                 } else {
-                    int64_t window = 3LL * AV_TIME_BASE;
-                    seek_min = bounded_target > window ? bounded_target - window : 0;
-                    seek_max = bounded_target + window;
-                    if (ic->duration > 0 && seek_max > ic->duration)
-                        seek_max = ic->duration;
+                    // 禁用网络流 timestamp seek 回退,避免触发 ff_seek_frame_binary 崩溃路径
                     av_log(ffp, AV_LOG_WARNING,
-                           "heanup: network timestamp seek fallback, target=%lld, seek_min=%lld, seek_max=%lld, file_size=%lld, duration=%lld\n",
-                           (long long)bounded_target, (long long)seek_min, (long long)seek_max,
-                           (long long)file_size, (long long)duration);
-                    ret = avformat_seek_file(is->ic, -1, seek_min, bounded_target, seek_max, seek_flags);
+                           "heanup: reject unsafe network seek, target=%lld, file_size=%lld, duration=%lld\n",
+                           (long long)bounded_target, (long long)file_size, (long long)duration);
+                    ret = -1;
                 }
                 if (ret < 0) {
                     av_log(ffp, AV_LOG_WARNING, "heanup: network seek failed(%d)\n", ret);

+ 13 - 1
lib/src/main/ets/LyricController.ets

@@ -10,6 +10,7 @@ const DEFAULT_EDGE_COLOR = "#ffffff"
 const DEFAULT_ANIM_DURATION = 300
 const DEFAULT_CACHE_SIZE = 4
 const DEFAULT_BLUR_DEGREE = 4
+const DEFAULT_ENABLE_WORD_BY_WORD_LYRIC = true
 
 /**
  * The controller for LyricView.
@@ -32,6 +33,7 @@ export class LyricController {
     private isHightLightCenter: boolean = true
     private textWeight: number = FontWeight.Medium
     private transverterType: number = 0
+    private enableWordByWordLyric: boolean = DEFAULT_ENABLE_WORD_BY_WORD_LYRIC
     /**
      *  Listener to observe the lyric data set changed. This is a inner function, do not call this.
      */
@@ -132,6 +134,16 @@ export class LyricController {
         return this.transverterType
     }
 
+    setEnableWordByWordLyric(enableWordByWordLyric: boolean): LyricController {
+        this.enableWordByWordLyric = enableWordByWordLyric
+        this.onInvalidated(false)
+        return this
+    }
+
+    getEnableWordByWordLyric(): boolean {
+        return this.enableWordByWordLyric
+    }
+
     setHightLightCenter(isHightLightCenter: boolean): LyricController {
         this.isHightLightCenter = isHightLightCenter
         this.onInvalidated(true)
@@ -342,4 +354,4 @@ export class LyricController {
     invalidate() {
         this.onInvalidated(false)
     }
-}
+}

+ 1 - 3
lib/src/main/ets/parse/LyricParser.ts

@@ -41,7 +41,6 @@ export class LyricParser implements IParser {
 
         for (let i = 0; i < src.length; i++) {
             let line = src[i]
-            console.info(`content The line of file:line ${line}`);
             if (line == "" || line == "\n" || line == "\r" || line == "\r\n" || line == "[Verse]" || line == "[Chorus]"
                 || line == "[PreChorus]" || line == "[PreChorus]"||line == "[Bridge]") {
                 printW("the lyric line is empty, carriage return or line feed, line index= " + i)
@@ -78,7 +77,6 @@ export class LyricParser implements IParser {
 
                 // 处理双语歌词的特殊情况(英文行+中文行交替)
                 if (this.isBilingualLyric(src,  i)) {
-                    console.info(`onecold isBilingualLyric 双语歌词处理中`)
                     const englishLine = src[i];
                     const chineseLine = src[i+1];
 
@@ -402,4 +400,4 @@ export class LyricParser implements IParser {
         let result = new Lyric(artist, title, album, by, offset, lyricLines, true)
         return result
     }
-}
+}

+ 20 - 7
lib/src/main/ets/view/LyricView2.ets

@@ -25,6 +25,11 @@ export struct LyricView2 {
      * If false, the onSeekAction callback will not invoke anymore.
      */
     enableSeek: boolean = true
+    /**
+     * Enable word-by-word lyric render or not.
+     * If false, lines with word timing will fallback to normal line render.
+     */
+    @State enableWordByWordLyric: boolean = true
     /**
      * The color of seek button and duration text.
      */
@@ -211,6 +216,7 @@ export struct LyricView2 {
         this.emptyHint = this.controller.getEmptyHint()
         this.alignMode = this.controller.getAlignMode()
         this.textWeight = this.controller.getTextWeight()
+        this.enableWordByWordLyric = this.controller.getEnableWordByWordLyric()
     }
 
     aboutToAppear() {
@@ -353,8 +359,10 @@ export struct LyricView2 {
                     : Visibility.Visible)
                 .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?'95%': '80%')
                 .blendMode(
-                    index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendMode.DST_IN : undefined,
-                    index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendApplyType.OFFSCREEN : undefined
+                    index == this.currentIndex && this.enableWordByWordLyric&&
+            !(this.currentLyric && this.currentLyric.isPlainText) ? BlendMode.DST_IN : undefined,
+                    index == this.currentIndex && this.enableWordByWordLyric
+                        && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendApplyType.OFFSCREEN : undefined
                 )
 
             // 中文翻译(整行显示)
@@ -371,21 +379,26 @@ export struct LyricView2 {
                         (index >= this.currentIndex && index <= this.currentIndex + 1 ? Visibility.Visible : Visibility.None)
                         : Visibility.Visible)
                     .blendMode(
-                        index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendMode.DST_IN : undefined,
-                        index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendApplyType.OFFSCREEN : undefined
+                        index == this.currentIndex && this.enableWordByWordLyric&&
+                            !(this.currentLyric && this.currentLyric.isPlainText) ? BlendMode.DST_IN : undefined,
+                        index == this.currentIndex && this.enableWordByWordLyric&&
+                            !(this.currentLyric && this.currentLyric.isPlainText) ? BlendApplyType.OFFSCREEN : undefined
                     )
                     .width(this.alignMode == 'center' ? '100%' :index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ?'95%': '80%')
 
             }
         }
         // 在 Row 上应用渐变
-        .linearGradient(index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? {
+        .linearGradient(index == this.currentIndex && this.enableWordByWordLyric&&
+            !(this.currentLyric && this.currentLyric.isPlainText) ? {
             direction: GradientDirection.Right,
             colors: this.getLyricItemLinearGradient(item, index)
         } : undefined)
         .blendMode(
-            index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendMode.SRC_OVER : undefined,
-            index == this.currentIndex && !(this.currentLyric && this.currentLyric.isPlainText) ? BlendApplyType.OFFSCREEN : undefined
+            index == this.currentIndex && this.enableWordByWordLyric&&
+                !(this.currentLyric && this.currentLyric.isPlainText) ? BlendMode.SRC_OVER : undefined,
+            index == this.currentIndex && this.enableWordByWordLyric&&
+                !(this.currentLyric && this.currentLyric.isPlainText) ? BlendApplyType.OFFSCREEN : undefined
         )
 
     }