二次开发源码提交
This commit is contained in:
@@ -0,0 +1,579 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cloudreve4_flutter/data/models/download_task_model.dart';
|
||||
import 'package:cloudreve4_flutter/presentation/providers/download_manager_provider.dart';
|
||||
import 'package:cloudreve4_flutter/presentation/widgets/download_progress_item.dart';
|
||||
import 'package:cloudreve4_flutter/core/utils/date_utils.dart' as date_utils;
|
||||
import 'package:cloudreve4_flutter/services/download_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:cloudreve4_flutter/presentation/widgets/toast_helper.dart';
|
||||
import 'package:cloudreve4_flutter/core/utils/app_logger.dart';
|
||||
|
||||
class DownloadTasksTab extends StatelessWidget {
|
||||
const DownloadTasksTab({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Consumer<DownloadManagerProvider>(
|
||||
builder: (context, downloadManager, _) {
|
||||
final allTasks = downloadManager.tasks;
|
||||
final activeTasks = allTasks.where((t) =>
|
||||
t.status == DownloadStatus.downloading || t.status == DownloadStatus.waiting || t.status == DownloadStatus.paused).toList();
|
||||
final completedTasks = allTasks.where((t) => t.status == DownloadStatus.completed).toList();
|
||||
final failedTasks = allTasks.where((t) =>
|
||||
t.status == DownloadStatus.failed || t.status == DownloadStatus.cancelled).toList();
|
||||
|
||||
if (allTasks.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(LucideIcons.download, size: 48, color: theme.hintColor.withValues(alpha: 0.4)),
|
||||
const SizedBox(height: 16),
|
||||
Text('暂无下载任务', style: TextStyle(color: theme.hintColor)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isDesktop = constraints.maxWidth >= 800;
|
||||
|
||||
if (isDesktop) {
|
||||
return _buildDesktopLayout(
|
||||
context,
|
||||
downloadManager,
|
||||
allTasks: allTasks,
|
||||
activeTasks: activeTasks,
|
||||
failedTasks: failedTasks,
|
||||
completedTasks: completedTasks,
|
||||
);
|
||||
}
|
||||
|
||||
return _buildMobileLayout(
|
||||
context,
|
||||
downloadManager,
|
||||
activeTasks: activeTasks,
|
||||
failedTasks: failedTasks,
|
||||
completedTasks: completedTasks,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileLayout(
|
||||
BuildContext context,
|
||||
DownloadManagerProvider downloadManager, {
|
||||
required List<DownloadTaskModel> activeTasks,
|
||||
required List<DownloadTaskModel> failedTasks,
|
||||
required List<DownloadTaskModel> completedTasks,
|
||||
}) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 80),
|
||||
children: [
|
||||
if (activeTasks.isNotEmpty) ...[
|
||||
_buildSectionHeader(context, '进行中', activeTasks.length),
|
||||
...activeTasks.map((task) => DownloadProgressItem(
|
||||
task: task,
|
||||
onPause: () => downloadManager.pauseDownload(task.id),
|
||||
onResume: () => downloadManager.resumeDownload(task.id),
|
||||
onCancel: () => downloadManager.cancelDownload(task.id),
|
||||
)),
|
||||
],
|
||||
if (failedTasks.isNotEmpty) ...[
|
||||
_buildSectionHeader(context, '失败', failedTasks.length,
|
||||
actionLabel: '清除失败',
|
||||
onAction: () => _confirmClear(context, '失败', failedTasks.length, () => downloadManager.clearFailedTasks())),
|
||||
...failedTasks.map((task) => DownloadProgressItem(
|
||||
task: task,
|
||||
onRetry: () => downloadManager.retryDownload(task.id),
|
||||
onDelete: () => _confirmDeleteDownloadTask(context, task, downloadManager),
|
||||
)),
|
||||
],
|
||||
if (completedTasks.isNotEmpty) ...[
|
||||
_buildSectionHeader(context, '已完成', completedTasks.length,
|
||||
actionLabel: '清除已完成',
|
||||
onAction: () => _confirmClear(context, '已完成', completedTasks.length, () => downloadManager.clearCompletedTasks())),
|
||||
...completedTasks.map((task) => DownloadProgressItem(
|
||||
task: task,
|
||||
onDelete: () => _confirmDeleteDownloadTask(context, task, downloadManager),
|
||||
)),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDesktopLayout(
|
||||
BuildContext context,
|
||||
DownloadManagerProvider downloadManager, {
|
||||
required List<DownloadTaskModel> allTasks,
|
||||
required List<DownloadTaskModel> activeTasks,
|
||||
required List<DownloadTaskModel> failedTasks,
|
||||
required List<DownloadTaskModel> completedTasks,
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final sortedTasks = [
|
||||
...activeTasks.reversed,
|
||||
...failedTasks.reversed,
|
||||
...completedTasks.reversed,
|
||||
];
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
if (failedTasks.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(LucideIcons.trash2, size: 14),
|
||||
label: const Text('清除失败', style: TextStyle(fontSize: 12)),
|
||||
onPressed: () => _confirmClear(context, '失败', failedTasks.length, () => downloadManager.clearFailedTasks()),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: colorScheme.error,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (completedTasks.isNotEmpty && failedTasks.isEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(LucideIcons.trash2, size: 14),
|
||||
label: const Text('清除已完成', style: TextStyle(fontSize: 12)),
|
||||
onPressed: () => _confirmClear(context, '已完成', completedTasks.length, () => downloadManager.clearCompletedTasks()),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: colorScheme.error,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.all(colorScheme.surfaceContainerHighest),
|
||||
columnSpacing: 24,
|
||||
columns: const [
|
||||
DataColumn(label: Text('名称')),
|
||||
DataColumn(label: Text('状态')),
|
||||
DataColumn(label: Text('进度')),
|
||||
DataColumn(label: Text('大小')),
|
||||
DataColumn(label: Text('速度/完成时间')),
|
||||
DataColumn(label: Text('操作')),
|
||||
],
|
||||
rows: sortedTasks.map((task) => _buildDownloadDataRow(context, task, downloadManager)).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
DataRow _buildDownloadDataRow(
|
||||
BuildContext context,
|
||||
DownloadTaskModel task,
|
||||
DownloadManagerProvider downloadManager,
|
||||
) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final errorColor = colorScheme.error;
|
||||
final statusColor = _getStatusColor(task.status, waitingForWifi: task.waitingForWifi);
|
||||
final statusIcon = _getStatusIcon(task.status, waitingForWifi: task.waitingForWifi);
|
||||
final isActive = task.status == DownloadStatus.downloading ||
|
||||
task.status == DownloadStatus.waiting ||
|
||||
task.status == DownloadStatus.paused;
|
||||
|
||||
return DataRow(
|
||||
cells: [
|
||||
// 名称 (with status icon)
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(statusIcon, size: 18, color: statusColor),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Flexible(
|
||||
child: Text(
|
||||
task.fileName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 状态
|
||||
DataCell(
|
||||
Text(
|
||||
task.statusText,
|
||||
style: TextStyle(color: statusColor, fontSize: 13),
|
||||
),
|
||||
),
|
||||
// 进度
|
||||
DataCell(
|
||||
isActive
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: LinearProgressIndicator(
|
||||
value: task.status == DownloadStatus.paused ? null : task.progress,
|
||||
backgroundColor: colorScheme.surfaceContainerHighest,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(colorScheme.primary),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
task.status == DownloadStatus.paused ? '已暂停' : task.progressText,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
task.status == DownloadStatus.completed ? '100%' : '-',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
// 大小
|
||||
DataCell(
|
||||
Text(
|
||||
DownloadService.getReadableFileSize(task.fileSize),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
// 速度/完成时间
|
||||
DataCell(
|
||||
Text(
|
||||
task.status == DownloadStatus.completed
|
||||
? (task.completedAt != null ? _formatDateTime(task.completedAt!) : '-')
|
||||
: (task.speedText.isNotEmpty ? task.speedText : '-'),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: task.status == DownloadStatus.completed
|
||||
? null
|
||||
: (task.speedText.isNotEmpty ? colorScheme.primary : null),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 操作
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _buildDesktopActionButtons(context, task, downloadManager, errorColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildDesktopActionButtons(
|
||||
BuildContext context,
|
||||
DownloadTaskModel task,
|
||||
DownloadManagerProvider downloadManager,
|
||||
Color errorColor,
|
||||
) {
|
||||
switch (task.status) {
|
||||
case DownloadStatus.waiting:
|
||||
if (task.waitingForWifi) {
|
||||
return [
|
||||
IconButton(
|
||||
icon: Icon(Icons.cancel, size: 18, color: errorColor),
|
||||
onPressed: () => downloadManager.cancelDownload(task.id),
|
||||
tooltip: '取消',
|
||||
),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
case DownloadStatus.downloading:
|
||||
return [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.pause, size: 18),
|
||||
onPressed: () => downloadManager.pauseDownload(task.id),
|
||||
tooltip: '暂停',
|
||||
),
|
||||
];
|
||||
case DownloadStatus.paused:
|
||||
return [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.play_arrow, size: 18),
|
||||
onPressed: () => downloadManager.resumeDownload(task.id),
|
||||
tooltip: '继续',
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.cancel, size: 18, color: errorColor),
|
||||
onPressed: () => downloadManager.cancelDownload(task.id),
|
||||
tooltip: '取消',
|
||||
),
|
||||
];
|
||||
case DownloadStatus.failed:
|
||||
return [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
onPressed: () => downloadManager.retryDownload(task.id),
|
||||
tooltip: '重试',
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete, size: 18, color: errorColor),
|
||||
onPressed: () => _confirmDeleteDownloadTask(context, task, downloadManager),
|
||||
tooltip: '删除',
|
||||
),
|
||||
];
|
||||
case DownloadStatus.completed:
|
||||
final showOpenFolder = !Platform.isAndroid;
|
||||
return [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.open_in_new, size: 18),
|
||||
onPressed: () => _openDownloadedFile(context, task),
|
||||
tooltip: '打开',
|
||||
),
|
||||
if (showOpenFolder)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.folder_open, size: 18),
|
||||
onPressed: () => _openFileFolder(context, task),
|
||||
tooltip: '打开文件夹',
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete_outline, size: 18, color: errorColor),
|
||||
onPressed: () => _confirmDeleteDownloadTask(context, task, downloadManager),
|
||||
tooltip: '删除',
|
||||
),
|
||||
];
|
||||
case DownloadStatus.cancelled:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getStatusIcon(DownloadStatus status, {bool waitingForWifi = false}) {
|
||||
switch (status) {
|
||||
case DownloadStatus.waiting:
|
||||
return waitingForWifi ? LucideIcons.wifi : LucideIcons.clock;
|
||||
case DownloadStatus.downloading:
|
||||
return LucideIcons.download;
|
||||
case DownloadStatus.completed:
|
||||
return LucideIcons.checkCircle2;
|
||||
case DownloadStatus.paused:
|
||||
return LucideIcons.pause;
|
||||
case DownloadStatus.failed:
|
||||
case DownloadStatus.cancelled:
|
||||
return LucideIcons.xCircle;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatusColor(DownloadStatus status, {bool waitingForWifi = false}) {
|
||||
switch (status) {
|
||||
case DownloadStatus.waiting:
|
||||
return waitingForWifi ? Colors.blue : Colors.grey;
|
||||
case DownloadStatus.downloading:
|
||||
return Colors.blue;
|
||||
case DownloadStatus.completed:
|
||||
return Colors.green;
|
||||
case DownloadStatus.paused:
|
||||
return Colors.orange;
|
||||
case DownloadStatus.failed:
|
||||
case DownloadStatus.cancelled:
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> checkInstallPermission() async {
|
||||
if (await Permission.requestInstallPackages.isDenied) {
|
||||
await Permission.requestInstallPackages.request();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openDownloadedFile(
|
||||
BuildContext context,
|
||||
DownloadTaskModel task,
|
||||
) async {
|
||||
final file = File(task.savePath);
|
||||
if (!await file.exists()) {
|
||||
if (context.mounted) {
|
||||
ToastHelper.error('文件不存在:${task.fileName}');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final ext = task.savePath.toString().split('.').last.toLowerCase();
|
||||
if (ext == 'apk') {
|
||||
await checkInstallPermission();
|
||||
}
|
||||
|
||||
OpenResult openResult = await OpenFile.open(task.savePath);
|
||||
AppLogger.d('下载对话框打开文件结果:${openResult.type}');
|
||||
if (openResult.type == ResultType.done) {
|
||||
AppLogger.d('成功打开文件:${task.fileName}');
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
ToastHelper.error(
|
||||
'无法打开文件:${task.fileName} 错误信息: ${openResult.message}',
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ToastHelper.error('打开文件失败:$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openFileFolder(
|
||||
BuildContext context,
|
||||
DownloadTaskModel task,
|
||||
) async {
|
||||
try {
|
||||
final dir = File(task.savePath).parent.path;
|
||||
final result = await OpenFile.open(dir);
|
||||
if (result.type != ResultType.done && context.mounted) {
|
||||
ToastHelper.error('无法打开文件夹:${result.message}');
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ToastHelper.error('打开文件夹失败:$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(
|
||||
BuildContext context,
|
||||
String title,
|
||||
int count, {
|
||||
String? actionLabel,
|
||||
VoidCallback? onAction,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 8, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'$title ($count)',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: theme.hintColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (actionLabel != null && onAction != null)
|
||||
TextButton.icon(
|
||||
icon: const Icon(LucideIcons.trash2, size: 14),
|
||||
label: Text(actionLabel, style: const TextStyle(fontSize: 12)),
|
||||
onPressed: onAction,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.error,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmClear(BuildContext context, String label, int count, VoidCallback onConfirm) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('清除$label'),
|
||||
content: Text('确定要清除 $count 个$label的任务吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
style: FilledButton.styleFrom(backgroundColor: Theme.of(ctx).colorScheme.error),
|
||||
child: const Text('清除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) onConfirm();
|
||||
}
|
||||
|
||||
Future<void> _confirmDeleteDownloadTask(
|
||||
BuildContext context,
|
||||
DownloadTaskModel task,
|
||||
DownloadManagerProvider downloadManager,
|
||||
) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('删除下载任务'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('确定要删除该任务吗?'),
|
||||
const SizedBox(height: 8),
|
||||
Text(task.fileName, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 4),
|
||||
Text('下载时间: ${_formatDateTime(task.createdAt)}', style: TextStyle(fontSize: 12, color: Theme.of(ctx).hintColor)),
|
||||
Text('文件大小: ${date_utils.DateUtils.formatFileSize(task.fileSize)}', style: TextStyle(fontSize: 12, color: Theme.of(ctx).hintColor)),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
style: FilledButton.styleFrom(backgroundColor: Theme.of(ctx).colorScheme.error),
|
||||
child: const Text('删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) downloadManager.deleteDownloadTask(task.id);
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime dateTime) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(dateTime);
|
||||
|
||||
if (difference.inSeconds < 60) {
|
||||
return '刚刚';
|
||||
} else if (difference.inMinutes < 60) {
|
||||
return '${difference.inMinutes}分钟前';
|
||||
} else if (difference.inHours < 24) {
|
||||
return '${difference.inHours}小时前';
|
||||
} else {
|
||||
return '${dateTime.month}/${dateTime.day} ${dateTime.hour}:${dateTime.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user