import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../../../backend/transfer/model.dart'; class TransferItem extends StatelessWidget { const TransferItem({ super.key, required this.transfer, this.onAccept, this.onReject, this.onCancel, this.onDelete, }); final Transfer transfer; final VoidCallback? onAccept; final VoidCallback? onReject; final VoidCallback? onCancel; final VoidCallback? onDelete; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Card( child: Padding( padding: const EdgeInsets.all(14), child: Column( children: [ Row( children: [ CircleAvatar( backgroundColor: theme.colorScheme.secondaryContainer, child: Icon( _iconForContentType(transfer.contentType), color: theme.colorScheme.onSecondaryContainer, ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( transfer.fileName.isNotEmpty ? transfer.fileName : '文本消息', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleMedium, ), Text( '${transfer.sender.name} · ${transfer.type.name}', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), _StatusChip(status: transfer.status), ], ), const SizedBox(height: 12), LinearProgressIndicator( value: _progressOrNull(transfer), color: _progressColor(theme, transfer.status), ), const SizedBox(height: 10), Row( children: [ Expanded( child: Text( '进度 ${_progressPercent(transfer).toStringAsFixed(0)}% 大小 ${_formatSize(transfer.fileSize)} 速度 ${_formatSpeed(transfer.speed)}', style: theme.textTheme.bodySmall, ), ), Wrap(spacing: 8, children: _buildActions(context)), ], ), ], ), ), ); } List _buildActions(BuildContext context) { final actions = []; if (transfer.status is TransferStatus_Pending) { if (onReject != null) { actions.add( OutlinedButton.icon( onPressed: onReject, icon: const Icon(Icons.close_rounded), label: const Text('拒绝'), ), ); } if (onAccept != null) { actions.add( FilledButton.icon( onPressed: onAccept, icon: const Icon(Icons.check_rounded), label: const Text('接收'), ), ); } return actions; } if ((transfer.status is TransferStatus_Active || transfer.status is TransferStatus_Accepted) && onCancel != null) { actions.add( OutlinedButton.icon( onPressed: onCancel, icon: const Icon(Icons.stop_circle_outlined), label: const Text('取消'), ), ); } final canPreviewText = transfer.type == TransferType.receive && transfer.contentType == ContentType.text && transfer.status is TransferStatus_Completed; if (canPreviewText) { actions.add( IconButton( tooltip: '复制文本', onPressed: () async { final messenger = ScaffoldMessenger.of(context); await Clipboard.setData(ClipboardData(text: transfer.text)); messenger.showSnackBar(const SnackBar(content: Text('文本已复制'))); }, icon: const Icon(Icons.copy_rounded), ), ); actions.add( IconButton( tooltip: '查看文本', onPressed: () { showDialog( context: context, builder: (_) => AlertDialog( title: const Text('接收文本内容'), content: SizedBox( width: 560, child: SingleChildScrollView( child: SelectableText(transfer.text), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('关闭'), ), ], ), ); }, icon: const Icon(Icons.visibility_rounded), ), ); } if ((transfer.status is TransferStatus_Completed || transfer.status is TransferStatus_Error || transfer.status is TransferStatus_Canceled) && onDelete != null) { actions.add( IconButton( tooltip: '删除记录', onPressed: onDelete, icon: const Icon(Icons.delete_outline_rounded), ), ); } return actions; } IconData _iconForContentType(ContentType contentType) { return switch (contentType) { ContentType.file => Icons.insert_drive_file_rounded, ContentType.folder => Icons.folder_zip_rounded, ContentType.text => Icons.text_snippet_rounded, }; } double? _progressOrNull(Transfer transfer) { if (transfer.status is TransferStatus_Pending) { return null; } return _progressFraction(transfer); } Color _progressColor(ThemeData theme, TransferStatus status) { return switch (status) { TransferStatus_Completed() => theme.colorScheme.primary, TransferStatus_Active() || TransferStatus_Accepted() => theme.colorScheme.tertiary, TransferStatus_Rejected() || TransferStatus_Error() => theme.colorScheme.error, TransferStatus_Canceled() => theme.colorScheme.outline, TransferStatus_Pending() => theme.colorScheme.secondary, }; } String _formatSpeed(double? bytesPerSec) { if (bytesPerSec == null || bytesPerSec <= 0) { return '--'; } return '${_formatSize(bytesPerSec)}/s'; } String _formatSize(double bytes) { if (bytes <= 0) { return '0 B'; } const units = ['B', 'KB', 'MB', 'GB', 'TB']; var size = bytes; var i = 0; while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; } final fixed = size >= 100 ? 0 : (size >= 10 ? 1 : 2); return '${size.toStringAsFixed(fixed)} ${units[i]}'; } double _progressedBytes(Transfer transfer) { final current = transfer.progress; if (current <= 0) { return 0; } if (transfer.fileSize <= 0) { return current; } return current.clamp(0, transfer.fileSize).toDouble(); } double _progressFraction(Transfer transfer) { if (transfer.fileSize <= 0) { return 0; } return (_progressedBytes(transfer) / transfer.fileSize) .clamp(0, 1) .toDouble(); } double _progressPercent(Transfer transfer) { return _progressFraction(transfer) * 100; } } class _StatusChip extends StatelessWidget { const _StatusChip({required this.status}); final TransferStatus status; @override Widget build(BuildContext context) { final label = switch (status) { TransferStatus_Pending() => 'Pending', TransferStatus_Accepted() => 'Accepted', TransferStatus_Rejected() => 'Rejected', TransferStatus_Completed() => 'Completed', TransferStatus_Error() => 'Error', TransferStatus_Canceled() => 'Canceled', TransferStatus_Active() => 'Active', }; return Chip(label: Text(label)); } }