This commit is contained in:
ddshi 2025-10-09 17:10:38 +08:00
parent 8f284075b6
commit 7114c367b6
18 changed files with 3796 additions and 16 deletions

View File

@ -2,5 +2,6 @@
"language_preferences": {
"documentation": "zh-CN",
"code_comments": "zh-CN"
}
},
"primaryApiKey": "xxx"
}

10
.claude/settings.json Normal file
View File

@ -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"
}
}

View File

@ -11,7 +11,8 @@
"Bash(flutter build:*)",
"Bash(flutter clean:*)",
"Bash(dart analyze:*)",
"Bash(dart test:*)"
"Bash(dart test:*)",
"Bash(grep:*)"
],
"deny": [],
"ask": []

View File

@ -18,6 +18,12 @@ class FolderDao {
return hiveFolder.id;
}
/// Hive文件夹 - HiveImageFolder对象
/// [hiveFolder] Hive文件夹对象
Future<void> 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<HiveImageFolder?> getHiveFolderById(String id) async {
return _foldersBox.get(id);
}
/// - 使
/// 使
Future<List<ImageFolder>> getAllFolders() async {
@ -41,14 +54,29 @@ class FolderDao {
/// 使
Future<List<ImageFolder>> 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<List<HiveImageFolder>> 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<void> updateHiveFolder(HiveImageFolder hiveFolder) async {
await _foldersBox.put(hiveFolder.id, hiveFolder);
}
/// 使 - 访
/// [folderId] ID
///
@ -165,15 +199,27 @@ class FolderDao {
///
Future<List<ImageFolder>> 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<List<HiveImageFolder>> 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<void> close() async {

View File

@ -18,6 +18,12 @@ class TagDao {
return hiveTag.id;
}
/// Hive标签 - HiveImageTag对象
/// [hiveTag] Hive标签对象
Future<void> 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<HiveImageTag?> getHiveTagById(String id) async {
return _tagsBox.get(id);
}
/// -
/// [name]
/// null
@ -55,6 +68,21 @@ class TagDao {
}
}
/// Hive标签 - Hive标签对象
/// [name]
/// Hive标签对象null
Future<HiveImageTag?> getHiveTagByName(String name) async {
try {
final hiveTag = _tagsBox.values.firstWhere(
(tag) => tag.name == name,
);
return hiveTag;
} catch (e) {
// null
return null;
}
}
/// - 使
/// 使
Future<List<ImageTag>> getAllTags() async {
@ -219,4 +247,83 @@ class TagDao {
Future<void> close() async {
await _tagsBox.close();
}
/// Hive标签 - HiveImageTag对象列表
/// Hive标签列表
Future<List<HiveImageTag>> getAllHiveTags() async {
final hiveTags = _tagsBox.values.toList()
..sort((a, b) => b.usageCount.compareTo(a.usageCount));
return hiveTags;
}
/// Hive标签 - HiveImageTag对象列表
/// [limit]
/// Hive标签列表
Future<List<HiveImageTag>> 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<List<HiveImageTag>> 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<List<HiveImageTag>> 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<List<HiveImageTag>> getUnusedHiveTags() async {
return _tagsBox.values
.where((tag) => tag.usageCount == 0)
.toList()
..sort((a, b) => b.lastUsedAt.compareTo(a.lastUsedAt));
}
/// Hive标签 - HiveImageTag对象
/// [hiveTag] Hive标签对象
Future<void> updateHiveTag(HiveImageTag hiveTag) async {
await _tagsBox.put(hiveTag.id, hiveTag);
}
/// 使Hive标签 - 使0
///
Future<int> cleanupUnusedHiveTags() async {
final unusedTags = await getUnusedHiveTags();
for (final tag in unusedTags) {
await _tagsBox.delete(tag.id);
}
return unusedTags.length;
}
/// 使 - 使0
///
Future<int> cleanupUnusedTags() async {
return await cleanupUnusedHiveTags();
}
}

View File

@ -0,0 +1,41 @@
import '../../domain/entities/image_folder.dart';
/// -
///
abstract class FolderRepository {
///
Future<List<ImageFolder>> getAllFolders();
/// ID获取文件夹
Future<ImageFolder?> getFolderById(String id);
///
Future<String> createFolder({
required String name,
required String icon,
String? coverImageId,
});
///
Future<void> updateFolder({
required String id,
required String name,
required String icon,
String? coverImageId,
});
///
Future<void> deleteFolder(String id);
/// 使
Future<void> updateFolderLastUsed(String id);
///
Future<int> getFolderImageCount(String id);
/// 使
Future<List<ImageFolder>> getRecentFolders({int limit = 10});
///
Future<List<ImageFolder>> searchFolders(String keyword);
}

View File

@ -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<List<ImageFolder>> getAllFolders() async {
try {
final folders = await _folderDao.getAllFolders();
return folders;
} catch (e) {
throw Exception('获取文件夹列表失败: $e');
}
}
@override
Future<ImageFolder?> getFolderById(String id) async {
try {
final folder = await _folderDao.getFolderById(id);
return folder;
} catch (e) {
throw Exception('获取文件夹失败: $e');
}
}
@override
Future<String> 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<void> 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<void> deleteFolder(String id) async {
try {
await _folderDao.deleteFolder(id);
} catch (e) {
throw Exception('删除文件夹失败: $e');
}
}
@override
Future<void> 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<int> getFolderImageCount(String id) async {
try {
//
// ImageDAO的相关方法
// 0
return 0;
} catch (e) {
throw Exception('获取文件夹图片数量失败: $e');
}
}
@override
Future<List<ImageFolder>> getRecentFolders({int limit = 10}) async {
try {
final folders = await _folderDao.getRecentFolders(limit: limit);
return folders;
} catch (e) {
throw Exception('获取最近使用的文件夹失败: $e');
}
}
@override
Future<List<ImageFolder>> 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;
}
}

View File

@ -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<String> createTag(ImageTag tag) async {
try {
final hiveTag = HiveImageTag.fromEntity(tag);
await _tagDao.insertHiveTag(hiveTag);
return hiveTag.id;
} catch (e) {
throw Exception('创建标签失败: $e');
}
}
///
Future<String> 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<List<String>> createTags(List<ImageTag> tags) async {
try {
final List<String> tagIds = [];
for (final tag in tags) {
final tagId = await createTag(tag);
tagIds.add(tagId);
}
return tagIds;
} catch (e) {
throw Exception('批量创建标签失败: $e');
}
}
@override
Future<ImageTag?> getTag(String id) async {
try {
final hiveTag = await _tagDao.getHiveTagById(id);
return hiveTag?.toEntity();
} catch (e) {
throw Exception('获取标签失败: $e');
}
}
@override
Future<ImageTag?> getTagByName(String name) async {
try {
final tag = await _tagDao.getTagByName(name);
return tag; // DAO已经返回ImageTag
} catch (e) {
throw Exception('根据名称获取标签失败: $e');
}
}
@override
Future<List<ImageTag>> getAllTags() async {
try {
final hiveTags = await _tagDao.getAllHiveTags();
return hiveTags.map((hiveTag) => hiveTag.toEntity()).toList();
} catch (e) {
throw Exception('获取所有标签失败: $e');
}
}
@override
Future<List<ImageTag>> 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<List<ImageTag>> 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<ImageTag> updateTag(ImageTag tag) async {
try {
final hiveTag = HiveImageTag.fromEntity(tag);
await _tagDao.updateHiveTag(hiveTag);
return hiveTag.toEntity();
} catch (e) {
throw Exception('更新标签失败: $e');
}
}
@override
Future<ImageTag?> incrementTagUsage(String tagId) async {
try {
final updatedTag = await _tagDao.incrementTagUsage(tagId);
return updatedTag; // DAO已经返回ImageTag
} catch (e) {
throw Exception('增加标签使用次数失败: $e');
}
}
/// 使
Future<void> 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<ImageTag?> decrementTagUsage(String tagId) async {
try {
final updatedTag = await _tagDao.decrementTagUsage(tagId);
return updatedTag; // DAO已经返回ImageTag
} catch (e) {
throw Exception('减少标签使用次数失败: $e');
}
}
@override
Future<bool> deleteTag(String id) async {
try {
await _tagDao.deleteTag(id);
return true;
} catch (e) {
throw Exception('删除标签失败: $e');
}
}
@override
Future<bool> deleteTags(List<String> ids) async {
try {
for (final id in ids) {
await _tagDao.deleteTag(id);
}
return true;
} catch (e) {
throw Exception('批量删除标签失败: $e');
}
}
@override
Future<bool> tagExists(String id) async {
try {
return await _tagDao.tagExists(id);
} catch (e) {
throw Exception('检查标签是否存在失败: $e');
}
}
@override
Future<bool> tagNameExists(String name) async {
try {
return await _tagDao.tagNameExists(name);
} catch (e) {
throw Exception('检查标签名称是否存在失败: $e');
}
}
@override
Future<int> getTagCount() async {
try {
return await _tagDao.getTagCount();
} catch (e) {
throw Exception('获取标签总数失败: $e');
}
}
@override
Future<List<ImageTag>> 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<List<ImageTag>> getUnusedTags() async {
try {
final hiveTags = await _tagDao.getUnusedHiveTags();
return hiveTags.map((hiveTag) => hiveTag.toEntity()).toList();
} catch (e) {
throw Exception('获取未使用的标签失败: $e');
}
}
@override
Future<int> 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;
}
}

