onecold 5 месяцев назад
Родитель
Сommit
d028869cf5

+ 18 - 6
entry/src/main/ets/common/util/DownloadCenterManager.ets

@@ -61,6 +61,7 @@ class DownloadPausedError extends Error {
 export class DownloadCenterManager {
 export class DownloadCenterManager {
   private static instance: DownloadCenterManager;
   private static instance: DownloadCenterManager;
   private observers: Array<() => void> = [];
   private observers: Array<() => void> = [];
+  private observerNotifyScheduled: boolean = false;
   private queue: DownloadRuntimeContext[] = [];
   private queue: DownloadRuntimeContext[] = [];
   private runtimeMap: Map<string, DownloadRuntimeContext> = new Map<string, DownloadRuntimeContext>();
   private runtimeMap: Map<string, DownloadRuntimeContext> = new Map<string, DownloadRuntimeContext>();
   private activeTasks: DownloadCenterTask[] = [];
   private activeTasks: DownloadCenterTask[] = [];
@@ -80,6 +81,9 @@ export class DownloadCenterManager {
   }
   }
 
 
   public subscribe(callback: () => void): void {
   public subscribe(callback: () => void): void {
+    if (this.observers.indexOf(callback) >= 0) {
+      return;
+    }
     this.observers.push(callback);
     this.observers.push(callback);
   }
   }
 
 
@@ -798,12 +802,20 @@ export class DownloadCenterManager {
   }
   }
 
 
   private notifyObservers(): void {
   private notifyObservers(): void {
-    for (let i: number = 0; i < this.observers.length; i += 1) {
-      try {
-        this.observers[i]();
-      } catch (error) {
-        Logger.warn(TAG, `通知下载观察者失败: ${(error as Error).message}`);
-      }
+    if (this.observerNotifyScheduled) {
+      return;
     }
     }
+    this.observerNotifyScheduled = true;
+    setTimeout((): void => {
+      this.observerNotifyScheduled = false;
+      const snapshot: Array<() => void> = this.observers.slice();
+      for (let i: number = 0; i < snapshot.length; i += 1) {
+        try {
+          snapshot[i]();
+        } catch (error) {
+          Logger.warn(TAG, `通知下载观察者失败: ${(error as Error).message}`);
+        }
+      }
+    }, 0);
   }
   }
 }
 }

+ 22 - 1
entry/src/main/ets/pages/NewIndex.ets

@@ -57,6 +57,7 @@ import { RemoteDriveType } from '../common/enums/RemoteDriveType';
 import { RemoteMusicPage } from '../view/RemoteMusicPage';
 import { RemoteMusicPage } from '../view/RemoteMusicPage';
 import { ServerLogUtil } from '../common/util/ServerLogUtil';
 import { ServerLogUtil } from '../common/util/ServerLogUtil';
 import { PlayStatus } from '../common/PlayStatus';
 import { PlayStatus } from '../common/PlayStatus';
+import { PointLightDefaultButton } from '../view/PointLight/PointLightDeFaultButton';
 
 
 // import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
 // import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
 
 
