二次开发源码提交
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import 'package:cloudreve4_flutter/data/models/upload_task_model.dart';
|
||||
import 'package:cloudreve4_flutter/presentation/providers/download_manager_provider.dart';
|
||||
import 'package:cloudreve4_flutter/presentation/providers/upload_manager_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'widgets/upload_tasks_tab.dart';
|
||||
import 'widgets/download_tasks_tab.dart';
|
||||
|
||||
class TasksPage extends StatelessWidget {
|
||||
const TasksPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('任务'),
|
||||
bottom: const _TasksTabBar(),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: [
|
||||
UploadTasksTab(),
|
||||
DownloadTasksTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TasksTabBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
const _TasksTabBar();
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(48);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Consumer2<UploadManagerProvider, DownloadManagerProvider>(
|
||||
builder: (context, uploadManager, downloadManager, _) {
|
||||
final uploadActiveCount = uploadManager.allTasks
|
||||
.where((t) =>
|
||||
t.status == UploadStatus.uploading ||
|
||||
t.status == UploadStatus.waiting ||
|
||||
t.status == UploadStatus.paused)
|
||||
.length;
|
||||
final downloadActiveCount = downloadManager.activeTaskCount;
|
||||
|
||||
return TabBar(
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(LucideIcons.upload, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
const Text('上传'),
|
||||
if (uploadActiveCount > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 18),
|
||||
child: Text(
|
||||
'$uploadActiveCount',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(LucideIcons.download, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
const Text('下载'),
|
||||
if (downloadActiveCount > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 18),
|
||||
child: Text(
|
||||
'$downloadActiveCount',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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')}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
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')}';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user