View File

@ -66,4 +66,8 @@ class ImageFolder {
lastUsedAt: lastUsedAt ?? this.lastUsedAt,
);
}
/// -
/// Repository查询
int get imageCount => 0;
}

View File

@ -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<AppHomePage> {
@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<AppHomePage> {
),
),
],
),
),
),
//
if (shareState.showShareUI && shareState.pendingFiles.isNotEmpty)
SaveDialog(
sharedFiles: shareState.pendingFiles,
onClose: () {
//
ref.read(shareProvider.notifier).clearPendingFiles();
},
),
],
),
//
bottomNavigationBar: NavigationBar(
destinations: const [

View File

@ -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<FolderModel> 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<FolderModel>? 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<String, dynamic> 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<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'icon': icon,
'imageCount': imageCount,
'createdAt': createdAt.toIso8601String(),
'lastUsedAt': lastUsedAt.toIso8601String(),
};
}
}
/// Provider -
class FolderProvider extends StateNotifier<FolderState> {
final FolderRepository _folderRepository;
FolderProvider(this._folderRepository) : super(FolderState.initial());
///
Future<void> 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<String?> 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<bool> 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<bool> deleteFolder(String id) async {
try {
await _folderRepository.deleteFolder(id);
//
await loadFolders();
return true;
} catch (e) {
state = state.copyWithLoading(error: e.toString());
return false;
}
}
/// 使
Future<void> updateLastUsedTime(String folderId) async {
try {
await _folderRepository.updateFolderLastUsed(folderId);
//
await loadFolders();
} catch (e) {
//
debugPrint('更新文件夹使用时间失败: $e');
}
}
}
/// Provider实例
final folderProvider = StateNotifierProvider<FolderProvider, FolderState>((ref) {
final folderRepository = ref.watch(folderRepositoryProvider);
return FolderProvider(folderRepository);
});
/// Repository Provider
final folderRepositoryProvider = Provider<FolderRepository>((ref) {
final folderDao = ref.watch(folderDaoProvider);
return FolderRepositoryImpl(folderDao);
});
/// DAO Provider
final folderDaoProvider = Provider<FolderDao>((ref) {
return FolderDao();
});