@@ -870,7 +871,7 @@ struct NewIndex {
 
 
   @Builder
   @Builder
   userInfoView() {
   userInfoView() {
-    Column() {
+    Stack({ alignContent: Alignment.BottomEnd }) {
       Row() {
       Row() {
         Stack({ alignContent: Alignment.BottomEnd }){
         Stack({ alignContent: Alignment.BottomEnd }){
           Image(this.userAvatarUrl && this.userAvatarUrl.length > 0 ? this.userAvatarUrl : this.userAvatar)
           Image(this.userAvatarUrl && this.userAvatarUrl.length > 0 ? this.userAvatarUrl : this.userAvatar)
@@ -949,6 +950,26 @@ struct NewIndex {
       })
       })
       .width('100%')
       .width('100%')
       .padding(10)
       .padding(10)
+
+      PointLightDefaultButton({
+        isPx: false,
+        isSysBol: true,
+        pointColor: $r('app.color.text_color'),
+        imageResource: $r('sys.symbol.gearshape'),
+        builderHeight: 32,
+        builderWidth: 32,
+        buttonScale: 1,
+        canShadow: false,
+      })
+        .margin({ right: 2, bottom: 2 })
+        .onClick(() => {
+          this.mType = 4
+          if (!this.isBigScreen()) {
+            this.getUIContext()?.animateTo({ duration: 555 }, () => {
+              this.isShowDrawer = false
+            })
+          }
+        })
     }
     }
     .backgroundColor($r('app.color.user_center_card_background'))
     .backgroundColor($r('app.color.user_center_card_background'))
     .borderRadius(24)
     .borderRadius(24)

+ 80 - 4
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -32,8 +32,9 @@ import { showCreatePlaylistDialog } from '../dialog/PlaylistDialog';
 import { Playlist } from '../viewmodel/Playlist';
 import { Playlist } from '../viewmodel/Playlist';
 import PlaylistTable from '../common/util/PlaylistTable';
 import PlaylistTable from '../common/util/PlaylistTable';
 import MediaTable from '../common/util/MediaTable';
 import MediaTable from '../common/util/MediaTable';
-import { DownloadCenterManager } from '../common/util/DownloadCenterManager';
+import { DownloadCenterManager, DownloadCenterTask } from '../common/util/DownloadCenterManager';
 import { DownloadCenter } from '../view/DownloadCenter';
 import { DownloadCenter } from '../view/DownloadCenter';
+import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil';
 
 
 /**
 /**
  * 歌单播放事件数据
  * 歌单播放事件数据
@@ -148,12 +149,19 @@ export struct WebDavMainPage {
   @State rootPath: string = ''
   @State rootPath: string = ''
   @State isShowDownloadCenter: boolean = false
   @State isShowDownloadCenter: boolean = false
   @State downloadCenterTabIndex: number[] = [0]
   @State downloadCenterTabIndex: number[] = [0]
+  @State downloadCenterRefreshVersion: number = 0
+  @State downloadCenterActiveTasks: DownloadCenterTask[] = []
+  @State downloadCenterCompletedTasks: DownloadCenterTask[] = []
   @State isSearchLoading: boolean = false
   @State isSearchLoading: boolean = false
   private readonly downloadFolderName: string = '下载'
   private readonly downloadFolderName: string = '下载'
   private readonly metadataUpdateEvent: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED };
   private readonly metadataUpdateEvent: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED };
   private thumbnailTaskToken: number = 0;
   private thumbnailTaskToken: number = 0;
   private thumbnailRunningKeys: Set<string> = new Set<string>();
   private thumbnailRunningKeys: Set<string> = new Set<string>();
   private readonly downloadCenterManager: DownloadCenterManager = DownloadCenterManager.getInstance();
   private readonly downloadCenterManager: DownloadCenterManager = DownloadCenterManager.getInstance();
+  private readonly downloadCenterListener: () => void = (): void => {
+    this.scheduleDownloadCenterRefresh();
+  };
+  private downloadCenterRefreshTimer: number = -1;
 
 
   private async runSerializedThumbnailFfmpeg(task: () => Promise<void>): Promise<void> {
   private async runSerializedThumbnailFfmpeg(task: () => Promise<void>): Promise<void> {
     const nextTask: Promise<void> = globalRemoteThumbFfmpegQueue.then(async (): Promise<void> => {
     const nextTask: Promise<void> = globalRemoteThumbFfmpegQueue.then(async (): Promise<void> => {
@@ -1234,9 +1242,33 @@ export struct WebDavMainPage {
   }
   }
 
 
   private openDownloadCenter(): void {
   private openDownloadCenter(): void {
+    this.syncDownloadCenterTasks();
     this.isShowDownloadCenter = true;
     this.isShowDownloadCenter = true;
   }
   }
 
 
+  private syncDownloadCenterTasks(): void {
+    this.downloadCenterActiveTasks = this.downloadCenterManager.getActiveTasks();
+    this.downloadCenterCompletedTasks = this.downloadCenterManager.getCompletedTasks();
+  }
+
+  private scheduleDownloadCenterRefresh(): void {
+    if (this.downloadCenterRefreshTimer >= 0) {
+      return;
+    }
+    this.downloadCenterRefreshTimer = setTimeout((): void => {
+      this.downloadCenterRefreshTimer = -1;
+      this.syncDownloadCenterTasks();
+      this.downloadCenterRefreshVersion += 1;
+    }, 0);
+  }
+
+  private stopDownloadCenterRefresh(): void {
+    if (this.downloadCenterRefreshTimer >= 0) {
+      clearTimeout(this.downloadCenterRefreshTimer);
+      this.downloadCenterRefreshTimer = -1;
+    }
+  }
+
   private resolveSongDownloadSizeText(song: VideoItem): string {
   private resolveSongDownloadSizeText(song: VideoItem): string {
     if (StrUtil.isNotEmpty(song.size)) {
     if (StrUtil.isNotEmpty(song.size)) {
       return decodeUrlEncodedString(song.size as string);
       return decodeUrlEncodedString(song.size as string);
@@ -1326,6 +1358,28 @@ export struct WebDavMainPage {
       }
       }
     }
     }
 
 
+    if (song.type === CommonConstants.TYPE_WEBDAV) {
+      try {
+        let account: WebDavAccount | null = null;
+        if (song.webdav_account_id) {
+          account = await this.webdavManager.getWebDavAccountById(song.webdav_account_id);
+        }
+        if (!account && this.selectedAccount?.webType === RemoteDriveType.WebDav) {
+          account = this.selectedAccount;
+        }
+        if (account) {
+          const relativePath = song.remote_rel_path || song.filePath;
+          const fullUrl = WebDavUrlUtil.buildFullUrl(account, relativePath);
+          if (StrUtil.isNotEmpty(fullUrl)) {
+            Logger.info(TAG, `下载中心使用WebDAV直链下载: ${song.name}, url=${fullUrl}`);
+            return fullUrl;
+          }
+        }
+      } catch (error) {
+        Logger.warn(TAG, `WebDAV下载直链构建失败,回退播放地址: ${(error as Error).message}`);
+      }
+    }
+
     return setVideoUrlForSong(song, {
     return setVideoUrlForSong(song, {
       context: getContext(this),
       context: getContext(this),
       autoParseMusicName: false,
       autoParseMusicName: false,
@@ -1515,6 +1569,7 @@ export struct WebDavMainPage {
 
 
     // 订阅WebDAV状态变化
     // 订阅WebDAV状态变化
     this.webdavManager.subscribe(this.eventHandler);
     this.webdavManager.subscribe(this.eventHandler);
+    this.downloadCenterManager.subscribe(this.downloadCenterListener);
     this.subscribeWebDavMetadataUpdates();
     this.subscribeWebDavMetadataUpdates();
 
 
 
 
@@ -1533,6 +1588,8 @@ export struct WebDavMainPage {
     this.thumbnailTaskToken += 1;
     this.thumbnailTaskToken += 1;
     this.thumbnailRunningKeys.clear();
     this.thumbnailRunningKeys.clear();
     this.webdavManager.unsubscribe(this.eventHandler);
     this.webdavManager.unsubscribe(this.eventHandler);
+    this.downloadCenterManager.unsubscribe(this.downloadCenterListener);
+    this.stopDownloadCenterRefresh();
     emitter.off(EventConstants.EVENT_WEBDAV_METADATA_UPDATED);
     emitter.off(EventConstants.EVENT_WEBDAV_METADATA_UPDATED);
   }
   }
 
 
@@ -2413,6 +2470,9 @@ export struct WebDavMainPage {
       appName: this.appName,
       appName: this.appName,
       topSafeHeight: this.topSafeHeight,
       topSafeHeight: this.topSafeHeight,
       bottomSafeHeight: this.bottomSafeHeight,
       bottomSafeHeight: this.bottomSafeHeight,
+      tasksVersion: this.downloadCenterRefreshVersion,
+      activeTasksProp: this.downloadCenterActiveTasks,
+      completedTasksProp: this.downloadCenterCompletedTasks,
       selectedIndexes: $downloadCenterTabIndex,
       selectedIndexes: $downloadCenterTabIndex,
       onPauseTask: (taskId: string) => {
       onPauseTask: (taskId: string) => {
         this.pauseDownloadTask(taskId);
         this.pauseDownloadTask(taskId);
@@ -2701,6 +2761,21 @@ export struct WebDavMainPage {
     .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
     .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
   }
   }
 
 
+  private getSearchLoadingTopOffset(): number {
+    return this.topSafeHeight + (this.webdavManager.currentPath !== '' ? 100 : 56)
+  }
+
+  private shouldShowSearchIndexingTip(): boolean {
+    return this.isSearchMode && this.searchText.length > 0 && this.isSearchLoading
+  }
+
+  private getListContentStartOffset(): number {
+    if (this.shouldShowSearchIndexingTip()) {
+      return 8
+    }
+    return this.topSafeHeight + 85
+  }
+
   @Builder
   @Builder
   breaker() {
   breaker() {
     // 面包屑导航
     // 面包屑导航
@@ -2801,7 +2876,7 @@ export struct WebDavMainPage {
         curve: Curve.Smooth // 可选动画曲线
         curve: Curve.Smooth // 可选动画曲线
       })
       })
 
 
-      if (this.isSearchMode && this.searchText.length > 0 && this.isSearchLoading) {
+      if (this.shouldShowSearchIndexingTip()) {
         Row({ space: 8 }) {
         Row({ space: 8 }) {
           LoadingProgress()
           LoadingProgress()
             .width(18)
             .width(18)
@@ -2812,7 +2887,8 @@ export struct WebDavMainPage {
             .fontColor($r('app.color.index_tab_font_color'))
             .fontColor($r('app.color.index_tab_font_color'))
             .opacity(0.65)
             .opacity(0.65)
         }
         }
-        .padding({ left: 20, right: 20, top: this.topSafeHeight + 74, bottom: 6 })
+        .alignSelf(ItemAlign.Center)
+        .padding({ left: 20, right: 20, top: this.getSearchLoadingTopOffset() })
       }
       }
 
 
 
 
@@ -2852,7 +2928,7 @@ export struct WebDavMainPage {
 
 
           return { offsetRemain: offset };
           return { offsetRemain: offset };
         })
         })
-        .contentStartOffset(this.topSafeHeight + 85)
+        .contentStartOffset(this.getListContentStartOffset())
         .contentEndOffset(this.bottomSafeHeight+70)
         .contentEndOffset(this.bottomSafeHeight+70)
         .layoutWeight(1)
         .layoutWeight(1)
         .margin({ top: 4 })
         .margin({ top: 4 })

+ 18 - 15
entry/src/main/ets/view/CueComptent.ets

@@ -76,24 +76,27 @@ export struct CueComptent {
         ListItem() {
         ListItem() {
           Button({ type: ButtonType.Normal, stateEffect: true }) {
           Button({ type: ButtonType.Normal, stateEffect: true }) {
             Row() {
             Row() {
-              Row(){
+              Row() {
                 SymbolGlyph($r('sys.symbol.media_center'))
                 SymbolGlyph($r('sys.symbol.media_center'))
                   .fontColor([this.themeColor])
                   .fontColor([this.themeColor])
-                  .fontSize(25)
+                  .fontSize(28)
                   .effectStrategy(1)
                   .effectStrategy(1)
-                Text(item.title || 'Unknown Title')
-                  .fontSize(14)
-                  .padding({ left: 6 })
-                  .fontColor(this.curIndex==index?this.themeColor:$r('app.color.text_color'))
-                  .maxLines(2)
-                  .textOverflow({ overflow: this.isLongNameRoLL?TextOverflow.MARQUEE:TextOverflow.Ellipsis })//超长滚动
+                Column({ space: 2 }) {
+                  Text(item.title || 'Unknown Title')
+                    .fontSize(14)
+                    .fontColor(this.curIndex==index?this.themeColor:$r('app.color.text_color'))
+                    .maxLines(1)
+                    .textOverflow({ overflow: this.isLongNameRoLL?TextOverflow.MARQUEE:TextOverflow.Ellipsis })//超长滚动
 
 
-                Text(item.performer || '')
-                  .fontSize(11)
-                  .padding({ left: 8 })
-                  .fontColor(this.curIndex==index?this.themeColor:Color.Gray)
-                  .maxLines(1)
-                  .textOverflow({ overflow: this.isLongNameRoLL?TextOverflow.MARQUEE:TextOverflow.Ellipsis })//超长滚动
+                  Text(item.performer || '')
+                    .fontSize(11)
+                    .fontColor(this.curIndex==index?this.themeColor:Color.Gray)
+                    .maxLines(1)
+                    .textOverflow({ overflow: this.isLongNameRoLL?TextOverflow.MARQUEE:TextOverflow.Ellipsis })//超长滚动
+                }
+                .layoutWeight(1)
+                .alignItems(HorizontalAlign.Start)
+                .padding({ left: 6 })
               }
               }
               .layoutWeight(1)
               .layoutWeight(1)
               .margin({ left: 10 })
               .margin({ left: 10 })
@@ -109,7 +112,7 @@ export struct CueComptent {
           .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.8 })
           .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.8 })
           .transition(TransitionEffect.move(TransitionEdge.BOTTOM).animation({ duration: 600, curve: Curve.Ease, delay: 60*index }))
           .transition(TransitionEffect.move(TransitionEdge.BOTTOM).animation({ duration: 600, curve: Curve.Ease, delay: 60*index }))
           .width('100%')
           .width('100%')
-          .height(50)
+          .height(60)
           .padding({ left: 15, right: 15 })
           .padding({ left: 15, right: 15 })
           .onClick(() => {
           .onClick(() => {
             this.curIndex=index
             this.curIndex=index

+ 263 - 311
entry/src/main/ets/view/DownloadCenter.ets

@@ -1,8 +1,220 @@
 import { SegmentButton, SegmentButtonItemTuple, SegmentButtonOptions } from '@kit.ArkUI';
 import { SegmentButton, SegmentButtonItemTuple, SegmentButtonOptions } from '@kit.ArkUI';
-import { DownloadCenterManager, DownloadCenterTask } from '../common/util/DownloadCenterManager';
+import { DownloadCenterTask } from '../common/util/DownloadCenterManager';
 import { CommonConstants } from '../common/constants/CommonConstants';
 import { CommonConstants } from '../common/constants/CommonConstants';
 import { StrUtil } from '@pura/harmony-utils';
 import { StrUtil } from '@pura/harmony-utils';
-import { LazyDataSource } from '../common/util/LazyDataSource';
+
+@Component
+struct DownloadCenterTaskRow {
+  @Prop themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
+  @Prop task: DownloadCenterTask;
+  @Prop isDownloading: boolean = true;
+  @Prop refreshVersion: number = 0;
+  onPauseTask: (taskId: string) => void = (_taskId: string): void => {};
+  onResumeTask: (taskId: string) => void = (_taskId: string): void => {};
+
+  build() {
+    ListItem() {
+      Column({ space: 8 }) {
+        Row({ space: 10 }) {
+          Image(StrUtil.isNotEmpty(this.task.coverPath) ? this.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(this.task.title)
+              .fontSize(14)
+              .fontWeight(FontWeight.Medium)
+              .fontColor($r('app.color.text_color'))
+              .maxLines(1)
+              .textOverflow({ overflow: TextOverflow.Ellipsis })
+
+            Row() {
+              Text(this.getTaskTotalSizeText())
+                .fontSize(12)
+                .fontColor($r('app.color.text_color'))
+                .opacity(0.6)
+              Text('  ·  ')
+                .fontSize(12)
+                .fontColor($r('app.color.text_color'))
+                .opacity(0.35)
+              Text(this.isDownloading ? this.getTaskStatusText() : '已完成')
+                .fontSize(12)
+                .fontColor(this.getTaskStatusColor())
+                .opacity(0.85)
+            }
+            .width('100%')
+          }
+          .alignItems(HorizontalAlign.Start)
+          .layoutWeight(1)
+
+          if (this.isDownloading) {
+            this.taskActionButtonBuilder()
+          }
+        }
+        .width('100%')
+
+        if (this.isDownloading) {
+          Progress({ value: this.task.progress, total: 100, type: ProgressType.Linear })
+            .width('100%')
+            .color(this.getTaskStatusColor())
+            .backgroundColor($r('app.color.track_color'))
+            .style({ strokeWidth: 5 })
+
+          Row() {
+            Text(this.getTaskProgressLabelText())
+              .fontSize(12)
+              .fontColor(this.getTaskStatusColor())
+              .maxLines(1)
+              .textOverflow({ overflow: TextOverflow.Ellipsis })
+              .layoutWeight(1)
+            Text(this.getTaskSpeedText())
+              .fontSize(12)
+              .fontColor($r('app.color.text_color'))
+              .opacity(0.75)
+              .margin({ right: 6 })
+            Text(this.getTaskProgressInfo())
+              .fontSize(12)
+              .fontColor($r('app.color.text_color'))
+              .opacity(0.6)
+          }
+          .width('100%')
+        }
+
+        Text(`${this.refreshVersion}`)
+          .fontSize(0.1)
+          .fontColor(Color.Transparent)
+          .opacity(0)
+          .width(0)
+          .height(0)
+      }
+      .width('100%')
+      .padding(12)
+      .backgroundColor($r('app.color.start_window_background'))
+      .borderRadius(10)
+    }
+  }
+
+  @Builder
+  private taskActionButtonBuilder() {
+    Button(this.getTaskActionText())
+      .fontSize(12)
+      .fontColor(this.isTaskRunning() ? $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() ? $r('app.color.bg_card') : this.themeColor)
+      .borderRadius(15)
+      .onClick(() => {
+        if (this.isTaskRunning()) {
+          this.onPauseTask(this.task.taskId);
+          return;
+        }
+        this.onResumeTask(this.task.taskId);
+      })
+  }
+
+  private isTaskRunning(): boolean {
+    return this.task.status === 'downloading';
+  }
+
+  private getTaskActionText(): string {
+    return this.isTaskRunning() ? '暂停' : '开始';
+  }
+
+  private getTaskTotalSizeText(): string {
+    if (this.task.totalBytes > 0) {
+      return this.formatBytes(this.task.totalBytes);
+    }
+    return StrUtil.isNotEmpty(this.task.sizeText) ? this.task.sizeText : '--';
+  }
+
+  private getTaskProgressInfo(): string {
+    const downloadedText: string = this.task.downloadedBytes > 0 ? this.formatBytes(this.task.downloadedBytes) : '0 B';
+    const totalText: string = this.getTaskTotalSizeText();
+    return `${downloadedText} / ${totalText}`;
+  }
+
+  private getTaskProgressLabel(): string {
+    const value = this.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(): string {
+    if (this.task.status === 'failed') {
+      return StrUtil.isNotEmpty(this.task.errorMessage) ? this.task.errorMessage : '下载失败';
+    }
+    if (this.task.status === 'paused') {
+      return `已暂停 ${this.getTaskProgressLabel()}`;
+    }
+    if (this.task.status === 'pending') {
+      return `等待中 ${this.getTaskProgressLabel()}`;
+    }
+    return this.getTaskProgressLabel();
+  }
+
+  private getTaskSpeedText(): string {
+    if (this.task.status === 'failed') {
+      return '--';
+    }
+    if (this.task.status === 'paused') {
+      return '已暂停';
+    }
+    if (this.task.status === 'pending') {
+      return '等待中';
+    }
+    if (this.task.speedBytesPerSec > 0) {
+      return `${this.formatBytes(this.task.speedBytesPerSec)}/s`;
+    }
+    return '0 B/s';
+  }
+
+  private getTaskStatusText(): string {
+    switch (this.task.status) {
+      case 'downloading':
+        return '下载中';
+      case 'paused':
+        return '已暂停';
+      case 'failed':
+        return '下载失败';
+      case 'pending':
+        return '等待中';
+      case 'completed':
+      default:
+        return '已完成';
+    }
+  }
+
+  private getTaskStatusColor() {
+    if (this.task.status === 'failed') {
+      return $r('app.color.btn_red');
+    }
+    if (this.task.status === 'paused' || this.task.status === 'pending') {
+      return $r('app.color.text_color');
+    }
+    return this.themeColor;
+  }
+
+  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]}`;
+  }
+}
 
 
 @Component
 @Component
 export struct DownloadCenter {
 export struct DownloadCenter {
@@ -11,29 +223,30 @@ export struct DownloadCenter {
   @Prop appName: string = '';
   @Prop appName: string = '';
   @Prop topSafeHeight: number = 0;
   @Prop topSafeHeight: number = 0;
   @Prop bottomSafeHeight: number = 0;
   @Prop bottomSafeHeight: number = 0;
+  @Prop @Watch('onTasksVersionChanged') tasksVersion: number = 0;
+  @Prop activeTasksProp: DownloadCenterTask[] = [];
+  @Prop completedTasksProp: DownloadCenterTask[] = [];
   @Link selectedIndexes: number[];
   @Link selectedIndexes: number[];
-  @State activeTaskCount: number = 0;
-  @State completedTaskCount: number = 0;
+  @State activeTasks: DownloadCenterTask[] = [];
+  @State completedTasks: DownloadCenterTask[] = [];
+  @State renderVersion: number = 0;
   onClose: () => void = () => {};
   onClose: () => void = () => {};
   onPauseTask: (taskId: string) => void = (_taskId: string): void => {};
   onPauseTask: (taskId: string) => void = (_taskId: string): void => {};
   onResumeTask: (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 {
   aboutToAppear(): void {
-    this.downloadCenterManager.subscribe(this.downloadCenterListener);
-    this.syncTasksFromManager();
-    this.startAutoRefresh();
+    this.syncTasksFromProps();
   }
   }
 
 
   aboutToDisappear(): void {
   aboutToDisappear(): void {
-    this.downloadCenterManager.unsubscribe(this.downloadCenterListener);
-    this.stopAutoRefresh();
+  }
+
+  aboutToReuse(): void {
+    this.syncTasksFromProps();
+  }
+
+  private onTasksVersionChanged(): void {
+    this.syncTasksFromProps();
   }
   }
 
 
   build() {
   build() {
@@ -95,58 +308,20 @@ export struct DownloadCenter {
     .backgroundColor($r('app.color.index_background'))
     .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);
-    }
+  private syncTasksFromProps(): void {
+    this.activeTasks = this.activeTasksProp.map((task: DownloadCenterTask): DownloadCenterTask => {
+      return this.cloneTask(task);
+    });
+    this.completedTasks = this.completedTasksProp.map((task: DownloadCenterTask): DownloadCenterTask => {
+      return this.cloneTask(task);
+    });
+    this.renderVersion = this.tasksVersion;
   }
   }
 
 
   @Builder
   @Builder
   private taskListBuilder() {
   private taskListBuilder() {
     if (this.getCurrentTabIndex() === 0) {
     if (this.getCurrentTabIndex() === 0) {
-      if (this.activeTaskCount <= 0) {
+      if (this.activeTasks.length <= 0) {
         Column() {
         Column() {
           Text('暂无下载任务')
           Text('暂无下载任务')
             .fontSize(14)
             .fontSize(14)
@@ -158,10 +333,17 @@ export struct DownloadCenter {
         .justifyContent(FlexAlign.Center)
         .justifyContent(FlexAlign.Center)
       } else {
       } else {
         List({ space: 10 }) {
         List({ space: 10 }) {
-          LazyForEach(this.activeTaskDataSource, (task: DownloadCenterTask) => {
-            this.taskListItemBuilder(task, true)
+          ForEach(this.activeTasks, (task: DownloadCenterTask) => {
+            DownloadCenterTaskRow({
+              themeColor: this.themeColor,
+              task: task,
+              isDownloading: true,
+              refreshVersion: this.renderVersion,
+              onPauseTask: this.onPauseTask,
+              onResumeTask: this.onResumeTask
+            })
           }, (task: DownloadCenterTask): string => {
           }, (task: DownloadCenterTask): string => {
-            return task.taskId;
+            return this.getTaskRenderKey(task);
           })
           })
         }
         }
         .scrollBar(BarState.Off)
         .scrollBar(BarState.Off)
@@ -169,7 +351,7 @@ export struct DownloadCenter {
         .width('100%')
         .width('100%')
       }
       }
     } else {
     } else {
-      if (this.completedTaskCount <= 0) {
+      if (this.completedTasks.length <= 0) {
         Column() {
         Column() {
           Text('暂无历史下载记录')
           Text('暂无历史下载记录')
             .fontSize(14)
             .fontSize(14)
@@ -181,10 +363,15 @@ export struct DownloadCenter {
         .justifyContent(FlexAlign.Center)
         .justifyContent(FlexAlign.Center)
       } else {
       } else {
         List({ space: 10 }) {
         List({ space: 10 }) {
-          LazyForEach(this.completedTaskDataSource, (task: DownloadCenterTask) => {
-            this.taskListItemBuilder(task, false)
+          ForEach(this.completedTasks, (task: DownloadCenterTask) => {
+            DownloadCenterTaskRow({
+              themeColor: this.themeColor,
+              task: task,
+              isDownloading: false,
+              refreshVersion: this.renderVersion
+            })
           }, (task: DownloadCenterTask): string => {
           }, (task: DownloadCenterTask): string => {
-            return task.taskId;
+            return this.getTaskRenderKey(task);
           })
           })
         }
         }
         .scrollBar(BarState.Off)
         .scrollBar(BarState.Off)
@@ -194,108 +381,6 @@ export struct DownloadCenter {
     }
     }
   }
   }
 
 
-  @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 {
   private showDownloadDirectoryDialog(): void {
     this.getUIContext().showAlertDialog({
     this.getUIContext().showAlertDialog({
       title: '下载目录说明',
       title: '下载目录说明',
@@ -318,16 +403,14 @@ export struct DownloadCenter {
   }
   }
 
 
   private resolveCurrentDownloadDirectory(): string {
   private resolveCurrentDownloadDirectory(): string {
-    const activeTasks: DownloadCenterTask[] = this.activeTaskDataSource.dataArray;
-    for (let i = 0; i < activeTasks.length; i += 1) {
-      const task: DownloadCenterTask = activeTasks[i];
+    for (let i = 0; i < this.activeTasks.length; i += 1) {
+      const task: DownloadCenterTask = this.activeTasks[i];
       if (StrUtil.isNotEmpty(task.downloadDir)) {
       if (StrUtil.isNotEmpty(task.downloadDir)) {
         return 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];
+    for (let i = 0; i < this.completedTasks.length; i += 1) {
+      const task: DownloadCenterTask = this.completedTasks[i];
       if (StrUtil.isNotEmpty(task.downloadDir)) {
       if (StrUtil.isNotEmpty(task.downloadDir)) {
         return task.downloadDir;
         return task.downloadDir;
       }
       }
@@ -335,124 +418,8 @@ export struct DownloadCenter {
     return '';
     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 getTaskRenderKey(task: DownloadCenterTask): string {
+    return task.taskId;
   }
   }
 
 
   private cloneTask(task: DownloadCenterTask): DownloadCenterTask {
   private cloneTask(task: DownloadCenterTask): DownloadCenterTask {
@@ -476,21 +443,6 @@ export struct DownloadCenter {
     };
     };
   }
   }
 
 
-  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 {
   private getCurrentTabIndex(): number {
     if (!this.selectedIndexes || this.selectedIndexes.length <= 0) {
     if (!this.selectedIndexes || this.selectedIndexes.length <= 0) {
       return 0;
       return 0;