snap_wish/lib/presentation/widgets/folder_selector.dart
2025-10-09 17:10:38 +08:00

696 lines
19 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/folder_provider.dart';
import 'custom_button.dart';
/// 文件夹选择器弹窗 - 用于选择保存图片的目标文件夹
/// 支持文件夹列表显示、新建文件夹、最近使用排序等功能
class FolderSelectorDialog extends ConsumerStatefulWidget {
/// 文件夹选择回调
final Function(String folderId) onFolderSelected;
/// 当前选中的文件夹ID
final String? currentFolderId;
const FolderSelectorDialog({
Key? key,
required this.onFolderSelected,
this.currentFolderId,
}) : super(key: key);
@override
ConsumerState<FolderSelectorDialog> createState() => _FolderSelectorDialogState();
}
class _FolderSelectorDialogState extends ConsumerState<FolderSelectorDialog> {
/// 搜索控制器
final TextEditingController _searchController = TextEditingController();
/// 搜索关键词
String searchKeyword = '';
@override
void initState() {
super.initState();
// 加载文件夹列表
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(folderProvider.notifier).loadFolders();
});
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
width: 480,
height: 600,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: theme.scaffoldBackgroundColor,
),
child: Column(
children: [
// 标题栏
_buildHeader(colorScheme),
// 搜索框
_buildSearchBar(colorScheme),
// 文件夹列表
Expanded(
child: _buildFolderList(colorScheme),
),
// 底部操作栏
_buildFooter(colorScheme),
],
),
),
);
}
/// 构建标题栏
Widget _buildHeader(ColorScheme colorScheme) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
Icon(
Icons.folder,
color: colorScheme.primary,
size: 28,
),
const SizedBox(width: 12),
Text(
'选择文件夹',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
tooltip: '关闭',
),
],
),
);
}
/// 构建搜索框
Widget _buildSearchBar(ColorScheme colorScheme) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '搜索文件夹...',
prefixIcon: const Icon(Icons.search),
suffixIcon: searchKeyword.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
setState(() {
searchKeyword = '';
});
},
icon: const Icon(Icons.clear),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
setState(() {
searchKeyword = value;
});
},
),
);
}
/// 构建文件夹列表
Widget _buildFolderList(ColorScheme colorScheme) {
final folderState = ref.watch(folderProvider);
if (folderState.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (folderState.error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: colorScheme.error,
),
const SizedBox(height: 16),
Text(
'加载文件夹失败',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: colorScheme.error,
),
),
const SizedBox(height: 16),
CustomButton(
text: '重试',
onPressed: () {
ref.read(folderProvider.notifier).loadFolders();
},
buttonType: ButtonType.outlined,
),
],
),
);
}
final folders = folderState.folders;
if (folders.isEmpty) {
return _buildEmptyState(colorScheme);
}
// 过滤文件夹
final filteredFolders = searchKeyword.isEmpty
? folders
: folders.where((folder) =>
folder.name.toLowerCase().contains(searchKeyword.toLowerCase())).toList();
if (filteredFolders.isEmpty) {
return _buildNoSearchResults(colorScheme);
}
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 20),
itemCount: filteredFolders.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final folder = filteredFolders[index];
final isSelected = folder.id == widget.currentFolderId;
return _buildFolderItem(folder, isSelected, colorScheme);
},
);
}
/// 构建空状态
Widget _buildEmptyState(ColorScheme colorScheme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.folder_outlined,
size: 64,
color: colorScheme.outline,
),
const SizedBox(height: 16),
Text(
'还没有文件夹',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: colorScheme.outline,
),
),
const SizedBox(height: 8),
Text(
'创建第一个文件夹来开始整理你的灵感',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.outline.withOpacity(0.7),
),
),
const SizedBox(height: 24),
CustomButton(
text: '创建文件夹',
onPressed: _showCreateFolderDialog,
prefixIcon: Icons.add,
buttonType: ButtonType.primary,
),
],
),
);
}
/// 构建无搜索结果状态
Widget _buildNoSearchResults(ColorScheme colorScheme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: colorScheme.outline,
),
const SizedBox(height: 16),
Text(
'未找到匹配的文件夹',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: colorScheme.outline,
),
),
const SizedBox(height: 8),
Text(
'尝试使用其他关键词搜索',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.outline.withOpacity(0.7),
),
),
],
),
);
}
/// 构建文件夹项
Widget _buildFolderItem(
dynamic folder,
bool isSelected,
ColorScheme colorScheme,
) {
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
widget.onFolderSelected(folder.id);
Navigator.of(context).pop();
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected
? colorScheme.primary
: colorScheme.outline.withOpacity(0.2),
width: isSelected ? 2 : 1,
),
color: isSelected
? colorScheme.primary.withOpacity(0.1)
: colorScheme.surface,
),
child: Row(
children: [
// 文件夹图标
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isSelected
? colorScheme.primary.withOpacity(0.2)
: colorScheme.primaryContainer.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
_getFolderIcon(folder.icon),
color: isSelected
? colorScheme.primary
: colorScheme.primary,
size: 24,
),
),
const SizedBox(width: 16),
// 文件夹信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
folder.name,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: isSelected
? colorScheme.primary
: colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
'${folder.imageCount} 张图片',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
),
// 选择标记
if (isSelected)
Icon(
Icons.check_circle,
color: colorScheme.primary,
size: 24,
),
],
),
),
),
);
}
/// 构建底部操作栏
Widget _buildFooter(ColorScheme colorScheme) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(16),
bottomRight: Radius.circular(16),
),
),
child: Row(
children: [
// 创建新文件夹按钮
Expanded(
child: CustomButton(
text: '新建文件夹',
onPressed: _showCreateFolderDialog,
prefixIcon: Icons.add,
buttonType: ButtonType.outlined,
),
),
const SizedBox(width: 16),
// 取消按钮
CustomButton(
text: '取消',
onPressed: () => Navigator.of(context).pop(),
buttonType: ButtonType.text,
),
],
),
);
}
/// 显示创建文件夹对话框
void _showCreateFolderDialog() {
showDialog(
context: context,
builder: (context) => CreateFolderDialog(
onFolderCreated: (folderId) {
Navigator.of(context).pop(); // 关闭创建对话框
widget.onFolderSelected(folderId);
Navigator.of(context).pop(); // 关闭选择器对话框
},
),
);
}
/// 获取文件夹图标
IconData _getFolderIcon(String? iconCode) {
// 默认文件夹图标
if (iconCode == null || iconCode.isEmpty) {
return Icons.folder;
}
// 尝试解析Material Icons名称
try {
// 这里需要根据实际的图标存储方式来实现
// 暂时返回默认图标
return Icons.folder;
} catch (e) {
return Icons.folder;
}
}
}
/// 创建文件夹对话框
class CreateFolderDialog extends ConsumerStatefulWidget {
/// 文件夹创建成功回调
final Function(String folderId) onFolderCreated;
const CreateFolderDialog({
Key? key,
required this.onFolderCreated,
}) : super(key: key);
@override
ConsumerState<CreateFolderDialog> createState() => _CreateFolderDialogState();
}
class _CreateFolderDialogState extends ConsumerState<CreateFolderDialog> {
/// 文件夹名称控制器
final TextEditingController _nameController = TextEditingController();
/// 选中的图标
String selectedIcon = 'folder';
/// 表单键
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
/// 是否正在创建
bool isCreating = false;
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
width: 400,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: theme.scaffoldBackgroundColor,
),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
'新建文件夹',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
// 文件夹名称输入
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: '文件夹名称',
hintText: '输入文件夹名称',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.folder),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入文件夹名称';
}
if (value.trim().length > 20) {
return '文件夹名称不能超过20个字符';
}
return null;
},
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _createFolder(),
),
const SizedBox(height: 20),
// 图标选择
Text(
'选择图标',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
_buildIconSelector(colorScheme),
const SizedBox(height: 24),
// 操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// 取消按钮
CustomButton(
text: '取消',
onPressed: () => Navigator.of(context).pop(),
buttonType: ButtonType.text,
),
const SizedBox(width: 16),
// 创建按钮
CustomButton(
text: '创建',
onPressed: isCreating ? null : _createFolder,
buttonType: ButtonType.primary,
isLoading: isCreating,
),
],
),
],
),
),
),
);
}
/// 构建图标选择器
Widget _buildIconSelector(ColorScheme colorScheme) {
final icons = [
'folder',
'bookmark',
'favorite',
'star',
'work',
'home',
'travel_explore',
'restaurant',
'shopping_bag',
'sports_esports',
];
return Wrap(
spacing: 12,
runSpacing: 12,
children: icons.map((icon) {
final isSelected = selectedIcon == icon;
return GestureDetector(
onTap: () {
setState(() {
selectedIcon = icon;
});
},
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isSelected
? colorScheme.primary.withOpacity(0.2)
: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected
? colorScheme.primary
: colorScheme.outline.withOpacity(0.2),
width: isSelected ? 2 : 1,
),
),
child: Icon(
_getIconData(icon),
color: isSelected
? colorScheme.primary
: colorScheme.onSurface,
size: 24,
),
),
);
}).toList(),
);
}
/// 创建文件夹
Future<void> _createFolder() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
isCreating = true;
});
try {
final folderId = await ref.read(folderProvider.notifier).createFolder(
name: _nameController.text.trim(),
icon: selectedIcon,
);
if (folderId != null) {
widget.onFolderCreated(folderId);
} else {
_showErrorSnackBar('创建文件夹失败');
}
} catch (e) {
_showErrorSnackBar('创建文件夹失败:${e.toString()}');
} finally {
if (mounted) {
setState(() {
isCreating = false;
});
}
}
}
/// 显示错误提示
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
/// 获取图标数据
IconData _getIconData(String iconName) {
switch (iconName) {
case 'folder':
return Icons.folder;
case 'bookmark':
return Icons.bookmark;
case 'favorite':
return Icons.favorite;
case 'star':
return Icons.star;
case 'work':
return Icons.work;
case 'home':
return Icons.home;
case 'travel_explore':
return Icons.travel_explore;
case 'restaurant':
return Icons.restaurant;
case 'shopping_bag':
return Icons.shopping_bag;
case 'sports_esports':
return Icons.sports_esports;
default:
return Icons.folder;
}
}
}