Files
app/lib/presentation/pages/tasks/widgets/upload_tasks_tab.dart
T
2026-05-15 09:03:09 +08:00

522 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:cloudreve4_flutter/data/models/upload_task_model.dart';
import 'package:cloudreve4_flutter/presentation/providers/file_manager_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/navigation_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/upload_manager_provider.dart';
import 'package:cloudreve4_flutter/presentation/widgets/upload_progress_item.dart';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:provider/provider.dart';
class UploadTasksTab extends StatelessWidget {
const UploadTasksTab({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Consumer<UploadManagerProvider>(
builder: (context, uploadManager, _) {
final allTasks = uploadManager.allTasks;
final activeTasks = allTasks.where((t) =>
t.status == UploadStatus.uploading || t.status == UploadStatus.waiting || t.status == UploadStatus.paused).toList();
final completedTasks = allTasks.where((t) => t.status == UploadStatus.completed).toList();
final failedTasks = allTasks.where((t) =>
t.status == UploadStatus.failed || t.status == UploadStatus.cancelled).toList();
if (allTasks.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(LucideIcons.upload, 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,
uploadManager,
allTasks: allTasks,
activeTasks: activeTasks,
failedTasks: failedTasks,
completedTasks: completedTasks,
);
}
return _buildMobileLayout(
context,
uploadManager,
activeTasks: activeTasks,
failedTasks: failedTasks,
completedTasks: completedTasks,
);
},
);
},
);
}
Widget _buildMobileLayout(
BuildContext context,
UploadManagerProvider uploadManager, {
required List<UploadTaskModel> activeTasks,
required List<UploadTaskModel> failedTasks,
required List<UploadTaskModel> completedTasks,
}) {
return ListView(
padding: const EdgeInsets.only(top: 8, bottom: 80),
children: [
if (activeTasks.isNotEmpty) ...[
_buildSectionHeader(context, '进行中', activeTasks.length),
...activeTasks.map((task) => UploadProgressItem(
task: task,
onPause: () => uploadManager.cancelUpload(task.id),
onResume: () => uploadManager.retryUpload(task.id),
onCancel: () => uploadManager.cancelUpload(task.id),
)),
],
if (failedTasks.isNotEmpty) ...[
_buildSectionHeader(context, '失败', failedTasks.length,
actionLabel: '清除失败',
onAction: () => _confirmClear(context, '失败', failedTasks.length, () => uploadManager.clearFailedTasks())),
...failedTasks.map((task) => UploadProgressItem(
task: task,
onRetry: () => uploadManager.retryUpload(task.id),
onDelete: () => _confirmDeleteUploadTask(context, task, uploadManager),
)),
],
if (completedTasks.isNotEmpty) ...[
_buildSectionHeader(context, '已完成', completedTasks.length,
actionLabel: '清除已完成',
onAction: () => _confirmClear(context, '已完成', completedTasks.length, () => uploadManager.clearCompletedTasks())),
...completedTasks.map((task) => UploadProgressItem(
task: task,
onNavigate: () => _navigateToUploadedFile(context, task),
onDelete: () => _confirmDeleteUploadTask(context, task, uploadManager),
)),
],
],
);
}
Widget _buildDesktopLayout(
BuildContext context,
UploadManagerProvider uploadManager, {
required List<UploadTaskModel> allTasks,
required List<UploadTaskModel> activeTasks,
required List<UploadTaskModel> failedTasks,
required List<UploadTaskModel> 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, () => uploadManager.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, () => uploadManager.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) => _buildUploadDataRow(context, task, uploadManager)).toList(),
),
),
),
],
),
);
}
DataRow _buildUploadDataRow(
BuildContext context,
UploadTaskModel task,
UploadManagerProvider uploadManager,
) {
final colorScheme = Theme.of(context).colorScheme;
final errorColor = colorScheme.error;
final statusColor = _getStatusColor(task.status);
final statusIcon = _getStatusIcon(task.status);
final isActive = task.status == UploadStatus.uploading ||
task.status == UploadStatus.waiting ||
task.status == UploadStatus.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 == UploadStatus.paused ? null : task.progress,
backgroundColor: colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation<Color>(colorScheme.primary),
),
),
const SizedBox(width: 8),
Text(
task.status == UploadStatus.paused ? '已暂停' : task.progressText,
style: const TextStyle(fontSize: 12),
),
],
)
: Text(
task.status == UploadStatus.completed ? '100%' : '-',
style: const TextStyle(fontSize: 12),
),
),
// 大小
DataCell(Text(task.readableFileSize, style: const TextStyle(fontSize: 13))),
// 速度/完成时间
DataCell(
Text(
task.status == UploadStatus.completed
? (task.completedAt != null ? _formatDateTime(task.completedAt!) : '-')
: (task.speedText.isNotEmpty ? task.speedText : '-'),
style: TextStyle(
fontSize: 13,
color: task.status == UploadStatus.completed
? null
: (task.speedText.isNotEmpty ? colorScheme.primary : null),
),
),
),
// 操作
DataCell(
Row(
mainAxisSize: MainAxisSize.min,
children: _buildDesktopActionButtons(context, task, uploadManager, errorColor),
),
),
],
);
}
List<Widget> _buildDesktopActionButtons(
BuildContext context,
UploadTaskModel task,
UploadManagerProvider uploadManager,
Color errorColor,
) {
switch (task.status) {
case UploadStatus.waiting:
case UploadStatus.uploading:
return [
IconButton(
icon: const Icon(Icons.pause, size: 18),
onPressed: () => uploadManager.cancelUpload(task.id),
tooltip: '暂停',
),
];
case UploadStatus.paused:
return [
IconButton(
icon: const Icon(Icons.play_arrow, size: 18),
onPressed: () => uploadManager.retryUpload(task.id),
tooltip: '继续',
),
IconButton(
icon: Icon(Icons.cancel, size: 18, color: errorColor),
onPressed: () => uploadManager.cancelUpload(task.id),
tooltip: '取消',
),
];
case UploadStatus.failed:
return [
IconButton(
icon: const Icon(Icons.refresh, size: 18),
onPressed: () => uploadManager.retryUpload(task.id),
tooltip: '重试',
),
IconButton(
icon: Icon(Icons.delete, size: 18, color: errorColor),
onPressed: () => _confirmDeleteUploadTask(context, task, uploadManager),
tooltip: '删除',
),
];
case UploadStatus.completed:
return [
IconButton(
icon: const Icon(LucideIcons.folderOpen, size: 18),
onPressed: () => _navigateToUploadedFile(context, task),
tooltip: '打开文件夹',
),
IconButton(
icon: Icon(Icons.delete_outline, size: 18, color: errorColor),
onPressed: () => _confirmDeleteUploadTask(context, task, uploadManager),
tooltip: '删除',
),
];
case UploadStatus.cancelled:
return [
IconButton(
icon: Icon(Icons.delete, size: 18, color: errorColor),
onPressed: () => _confirmDeleteUploadTask(context, task, uploadManager),
tooltip: '删除',
),
];
}
}
IconData _getStatusIcon(UploadStatus status) {
switch (status) {
case UploadStatus.waiting:
return LucideIcons.clock;
case UploadStatus.uploading:
return LucideIcons.upload;
case UploadStatus.completed:
return LucideIcons.checkCircle2;
case UploadStatus.paused:
return LucideIcons.pause;
case UploadStatus.failed:
case UploadStatus.cancelled:
return LucideIcons.xCircle;
}
}
Color _getStatusColor(UploadStatus status) {
switch (status) {
case UploadStatus.waiting:
return Colors.orange;
case UploadStatus.uploading:
return Colors.blue;
case UploadStatus.completed:
return Colors.green;
case UploadStatus.paused:
return Colors.orange;
case UploadStatus.failed:
case UploadStatus.cancelled:
return Colors.red;
}
}
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> _confirmDeleteUploadTask(
BuildContext context,
UploadTaskModel task,
UploadManagerProvider uploadManager,
) 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('文件大小: ${task.readableFileSize}', 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) uploadManager.removeTask(task.id);
}
void _navigateToUploadedFile(BuildContext context, UploadTaskModel task) {
// targetPath 格式: cloudreve://my/folder
final targetPath = task.targetPath;
String relativePath;
if (targetPath.startsWith('cloudreve://my')) {
relativePath = targetPath.replaceFirst('cloudreve://my', '');
if (relativePath.isEmpty) relativePath = '/';
} else {
relativePath = targetPath;
}
// 构造文件完整路径用于高亮
final filePath = targetPath.endsWith('/')
? '$targetPath${task.fileName}'
: '$targetPath/${task.fileName}';
final fileManager = Provider.of<FileManagerProvider>(context, listen: false);
final navProvider = Provider.of<NavigationProvider>(context, listen: false);
fileManager.navigateAndHighlight(relativePath, filePath);
navProvider.setIndex(1);
}
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')}';
}
}
}