init
This commit is contained in:
282
lib/app/features/transfer/widgets/transfer_item.dart
Normal file
282
lib/app/features/transfer/widgets/transfer_item.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user