Files
app/lib/presentation/widgets/file_list_item.dart
T
2026-05-27 19:19:11 +08:00

393 lines
12 KiB
Dart

import 'package:flutter/material.dart' hide DateUtils;
import 'package:lucide_icons/lucide_icons.dart';
import '../../data/models/file_model.dart';
import '../../core/utils/date_utils.dart';
import '../../core/utils/file_icon_utils.dart';
import '../../services/file_service.dart';
import 'file_menu_helper.dart';
/// 文件列表项
class FileListItem extends StatelessWidget {
final FileModel file;
final bool isSelected;
final bool isHighlighted;
final bool showCheckbox;
final int index;
final bool isDesktop;
final VoidCallback? onTap;
final VoidCallback? onSelect;
final VoidCallback? onDownload;
final VoidCallback? onOpenInBrowser;
final VoidCallback? onOpenInCloudreveApp;
final VoidCallback? onRename;
final VoidCallback? onMove;
final VoidCallback? onCopy;
final VoidCallback? onShare;
final VoidCallback? onDelete;
final VoidCallback? onRestore;
final VoidCallback? onInfo;
final bool tapToShowMenu;
const FileListItem({
super.key,
required this.file,
this.isSelected = false,
this.isHighlighted = false,
this.showCheckbox = false,
this.index = 0,
this.isDesktop = true,
this.tapToShowMenu = false,
this.onTap,
this.onSelect,
this.onDownload,
this.onOpenInBrowser,
this.onOpenInCloudreveApp,
this.onRename,
this.onMove,
this.onCopy,
this.onShare,
this.onDelete,
this.onRestore,
this.onInfo,
});
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: _FileListItemHover(
file: file,
isSelected: isSelected,
isHighlighted: isHighlighted,
index: index,
isDesktop: isDesktop,
showCheckbox: showCheckbox,
tapToShowMenu: tapToShowMenu,
onTap: tapToShowMenu ? null : onTap,
onLongPress: () => _showMenu(context),
onSelect: onSelect,
),
);
}
Future<void> _showMenu(BuildContext context) async {
final result = await showFileMenu(
context: context,
hasSelect: onSelect != null,
hasDownload: onDownload != null,
hasOpenInBrowser: onOpenInBrowser != null,
hasOpenInCloudreveApp: onOpenInCloudreveApp != null,
hasRename: onRename != null,
hasMove: onMove != null,
hasCopy: onCopy != null,
hasShare: onShare != null,
hasDelete: onDelete != null,
hasRestore: onRestore != null,
hasInfo: onInfo != null,
);
switch (result) {
case FileMenuAction.select:
onSelect?.call();
case FileMenuAction.download:
onDownload?.call();
case FileMenuAction.openInBrowser:
onOpenInBrowser?.call();
case FileMenuAction.openInCloudreveApp:
onOpenInCloudreveApp?.call();
case FileMenuAction.rename:
onRename?.call();
case FileMenuAction.move:
onMove?.call();
case FileMenuAction.copy:
onCopy?.call();
case FileMenuAction.share:
onShare?.call();
case FileMenuAction.delete:
onDelete?.call();
case FileMenuAction.restore:
onRestore?.call();
case FileMenuAction.info:
onInfo?.call();
case null:
break;
}
}
}
class _FileListItemHover extends StatefulWidget {
final FileModel file;
final bool isSelected;
final bool isHighlighted;
final int index;
final bool isDesktop;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final bool showCheckbox;
final VoidCallback? onSelect;
final bool tapToShowMenu;
const _FileListItemHover({
required this.file,
required this.isSelected,
required this.isHighlighted,
required this.index,
required this.isDesktop,
this.onTap,
this.onLongPress,
required this.showCheckbox,
this.onSelect,
this.tapToShowMenu = false,
});
@override
State<_FileListItemHover> createState() => _FileListItemHoverState();
}
class _FileListItemHoverState extends State<_FileListItemHover> {
bool _isHovered = false;
String? _folderSizeText;
bool _isCalculatingFolder = false;
Future<void> _calculateFolderSize() async {
setState(() => _isCalculatingFolder = true);
try {
final response = await FileService().getFileInfo(
uri: widget.file.relativePath,
folderSummary: true,
);
final summary = response['folder_summary'];
if (summary is Map<String, dynamic> && summary.containsKey('size')) {
if (mounted) {
setState(() {
_folderSizeText = DateUtils.formatFileSize(summary['size'] as int);
_isCalculatingFolder = false;
});
}
} else {
if (mounted) setState(() => _isCalculatingFolder = false);
}
} catch (_) {
if (mounted) setState(() => _isCalculatingFolder = false);
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
Color bgColor;
if (widget.isSelected) {
bgColor = colorScheme.primary.withValues(alpha: 0.08);
} else if (widget.isHighlighted) {
bgColor = colorScheme.primary.withValues(alpha: 0.06);
} else if (_isHovered) {
bgColor = colorScheme.primary.withValues(alpha: 0.05);
} else if (widget.index.isOdd) {
bgColor = colorScheme.surfaceContainerLow;
} else {
bgColor = colorScheme.surface;
}
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: GestureDetector(
onTap: widget.tapToShowMenu ? widget.onLongPress : widget.onTap,
onLongPress: widget.onLongPress,
onSecondaryTap: widget.onLongPress,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(4),
),
margin: const EdgeInsets.symmetric(vertical: 1, horizontal: 4),
padding: EdgeInsets.symmetric(
horizontal: widget.isDesktop ? 24 : 16,
vertical: 8,
),
child: widget.isDesktop
? _buildDesktopRow(context)
: _buildMobileRow(context),
),
),
);
}
Widget _buildSizeCell(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
if (!widget.file.isFolder) {
return Text(
DateUtils.formatFileSize(widget.file.size),
style: TextStyle(fontSize: 13, color: theme.hintColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
}
// 文件夹:已计算 -> 显示大小,未计算 -> 小按钮
if (_folderSizeText != null) {
return Text(
_folderSizeText!,
style: TextStyle(fontSize: 13, color: theme.hintColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
}
return _buildCalcButton(context, colorScheme);
}
Widget _buildCalcButton(BuildContext context, ColorScheme colorScheme) {
if (_isCalculatingFolder) {
return SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 1.5,
color: colorScheme.primary,
),
);
}
return InkWell(
onTap: _calculateFolderSize,
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: colorScheme.primary.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(LucideIcons.calculator, size: 11, color: colorScheme.primary),
const SizedBox(width: 3),
Text(
'计算',
style: TextStyle(fontSize: 11, color: colorScheme.primary, fontWeight: FontWeight.w500),
),
],
),
),
);
}
/// 桌面端:三列对齐 Row
Widget _buildDesktopRow(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final nameColor = widget.isSelected ? colorScheme.primary : colorScheme.onSurface;
return Row(
children: [
if (widget.showCheckbox)
SizedBox(
width: 40,
child: Checkbox(
value: widget.isSelected,
onChanged: (_) => widget.onSelect?.call(),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
FileIconUtils.buildIconWidget(context: context, file: widget.file),
const SizedBox(width: 16),
Expanded(
flex: 5,
child: Text(
widget.file.name,
style: TextStyle(
fontWeight: widget.isSelected ? FontWeight.w500 : FontWeight.normal,
fontSize: 14,
color: nameColor,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
flex: 2,
child: Text(
DateUtils.formatDateTime(widget.file.updatedAt),
style: TextStyle(fontSize: 13, color: theme.hintColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
flex: 1,
child: _buildSizeCell(context),
),
],
);
}
/// 窄屏端:两行紧凑布局
Widget _buildMobileRow(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final nameColor = widget.isSelected ? colorScheme.primary : colorScheme.onSurface;
// 构建第二行内容
final typeLabel = FileIconUtils.getFileTypeLabel(widget.file.name, isFolder: widget.file.isFolder);
final dateStr = DateUtils.formatDateTime(widget.file.updatedAt);
return Row(
children: [
if (widget.showCheckbox)
SizedBox(
width: 40,
child: Checkbox(
value: widget.isSelected,
onChanged: (_) => widget.onSelect?.call(),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
FileIconUtils.buildIconWidget(context: context, file: widget.file),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.file.name,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
color: nameColor,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
// 第二行:文件夹显示 计算按钮,文件显示类型|大小|日期
if (widget.file.isFolder)
Row(
children: [
Text('$typeLabel | $dateStr', style: TextStyle(fontSize: 12, color: theme.hintColor)),
const SizedBox(width: 6),
if (_folderSizeText != null)
Text('| $_folderSizeText', style: TextStyle(fontSize: 12, color: theme.hintColor))
else
_buildCalcButton(context, colorScheme),
],
)
else
Text(
'$typeLabel | ${DateUtils.formatFileSize(widget.file.size)} | $dateStr',
style: TextStyle(fontSize: 12, color: theme.hintColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
}
}