import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../models/cloud_file.dart'; import 'file_icon.dart'; /// A file row with Finder-style inline rename support and a context menu. class FileListTile extends StatefulWidget { final CloudFile file; final bool isSelected; final VoidCallback? onSelect; final VoidCallback? onOpen; final VoidCallback? onRename; final Future Function(String newName)? onRenameConfirm; final VoidCallback? onCopy; final VoidCallback? onCut; final VoidCallback? onDownload; final VoidCallback? onShare; final VoidCallback? onCopyFastTransfer; final VoidCallback? onDelete; final bool isRecycleItem; const FileListTile({ super.key, required this.file, this.isSelected = false, this.onSelect, this.onOpen, this.onRename, this.onRenameConfirm, this.onCopy, this.onCut, this.onDownload, this.onShare, this.onCopyFastTransfer, this.onDelete, this.isRecycleItem = false, }); @override State createState() => _FileListTileState(); } class _FileListTileState extends State { late final TextEditingController _renameController; late final FocusNode _renameFocusNode; var _isRenaming = false; var _isSubmittingRename = false; @override void initState() { super.initState(); _renameController = TextEditingController(text: widget.file.name); _renameFocusNode = FocusNode(debugLabel: 'rename-${widget.file.id}'); } @override void didUpdateWidget(covariant FileListTile oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.file.id != widget.file.id || (!_isRenaming && oldWidget.file.name != widget.file.name)) { _renameController.text = widget.file.name; } } @override void dispose() { _renameController.dispose(); _renameFocusNode.dispose(); super.dispose(); } void _beginRename() { if (widget.onRenameConfirm == null) { widget.onRename?.call(); return; } setState(() { _renameController.text = widget.file.name; _renameController.selection = TextSelection( baseOffset: 0, extentOffset: _renameController.text.length, ); _isRenaming = true; }); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _renameFocusNode.requestFocus(); }); } void _cancelRename() { setState(() { _renameController.text = widget.file.name; _isRenaming = false; }); } Future _confirmRename() async { final newName = _renameController.text.trim(); if (_isSubmittingRename || newName.isEmpty) return; if (newName == widget.file.name) { _cancelRename(); return; } final confirmed = await showShadDialog( context: context, builder: (dialogContext) => ShadDialog( title: const Text('确认重命名?'), description: Text('“${widget.file.name}” 将重命名为 “$newName”'), actions: [ ShadButton.outline( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('取消'), ), ShadButton( onPressed: () => Navigator.of(dialogContext).pop(true), child: const Text('确认重命名'), ), ], ), ); if (confirmed != true || !mounted) return; setState(() => _isSubmittingRename = true); try { await widget.onRenameConfirm!(newName); if (mounted) setState(() => _isRenaming = false); } finally { if (mounted) setState(() => _isSubmittingRename = false); } } @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); final isDark = theme.brightness == Brightness.dark; return ShadContextMenuRegion( items: [ ShadContextMenuItem.inset( leading: const Icon(LucideIcons.folderOpen, size: 16), trailing: const Icon(LucideIcons.chevronRight), onPressed: widget.onOpen, child: const Text('打开'), ), ShadContextMenuItem.inset( leading: const Icon(LucideIcons.pencil, size: 16), onPressed: _beginRename, child: const Text('重命名'), ), ShadContextMenuItem.inset( leading: const Icon(LucideIcons.copy, size: 16), trailing: const Icon(LucideIcons.chevronRight), onPressed: widget.onCopy, child: const Text('复制'), ), ShadContextMenuItem.inset( leading: const Icon(LucideIcons.scissors, size: 16), trailing: const Icon(LucideIcons.chevronRight), onPressed: widget.onCut, child: const Text('剪切'), ), const Divider(height: 8), ShadContextMenuItem.inset( leading: const Icon(LucideIcons.download, size: 16), trailing: const Icon(LucideIcons.chevronRight), onPressed: widget.onDownload, child: const Text('下载'), ), ShadContextMenuItem.inset( leading: const Icon(LucideIcons.share2, size: 16), trailing: const Icon(LucideIcons.chevronRight), onPressed: widget.onShare, child: const Text('分享'), ), ShadContextMenuItem.inset( leading: const Icon(LucideIcons.zap, size: 16), onPressed: widget.onCopyFastTransfer, child: const Text('复制秒传'), ), const Divider(height: 8), ShadContextMenuItem.inset( leading: Icon( widget.isRecycleItem ? LucideIcons.rotateCcw : LucideIcons.trash2, size: 16, color: widget.isRecycleItem ? theme.colorScheme.primary : theme.colorScheme.destructive, ), trailing: Icon( LucideIcons.chevronRight, color: widget.isRecycleItem ? theme.colorScheme.primary : theme.colorScheme.destructive, ), onPressed: widget.onDelete, child: Text( widget.isRecycleItem ? '恢复' : '删除', style: TextStyle( color: widget.isRecycleItem ? theme.colorScheme.primary : theme.colorScheme.destructive, ), ), ), ], child: GestureDetector( onTap: _isRenaming ? null : widget.onSelect, onDoubleTap: _isRenaming ? null : widget.onOpen, child: Container( height: 48, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: widget.isSelected ? (isDark ? theme.colorScheme.primary.withAlpha(30) : theme.colorScheme.primary.withAlpha(15)) : (isDark ? Colors.white.withAlpha(3) : Colors.black.withAlpha(2)), border: Border( bottom: BorderSide( color: isDark ? Colors.white.withAlpha(10) : Colors.black.withAlpha(8), width: 0.5, ), ), ), child: Row( children: [ if (widget.isSelected) Padding( padding: const EdgeInsets.only(right: 8), child: Icon( LucideIcons.checkCircle, size: 18, color: theme.colorScheme.primary, ), ), SizedBox( width: 32, height: 32, child: FileIcon(file: widget.file), ), const SizedBox(width: 12), Expanded( child: _isRenaming ? Row( children: [ Expanded( child: ShadInput( controller: _renameController, focusNode: _renameFocusNode, autofocus: true, enabled: !_isSubmittingRename, onSubmitted: (_) => _confirmRename(), ), ), const SizedBox(width: 4), ShadButton.ghost( size: ShadButtonSize.sm, onPressed: _isSubmittingRename ? null : _confirmRename, child: const Icon(Icons.check_rounded, size: 17), ), ShadButton.ghost( size: ShadButtonSize.sm, onPressed: _isSubmittingRename ? null : _cancelRename, child: const Icon(Icons.close_rounded, size: 17), ), ], ) : Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.file.name, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: theme.colorScheme.foreground, ), overflow: TextOverflow.ellipsis, ), if (widget.file.isDirectory && widget.file.subFileCount != null) Text( '${widget.file.subFileCount} 个项目', style: TextStyle( fontSize: 11, color: theme.colorScheme.mutedForeground, ), ), ], ), ), SizedBox( width: 80, child: Text( widget.file.formattedSize, textAlign: TextAlign.right, style: TextStyle( fontSize: 12, color: theme.colorScheme.mutedForeground, ), ), ), const SizedBox(width: 12), SizedBox( width: 120, child: Text( widget.file.modifiedAt, textAlign: TextAlign.right, style: TextStyle( fontSize: 11, color: theme.colorScheme.mutedForeground, ), overflow: TextOverflow.ellipsis, ), ), ], ), ), ), ); } }