View File

@ -348,11 +348,93 @@ class ShareNotifier extends StateNotifier<ShareState> {
_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<void> saveBatchImages({
required List<SharedMediaFile> sharedFiles,
required String folderId,
required List<String> 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<void> saveSingleImage({
required SharedMediaFile sharedFile,
required String folderId,
required List<String> 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<void> dispose() async {

View File

@ -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<TagModel> 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<TagModel>? 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<String, dynamic> 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<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'icon': icon,
'color': color,
'usageCount': usageCount,
'lastUsedAt': lastUsedAt.toIso8601String(),
};
}
}
/// Provider -
class TagProvider extends StateNotifier<TagState> {
final TagRepository _tagRepository;
TagProvider(this._tagRepository) : super(TagState.initial());
///
Future<void> 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<String?> 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<bool> 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<bool> deleteTag(String id) async {
try {
await _tagRepository.deleteTag(id);
//
await loadTags();
return true;
} catch (e) {
state = state.copyWithLoading(error: e.toString());
return false;
}
}
/// 使
Future<void> 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<TagModel> getPopularTags({int limit = 10}) {
final sortedTags = List<TagModel>.from(state.tags)
..sort((a, b) => b.usageCount.compareTo(a.usageCount));
return sortedTags.take(limit).toList();
}
///
List<TagModel> 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<TagProvider, TagState>((ref) {
final tagRepository = ref.watch(tagRepositoryProvider);
return TagProvider(tagRepository);
});
/// Repository Provider
final tagRepositoryProvider = Provider<TagRepository>((ref) {
final tagDao = ref.watch(tagDaoProvider);
return TagRepositoryImpl(tagDao);
});
/// DAO Provider
final tagDaoProvider = Provider<TagDao>((ref) {
return TagDao();
});

View File

@ -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<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;
}
}
}

View File

@ -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<SharedMediaFile> images;
///
final List<int> 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<Color>(
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<void>(
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<SharedMediaFile> images;
///
final int initialIndex;
const ImagePreviewPage({
Key? key,
required this.images,
required this.initialIndex,
}) : super(key: key);
@override
State<ImagePreviewPage> createState() => _ImagePreviewPageState();
}
class _ImagePreviewPageState extends State<ImagePreviewPage>
with SingleTickerProviderStateMixin {
late PageController _pageController;
late AnimationController _animationController;
late Animation<double> _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<double>(
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');
}
}

View File

@ -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<SharedMediaFile> sharedFiles;
///
final VoidCallback onClose;
const SaveDialog({
Key? key,
required this.sharedFiles,
required this.onClose,
}) : super(key: key);
@override
ConsumerState<SaveDialog> createState() => _SaveDialogState();
}
class _SaveDialogState extends ConsumerState<SaveDialog> {
/// ID
String? selectedFolderId;
///
final List<String> 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<void> 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,
),
);
}
}

View File

@ -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}个测试文件');
}
}

View File

@ -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<String> selectedTags;
///
final Function(List<String> 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<TagSelector> createState() => _TagSelectorState();
}
class _TagSelectorState extends ConsumerState<TagSelector> {
///
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<String>.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<String>.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<CreateTagDialog> createState() => _CreateTagDialogState();
}
class _CreateTagDialogState extends ConsumerState<CreateTagDialog> {
///
late TextEditingController _nameController;
///
String selectedIcon = 'local_offer';
///
String selectedColor = '#2196F3'; //
///
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
///
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<void> _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;
}
}
}