283 lines
8.0 KiB
Dart
283 lines
8.0 KiB
Dart
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));
|
|
}
|
|
}
|