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 _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 _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 && 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, ), ], ), ), ], ); } }