This commit is contained in:
2026-02-27 21:12:56 +08:00
commit a878084cbb
233 changed files with 22988 additions and 0 deletions

View File

@@ -0,0 +1,282 @@
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<Widget> _buildActions(BuildContext context) {
final actions = <Widget>[];
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<void>(
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));
}
}