import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../data/models/sync_task_model.dart'; import '../../providers/sync_provider.dart'; import '../../widgets/toast_helper.dart'; import 'sync_settings_page.dart'; /// 同步 Tab 页 - 展示实时同步状态、活跃任务和已完成任务 class SyncPage extends StatefulWidget { const SyncPage({super.key}); @override State createState() => _SyncPageState(); } class _SyncPageState extends State { // 展开的任务 ID final Set _expandedTasks = {}; // 正在加载详情的任务 ID final Set _loadingDetails = {}; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().loadRecentTasks(); }); } @override Widget build(BuildContext context) { final sync = context.watch(); final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: const Text('文件同步'), actions: [ IconButton( icon: const Icon(Icons.settings_outlined), onPressed: () => _navigateToSettings(), tooltip: '同步设置', ), ], ), body: RefreshIndicator( onRefresh: () async { final sync = context.read(); sync.invalidateAllTaskDetails(); await sync.loadRecentTasks(); }, child: ListView( padding: const EdgeInsets.symmetric(vertical: 8), children: [ _buildStatusCard(sync, theme), const SizedBox(height: 8), _buildActiveTasksSection(sync, theme), const SizedBox(height: 8), _buildCompletedTasksSection(sync, theme), const SizedBox(height: 32), ], ), ), ); } void _navigateToSettings() { Navigator.of(context).push( MaterialPageRoute(builder: (_) => const SyncSettingsPage()), ); } Widget _buildStatusCard(SyncProvider sync, ThemeData theme) { final isActive = sync.isActive; final isPaused = sync.isPaused; final hasError = sync.hasError; Color statusColor; IconData statusIcon; String statusText; if (isActive) { statusColor = theme.colorScheme.primary; statusIcon = Icons.sync; statusText = _syncModeLabel(sync); } else if (isPaused) { statusColor = Colors.orange; statusIcon = Icons.pause_circle_outline; statusText = '已暂停'; } else if (hasError) { statusColor = theme.colorScheme.error; statusIcon = Icons.error_outline; statusText = '同步错误'; } else if (sync.state == SyncState.stopped) { statusColor = theme.disabledColor; statusIcon = Icons.stop_circle_outlined; statusText = '已停止'; } else { statusColor = theme.disabledColor; statusIcon = Icons.cloud_off; statusText = '未启动'; } return Card( margin: const EdgeInsets.symmetric(horizontal: 5), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(statusIcon, color: statusColor, size: 28), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( statusText, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), if (sync.errorMessage != null) Text( sync.errorMessage!, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.error, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), if (sync.activeWorkerCount > 0) Badge( label: Text('${sync.activeWorkerCount}'), child: Icon(LucideIcons.loader, color: statusColor, size: 24), ), ], ), if (isActive || isPaused) ...[ const SizedBox(height: 12), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: sync.totalFiles > 0 ? sync.progress : null, minHeight: 6, ), ), const SizedBox(height: 8), Text( sync.totalFiles > 0 ? '${sync.syncedFiles} / ${sync.totalFiles} 文件' : sync.state == SyncState.continuous ? '持续同步中' : '正在同步...', style: theme.textTheme.bodySmall, ), ], if (sync.lastSummary != null) ...[ const SizedBox(height: 12), Wrap( spacing: 12, runSpacing: 4, children: [ _summaryChip(theme, '上传', sync.lastSummary!.uploaded), _summaryChip(theme, '下载', sync.lastSummary!.downloaded), _summaryChip(theme, '冲突', sync.lastSummary!.conflicts), _summaryChip(theme, '失败', sync.lastSummary!.failed), _summaryChip(theme, '重命名', sync.lastSummary!.renamed), _summaryChip(theme, '移动', sync.lastSummary!.moved), ], ), ], const SizedBox(height: 12), Row( children: [ if (!sync.isActive && !sync.isPaused) Expanded( child: FilledButton.icon( onPressed: () => _navigateToSettings(), icon: const Icon(Icons.play_arrow, size: 18), label: const Text('开始同步'), ), ), if (sync.isActive) Expanded( child: OutlinedButton.icon( onPressed: () => sync.pause(), icon: const Icon(Icons.pause, size: 18), label: const Text('暂停'), ), ), if (sync.isPaused) Expanded( child: FilledButton.icon( onPressed: () => sync.resume(), icon: const Icon(Icons.play_arrow, size: 18), label: const Text('恢复'), ), ), if (sync.isActive || sync.isPaused) ...[ const SizedBox(width: 8), Expanded( child: OutlinedButton.icon( onPressed: () => _stopSync(sync), icon: const Icon(Icons.stop, size: 18), label: const Text('停止'), style: OutlinedButton.styleFrom( foregroundColor: theme.colorScheme.error, ), ), ), ], ], ), ], ), ), ); } Widget _summaryChip(ThemeData theme, String label, int value) { return Row( mainAxisSize: MainAxisSize.min, children: [ Text('$label:', style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor)), Text(' $value', style: theme.textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600)), ], ); } String _syncModeLabel(SyncProvider sync) { final mode = sync.persistedConfig?.syncMode ?? 'full'; return switch (mode) { 'full' => '全量同步中', 'upload_only' => '仅上传中', 'download_only' => '仅下载中', 'album' => '相册同步中', 'mirror_wcf' => '镜像同步中', _ => '同步中', }; } Widget _buildActiveTasksSection(SyncProvider sync, ThemeData theme) { final tasks = sync.activeTasks; return _buildTaskSection( theme: theme, title: '正在同步', icon: LucideIcons.loader, tasks: tasks, emptyText: '暂无正在进行的同步任务', ); } Widget _buildCompletedTasksSection(SyncProvider sync, ThemeData theme) { final tasks = sync.recentTasks .where((t) => (t.status == 'completed' || t.status == 'failed' || t.status == 'cancelled') && t.totalCount > 0) .toList(); return _buildTaskSection( theme: theme, title: '已完成', icon: LucideIcons.checkCircle2, tasks: tasks, emptyText: '暂无已完成的同步任务', ); } Widget _buildTaskSection({ required ThemeData theme, required String title, required IconData icon, required List tasks, required String emptyText, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 8), child: Row( children: [ Icon(icon, size: 18, color: theme.colorScheme.primary), const SizedBox(width: 8), Text( title, style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), ), ], ), ), if (tasks.isEmpty) Card( margin: const EdgeInsets.symmetric(horizontal: 5), child: Padding( padding: const EdgeInsets.all(24), child: Center( child: Text(emptyText, style: TextStyle(color: theme.hintColor)), ), ), ) else ...tasks.map((task) => _buildTaskCard(task, theme)), ], ); } Widget _buildTaskCard(SyncTaskModel task, ThemeData theme) { final isExpanded = _expandedTasks.contains(task.id); return Card( margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), child: Column( children: [ InkWell( onTap: () => _toggleTaskExpand(task.id), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 12), child: Row( children: [ GestureDetector( onTap: () { Clipboard.setData(ClipboardData(text: task.id)); ToastHelper.success('已复制任务 ID'); }, child: Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 72, child: Text( task.shortId, style: theme.textTheme.bodySmall?.copyWith( fontFamily: 'monospace', color: theme.hintColor, ), ), ), Icon(Icons.copy, size: 14, color: theme.hintColor), ], ), ), Expanded( child: Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: _statusColor(task.status, theme).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Text( task.statusLabel, style: theme.textTheme.bodySmall?.copyWith( color: _statusColor(task.status, theme), fontWeight: FontWeight.w600, ), ), ), const SizedBox(width: 12), Text( '${task.completedCount}/${task.totalCount}', style: theme.textTheme.bodySmall, ), if (task.failedCount > 0) ...[ const SizedBox(width: 8), Text( '失败${task.failedCount}', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.error, ), ), ], ], ), ), Icon( isExpanded ? Icons.expand_less : Icons.expand_more, size: 20, color: theme.hintColor, ), ], ), ), ), if (isExpanded) _buildTaskDetailList(task, theme), ], ), ); } Widget _buildTaskDetailList(SyncTaskModel task, ThemeData theme) { final sync = context.watch(); final items = sync.getCachedTaskDetail(task.id); if (_loadingDetails.contains(task.id)) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))), ); } if (items == null || items.isEmpty) { return Padding( padding: const EdgeInsets.all(16), child: Text('暂无任务项', style: TextStyle(color: theme.hintColor)), ); } final hasMore = sync.hasMoreTaskDetail(task.id); return Column( children: [ const Divider(height: 1, indent: 16, endIndent: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 6), child: Row( children: [ SizedBox(width: 70, child: Text('操作', style: theme.textTheme.labelSmall?.copyWith(color: theme.hintColor))), Expanded(child: Text('文件名', style: theme.textTheme.labelSmall?.copyWith(color: theme.hintColor))), SizedBox(width: 55, child: Text('状态', style: theme.textTheme.labelSmall?.copyWith(color: theme.hintColor))), SizedBox(width: 110, child: Text('时间', style: theme.textTheme.labelSmall?.copyWith(color: theme.hintColor))), ], ), ), ...items.map((item) => _buildTaskItemRow(item, theme)), if (hasMore) Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: TextButton.icon( onPressed: () => _loadMoreDetail(task.id), icon: const Icon(Icons.expand_more, size: 16), label: const Text('加载更多'), ), ), const SizedBox(height: 4), ], ); } Widget _buildTaskItemRow(SyncTaskItemModel item, ThemeData theme) { final timeStr = _formatTime(item.updatedAt); return Padding( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3), child: Row( children: [ SizedBox( width: 70, child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration( color: _actionColor(item.actionType, theme).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(3), ), child: Text( item.actionLabel, style: theme.textTheme.bodySmall?.copyWith( fontSize: 11, color: _actionColor(item.actionType, theme), ), ), ), ), const SizedBox(width: 8), Expanded( child: Text( item.filename, style: theme.textTheme.bodySmall, overflow: TextOverflow.ellipsis, maxLines: 1, ), ), SizedBox( width: 55, child: Text( item.statusLabel, style: theme.textTheme.bodySmall?.copyWith( color: _itemStatusColor(item.status, theme), ), ), ), SizedBox( width: 110, child: Text( timeStr, style: theme.textTheme.bodySmall?.copyWith( fontFamily: 'monospace', fontSize: 11, ), ), ), ], ), ); } Color _statusColor(String status, ThemeData theme) { return switch (status) { 'running' => theme.colorScheme.primary, 'completed' => Colors.green, 'failed' => theme.colorScheme.error, 'cancelled' => theme.hintColor, _ => theme.hintColor, }; } Color _actionColor(String actionType, ThemeData theme) { return switch (actionType) { 'upload' => Colors.blue, 'download' => Colors.green, 'create_placeholder' => Colors.teal, 'delete_local' || 'delete_remote' => theme.colorScheme.error, 'rename' || 'move' => Colors.orange, 'conflict_resolve' => Colors.purple, _ => theme.colorScheme.primary, }; } Color _itemStatusColor(String status, ThemeData theme) { return switch (status) { 'completed' => Colors.green, 'failed' => theme.colorScheme.error, 'running' => theme.colorScheme.primary, _ => theme.hintColor, }; } String _formatTime(String isoTime) { try { final dt = DateTime.parse(isoTime).toLocal(); return '${dt.year}/${dt.month.toString().padLeft(2, '0')}/${dt.day.toString().padLeft(2, '0')} ' '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}'; } catch (_) { return isoTime; } } void _toggleTaskExpand(String taskId) { final sync = context.read(); if (_expandedTasks.contains(taskId)) { setState(() => _expandedTasks.remove(taskId)); sync.unwatchTaskDetail(taskId); return; } setState(() => _expandedTasks.add(taskId)); sync.watchTaskDetail(taskId); if (sync.getCachedTaskDetail(taskId) == null && !_loadingDetails.contains(taskId)) { setState(() => _loadingDetails.add(taskId)); sync.getTaskDetail(taskId).whenComplete(() { if (mounted) { setState(() => _loadingDetails.remove(taskId)); } }); } } void _loadMoreDetail(String taskId) { final sync = context.read(); sync.loadMoreTaskDetail(taskId); } Future _stopSync(SyncProvider sync) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('停止同步'), content: const Text('确定要停止文件同步吗?'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('取消'), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), style: FilledButton.styleFrom( backgroundColor: Theme.of(ctx).colorScheme.error, ), child: const Text('停止'), ), ], ), ); if (confirmed == true) { await sync.stop(); } } }