import { SegmentButton, SegmentButtonItemTuple, SegmentButtonOptions } from '@kit.ArkUI'; import { DownloadCenterTask } from '../common/util/DownloadCenterManager'; import { CommonConstants } from '../common/constants/CommonConstants'; import { StrUtil } from '@pura/harmony-utils'; import { PointLightDefaultButton } from './PointLight/PointLightDeFaultButton'; import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil'; @Observed class DownloadCenterTaskState implements DownloadCenterTask { taskId: string = ''; title: string = ''; fileName: string = ''; coverPath: string = ''; sizeText: string = ''; sourceUrl: string = ''; targetPath: string = ''; downloadDir: string = ''; totalBytes: number = 0; downloadedBytes: number = 0; progress: number = 0; speedBytesPerSec: number = 0; status: 'pending' | 'downloading' | 'paused' | 'completed' | 'failed' = 'pending'; errorMessage: string = ''; createdAt: number = 0; finishedAt: number = 0; constructor(task: DownloadCenterTask) { this.apply(task); } apply(task: DownloadCenterTask): void { this.taskId = task.taskId; this.title = task.title; this.fileName = task.fileName; this.coverPath = task.coverPath; this.sizeText = task.sizeText; this.sourceUrl = task.sourceUrl; this.targetPath = task.targetPath; this.downloadDir = task.downloadDir; this.totalBytes = task.totalBytes; this.downloadedBytes = task.downloadedBytes; this.progress = task.progress; this.speedBytesPerSec = task.speedBytesPerSec; this.status = task.status; this.errorMessage = task.errorMessage; this.createdAt = task.createdAt; this.finishedAt = task.finishedAt; } } @Component struct DownloadCenterTaskRow { @Prop themeColor: string = CommonConstants.DEFAULT_THEME_COLOR; @ObjectLink task: DownloadCenterTaskState; @Prop isDownloading: boolean = true; @Prop refreshVersion: number = 0; @Prop isSelectionMode: boolean = false; @Prop isSelected: boolean = false; onSelectionChange: (taskId: string, selected: boolean) => void = (_taskId: string, _selected: boolean): void => {}; onPauseTask: (taskId: string) => void = (_taskId: string): void => {}; onResumeTask: (taskId: string) => void = (_taskId: string): void => {}; onDeleteTask: (taskId: string) => void = (_taskId: string): void => {}; onRowClick: (taskId: string) => void = (_taskId: string): void => {}; build() { ListItem() { Column({ space: 8 }) { Row({ space: 10 }) { if (this.isSelectionMode && this.isDownloading) { Checkbox({ name: this.task.taskId }) .select(this.isSelected) .onChange((value: boolean) => { this.onSelectionChange(this.task.taskId, value) }) } Image(StrUtil.isNotEmpty(this.task.coverPath) ? this.task.coverPath : $r('app.media.alt')) .width(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) .padding({bottom:4}) .textOverflow({ overflow: TextOverflow.Ellipsis }) if (this.isDownloading) { Progress({ value: this.task.progress, total: 100, type: ProgressType.Linear }) .width('100%') .color(this.getTaskStatusColor()) .padding({bottom:4}) .backgroundColor($r('app.color.track_color')) .style({ strokeWidth: 5 }) Row() { Text(this.getTaskProgressLabelText()) .fontSize(12) .fontColor(this.getTaskStatusColor()) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) Text(this.getTaskSpeedText()) .fontSize(12) .fontColor($r('app.color.text_color')) .opacity(0.72) .margin({ right: 8 }) Text(this.getTaskProgressInfo()) .fontSize(12) .fontColor($r('app.color.text_color')) .opacity(0.6) } .width('100%') } else { Row() { Text(this.getTaskTotalSizeText()) .fontSize(12) .fontColor($r('app.color.text_color')) .opacity(0.6) Text(' · ') .fontSize(12) .fontColor($r('app.color.text_color')) .opacity(0.35) Text('已完成') .fontSize(12) .fontColor($r('app.color.text_color')) .opacity(0.85) } .width('100%') } } .alignItems(HorizontalAlign.Start) .layoutWeight(1) if (this.isDownloading) { this.taskActionButtonBuilder() } } .width('100%') Text(`${this.refreshVersion}`) .fontSize(0.1) .fontColor(Color.Transparent) .opacity(0) .width(0) .height(0) } .width('100%') .padding(10) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 }) .border({ color: $r('app.color.index_background'), width: 1.8 }) .borderRadius(15) } .swipeAction({ end: this.deleteActionBuilder(), edgeEffect: SwipeEdgeEffect.None }) .onClick(() => { if (this.isSelectionMode && this.isDownloading) { this.onSelectionChange(this.task.taskId, !this.isSelected) return } this.onRowClick(this.task.taskId) }) } @Builder private taskActionButtonBuilder() { Button() { PointLightDefaultButton({ isPx: false, isSysBol: true, pointColor: this.isTaskRunning() ? $r('app.color.text_color') : this.themeColor, imageResource: this.isTaskRunning() ? $r('sys.symbol.pause') : $r('sys.symbol.play_fill'), builderHeight: 36, builderWidth: 36, buttonScale: 1, canShadow: true, }) } .width(38) .height(38) .backgroundColor(Color.Transparent) .borderRadius(16) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 }) .stateEffect(false) .onClick(() => { if (this.isTaskRunning()) { this.onPauseTask(this.task.taskId); return; } this.onResumeTask(this.task.taskId); }) } @Builder private deleteActionBuilder() { Row() { Button('删除') .width(72) .height(52) .fontSize(14) .fontColor(Color.White) .backgroundColor($r('app.color.btn_red')) .borderRadius(14) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 }) .onClick(() => { this.onDeleteTask(this.task.taskId) }) } .padding({ left: 8, right: 4 }) .justifyContent(FlexAlign.Center) } private isTaskRunning(): boolean { return this.task.status === 'downloading'; } 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 getEffectiveTaskStatus(): 'pending' | 'downloading' | 'paused' | 'completed' | 'failed' { if (this.shouldUsePauseStyleForFailure()) { return 'paused'; } return this.task.status; } private shouldUsePauseStyleForFailure(): boolean { if (this.task.status !== 'failed') { return false; } const message = StrUtil.isNotEmpty(this.task.errorMessage) ? this.task.errorMessage.toLowerCase() : ''; return message.includes('failed writing received') || message.includes('failed wwiting received') || message.includes('writing received'); } private getTaskProgressLabelText(): string { const status = this.getEffectiveTaskStatus(); if (status === 'failed') { return StrUtil.isNotEmpty(this.task.errorMessage) ? this.task.errorMessage : '下载失败'; } if (status === 'paused') { return `已暂停 ${this.getTaskProgressLabel()}`; } if (status === 'pending') { return `等待中 ${this.getTaskProgressLabel()}`; } return this.getTaskProgressLabel(); } private getTaskSpeedText(): string { const status = this.getEffectiveTaskStatus(); if (status === 'failed') { return '--'; } if (status === 'paused' || status === 'pending') { return '0 B/s'; } if (this.task.speedBytesPerSec > 0) { return `${this.formatBytes(this.task.speedBytesPerSec)}/s`; } return '0 B/s'; } private getTaskStatusText(): string { switch (this.getEffectiveTaskStatus()) { case 'downloading': return '下载中'; case 'paused': return '已暂停'; case 'failed': return '下载失败'; case 'pending': return '等待中'; case 'completed': default: return '已完成'; } } private getTaskStatusColor() { const status = this.getEffectiveTaskStatus(); if (status === 'failed') { return $r('app.color.btn_red'); } if (status === 'paused' || 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 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; @Prop @Watch('onTasksVersionChanged') tasksVersion: number = 0; @Prop activeTasksProp: DownloadCenterTask[] = []; @Prop completedTasksProp: DownloadCenterTask[] = []; @Link selectedIndexes: number[]; @State activeTasks: DownloadCenterTaskState[] = []; @State completedTasks: DownloadCenterTaskState[] = []; @State renderVersion: number = 0; @State isSelectionMode: boolean = false; @State selectedTaskIds: string[] = []; onClose: () => void = () => {}; onPauseTask: (taskId: string) => void = (_taskId: string): void => {}; onResumeTask: (taskId: string) => void = (_taskId: string): void => {}; onDeleteTask: (taskId: string) => void = (_taskId: string): void => {}; onPlayCompletedTask: (taskId: string) => void = (_taskId: string): void => {}; aboutToAppear(): void { this.syncTasksFromProps(); } aboutToDisappear(): void { } aboutToReuse(): void { this.syncTasksFromProps(); } private onTasksVersionChanged(): void { this.syncTasksFromProps(); } build() { Column({ space: 12 }) { Row({ space: 10 }) { Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.chevron_left')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .attributeModifier(new ShadowModifier()) .onClick(() => this.onClose()) Text('下载中心') .fontSize(20) .fontWeight(FontWeight.Medium) .fontColor($r('app.color.text_color')) .layoutWeight(1) if (this.getCurrentTabIndex() === 0 && this.activeTasks.length >= 2) { Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph(this.isSelectionMode ? $r('sys.symbol.checkmark_circle') : $r('sys.symbol.checkmark_square_on_square')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .attributeModifier(new ShadowModifier()) .onClick(() => { this.toggleSelectionMode() }) } Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.exclamationmark')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .attributeModifier(new ShadowModifier()) .zIndex(0) .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) if (this.isSelectionMode && this.getCurrentTabIndex() === 0) { this.selectionActionBarBuilder() } } .width('100%') .height('100%') .padding({ top: this.topSafeHeight + 10, left: 12, right: 12, bottom: this.bottomSafeHeight + 12 }) .backgroundColor($r('app.color.start_window_background')) } private syncTasksFromProps(): void { this.activeTasks = this.mergeTaskStates(this.activeTasks, this.activeTasksProp); this.completedTasks = this.mergeTaskStates(this.completedTasks, this.completedTasksProp); this.selectedTaskIds = this.filterExistingSelectedTaskIds(); this.renderVersion = this.tasksVersion; } @Builder private taskListBuilder() { if (this.getCurrentTabIndex() === 0) { if (this.activeTasks.length <= 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 }) { ForEach(this.activeTasks, (task: DownloadCenterTaskState) => { DownloadCenterTaskRow({ themeColor: this.themeColor, task: task, isDownloading: true, refreshVersion: this.renderVersion, isSelectionMode: this.isSelectionMode, isSelected: this.isTaskSelected(task.taskId), onSelectionChange: (taskId: string, selected: boolean) => { this.setTaskSelection(taskId, selected) }, onPauseTask: this.onPauseTask, onResumeTask: this.onResumeTask, onDeleteTask: this.handleDeleteTask.bind(this), onRowClick: (_taskId: string): void => {} }) }, (task: DownloadCenterTaskState): string => { return this.getTaskRenderKey(task); }) } .scrollBar(BarState.Off) .height('100%') .width('100%') } } else { if (this.completedTasks.length <= 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 }) { ForEach(this.completedTasks, (task: DownloadCenterTaskState) => { DownloadCenterTaskRow({ themeColor: this.themeColor, task: task, isDownloading: false, refreshVersion: this.renderVersion, onDeleteTask: this.handleDeleteTask.bind(this), onRowClick: this.handleCompletedTaskClick.bind(this) }) }, (task: DownloadCenterTaskState): string => { return this.getTaskRenderKey(task); }) } .scrollBar(BarState.Off) .height('100%') .width('100%') } } } @Builder private selectionActionBarBuilder() { Row({ space: 10 }) { Button(this.isAllActiveTasksSelected() ? '反选' : '全选') .layoutWeight(1) .height(38) .fontSize(13) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 }) .backgroundColor($r('app.color.bg_card')) .fontColor($r('app.color.text_color')) .onClick(() => { this.toggleSelectAllActiveTasks() }) Button('开始') .layoutWeight(1) .height(38) .fontSize(13) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 }) .backgroundColor(this.themeColor) .fontColor(Color.White) .onClick(() => { this.resumeSelectedTasks() }) Button('暂停') .layoutWeight(1) .height(38) .fontSize(13) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 }) .backgroundColor($r('app.color.bg_card')) .fontColor($r('app.color.text_color')) .onClick(() => { this.pauseSelectedTasks() }) Button('删除') .layoutWeight(1) .height(38) .fontSize(13) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 }) .backgroundColor($r('app.color.btn_red')) .fontColor(Color.White) .onClick(() => { this.deleteSelectedTasks() }) } .width('100%') } private showDownloadDirectoryDialog(): void { this.getUIContext().showAlertDialog({ title: '下载目录说明', 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 { for (let i = 0; i < this.activeTasks.length; i += 1) { const task: DownloadCenterTaskState = this.activeTasks[i]; if (StrUtil.isNotEmpty(task.downloadDir)) { return task.downloadDir; } } for (let i = 0; i < this.completedTasks.length; i += 1) { const task: DownloadCenterTaskState = this.completedTasks[i]; if (StrUtil.isNotEmpty(task.downloadDir)) { return task.downloadDir; } } return ''; } private mergeTaskStates(currentTasks: DownloadCenterTaskState[], nextTasks: DownloadCenterTask[]): DownloadCenterTaskState[] { const nextStateMap: Map = new Map(); for (let i = 0; i < currentTasks.length; i += 1) { const state: DownloadCenterTaskState = currentTasks[i]; nextStateMap.set(state.taskId, state); } const mergedTasks: DownloadCenterTaskState[] = []; for (let i = 0; i < nextTasks.length; i += 1) { const nextTask: DownloadCenterTask = nextTasks[i]; const existingState: DownloadCenterTaskState | undefined = nextStateMap.get(nextTask.taskId); if (existingState) { existingState.apply(nextTask); mergedTasks.push(existingState); continue; } mergedTasks.push(new DownloadCenterTaskState(nextTask)); } return mergedTasks; } private toggleSelectionMode(): void { this.isSelectionMode = !this.isSelectionMode; if (!this.isSelectionMode) { this.selectedTaskIds = []; } } private setTaskSelection(taskId: string, selected: boolean): void { const index: number = this.selectedTaskIds.indexOf(taskId); if (selected) { if (index < 0) { this.selectedTaskIds = [...this.selectedTaskIds, taskId]; } return; } if (index >= 0) { const nextSelected: string[] = this.selectedTaskIds.slice(); nextSelected.splice(index, 1); this.selectedTaskIds = nextSelected; } } private isTaskSelected(taskId: string): boolean { return this.selectedTaskIds.indexOf(taskId) >= 0; } private isAllActiveTasksSelected(): boolean { return this.activeTasks.length > 0 && this.selectedTaskIds.length === this.activeTasks.length; } private toggleSelectAllActiveTasks(): void { if (this.isAllActiveTasksSelected()) { this.selectedTaskIds = []; return; } this.selectedTaskIds = this.activeTasks.map((task: DownloadCenterTaskState): string => task.taskId); } private pauseSelectedTasks(): void { for (let i = 0; i < this.selectedTaskIds.length; i += 1) { this.onPauseTask(this.selectedTaskIds[i]); } } private resumeSelectedTasks(): void { for (let i = 0; i < this.selectedTaskIds.length; i += 1) { this.onResumeTask(this.selectedTaskIds[i]); } } private deleteSelectedTasks(): void { const taskIds: string[] = [...this.selectedTaskIds]; this.removeTasksLocally(taskIds); for (let i = 0; i < taskIds.length; i += 1) { this.onDeleteTask(taskIds[i]); } this.selectedTaskIds = []; this.isSelectionMode = false; } private handleDeleteTask(taskId: string): void { if (StrUtil.isEmpty(taskId)) { return } this.removeTasksLocally([taskId]) this.onDeleteTask(taskId) } private handleCompletedTaskClick(taskId: string): void { if (StrUtil.isEmpty(taskId)) { return } this.onPlayCompletedTask(taskId) } private getTaskRenderKey(task: DownloadCenterTaskState): string { return task.taskId; } private filterExistingSelectedTaskIds(): string[] { if (this.selectedTaskIds.length <= 0) { return []; } const activeTaskIds: Set = new Set(); for (let i = 0; i < this.activeTasks.length; i += 1) { activeTaskIds.add(this.activeTasks[i].taskId); } const nextSelected: string[] = []; for (let i = 0; i < this.selectedTaskIds.length; i += 1) { const taskId: string = this.selectedTaskIds[i]; if (activeTaskIds.has(taskId)) { nextSelected.push(taskId); } } return nextSelected; } private removeTasksLocally(taskIds: string[]): void { if (taskIds.length <= 0) { return; } this.activeTasks = this.activeTasks.filter((task: DownloadCenterTaskState): boolean => { return taskIds.indexOf(task.taskId) < 0; }); this.completedTasks = this.completedTasks.filter((task: DownloadCenterTaskState): boolean => { return taskIds.indexOf(task.taskId) < 0; }); } private getCurrentTabIndex(): number { if (!this.selectedIndexes || this.selectedIndexes.length <= 0) { return 0; } return this.selectedIndexes[0]; } }