diff --git a/.claude/config.json b/.claude/config.json index 0032a99..f1bc95e 100644 --- a/.claude/config.json +++ b/.claude/config.json @@ -2,5 +2,6 @@ "language_preferences": { "documentation": "zh-CN", "code_comments": "zh-CN" - } + }, + "primaryApiKey": "xxx" } \ No newline at end of file diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..14751c3 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,10 @@ +{ + "env": { + "ANTHROPIC_AUTH_TOKEN": "afca3c573b634e1fb55f1902ffcb8aed.hgmMkxrwPRFbNDn1", + "ANTHROPIC_BASE_URL": "https://open.bigmodel.cn/api/anthropic", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "GLM-4.5-Air", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "GLM-4.6", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "GLM-4.6", + "ANTHROPIC_MODEL": "GLM-4.6" + } +} \ No newline at end of file diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 542d1cb..dd06206 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -11,7 +11,8 @@ "Bash(flutter build:*)", "Bash(flutter clean:*)", "Bash(dart analyze:*)", - "Bash(dart test:*)" + "Bash(dart test:*)", + "Bash(grep:*)" ], "deny": [], "ask": [] diff --git a/lib/data/datasources/local/folder_dao.dart b/lib/data/datasources/local/folder_dao.dart index ca21e3b..152fd8c 100644 --- a/lib/data/datasources/local/folder_dao.dart +++ b/lib/data/datasources/local/folder_dao.dart @@ -18,6 +18,12 @@ class FolderDao { return hiveFolder.id; } + /// 添加Hive文件夹 - 直接插入HiveImageFolder对象 + /// [hiveFolder] 要创建的Hive文件夹对象 + Future insertHiveFolder(HiveImageFolder hiveFolder) async { + await _foldersBox.put(hiveFolder.id, hiveFolder); + } + /// 根据ID获取文件夹 - 通过唯一标识符查找文件夹 /// [id] 文件夹的唯一标识符 /// 返回找到的文件夹实体,如果不存在则返回null @@ -26,6 +32,13 @@ class FolderDao { return hiveFolder?.toEntity(); } + /// 根据ID获取Hive文件夹 - 直接返回HiveImageFolder对象 + /// [id] 文件夹的唯一标识符 + /// 返回找到的Hive文件夹对象,如果不存在则返回null + Future getHiveFolderById(String id) async { + return _foldersBox.get(id); + } + /// 获取所有文件夹 - 按最近使用时间倒序排列 /// 返回所有文件夹实体列表,最近使用的文件夹在前 Future> getAllFolders() async { @@ -41,14 +54,29 @@ class FolderDao { /// 返回最近使用的文件夹列表 Future> getRecentFolders({int? limit}) async { final allFolders = await getAllFolders(); - + if (limit == null || limit >= allFolders.length) { return allFolders; } - + return allFolders.sublist(0, limit); } + /// 获取最近使用的Hive文件夹 - 返回HiveImageFolder对象列表 + /// [limit] 返回数量限制 + /// 返回最近使用的Hive文件夹列表 + Future> getRecentHiveFolders(int limit) async { + final hiveFolders = _foldersBox.values + .toList() + ..sort((a, b) => b.lastUsedAt.compareTo(a.lastUsedAt)); + + if (limit >= hiveFolders.length) { + return hiveFolders; + } + + return hiveFolders.sublist(0, limit); + } + /// 更新文件夹信息 - 修改文件夹的元数据 /// [folder] 包含更新数据的文件夹实体 /// 返回更新后的文件夹实体 @@ -62,11 +90,17 @@ class FolderDao { updatedAt: DateTime.now(), // 更新时间 lastUsedAt: folder.lastUsedAt, ); - + await _foldersBox.put(hiveFolder.id, hiveFolder); return hiveFolder.toEntity(); } + /// 更新Hive文件夹 - 直接更新HiveImageFolder对象 + /// [hiveFolder] 要更新的Hive文件夹对象 + Future updateHiveFolder(HiveImageFolder hiveFolder) async { + await _foldersBox.put(hiveFolder.id, hiveFolder); + } + /// 更新文件夹使用时间 - 记录文件夹的最近访问时间 /// [folderId] 文件夹ID /// 返回更新后的文件夹实体 @@ -165,15 +199,27 @@ class FolderDao { /// 返回匹配的文件夹列表 Future> searchFolders(String query) async { final lowerQuery = query.toLowerCase(); - + final hiveFolders = _foldersBox.values .where((folder) => folder.name.toLowerCase().contains(lowerQuery)) .toList() ..sort((a, b) => b.lastUsedAt.compareTo(a.lastUsedAt)); - + return hiveFolders.map((hiveFolder) => hiveFolder.toEntity()).toList(); } + /// 搜索Hive文件夹 - 返回HiveImageFolder对象列表 + /// [query] 搜索关键词 + /// 返回匹配的Hive文件夹列表 + Future> searchHiveFolders(String query) async { + final lowerQuery = query.toLowerCase(); + + return _foldersBox.values + .where((folder) => folder.name.toLowerCase().contains(lowerQuery)) + .toList() + ..sort((a, b) => b.lastUsedAt.compareTo(a.lastUsedAt)); + } + /// 关闭数据盒 - 释放数据库资源 /// 通常在应用退出时调用 Future close() async { diff --git a/lib/data/datasources/local/tag_dao.dart b/lib/data/datasources/local/tag_dao.dart index f3c4ccd..b23365e 100644 --- a/lib/data/datasources/local/tag_dao.dart +++ b/lib/data/datasources/local/tag_dao.dart @@ -18,6 +18,12 @@ class TagDao { return hiveTag.id; } + /// 添加Hive标签 - 直接插入HiveImageTag对象 + /// [hiveTag] 要创建的Hive标签对象 + Future insertHiveTag(HiveImageTag hiveTag) async { + await _tagsBox.put(hiveTag.id, hiveTag); + } + /// 批量添加标签 - 一次性创建多个标签 /// [tags] 要创建的标签实体列表 /// 返回创建成功的标签ID列表 @@ -40,6 +46,13 @@ class TagDao { return hiveTag?.toEntity(); } + /// 根据ID获取Hive标签 - 直接返回HiveImageTag对象 + /// [id] 标签的唯一标识符 + /// 返回找到的Hive标签对象,如果不存在则返回null + Future getHiveTagById(String id) async { + return _tagsBox.get(id); + } + /// 根据名称获取标签 - 通过标签名称精确查找 /// [name] 标签名称 /// 返回找到的标签实体,如果不存在则返回null @@ -55,6 +68,21 @@ class TagDao { } } + /// 根据名称获取Hive标签 - 通过标签名称精确查找Hive标签对象 + /// [name] 标签名称 + /// 返回找到的Hive标签对象,如果不存在则返回null + Future getHiveTagByName(String name) async { + try { + final hiveTag = _tagsBox.values.firstWhere( + (tag) => tag.name == name, + ); + return hiveTag; + } catch (e) { + // 没有找到标签时返回null + return null; + } + } + /// 获取所有标签 - 按使用次数倒序排列 /// 返回所有标签实体列表,使用次数多的标签在前 Future> getAllTags() async { @@ -219,4 +247,83 @@ class TagDao { Future close() async { await _tagsBox.close(); } + + /// 获取所有Hive标签 - 返回HiveImageTag对象列表 + /// 返回所有Hive标签列表 + Future> getAllHiveTags() async { + final hiveTags = _tagsBox.values.toList() + ..sort((a, b) => b.usageCount.compareTo(a.usageCount)); + return hiveTags; + } + + /// 获取热门Hive标签 - 返回HiveImageTag对象列表 + /// [limit] 返回数量限制 + /// 返回热门的Hive标签列表 + Future> getPopularHiveTags(int limit) async { + final hiveTags = _tagsBox.values.toList() + ..sort((a, b) => b.usageCount.compareTo(a.usageCount)); + + if (limit >= hiveTags.length) { + return hiveTags; + } + + return hiveTags.sublist(0, limit); + } + + /// 获取最近使用的Hive标签 - 返回HiveImageTag对象列表 + /// [limit] 返回数量限制 + /// 返回最近使用的Hive标签列表 + Future> getRecentHiveTags(int limit) async { + final hiveTags = _tagsBox.values.toList() + ..sort((a, b) => b.lastUsedAt.compareTo(a.lastUsedAt)); + + if (limit >= hiveTags.length) { + return hiveTags; + } + + return hiveTags.sublist(0, limit); + } + + /// 搜索Hive标签 - 返回HiveImageTag对象列表 + /// [query] 搜索关键词 + /// 返回匹配的Hive标签列表 + Future> searchHiveTags(String query) async { + final lowerQuery = query.toLowerCase(); + + return _tagsBox.values + .where((tag) => tag.name.toLowerCase().contains(lowerQuery)) + .toList() + ..sort((a, b) => b.usageCount.compareTo(a.usageCount)); + } + + /// 获取未使用的Hive标签 - 返回HiveImageTag对象列表 + /// 返回未使用的Hive标签列表 + Future> getUnusedHiveTags() async { + return _tagsBox.values + .where((tag) => tag.usageCount == 0) + .toList() + ..sort((a, b) => b.lastUsedAt.compareTo(a.lastUsedAt)); + } + + /// 更新Hive标签 - 直接更新HiveImageTag对象 + /// [hiveTag] 要更新的Hive标签对象 + Future updateHiveTag(HiveImageTag hiveTag) async { + await _tagsBox.put(hiveTag.id, hiveTag); + } + + /// 清理未使用的Hive标签 - 删除所有使用次数为0的标签 + /// 返回删除的标签数量 + Future cleanupUnusedHiveTags() async { + final unusedTags = await getUnusedHiveTags(); + for (final tag in unusedTags) { + await _tagsBox.delete(tag.id); + } + return unusedTags.length; + } + + /// 清理未使用的标签 - 删除所有使用次数为0的标签 + /// 返回删除的标签数量 + Future cleanupUnusedTags() async { + return await cleanupUnusedHiveTags(); + } } \ No newline at end of file diff --git a/lib/data/repositories/folder_repository.dart b/lib/data/repositories/folder_repository.dart new file mode 100644 index 0000000..7ca927b --- /dev/null +++ b/lib/data/repositories/folder_repository.dart @@ -0,0 +1,41 @@ +import '../../domain/entities/image_folder.dart'; + +/// 文件夹仓库接口 - 定义文件夹数据操作的抽象接口 +/// 负责管理文件夹的增删改查等操作 +abstract class FolderRepository { + /// 获取所有文件夹 + Future> getAllFolders(); + + /// 根据ID获取文件夹 + Future getFolderById(String id); + + /// 创建新文件夹 + Future createFolder({ + required String name, + required String icon, + String? coverImageId, + }); + + /// 更新文件夹信息 + Future updateFolder({ + required String id, + required String name, + required String icon, + String? coverImageId, + }); + + /// 删除文件夹 + Future deleteFolder(String id); + + /// 更新文件夹最后使用时间 + Future updateFolderLastUsed(String id); + + /// 获取文件夹中的图片数量 + Future getFolderImageCount(String id); + + /// 获取最近使用的文件夹 + Future> getRecentFolders({int limit = 10}); + + /// 搜索文件夹 + Future> searchFolders(String keyword); +} \ No newline at end of file diff --git a/lib/data/repositories/folder_repository_impl.dart b/lib/data/repositories/folder_repository_impl.dart new file mode 100644 index 0000000..ba6e015 --- /dev/null +++ b/lib/data/repositories/folder_repository_impl.dart @@ -0,0 +1,168 @@ +import '../datasources/local/folder_dao.dart'; +import '../models/hive_image_folder.dart'; +import '../../domain/entities/image_folder.dart'; +import 'folder_repository.dart'; + +/// 文件夹仓库实现类 - 基于Hive数据库的文件夹操作实现 +/// 负责处理文件夹数据的持久化存储和检索 +class FolderRepositoryImpl implements FolderRepository { + final FolderDao _folderDao; + + FolderRepositoryImpl(this._folderDao); + + @override + Future> getAllFolders() async { + try { + final folders = await _folderDao.getAllFolders(); + return folders; + } catch (e) { + throw Exception('获取文件夹列表失败: $e'); + } + } + + @override + Future getFolderById(String id) async { + try { + final folder = await _folderDao.getFolderById(id); + return folder; + } catch (e) { + throw Exception('获取文件夹失败: $e'); + } + } + + @override + Future createFolder({ + required String name, + required String icon, + String? coverImageId, + }) async { + try { + final now = DateTime.now(); + final hiveFolder = HiveImageFolder( + id: _generateId(), + name: name, + coverImageId: coverImageId, + icon: icon, + createdAt: now, + updatedAt: now, + lastUsedAt: now, + ); + + await _folderDao.insertHiveFolder(hiveFolder); + return hiveFolder.id; + } catch (e) { + throw Exception('创建文件夹失败: $e'); + } + } + + @override + Future updateFolder({ + required String id, + required String name, + required String icon, + String? coverImageId, + }) async { + try { + final existingFolder = await _folderDao.getHiveFolderById(id); + if (existingFolder == null) { + throw Exception('文件夹不存在'); + } + + final updatedFolder = HiveImageFolder( + id: id, + name: name, + coverImageId: coverImageId, + icon: icon, + createdAt: existingFolder.createdAt, + updatedAt: DateTime.now(), + lastUsedAt: existingFolder.lastUsedAt, + ); + + await _folderDao.updateHiveFolder(updatedFolder); + } catch (e) { + throw Exception('更新文件夹失败: $e'); + } + } + + @override + Future deleteFolder(String id) async { + try { + await _folderDao.deleteFolder(id); + } catch (e) { + throw Exception('删除文件夹失败: $e'); + } + } + + @override + Future updateFolderLastUsed(String id) async { + try { + final existingFolder = await _folderDao.getHiveFolderById(id); + if (existingFolder == null) { + throw Exception('文件夹不存在'); + } + + final updatedFolder = HiveImageFolder( + id: id, + name: existingFolder.name, + coverImageId: existingFolder.coverImageId, + icon: existingFolder.icon, + createdAt: existingFolder.createdAt, + updatedAt: DateTime.now(), + lastUsedAt: DateTime.now(), + ); + + await _folderDao.updateHiveFolder(updatedFolder); + } catch (e) { + throw Exception('更新文件夹使用时间失败: $e'); + } + } + + @override + Future getFolderImageCount(String id) async { + try { + // 这里需要实现获取文件夹中图片数量的逻辑 + // 可能需要调用ImageDAO的相关方法 + // 暂时返回0,后续实现 + return 0; + } catch (e) { + throw Exception('获取文件夹图片数量失败: $e'); + } + } + + @override + Future> getRecentFolders({int limit = 10}) async { + try { + final folders = await _folderDao.getRecentFolders(limit: limit); + return folders; + } catch (e) { + throw Exception('获取最近使用的文件夹失败: $e'); + } + } + + @override + Future> searchFolders(String keyword) async { + try { + final folders = await _folderDao.searchFolders(keyword); + return folders; + } catch (e) { + throw Exception('搜索文件夹失败: $e'); + } + } + + /// 生成唯一ID + String _generateId() { + return DateTime.now().millisecondsSinceEpoch.toString() + + _generateRandomString(8); + } + + /// 生成随机字符串 + String _generateRandomString(int length) { + const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + final random = DateTime.now().millisecondsSinceEpoch; + String result = ''; + for (int i = 0; i < length; i++) { + result += chars[(random + i) % chars.length]; + } + return result; + } +} \ No newline at end of file diff --git a/lib/data/repositories/tag_repository_impl.dart b/lib/data/repositories/tag_repository_impl.dart new file mode 100644 index 0000000..91d07cf --- /dev/null +++ b/lib/data/repositories/tag_repository_impl.dart @@ -0,0 +1,260 @@ +import '../datasources/local/tag_dao.dart'; +import '../models/hive_image_tag.dart'; +import '../../domain/entities/image_tag.dart'; +import '../../domain/repositories/tag_repository.dart'; + +/// 标签仓库实现类 - 基于Hive数据库的标签操作实现 +/// 负责处理标签数据的持久化存储和检索 +class TagRepositoryImpl implements TagRepository { + final TagDao _tagDao; + + TagRepositoryImpl(this._tagDao); + + @override + Future createTag(ImageTag tag) async { + try { + final hiveTag = HiveImageTag.fromEntity(tag); + await _tagDao.insertHiveTag(hiveTag); + return hiveTag.id; + } catch (e) { + throw Exception('创建标签失败: $e'); + } + } + + /// 简化的创建标签方法,直接接受参数而不是实体 + Future createTagWithParams({ + required String name, + required String icon, + required String color, + }) async { + try { + final now = DateTime.now(); + final hiveTag = HiveImageTag( + id: _generateId(), + name: name, + icon: icon, + color: color, + usageCount: 0, + lastUsedAt: now, + ); + + await _tagDao.insertHiveTag(hiveTag); + return hiveTag.id; + } catch (e) { + throw Exception('创建标签失败: $e'); + } + } + + @override + Future> createTags(List tags) async { + try { + final List tagIds = []; + for (final tag in tags) { + final tagId = await createTag(tag); + tagIds.add(tagId); + } + return tagIds; + } catch (e) { + throw Exception('批量创建标签失败: $e'); + } + } + + @override + Future getTag(String id) async { + try { + final hiveTag = await _tagDao.getHiveTagById(id); + return hiveTag?.toEntity(); + } catch (e) { + throw Exception('获取标签失败: $e'); + } + } + + @override + Future getTagByName(String name) async { + try { + final tag = await _tagDao.getTagByName(name); + return tag; // DAO已经返回ImageTag + } catch (e) { + throw Exception('根据名称获取标签失败: $e'); + } + } + + @override + Future> getAllTags() async { + try { + final hiveTags = await _tagDao.getAllHiveTags(); + return hiveTags.map((hiveTag) => hiveTag.toEntity()).toList(); + } catch (e) { + throw Exception('获取所有标签失败: $e'); + } + } + + @override + Future> getPopularTags({int limit = 10}) async { + try { + final hiveTags = await _tagDao.getPopularHiveTags(limit); + return hiveTags.map((hiveTag) => hiveTag.toEntity()).toList(); + } catch (e) { + throw Exception('获取热门标签失败: $e'); + } + } + + @override + Future> getRecentTags({int limit = 10}) async { + try { + final hiveTags = await _tagDao.getRecentHiveTags(limit); + return hiveTags.map((hiveTag) => hiveTag.toEntity()).toList(); + } catch (e) { + throw Exception('获取最近使用的标签失败: $e'); + } + } + + @override + Future updateTag(ImageTag tag) async { + try { + final hiveTag = HiveImageTag.fromEntity(tag); + await _tagDao.updateHiveTag(hiveTag); + return hiveTag.toEntity(); + } catch (e) { + throw Exception('更新标签失败: $e'); + } + } + + @override + Future incrementTagUsage(String tagId) async { + try { + final updatedTag = await _tagDao.incrementTagUsage(tagId); + return updatedTag; // DAO已经返回ImageTag + } catch (e) { + throw Exception('增加标签使用次数失败: $e'); + } + } + + /// 根据标签名称增加使用次数 + Future incrementTagUsageByName(String tagName) async { + try { + final existingTag = await _tagDao.getTagByName(tagName); + if (existingTag != null) { + // 需要通过Hive标签ID来增加使用次数 + final hiveTag = await _tagDao.getHiveTagByName(tagName); + if (hiveTag != null) { + await _tagDao.incrementTagUsage(hiveTag.id); + } + } else { + // 标签不存在,创建新标签 + await createTagWithParams( + name: tagName, + icon: 'local_offer', + color: '#2196F3', + ); + } + } catch (e) { + throw Exception('更新标签使用次数失败: $e'); + } + } + + @override + Future decrementTagUsage(String tagId) async { + try { + final updatedTag = await _tagDao.decrementTagUsage(tagId); + return updatedTag; // DAO已经返回ImageTag + } catch (e) { + throw Exception('减少标签使用次数失败: $e'); + } + } + + @override + Future deleteTag(String id) async { + try { + await _tagDao.deleteTag(id); + return true; + } catch (e) { + throw Exception('删除标签失败: $e'); + } + } + + @override + Future deleteTags(List ids) async { + try { + for (final id in ids) { + await _tagDao.deleteTag(id); + } + return true; + } catch (e) { + throw Exception('批量删除标签失败: $e'); + } + } + + @override + Future tagExists(String id) async { + try { + return await _tagDao.tagExists(id); + } catch (e) { + throw Exception('检查标签是否存在失败: $e'); + } + } + + @override + Future tagNameExists(String name) async { + try { + return await _tagDao.tagNameExists(name); + } catch (e) { + throw Exception('检查标签名称是否存在失败: $e'); + } + } + + @override + Future getTagCount() async { + try { + return await _tagDao.getTagCount(); + } catch (e) { + throw Exception('获取标签总数失败: $e'); + } + } + + @override + Future> searchTags(String query) async { + try { + final hiveTags = await _tagDao.searchHiveTags(query); + return hiveTags.map((hiveTag) => hiveTag.toEntity()).toList(); + } catch (e) { + throw Exception('搜索标签失败: $e'); + } + } + + @override + Future> getUnusedTags() async { + try { + final hiveTags = await _tagDao.getUnusedHiveTags(); + return hiveTags.map((hiveTag) => hiveTag.toEntity()).toList(); + } catch (e) { + throw Exception('获取未使用的标签失败: $e'); + } + } + + @override + Future cleanupUnusedTags() async { + try { + return await _tagDao.cleanupUnusedTags(); + } catch (e) { + throw Exception('清理未使用的标签失败: $e'); + } + } + + /// 生成唯一ID + String _generateId() { + return DateTime.now().millisecondsSinceEpoch.toString() + + _generateRandomString(8); + } + + /// 生成随机字符串 + String _generateRandomString(int length) { + const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + final random = DateTime.now().millisecondsSinceEpoch; + String result = ''; + for (int i = 0; i < length; i++) { + result += chars[(random + i) % chars.length]; + } + return result; + } +} \ No newline at end of file diff --git a/lib/domain/entities/image_folder.dart b/lib/domain/entities/image_folder.dart index e311322..74a88cf 100644 --- a/lib/domain/entities/image_folder.dart +++ b/lib/domain/entities/image_folder.dart @@ -66,4 +66,8 @@ class ImageFolder { lastUsedAt: lastUsedAt ?? this.lastUsedAt, ); } + + /// 文件夹图片数量 - 计算属性,动态获取文件夹中包含的图片数量 + /// 注意:这是一个占位符属性,实际实现需要通过Repository查询 + int get imageCount => 0; } \ No newline at end of file diff --git a/lib/presentation/app_widget.dart b/lib/presentation/app_widget.dart index f5ac683..e5918eb 100644 --- a/lib/presentation/app_widget.dart +++ b/lib/presentation/app_widget.dart @@ -7,6 +7,7 @@ import '../core/theme/app_theme.dart'; import '../core/utils/logger.dart'; import 'providers/share_provider.dart'; import 'widgets/share_test_widget.dart'; +import 'widgets/save_dialog.dart'; /// 主应用组件 /// 负责配置MaterialApp和全局设置 @@ -110,19 +111,28 @@ class _AppHomePageState extends ConsumerState { @override Widget build(BuildContext context) { final theme = Theme.of(context); - - Logger.debug('构建AppHomePage'); - + final shareState = ref.watch(shareProvider); + + Logger.debug('构建AppHomePage, 分享状态: showShareUI=${shareState.showShareUI}, 文件数量=${shareState.pendingFiles.length}'); + + // 如果有错误,打印错误信息 + if (shareState.error != null) { + Logger.error('分享状态错误: ${shareState.error}'); + } + return Scaffold( appBar: AppBar( title: const Text('想拍'), centerTitle: true, elevation: 0, ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ + body: Stack( + children: [ + // 主页面内容 + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ // 应用Logo(占位符) Icon( Icons.photo_library, @@ -182,9 +192,21 @@ class _AppHomePageState extends ConsumerState { ), ), ], - ), + ), + ), + + // 分享保存对话框 + if (shareState.showShareUI && shareState.pendingFiles.isNotEmpty) + SaveDialog( + sharedFiles: shareState.pendingFiles, + onClose: () { + // 关闭分享界面并清除待处理文件 + ref.read(shareProvider.notifier).clearPendingFiles(); + }, + ), + ], ), - + // 底部导航栏(占位符) bottomNavigationBar: NavigationBar( destinations: const [ diff --git a/lib/presentation/providers/folder_provider.dart b/lib/presentation/providers/folder_provider.dart new file mode 100644 index 0000000..884433d --- /dev/null +++ b/lib/presentation/providers/folder_provider.dart @@ -0,0 +1,218 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../data/repositories/folder_repository.dart'; +import '../../data/repositories/folder_repository_impl.dart'; +import '../../data/datasources/local/folder_dao.dart'; + +/// 文件夹状态类 - 管理文件夹列表的状态 +class FolderState { + /// 文件夹列表 + final List folders; + + /// 是否正在加载 + final bool isLoading; + + /// 错误信息 + final String? error; + + FolderState({ + required this.folders, + this.isLoading = false, + this.error, + }); + + /// 创建初始状态 + factory FolderState.initial() { + return FolderState(folders: []); + } + + /// 创建加载状态 + FolderState copyWithLoading({ + List? folders, + bool? isLoading, + String? error, + }) { + return FolderState( + folders: folders ?? this.folders, + isLoading: isLoading ?? this.isLoading, + error: error, + ); + } +} + +/// 文件夹模型 - 简化的文件夹数据模型 +class FolderModel { + /// 文件夹ID + final String id; + + /// 文件夹名称 + final String name; + + /// 文件夹图标 + final String icon; + + /// 文件夹中的图片数量 + final int imageCount; + + /// 创建时间 + final DateTime createdAt; + + /// 最后使用时间 + final DateTime lastUsedAt; + + FolderModel({ + required this.id, + required this.name, + required this.icon, + this.imageCount = 0, + required this.createdAt, + required this.lastUsedAt, + }); + + /// 从Map创建FolderModel + factory FolderModel.fromMap(Map map) { + return FolderModel( + id: map['id'] as String, + name: map['name'] as String, + icon: map['icon'] as String? ?? 'folder', + imageCount: map['imageCount'] as int? ?? 0, + createdAt: DateTime.parse(map['createdAt'] as String), + lastUsedAt: DateTime.parse(map['lastUsedAt'] as String), + ); + } + + /// 转换为Map + Map toMap() { + return { + 'id': id, + 'name': name, + 'icon': icon, + 'imageCount': imageCount, + 'createdAt': createdAt.toIso8601String(), + 'lastUsedAt': lastUsedAt.toIso8601String(), + }; + } +} + +/// 文件夹Provider - 提供文件夹管理功能 +class FolderProvider extends StateNotifier { + final FolderRepository _folderRepository; + + FolderProvider(this._folderRepository) : super(FolderState.initial()); + + /// 加载文件夹列表 + Future loadFolders() async { + state = state.copyWithLoading(isLoading: true, error: null); + + try { + final folders = await _folderRepository.getAllFolders(); + final folderModels = folders.map((folder) => FolderModel.fromMap({ + 'id': folder.id, + 'name': folder.name, + 'icon': folder.icon, + 'imageCount': folder.imageCount, + 'createdAt': folder.createdAt.toIso8601String(), + 'lastUsedAt': folder.lastUsedAt.toIso8601String(), + })).toList(); + + state = state.copyWithLoading( + folders: folderModels, + isLoading: false, + ); + } catch (e) { + state = state.copyWithLoading( + isLoading: false, + error: e.toString(), + ); + } + } + + /// 创建新文件夹 + Future createFolder({ + required String name, + required String icon, + }) async { + try { + final folderId = await _folderRepository.createFolder( + name: name, + icon: icon, + ); + + // 重新加载文件夹列表 + await loadFolders(); + + return folderId; + } catch (e) { + state = state.copyWithLoading(error: e.toString()); + return null; + } + } + + /// 更新文件夹信息 + Future updateFolder({ + required String id, + required String name, + required String icon, + }) async { + try { + await _folderRepository.updateFolder( + id: id, + name: name, + icon: icon, + ); + + // 重新加载文件夹列表 + await loadFolders(); + + return true; + } catch (e) { + state = state.copyWithLoading(error: e.toString()); + return false; + } + } + + /// 删除文件夹 + Future deleteFolder(String id) async { + try { + await _folderRepository.deleteFolder(id); + + // 重新加载文件夹列表 + await loadFolders(); + + return true; + } catch (e) { + state = state.copyWithLoading(error: e.toString()); + return false; + } + } + + /// 更新文件夹最后使用时间 + Future updateLastUsedTime(String folderId) async { + try { + await _folderRepository.updateFolderLastUsed(folderId); + + // 重新加载文件夹列表 + await loadFolders(); + } catch (e) { + // 静默处理错误,不显示给用户 + debugPrint('更新文件夹使用时间失败: $e'); + } + } +} + +/// 文件夹Provider实例 +final folderProvider = StateNotifierProvider((ref) { + final folderRepository = ref.watch(folderRepositoryProvider); + return FolderProvider(folderRepository); +}); + +/// 文件夹Repository Provider +final folderRepositoryProvider = Provider((ref) { + final folderDao = ref.watch(folderDaoProvider); + return FolderRepositoryImpl(folderDao); +}); + +/// 文件夹DAO Provider +final folderDaoProvider = Provider((ref) { + return FolderDao(); +}); \ No newline at end of file diff --git a/lib/presentation/providers/share_provider.dart b/lib/presentation/providers/share_provider.dart index 8682e64..8ccccc1 100644 --- a/lib/presentation/providers/share_provider.dart +++ b/lib/presentation/providers/share_provider.dart @@ -348,11 +348,93 @@ class ShareNotifier extends StateNotifier { _checkPendingShares(); } + /// 清除待处理文件 - 清空当前待处理的分享文件 + void clearPendingFiles() { + Logger.info('清除待处理分享文件'); + state = state.copyWith( + pendingFiles: [], + showShareUI: false, + isProcessing: false, + error: null, + ); + + // 同时清除仓库中的待处理文件 + _shareRepository.clearCurrentShare(); + } + /// 获取分享文件数量 - 获取当前待处理的分享文件数量 int getShareFileCount() { return _shareRepository.getShareFileCount(); } + /// 批量保存图片 - 保存多张分享的图片 + /// [sharedFiles] 分享的媒体文件列表 + /// [folderId] 目标文件夹ID + /// [tags] 标签列表 + /// [note] 备注内容 + Future saveBatchImages({ + required List sharedFiles, + required String folderId, + required List tags, + String? note, + }) async { + try { + Logger.info('开始批量保存${sharedFiles.length}张图片'); + + // 更新状态为处理中 + state = state.copyWith(isProcessing: true, error: null); + + // 模拟保存过程,实际实现中会调用真实的保存逻辑 + await _simulateSaveProcess(folderId, tags, note); + + // 保存完成 + state = state.copyWith(isProcessing: false); + Logger.info('批量保存完成'); + + } catch (e) { + Logger.error('批量保存失败', error: e); + state = state.copyWith( + isProcessing: false, + error: '保存失败: $e', + ); + rethrow; + } + } + + /// 单张保存图片 - 保存单张分享的图片 + /// [sharedFile] 分享的媒体文件 + /// [folderId] 目标文件夹ID + /// [tags] 标签列表 + /// [note] 备注内容 + Future saveSingleImage({ + required SharedMediaFile sharedFile, + required String folderId, + required List tags, + String? note, + }) async { + try { + Logger.info('开始单张保存图片: ${sharedFile.path}'); + + // 更新状态为处理中 + state = state.copyWith(isProcessing: true, error: null); + + // 模拟保存过程,实际实现中会调用真实的保存逻辑 + await _simulateSaveProcess(folderId, tags, note); + + // 保存完成 + state = state.copyWith(isProcessing: false); + Logger.info('单张保存完成'); + + } catch (e) { + Logger.error('单张保存失败', error: e); + state = state.copyWith( + isProcessing: false, + error: '保存失败: $e', + ); + rethrow; + } + } + /// 释放资源 - 清理分享相关资源 @override Future dispose() async { diff --git a/lib/presentation/providers/tag_provider.dart b/lib/presentation/providers/tag_provider.dart new file mode 100644 index 0000000..e81798a --- /dev/null +++ b/lib/presentation/providers/tag_provider.dart @@ -0,0 +1,280 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../domain/repositories/tag_repository.dart'; +import '../../domain/entities/image_tag.dart'; +import '../../data/repositories/tag_repository_impl.dart'; +import '../../data/datasources/local/tag_dao.dart'; + +/// 标签状态类 - 管理标签列表的状态 +class TagState { + /// 标签列表 + final List tags; + + /// 是否正在加载 + final bool isLoading; + + /// 错误信息 + final String? error; + + TagState({ + required this.tags, + this.isLoading = false, + this.error, + }); + + /// 创建初始状态 + factory TagState.initial() { + return TagState(tags: []); + } + + /// 创建加载状态 + TagState copyWithLoading({ + List? tags, + bool? isLoading, + String? error, + }) { + return TagState( + tags: tags ?? this.tags, + isLoading: isLoading ?? this.isLoading, + error: error, + ); + } +} + +/// 标签模型 - 简化的标签数据模型 +class TagModel { + /// 标签ID + final String id; + + /// 标签名称 + final String name; + + /// 标签图标 + final String icon; + + /// 标签颜色 + final String color; + + /// 使用次数 + final int usageCount; + + /// 最后使用时间 + final DateTime lastUsedAt; + + TagModel({ + required this.id, + required this.name, + required this.icon, + required this.color, + this.usageCount = 0, + required this.lastUsedAt, + }); + + /// 从Map创建TagModel + factory TagModel.fromMap(Map map) { + return TagModel( + id: map['id'] as String, + name: map['name'] as String, + icon: map['icon'] as String? ?? 'local_offer', + color: map['color'] as String? ?? '#2196F3', + usageCount: map['usageCount'] as int? ?? 0, + lastUsedAt: DateTime.parse(map['lastUsedAt'] as String), + ); + } + + /// 转换为Map + Map toMap() { + return { + 'id': id, + 'name': name, + 'icon': icon, + 'color': color, + 'usageCount': usageCount, + 'lastUsedAt': lastUsedAt.toIso8601String(), + }; + } +} + +/// 标签Provider - 提供标签管理功能 +class TagProvider extends StateNotifier { + final TagRepository _tagRepository; + + TagProvider(this._tagRepository) : super(TagState.initial()); + + /// 加载标签列表 + Future loadTags() async { + state = state.copyWithLoading(isLoading: true, error: null); + + try { + final tags = await _tagRepository.getAllTags(); + final tagModels = tags.map((tag) => TagModel.fromMap({ + 'id': tag.id, + 'name': tag.name, + 'icon': tag.icon, + 'color': tag.color, + 'usageCount': tag.usageCount, + 'lastUsedAt': tag.lastUsedAt.toIso8601String(), + })).toList(); + + state = state.copyWithLoading( + tags: tagModels, + isLoading: false, + ); + } catch (e) { + state = state.copyWithLoading( + isLoading: false, + error: e.toString(), + ); + } + } + + /// 创建新标签 + Future createTag({ + required String name, + required String icon, + required String color, + }) async { + try { + final now = DateTime.now(); + final newTag = ImageTag( + id: '', // 将在Repository中生成 + name: name, + icon: icon, + color: color, + usageCount: 0, + lastUsedAt: now, + ); + + final tagId = await _tagRepository.createTag(newTag); + + // 重新加载标签列表 + await loadTags(); + + return tagId; + } catch (e) { + state = state.copyWithLoading(error: e.toString()); + return null; + } + } + + /// 更新标签信息 + Future updateTag({ + required String id, + required String name, + required String icon, + required String color, + }) async { + try { + // 先获取现有标签 + final existingTag = await _tagRepository.getTag(id); + if (existingTag == null) { + state = state.copyWithLoading(error: '标签不存在'); + return false; + } + + final updatedTag = ImageTag( + id: id, + name: name, + icon: icon, + color: color, + usageCount: existingTag.usageCount, + lastUsedAt: existingTag.lastUsedAt, + ); + + await _tagRepository.updateTag(updatedTag); + + // 重新加载标签列表 + await loadTags(); + + return true; + } catch (e) { + state = state.copyWithLoading(error: e.toString()); + return false; + } + } + + /// 删除标签 + Future deleteTag(String id) async { + try { + await _tagRepository.deleteTag(id); + + // 重新加载标签列表 + await loadTags(); + + return true; + } catch (e) { + state = state.copyWithLoading(error: e.toString()); + return false; + } + } + + /// 增加标签使用次数 + Future incrementTagUsage(String tagName) async { + try { + // 查找标签 + final existingTag = state.tags.firstWhere( + (tag) => tag.name == tagName, + orElse: () => TagModel( + id: '', + name: tagName, + icon: 'local_offer', + color: '#2196F3', + lastUsedAt: DateTime.now(), + ), + ); + + if (existingTag.id.isNotEmpty) { + // 标签已存在,更新使用次数 + final tagRepo = _tagRepository as TagRepositoryImpl; + await tagRepo.incrementTagUsageByName(tagName); + await loadTags(); + } else { + // 标签不存在,创建新标签 + final tagRepo = _tagRepository as TagRepositoryImpl; + await tagRepo.createTagWithParams( + name: tagName, + icon: 'local_offer', + color: '#2196F3', + ); + } + } catch (e) { + // 静默处理错误,不显示给用户 + debugPrint('更新标签使用次数失败: $e'); + } + } + + /// 获取热门标签 + List getPopularTags({int limit = 10}) { + final sortedTags = List.from(state.tags) + ..sort((a, b) => b.usageCount.compareTo(a.usageCount)); + + return sortedTags.take(limit).toList(); + } + + /// 搜索标签 + List searchTags(String keyword) { + if (keyword.isEmpty) { + return state.tags; + } + + return state.tags.where((tag) => + tag.name.toLowerCase().contains(keyword.toLowerCase())).toList(); + } +} + +/// 标签Provider实例 +final tagProvider = StateNotifierProvider((ref) { + final tagRepository = ref.watch(tagRepositoryProvider); + return TagProvider(tagRepository); +}); + +/// 标签Repository Provider +final tagRepositoryProvider = Provider((ref) { + final tagDao = ref.watch(tagDaoProvider); + return TagRepositoryImpl(tagDao); +}); + +/// 标签DAO Provider +final tagDaoProvider = Provider((ref) { + return TagDao(); +}); \ No newline at end of file diff --git a/lib/presentation/widgets/folder_selector.dart b/lib/presentation/widgets/folder_selector.dart new file mode 100644 index 0000000..99f8383 --- /dev/null +++ b/lib/presentation/widgets/folder_selector.dart @@ -0,0 +1,696 @@ +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 createState() => _FolderSelectorDialogState(); +} + +class _FolderSelectorDialogState extends ConsumerState { + /// 搜索控制器 + 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 createState() => _CreateFolderDialogState(); +} + +class _CreateFolderDialogState extends ConsumerState { + /// 文件夹名称控制器 + final TextEditingController _nameController = TextEditingController(); + + /// 选中的图标 + String selectedIcon = 'folder'; + + /// 表单键 + final GlobalKey _formKey = GlobalKey(); + + /// 是否正在创建 + 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 _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; + } + } +} \ No newline at end of file diff --git a/lib/presentation/widgets/image_grid_preview.dart b/lib/presentation/widgets/image_grid_preview.dart new file mode 100644 index 0000000..0e25e93 --- /dev/null +++ b/lib/presentation/widgets/image_grid_preview.dart @@ -0,0 +1,488 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; + +/// 图片网格预览组件 - 用于批量模式下显示多张图片的网格布局 +/// 支持图片预览、选择状态和错误处理 +class ImageGridPreview extends StatelessWidget { + /// 分享的图片文件列表 + final List images; + + /// 选中的图片索引列表 + final List selectedIndices; + + /// 图片选择状态变更回调 + final Function(int index, bool selected) onSelectionChanged; + + /// 是否显示选择框 + final bool showSelection; + + /// 网格列数 + final int crossAxisCount; + + /// 图片间距 + final double spacing; + + /// 边距 + final EdgeInsets padding; + + const ImageGridPreview({ + Key? key, + required this.images, + required this.selectedIndices, + required this.onSelectionChanged, + this.showSelection = true, + this.crossAxisCount = 2, + this.spacing = 8.0, + this.padding = const EdgeInsets.all(16), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (images.isEmpty) { + return _buildEmptyState(context); + } + + return Padding( + padding: padding, + child: GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: spacing, + mainAxisSpacing: spacing, + childAspectRatio: 1.0, + ), + itemCount: images.length, + itemBuilder: (context, index) => _buildImageItem(context, index), + ), + ); + } + + /// 构建空状态 + Widget _buildEmptyState(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.photo_library_outlined, + size: 64, + color: Theme.of(context).colorScheme.outline, + ), + const SizedBox(height: 16), + Text( + '暂无图片', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.outline, + ), + ), + ], + ), + ); + } + + /// 构建单个图片项 + Widget _buildImageItem(BuildContext context, int index) { + final imageFile = images[index]; + final isSelected = selectedIndices.contains(index); + + return GestureDetector( + onTap: () { + if (showSelection) { + onSelectionChanged(index, !isSelected); + } else { + _showImagePreview(context, index); + } + }, + onLongPress: () => _showImagePreview(context, index), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: isSelected + ? Border.all( + color: Theme.of(context).colorScheme.primary, + width: 3, + ) + : null, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(5), // 稍小以避免边框溢出 + child: Stack( + fit: StackFit.expand, + children: [ + // 图片显示 + Image.file( + File(imageFile.path), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + _buildErrorPlaceholder(context), + frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { + if (frame == null) return _buildLoadingPlaceholder(context); + return child; + }, + ), + + // 选择标记 + if (showSelection) + Positioned( + top: 8, + right: 8, + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: isSelected + ? Theme.of(context).colorScheme.primary + : Colors.white.withOpacity(0.7), + shape: BoxShape.circle, + border: Border.all( + color: isSelected + ? Theme.of(context).colorScheme.primary + : Colors.grey.withOpacity(0.5), + width: 2, + ), + ), + child: isSelected + ? const Icon( + Icons.check, + color: Colors.white, + size: 16, + ) + : null, + ), + ), + + // 文件类型标记 + if (_isGifImage(imageFile.path)) + Positioned( + bottom: 8, + left: 8, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + 'GIF', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + /// 构建加载中的占位符 + Widget _buildLoadingPlaceholder(BuildContext context) { + return Container( + color: Colors.grey[200], + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + ), + ), + ); + } + + /// 构建错误占位符 + Widget _buildErrorPlaceholder(BuildContext context) { + return Container( + color: Colors.grey[200], + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.broken_image, + size: 32, + color: Colors.grey[400], + ), + const SizedBox(height: 4), + Text( + '加载失败', + style: TextStyle( + color: Colors.grey[600], + fontSize: 12, + ), + ), + ], + ), + ); + } + + /// 显示图片预览 + void _showImagePreview(BuildContext context, int index) { + Navigator.of(context).push( + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => + ImagePreviewPage( + images: images, + initialIndex: index, + ), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + ), + ); + } + + /// 检查是否为GIF图片 + bool _isGifImage(String path) { + return path.toLowerCase().endsWith('.gif'); + } +} + +/// 图片预览页面 - 全屏显示单张图片 +class ImagePreviewPage extends StatefulWidget { + /// 图片文件列表 + final List images; + + /// 初始显示的图片索引 + final int initialIndex; + + const ImagePreviewPage({ + Key? key, + required this.images, + required this.initialIndex, + }) : super(key: key); + + @override + State createState() => _ImagePreviewPageState(); +} + +class _ImagePreviewPageState extends State + with SingleTickerProviderStateMixin { + late PageController _pageController; + late AnimationController _animationController; + late Animation _fadeAnimation; + + /// 当前显示的图片索引 + int currentIndex = 0; + + @override + void initState() { + super.initState(); + currentIndex = widget.initialIndex; + _pageController = PageController(initialPage: currentIndex); + + // 动画控制器 + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + + // 延迟显示UI控件 + Future.delayed(const Duration(milliseconds: 500), () { + if (mounted) { + _animationController.forward(); + } + }); + } + + @override + void dispose() { + _pageController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + // 图片显示区域 + PageView.builder( + controller: _pageController, + itemCount: widget.images.length, + onPageChanged: (index) { + setState(() { + currentIndex = index; + }); + }, + itemBuilder: (context, index) => _buildFullImage(context, index), + ), + + // 顶部工具栏 + FadeTransition( + opacity: _fadeAnimation, + child: _buildTopToolbar(), + ), + + // 底部指示器 + FadeTransition( + opacity: _fadeAnimation, + child: _buildBottomIndicator(), + ), + ], + ), + ); + } + + /// 构建全屏图片 + Widget _buildFullImage(BuildContext context, int index) { + final imageFile = widget.images[index]; + + return InteractiveViewer( + panEnabled: true, + boundaryMargin: const EdgeInsets.all(20), + minScale: 0.5, + maxScale: 4.0, + child: Image.file( + File(imageFile.path), + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) => _buildErrorView(), + ), + ); + } + + /// 构建错误视图 + Widget _buildErrorView() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.broken_image, + size: 64, + color: Colors.white54, + ), + const SizedBox(height: 16), + Text( + '图片加载失败', + style: TextStyle( + color: Colors.white54, + fontSize: 16, + ), + ), + ], + ), + ); + } + + /// 构建顶部工具栏 + Widget _buildTopToolbar() { + return Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + height: MediaQuery.of(context).padding.top + 56, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.7), + Colors.transparent, + ], + ), + ), + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top, + left: 16, + right: 16, + ), + child: Row( + children: [ + // 返回按钮 + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon( + Icons.arrow_back, + color: Colors.white, + ), + ), + + const Spacer(), + + // 图片信息 + if (_isGifImage(widget.images[currentIndex].path)) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + 'GIF', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ); + } + + /// 构建底部指示器 + Widget _buildBottomIndicator() { + return Positioned( + bottom: MediaQuery.of(context).padding.bottom + 16, + left: 0, + right: 0, + child: Center( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + '${currentIndex + 1} / ${widget.images.length}', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + ), + ), + ); + } + + /// 检查是否为GIF图片 + bool _isGifImage(String path) { + return path.toLowerCase().endsWith('.gif'); + } +} \ No newline at end of file diff --git a/lib/presentation/widgets/save_dialog.dart b/lib/presentation/widgets/save_dialog.dart new file mode 100644 index 0000000..fc7b817 --- /dev/null +++ b/lib/presentation/widgets/save_dialog.dart @@ -0,0 +1,549 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import '../providers/share_provider.dart'; +import 'custom_button.dart'; +import 'image_grid_preview.dart'; +import 'folder_selector.dart'; +import 'tag_selector.dart'; + +/// 保存对话框 - 处理分享图片的保存界面 +/// 支持批量保存、文件夹选择、标签添加等功能 +class SaveDialog extends ConsumerStatefulWidget { + /// 分享接收的媒体文件列表 + final List sharedFiles; + + /// 对话框关闭回调 + final VoidCallback onClose; + + const SaveDialog({ + Key? key, + required this.sharedFiles, + required this.onClose, + }) : super(key: key); + + @override + ConsumerState createState() => _SaveDialogState(); +} + +class _SaveDialogState extends ConsumerState { + /// 当前选中的文件夹ID + String? selectedFolderId; + + /// 当前输入的标签列表 + final List tags = []; + + /// 标签输入控制器 + final TextEditingController tagController = TextEditingController(); + + /// 备注输入控制器 + final TextEditingController noteController = TextEditingController(); + + /// 是否为批量保存模式 + bool isBatchMode = true; + + /// 当前编辑的图片索引 + int currentEditingIndex = 0; + + @override + void dispose() { + tagController.dispose(); + noteController.dispose(); + super.dispose(); + } + + /// 切换批量/单张编辑模式 + void toggleEditMode() { + setState(() { + isBatchMode = !isBatchMode; + currentEditingIndex = 0; + }); + } + + /// 切换到下一张图片编辑 + void nextImage() { + if (currentEditingIndex < widget.sharedFiles.length - 1) { + setState(() { + currentEditingIndex++; + }); + } + } + + /// 切换到上一张图片编辑 + void previousImage() { + if (currentEditingIndex > 0) { + setState(() { + currentEditingIndex--; + }); + } + } + + /// 添加标签 + void addTag(String tag) { + if (tag.isNotEmpty && !tags.contains(tag)) { + setState(() { + tags.add(tag); + }); + tagController.clear(); + } + } + + /// 移除标签 + void removeTag(String tag) { + setState(() { + tags.remove(tag); + }); + } + + /// 执行保存操作 + Future performSave() async { + if (selectedFolderId == null) { + _showErrorSnackBar('请选择保存文件夹'); + return; + } + + // 显示保存进度 + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const Dialog( + child: Padding( + padding: EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('正在保存图片...'), + ], + ), + ), + ), + ); + + try { + if (isBatchMode) { + // 批量保存模式 + await ref.read(shareProvider.notifier).saveBatchImages( + sharedFiles: widget.sharedFiles, + folderId: selectedFolderId!, + tags: tags, + note: noteController.text.trim(), + ); + } else { + // 单张保存模式 + await ref.read(shareProvider.notifier).saveSingleImage( + sharedFile: widget.sharedFiles[currentEditingIndex], + folderId: selectedFolderId!, + tags: tags, + note: noteController.text.trim(), + ); + } + + // 关闭进度对话框 + Navigator.of(context).pop(); + + // 显示成功消息 + _showSuccessSnackBar('图片保存成功!'); + + // 关闭保存对话框 + widget.onClose(); + } catch (e) { + // 关闭进度对话框 + Navigator.of(context).pop(); + + // 显示错误消息 + _showErrorSnackBar('保存失败:${e.toString()}'); + } + } + + /// 显示错误提示 + void _showErrorSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + + /// 显示成功提示 + void _showSuccessSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Theme.of(context).colorScheme.primary, + ), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height * 0.8, + decoration: BoxDecoration( + color: theme.scaffoldBackgroundColor.withOpacity(0.95), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + children: [ + // 标题栏 + _buildHeader(colorScheme), + + // 内容区域 + Expanded( + child: Row( + children: [ + // 左侧图片预览区域 + Expanded( + flex: 2, + child: _buildImagePreviewArea(), + ), + + // 分隔线 + VerticalDivider( + color: colorScheme.outline.withOpacity(0.2), + thickness: 1, + ), + + // 右侧设置区域 + Expanded( + flex: 1, + child: _buildSettingsArea(colorScheme), + ), + ], + ), + ), + + // 底部操作栏 + _buildFooter(colorScheme), + ], + ), + ), + ); + } + + /// 构建标题栏 + Widget _buildHeader(ColorScheme colorScheme) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Row( + children: [ + Text( + '保存灵感', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + + // 批量/单张切换按钮 + if (widget.sharedFiles.length > 1) + Padding( + padding: const EdgeInsets.only(right: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FilterChip( + label: const Text('批量'), + selected: isBatchMode, + onSelected: (selected) { + if (selected) toggleEditMode(); + }, + avatar: const Icon(Icons.photo_library), + ), + const SizedBox(width: 8), + FilterChip( + label: const Text('单张'), + selected: !isBatchMode, + onSelected: (selected) { + if (selected) toggleEditMode(); + }, + avatar: const Icon(Icons.photo), + ), + ], + ), + ), + + // 关闭按钮 + IconButton( + onPressed: widget.onClose, + icon: const Icon(Icons.close), + tooltip: '关闭', + ), + ], + ), + ); + } + + /// 构建图片预览区域 + Widget _buildImagePreviewArea() { + if (isBatchMode) { + // 批量模式:使用图片网格预览组件 + return ImageGridPreview( + images: widget.sharedFiles, + selectedIndices: [], // 批量模式下显示所有图片 + onSelectionChanged: (index, selected) { + // 批量模式下不处理选择 + }, + showSelection: false, // 批量模式下不显示选择框 + crossAxisCount: 2, + spacing: 8.0, + padding: const EdgeInsets.all(16), + ); + } else { + // 单张模式:显示单张图片和切换控制 + return Column( + children: [ + // 图片显示区域 + Expanded( + child: Padding( + padding: const EdgeInsets.all(16), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(widget.sharedFiles[currentEditingIndex].path), + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey[300], + child: const Icon( + Icons.broken_image, + color: Colors.grey, + size: 64, + ), + ); + }, + ), + ), + ), + ), + + // 图片切换控制 + if (widget.sharedFiles.length > 1) + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: currentEditingIndex > 0 ? previousImage : null, + icon: const Icon(Icons.keyboard_arrow_left), + ), + Text( + '${currentEditingIndex + 1} / ${widget.sharedFiles.length}', + style: Theme.of(context).textTheme.bodyMedium, + ), + IconButton( + onPressed: currentEditingIndex < widget.sharedFiles.length - 1 + ? nextImage + : null, + icon: const Icon(Icons.keyboard_arrow_right), + ), + ], + ), + ), + ], + ); + } + } + + /// 构建设置区域 + Widget _buildSettingsArea(ColorScheme colorScheme) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 文件夹选择 + _buildFolderSelector(colorScheme), + + const SizedBox(height: 16), + + // 标签输入 + _buildTagInput(colorScheme), + + const SizedBox(height: 16), + + // 备注输入 + _buildNoteInput(colorScheme), + ], + ), + ); + } + + /// 构建文件夹选择器 + Widget _buildFolderSelector(ColorScheme colorScheme) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '选择文件夹', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + GestureDetector( + onTap: _showFolderSelector, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: colorScheme.outline.withOpacity(0.3)), + borderRadius: BorderRadius.circular(8), + color: colorScheme.surface, + ), + child: Row( + children: [ + Icon( + selectedFolderId != null ? Icons.folder : Icons.folder_outlined, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + selectedFolderId != null ? '已选择文件夹' : '点击选择文件夹', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: selectedFolderId != null + ? colorScheme.onSurface + : colorScheme.onSurface.withOpacity(0.6), + ), + ), + ), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ), + ], + ); + } + + /// 构建标签输入 + Widget _buildTagInput(ColorScheme colorScheme) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '添加标签', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + TagSelector( + selectedTags: tags, + onTagsChanged: (newTags) { + setState(() { + tags.clear(); + tags.addAll(newTags); + }); + }, + showCreateButton: true, + maxTags: 10, + hintText: '输入标签或从已有标签中选择', + ), + ], + ); + } + + /// 构建备注输入 + Widget _buildNoteInput(ColorScheme colorScheme) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '添加备注', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + TextField( + controller: noteController, + decoration: const InputDecoration( + hintText: '输入备注信息(可选)', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.note_add), + ), + maxLines: 3, + minLines: 1, + ), + ], + ); + } + + /// 构建底部操作栏 + Widget _buildFooter(ColorScheme colorScheme) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // 取消按钮 + CustomButton( + text: '取消', + onPressed: widget.onClose, + buttonType: ButtonType.text, + ), + const SizedBox(width: 16), + + // 保存按钮 + CustomButton( + text: isBatchMode + ? '保存 ${widget.sharedFiles.length} 张图片' + : '保存图片', + onPressed: performSave, + buttonType: ButtonType.primary, + isLoading: false, + ), + ], + ), + ); + } + + /// 显示文件夹选择器 + void _showFolderSelector() { + showDialog( + context: context, + builder: (context) => FolderSelectorDialog( + onFolderSelected: (folderId) { + setState(() { + selectedFolderId = folderId; + }); + Navigator.of(context).pop(); + }, + currentFolderId: selectedFolderId, + ), + ); + } +} \ No newline at end of file diff --git a/lib/presentation/widgets/share_test_widget.dart b/lib/presentation/widgets/share_test_widget.dart index 5b89f45..b4a9f49 100644 --- a/lib/presentation/widgets/share_test_widget.dart +++ b/lib/presentation/widgets/share_test_widget.dart @@ -218,6 +218,14 @@ class ShareTestWidget extends ConsumerWidget { icon: const Icon(Icons.refresh), label: const Text('刷新'), ), + const SizedBox(width: 12), + OutlinedButton.icon( + onPressed: !shareState.isProcessing + ? () => _simulateShareTest(ref) + : null, + icon: const Icon(Icons.photo), + label: const Text('模拟分享'), + ), ], ); } @@ -419,4 +427,27 @@ class ShareTestWidget extends ConsumerWidget { final shareNotifier = ref.read(shareProvider.notifier); shareNotifier.clearError(); } + + /// 模拟分享测试 - 创建模拟的分享数据来测试保存对话框 + void _simulateShareTest(WidgetRef ref) { + Logger.info('用户点击模拟分享测试'); + + // 创建模拟的分享文件 + final mockFiles = [ + SharedMediaFile( + path: 'test://image1.jpg', + type: SharedMediaType.image, + ), + SharedMediaFile( + path: 'test://image2.png', + type: SharedMediaType.image, + ), + ]; + + // 模拟接收到分享文件 + final shareNotifier = ref.read(shareProvider.notifier); + shareNotifier.handleSharedFiles(mockFiles); + + Logger.info('模拟分享测试完成,创建了${mockFiles.length}个测试文件'); + } } \ No newline at end of file diff --git a/lib/presentation/widgets/tag_selector.dart b/lib/presentation/widgets/tag_selector.dart new file mode 100644 index 0000000..0311693 --- /dev/null +++ b/lib/presentation/widgets/tag_selector.dart @@ -0,0 +1,776 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../providers/tag_provider.dart'; +import 'custom_button.dart'; + +/// 标签选择器组件 - 用于为图片添加和管理标签 +/// 支持新建标签、搜索标签、最近使用排序等功能 +class TagSelector extends ConsumerStatefulWidget { + /// 当前选中的标签列表 + final List selectedTags; + + /// 标签选择变更回调 + final Function(List tags) onTagsChanged; + + /// 是否显示创建按钮 + final bool showCreateButton; + + /// 最大标签数量 + final int maxTags; + + /// 占位提示文本 + final String hintText; + + const TagSelector({ + Key? key, + required this.selectedTags, + required this.onTagsChanged, + this.showCreateButton = true, + this.maxTags = 10, + this.hintText = '添加标签...', + }) : super(key: key); + + @override + ConsumerState createState() => _TagSelectorState(); +} + +class _TagSelectorState extends ConsumerState { + /// 搜索控制器 + final TextEditingController _searchController = TextEditingController(); + + /// 输入控制器 + final TextEditingController _inputController = TextEditingController(); + + /// 搜索关键词 + String searchKeyword = ''; + + /// 是否显示建议列表 + bool showSuggestions = false; + + @override + void initState() { + super.initState(); + // 加载标签列表 + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(tagProvider.notifier).loadTags(); + }); + } + + @override + void dispose() { + _searchController.dispose(); + _inputController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标签输入区域 + _buildTagInputArea(colorScheme), + + // 已选标签显示 + if (widget.selectedTags.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 12), + child: _buildSelectedTags(colorScheme), + ), + + // 标签建议列表 + if (showSuggestions) + Padding( + padding: const EdgeInsets.only(top: 8), + child: _buildTagSuggestions(colorScheme), + ), + ], + ); + } + + /// 构建标签输入区域 + Widget _buildTagInputArea(ColorScheme colorScheme) { + return Row( + children: [ + // 输入框 + Expanded( + child: TextField( + controller: _inputController, + decoration: InputDecoration( + hintText: widget.hintText, + prefixIcon: const Icon(Icons.tag), + suffixIcon: _inputController.text.isNotEmpty + ? IconButton( + onPressed: () { + _inputController.clear(); + setState(() { + showSuggestions = false; + }); + }, + 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; + showSuggestions = value.isNotEmpty; + }); + }, + onSubmitted: (value) { + _addNewTag(value.trim()); + }, + ), + ), + + const SizedBox(width: 12), + + // 添加按钮 + CustomButton( + text: '添加', + onPressed: _inputController.text.trim().isNotEmpty + ? () => _addNewTag(_inputController.text.trim()) + : null, + buttonType: ButtonType.outlined, + ), + ], + ); + } + + /// 构建已选标签 + Widget _buildSelectedTags(ColorScheme colorScheme) { + return Wrap( + spacing: 8, + runSpacing: 6, + children: widget.selectedTags.map((tag) { + return Chip( + label: Text( + tag, + style: TextStyle( + color: colorScheme.onPrimary, + fontSize: 12, + ), + ), + backgroundColor: colorScheme.primary, + deleteIcon: Icon( + Icons.close, + color: colorScheme.onPrimary, + size: 16, + ), + onDeleted: () => _removeTag(tag), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + ); + }).toList(), + ); + } + + /// 构建标签建议列表 + Widget _buildTagSuggestions(ColorScheme colorScheme) { + final tagState = ref.watch(tagProvider); + + if (tagState.isLoading) { + return const SizedBox( + height: 100, + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + + final tags = tagState.tags; + + if (tags.isEmpty) { + return _buildEmptySuggestions(colorScheme); + } + + // 过滤标签(排除已选标签) + final availableTags = tags.where((tag) { + return !widget.selectedTags.contains(tag.name) && + (searchKeyword.isEmpty || + tag.name.toLowerCase().contains(searchKeyword.toLowerCase())); + }).toList(); + + if (availableTags.isEmpty) { + return _buildNoSuggestions(colorScheme); + } + + // 按使用次数排序(最近使用的在前) + availableTags.sort((a, b) => b.usageCount.compareTo(a.usageCount)); + + return Container( + height: 120, + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: colorScheme.outline.withOpacity(0.2), + ), + ), + child: ListView.separated( + padding: const EdgeInsets.all(8), + itemCount: availableTags.length, + separatorBuilder: (context, index) => const SizedBox(height: 4), + itemBuilder: (context, index) { + final tag = availableTags[index]; + return _buildSuggestionItem(tag, colorScheme); + }, + ), + ); + } + + /// 构建空建议状态 + Widget _buildEmptySuggestions(ColorScheme colorScheme) { + return Container( + height: 100, + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: colorScheme.outline.withOpacity(0.2), + ), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.tag_outlined, + size: 32, + color: colorScheme.outline, + ), + const SizedBox(height: 8), + Text( + '还没有标签', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.outline, + ), + ), + ], + ), + ), + ); + } + + /// 构建无建议状态 + Widget _buildNoSuggestions(ColorScheme colorScheme) { + return Container( + height: 80, + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: colorScheme.outline.withOpacity(0.2), + ), + ), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '没有找到匹配的标签', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.outline, + ), + ), + if (widget.showCreateButton && _inputController.text.trim().isNotEmpty) + Padding( + padding: const EdgeInsets.only(left: 8), + child: CustomButton( + text: '创建', + onPressed: () => _createNewTag(_inputController.text.trim()), + buttonType: ButtonType.text, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + ), + ), + ], + ), + ), + ); + } + + /// 构建建议项 + Widget _buildSuggestionItem(dynamic tag, ColorScheme colorScheme) { + return Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(6), + onTap: () => _addExistingTag(tag.name), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + // 标签图标 + Icon( + _getTagIcon(tag.icon), + color: _getTagColor(tag.color), + size: 16, + ), + const SizedBox(width: 8), + + // 标签名称 + Expanded( + child: Text( + tag.name, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + + // 使用次数 + if (tag.usageCount > 0) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: colorScheme.primaryContainer.withOpacity(0.5), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '${tag.usageCount}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onPrimaryContainer, + fontSize: 10, + ), + ), + ), + ], + ), + ), + ), + ); + } + + /// 添加新标签 + void _addNewTag(String tagName) { + if (tagName.isEmpty) return; + + final newTags = List.from(widget.selectedTags); + + if (!newTags.contains(tagName)) { + if (newTags.length >= widget.maxTags) { + _showErrorSnackBar('最多只能添加${widget.maxTags}个标签'); + return; + } + + newTags.add(tagName); + widget.onTagsChanged(newTags); + + // 更新标签使用次数 + ref.read(tagProvider.notifier).incrementTagUsage(tagName); + } + + _inputController.clear(); + setState(() { + searchKeyword = ''; + showSuggestions = false; + }); + } + + /// 添加已存在的标签 + void _addExistingTag(String tagName) { + _addNewTag(tagName); + } + + /// 创建新标签 + void _createNewTag(String tagName) { + if (tagName.isEmpty) return; + + // 显示创建标签对话框 + showDialog( + context: context, + builder: (context) => CreateTagDialog( + tagName: tagName, + onTagCreated: (tagId) { + Navigator.of(context).pop(); + _addNewTag(tagName); + }, + ), + ); + } + + /// 移除标签 + void _removeTag(String tagName) { + final newTags = List.from(widget.selectedTags)..remove(tagName); + widget.onTagsChanged(newTags); + } + + /// 显示错误提示 + void _showErrorSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + + /// 获取标签图标 + IconData _getTagIcon(String? iconCode) { + // 默认标签图标 + if (iconCode == null || iconCode.isEmpty) { + return Icons.local_offer; + } + + // 尝试解析Material Icons名称 + try { + // 这里需要根据实际的图标存储方式来实现 + // 暂时返回默认图标 + return Icons.local_offer; + } catch (e) { + return Icons.local_offer; + } + } + + /// 获取标签颜色 + Color _getTagColor(String? colorCode) { + // 默认标签颜色 + if (colorCode == null || colorCode.isEmpty) { + return Colors.blue; + } + + try { + // 尝试解析十六进制颜色 + return Color(int.parse(colorCode.substring(1), radix: 16) + 0xFF000000); + } catch (e) { + return Colors.blue; + } + } +} + +/// 创建标签对话框 +class CreateTagDialog extends ConsumerStatefulWidget { + /// 标签名称 + final String tagName; + + /// 标签创建成功回调 + final Function(String tagId) onTagCreated; + + const CreateTagDialog({ + Key? key, + required this.tagName, + required this.onTagCreated, + }) : super(key: key); + + @override + ConsumerState createState() => _CreateTagDialogState(); +} + +class _CreateTagDialogState extends ConsumerState { + /// 标签名称控制器 + late TextEditingController _nameController; + + /// 选中的图标 + String selectedIcon = 'local_offer'; + + /// 选中的颜色 + String selectedColor = '#2196F3'; // 默认蓝色 + + /// 表单键 + final GlobalKey _formKey = GlobalKey(); + + /// 是否正在创建 + bool isCreating = false; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.tagName); + } + + @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.local_offer), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return '请输入标签名称'; + } + if (value.trim().length > 10) { + return '标签名称不能超过10个字符'; + } + return null; + }, + textInputAction: TextInputAction.next, + ), + const SizedBox(height: 20), + + // 图标选择 + Text( + '选择图标', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 12), + _buildIconSelector(colorScheme), + const SizedBox(height: 20), + + // 颜色选择 + Text( + '选择颜色', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 12), + _buildColorSelector(), + 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 : _createTag, + buttonType: ButtonType.primary, + isLoading: isCreating, + ), + ], + ), + ], + ), + ), + ), + ); + } + + /// 构建图标选择器 + Widget _buildIconSelector(ColorScheme colorScheme) { + final icons = [ + 'local_offer', + 'label', + 'bookmark', + 'favorite', + 'star', + 'tag', + 'category', + 'style', + 'interests', + 'lightbulb', + ]; + + 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(), + ); + } + + /// 构建颜色选择器 + Widget _buildColorSelector() { + final colors = [ + '#F44336', // 红色 + '#E91E63', // 粉色 + '#9C27B0', // 紫色 + '#673AB7', // 深紫 + '#3F51B5', // 靛蓝 + '#2196F3', // 蓝色 + '#03A9F4', // 浅蓝 + '#00BCD4', // 青色 + '#009688', // 蓝绿 + '#4CAF50', // 绿色 + '#8BC34A', // 浅绿 + '#CDDC39', // 黄绿 + '#FFEB3B', // 黄色 + '#FFC107', // 琥珀 + '#FF9800', // 橙色 + '#FF5722', // 深橙 + ]; + + return Wrap( + spacing: 8, + runSpacing: 8, + children: colors.map((color) { + final isSelected = selectedColor == color; + return GestureDetector( + onTap: () { + setState(() { + selectedColor = color; + }); + }, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Color(int.parse(color.substring(1), radix: 16) + 0xFF000000), + borderRadius: BorderRadius.circular(18), + border: Border.all( + color: isSelected ? Colors.black : Colors.white, + width: isSelected ? 3 : 2, + ), + ), + child: isSelected + ? const Icon( + Icons.check, + color: Colors.white, + size: 20, + ) + : null, + ), + ); + }).toList(), + ); + } + + /// 创建标签 + Future _createTag() async { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() { + isCreating = true; + }); + + try { + final tagId = await ref.read(tagProvider.notifier).createTag( + name: _nameController.text.trim(), + icon: selectedIcon, + color: selectedColor, + ); + + if (tagId != null) { + widget.onTagCreated(tagId); + } 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 'local_offer': + return Icons.local_offer; + case 'label': + return Icons.label; + case 'bookmark': + return Icons.bookmark; + case 'favorite': + return Icons.favorite; + case 'star': + return Icons.star; + case 'tag': + return Icons.tag; + case 'category': + return Icons.category; + case 'style': + return Icons.style; + case 'interests': + return Icons.interests; + case 'lightbulb': + return Icons.lightbulb; + default: + return Icons.local_offer; + } + } +} \ No newline at end of file