import 'package:flutter/material.dart'; import '../../../data/models/file_model.dart'; import '../../../services/file_service.dart'; import '../../widgets/file_grid_item.dart'; import '../../widgets/file_list_item.dart'; import '../../widgets/gesture_handler_mixin.dart'; import '../../widgets/toast_helper.dart'; /// 回收站页面 class RecycleBinPage extends StatefulWidget { const RecycleBinPage({super.key}); @override State createState() => _RecycleBinPageState(); } class _RecycleBinPageState extends State with GestureHandlerMixin { List _files = []; Set _selectedFiles = {}; bool _isLoading = false; String? _errorMessage; FileViewType _viewType = FileViewType.list; @override void initState() { super.initState(); _loadFiles(); } @override Widget build(BuildContext context) { return PopScope( canPop: !_hasSelection, onPopInvokedWithResult: (didPop, result) async { if (!didPop && _hasSelection) { setState(() { _selectedFiles.clear(); }); } }, child: Scaffold( appBar: _buildAppBar(context), body: _buildBody(context), bottomNavigationBar: _buildBottomBar(context), ), ); } PreferredSizeWidget _buildAppBar(BuildContext context) { return AppBar( title: const Text('回收站'), actions: [ IconButton( icon: const Icon(Icons.select_all), onPressed: () { if (_hasSelection) { setState(() { _selectedFiles.clear(); }); } else { setState(() { _selectedFiles = _files.map((f) => f.path).toSet(); }); } }, tooltip: _hasSelection ? '取消选择' : '全选', ), IconButton( icon: Icon( _viewType == FileViewType.list ? Icons.grid_view : Icons.view_list, ), onPressed: () { setState(() { _viewType = _viewType == FileViewType.list ? FileViewType.grid : FileViewType.list; }); }, tooltip: _viewType == FileViewType.list ? '网格视图' : '列表视图', ), ], ); } Widget _buildBody(BuildContext context) { if (_isLoading && _files.isEmpty) { return const Center(child: CircularProgressIndicator()); } return RefreshIndicator( onRefresh: _refreshFiles, child: _buildContent(context), ); } Widget _buildContent(BuildContext context) { if (_errorMessage != null) { return CustomScrollView( slivers: [ SliverFillRemaining( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 64, color: Colors.red), const SizedBox(height: 16), Text( _errorMessage!, textAlign: TextAlign.center, style: TextStyle(color: Colors.grey.shade600), ), const SizedBox(height: 16), FilledButton( onPressed: _loadFiles, child: const Text('重试'), ), ], ), ), ), ], ); } if (_files.isEmpty) { return CustomScrollView( slivers: [ SliverFillRemaining( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.restore_outlined, size: 64, color: Colors.grey.shade400), const SizedBox(height: 16), Text( '回收站为空', style: TextStyle(fontSize: 18, color: Colors.grey.shade600), ), const SizedBox(height: 8), Text( '删除的文件会出现在这里', style: TextStyle(color: Colors.grey.shade500), ), ], ), ), ), ], ); } if (_viewType == FileViewType.list) { return _buildListView(context); } return _buildGridView(context); } Widget _buildListView(BuildContext context) { final screenWidth = MediaQuery.sizeOf(context).width; final isDesktop = screenWidth >= 1000; return ListView.builder( itemCount: _files.length, itemBuilder: (context, index) { final file = _files[index]; final isSelected = _selectedFiles.contains(file.path); return FileListItem( key: ValueKey('trash_file_${file.id}'), file: file, isSelected: isSelected, showCheckbox: _hasSelection, index: index, isDesktop: isDesktop, tapToShowMenu: !_hasSelection, onTap: () { if (_hasSelection) { _toggleSelection(file.path); } }, onSelect: () => _toggleSelection(file.path), onRestore: () => _restoreFile(context, file), onDelete: () => _deleteFile(context, file), ); }, ); } Widget _buildGridView(BuildContext context) { final screenWidth = MediaQuery.sizeOf(context).width; final padding = 16.0; final spacing = 16.0; final availableWidth = screenWidth - padding * 2; int crossAxisCount; if (screenWidth < 400) { crossAxisCount = 2; } else if (screenWidth < 600) { crossAxisCount = 3; } else if (screenWidth < 900) { crossAxisCount = 4; } else { crossAxisCount = 5; } final itemWidth = (availableWidth - spacing * (crossAxisCount - 1)) / crossAxisCount; final childAspectRatio = itemWidth / 140; return GridView.builder( padding: const EdgeInsets.all(8), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, mainAxisSpacing: spacing / 2, crossAxisSpacing: spacing / 2, childAspectRatio: childAspectRatio, ), itemCount: _files.length, itemBuilder: (context, index) { final file = _files[index]; final isSelected = _selectedFiles.contains(file.path); return FileGridItem( key: ValueKey('trash_file_grid_${file.id}'), file: file, isSelected: isSelected, showCheckbox: _hasSelection, tapToShowMenu: !_hasSelection, onTap: () { if (_hasSelection) { _toggleSelection(file.path); } }, onSelect: () => _toggleSelection(file.path), onRestore: () => _restoreFile(context, file), onDelete: () => _deleteFile(context, file), ); }, ); } Widget _buildBottomBar(BuildContext context) { if (_hasSelection) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, -2), ), ], ), child: SafeArea( child: Row( children: [ Text('已选择 ${_selectedFiles.length} 项'), const Spacer(), TextButton.icon( onPressed: _restoreSelected, icon: const Icon(Icons.restore), label: const Text('恢复'), ), TextButton.icon( onPressed: _deleteSelected, icon: const Icon(Icons.delete_forever, color: Colors.red), label: const Text('彻底删除', style: TextStyle(color: Colors.red)), ), ], ), ), ); } return const SizedBox.shrink(); } bool get _hasSelection => _selectedFiles.isNotEmpty; void _toggleSelection(String path) { setState(() { if (_selectedFiles.contains(path)) { _selectedFiles.remove(path); } else { _selectedFiles.add(path); } }); } Future _refreshFiles() async { await _loadFiles(); } Future _loadFiles() async { setState(() { _isLoading = true; _errorMessage = null; }); try { final response = await FileService().listTrashFiles(page: 0); final filesData = response['files'] as List? ?? []; final files = filesData .map((f) => FileModel.fromJson(f as Map)) .toList(); setState(() { _files = files; _isLoading = false; }); } catch (e) { setState(() { _isLoading = false; _errorMessage = e.toString(); }); } } Future _restoreFile(BuildContext context, FileModel file) async { final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('恢复文件'), content: Text('确定要恢复 "${file.name}" 吗?'), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('取消'), ), FilledButton( onPressed: () => Navigator.of(dialogContext).pop(true), child: const Text('恢复'), ), ], ), ); if (confirmed == true) { await _performRestore([file.path]); } } Future _restoreSelected() async { final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('恢复文件'), content: Text('确定要恢复选中的 ${_selectedFiles.length} 个文件吗?'), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('取消'), ), FilledButton( onPressed: () => Navigator.of(dialogContext).pop(true), child: const Text('恢复'), ), ], ), ); if (confirmed == true) { await _performRestore(_selectedFiles.toList()); setState(() { _selectedFiles.clear(); }); } } Future _performRestore(List uris) async { try { await FileService().restoreFiles(uris: uris); if (mounted) { ToastHelper.success('恢复成功'); await _loadFiles(); } } catch (e) { if (mounted) { ToastHelper.failure('恢复失败: $e'); } } } Future _deleteFile(BuildContext context, FileModel file) async { final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('彻底删除'), content: Text( '确定要彻底删除 "${file.name}" 吗?\n此操作不可撤销!', ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('取消'), ), FilledButton( onPressed: () => Navigator.of(dialogContext).pop(true), style: FilledButton.styleFrom(backgroundColor: Colors.red), child: const Text('彻底删除'), ), ], ), ); if (confirmed == true) { await _performDelete([file.path]); } } Future _deleteSelected() async { final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('彻底删除'), content: Text( '确定要彻底删除选中的 ${_selectedFiles.length} 个文件吗?\n此操作不可撤销!', ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('取消'), ), FilledButton( onPressed: () => Navigator.of(dialogContext).pop(true), style: FilledButton.styleFrom(backgroundColor: Colors.red), child: const Text('彻底删除'), ), ], ), ); if (confirmed == true) { await _performDelete(_selectedFiles.toList()); setState(() { _selectedFiles.clear(); }); } } Future _performDelete(List uris) async { try { await FileService().deleteFiles( uris: uris, unlink: false, skipSoftDelete: true, ); if (mounted) { ToastHelper.success('删除成功'); await _loadFiles(); } } catch (e) { if (mounted) { ToastHelper.failure('删除失败: $e'); } } } } enum FileViewType { list, grid, }