Files
app/lib/presentation/pages/recycle_bin/recycle_bin_page.dart
T
2026-05-15 09:03:09 +08:00

471 lines
13 KiB
Dart

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<RecycleBinPage> createState() => _RecycleBinPageState();
}
class _RecycleBinPageState extends State<RecycleBinPage>
with GestureHandlerMixin {
List<FileModel> _files = [];
Set<String> _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<void> _refreshFiles() async {
await _loadFiles();
}
Future<void> _loadFiles() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final response = await FileService().listTrashFiles(page: 0);
final filesData = response['files'] as List<dynamic>? ?? [];
final files = filesData
.map((f) => FileModel.fromJson(f as Map<String, dynamic>))
.toList();
setState(() {
_files = files;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = e.toString();
});
}
}
Future<void> _restoreFile(BuildContext context, FileModel file) async {
final confirmed = await showDialog<bool>(
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<void> _restoreSelected() async {
final confirmed = await showDialog<bool>(
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<void> _performRestore(List<String> uris) async {
try {
await FileService().restoreFiles(uris: uris);
if (mounted) {
ToastHelper.success('恢复成功');
await _loadFiles();
}
} catch (e) {
if (mounted) {
ToastHelper.failure('恢复失败: $e');
}
}
}
Future<void> _deleteFile(BuildContext context, FileModel file) async {
final confirmed = await showDialog<bool>(
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<void> _deleteSelected() async {
final confirmed = await showDialog<bool>(
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<void> _performDelete(List<String> 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,
}