Files
app/lib/presentation/widgets/share_list_item.dart
T
2026-05-15 09:03:09 +08:00

293 lines
9.7 KiB
Dart

import 'package:flutter/material.dart';
import '../../data/models/share_model.dart';
/// 截断文件名,智能截断避免多个小数点
String _truncateFileName(String name, int maxLength) {
if (name.length <= maxLength) {
return name;
}
// 查找最后一个点
final lastDotIndex = name.lastIndexOf('.');
// 如果没有点,或者点在开头或末尾,直接截断
if (lastDotIndex <= 0 || lastDotIndex >= name.length - 1) {
return '${name.substring(0, maxLength - 3)}...';
}
// 有扩展名的情况
final extension = name.substring(lastDotIndex);
final nameWithoutExt = name.substring(0, lastDotIndex);
// 扩展名太长,直接截断整个文件名
if (extension.length >= maxLength) {
return '${name.substring(0, maxLength - 3)}...';
}
// 计算主体部分能显示的长度
const ellipsis = '...';
final maxNameLength = maxLength - extension.length - ellipsis.length;
// 空间太小,主体部分至少保留3个字符
if (maxNameLength < 3) {
return '${name.substring(0, maxLength - 3)}...';
}
// 主体部分能完整显示
if (nameWithoutExt.length <= maxNameLength) {
return '$nameWithoutExt$ellipsis$extension';
}
// 主体部分需要截断
return '${nameWithoutExt.substring(0, maxNameLength)}$ellipsis$extension';
}
/// 文件名显示组件
class _FileNameDisplay extends StatelessWidget {
final String name;
final Color? textColor;
const _FileNameDisplay({
required this.name,
this.textColor,
});
/// 精确计算文本宽度
String _fitText(String text, double maxWidth, TextStyle style) {
final textPainter = TextPainter(
text: TextSpan(text: text, style: style),
maxLines: 1,
textDirection: TextDirection.ltr,
);
textPainter.layout();
if (textPainter.width <= maxWidth) {
return text;
}
// 二分查找最佳长度
int min = 1;
int max = text.length;
int best = 0;
while (min <= max) {
final mid = ((min + max) / 2).floor();
final candidate = _truncateFileName(text, mid);
final painter = TextPainter(
text: TextSpan(text: candidate, style: style),
maxLines: 1,
textDirection: TextDirection.ltr,
);
painter.layout();
if (painter.width <= maxWidth) {
best = mid;
min = mid + 1;
} else {
max = mid - 1;
}
}
return _truncateFileName(text, best);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
final style = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: textColor,
);
final fittedName = _fitText(name, maxWidth, style);
return Text(
fittedName,
style: style,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
},
);
}
}
/// 分享列表项
class ShareListItem extends StatelessWidget {
final ShareModel share;
final VoidCallback? onEdit;
final VoidCallback? onDelete;
const ShareListItem({
super.key,
required this.share,
this.onEdit,
this.onDelete,
});
@override
Widget build(BuildContext context) {
final expired = share.expired;
final textColor = expired ? Colors.grey.shade600 : null;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: InkWell(
onTap: onEdit,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
flex: 65,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
share.isFolder
? Icons.folder
: Icons.insert_drive_file_outlined,
color: textColor,
size: 24,
),
const SizedBox(width: 12),
Expanded(
child: _FileNameDisplay(
name: share.name,
textColor: textColor,
),
),
],
),
const SizedBox(height: 4),
Text(
share.url,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
Expanded(
flex: 35,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (share.isPrivate ?? false) ...[
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.lock, size: 14, color: Colors.blue),
],
),
),
],
if (share.downloaded != null) ...[
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${share.downloaded}',
style: TextStyle(fontSize: 10, color: Colors.grey.shade600),
),
Icon(Icons.download, size: 14, color: Colors.green.shade700),
],
),
),
],
...[
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${share.visited}',
style: TextStyle(fontSize: 10, color: Colors.grey.shade600),
),
Icon(Icons.visibility, size: 14, color: Colors.yellow.shade700),
],
),
),
],
],
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (onEdit != null) ...[
IconButton(
icon: const Icon(Icons.edit, size: 24),
onPressed: onEdit,
tooltip: '编辑',
constraints: const BoxConstraints(
minWidth: 24,
minHeight: 24,
),
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
],
if (onDelete != null) ...[
IconButton(
icon: const Icon(Icons.delete, size: 24),
onPressed: onDelete,
tooltip: '删除',
constraints: const BoxConstraints(
minWidth: 24,
minHeight: 24,
),
style: IconButton.styleFrom(
foregroundColor: Colors.red.shade700,
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
],
],
),
],
),
),
],
),
),
),
);
}
}