diff --git a/CLAUDE.md b/CLAUDE.md index 098e83c..f819c23 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -146,11 +146,11 @@ flutter clean - ✅ 错误提示组件:多种错误类型(网络、服务器、权限、数据等),详细错误信息显示 #### 📤 Phase 2: 分享功能(2-3天) -**任务2.1: 分享接收机制** -- [ ] 配置receive_sharing_intent插件 -- [ ] 实现Android分享接收配置 -- [ ] 实现iOS分享接收配置 -- [ ] 处理多张图片接收逻辑 +**任务2.1: 分享接收机制** ✅ +- [x] 配置receive_sharing_intent插件 +- [x] 实现Android分享接收配置 +- [x] 实现iOS分享接收配置(已添加TODO注释,跳过iOS设备) +- [x] 处理多张图片接收逻辑(支持批量处理和队列管理) **任务2.2: 保存界面UI** - [ ] 创建半透明模态框界面 @@ -379,18 +379,18 @@ class UserService { - [x] Phase 1.2: 数据层架构搭建(4/4)✅ - [x] Phase 1.3: 核心工具类(4/4)✅ - [x] Phase 1.4: 基础UI组件(4/4)✅ -- [ ] Phase 2: 分享功能(0/4) +- [x] Phase 2.1: 分享接收机制(4/4)✅ +- [ ] Phase 2.2: 保存界面UI(0/4) ### 🎯 当前任务详情 -**任务编号**:2.1 -**任务名称**:分享接收机制 +**任务编号**:2.2 +**任务名称**:保存界面UI **任务状态**:待开始 -**预计完成**:2025年9月18日 -**依赖项**:Phase 1.4 完成 +**预计完成**:2025年9月19日 +**依赖项**:Phase 2.1 完成 **任务验收标准**: -- 分享接收机制配置完成 -- Android分享接收配置完成 -- iOS分享接收配置完成 -- 多张图片接收逻辑处理 -- 分享功能测试通过 \ No newline at end of file +- 半透明模态框界面创建完成 +- 图片网格预览组件实现 +- 文件夹选择器弹窗完成 +- 标签输入和选择组件实现 \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 30bbb26..b54eadb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -23,6 +23,18 @@ + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/snapwish/daodaoshi/snap_wish/MainActivity.kt b/android/app/src/main/kotlin/com/snapwish/daodaoshi/snap_wish/MainActivity.kt index a9f9444..6aab319 100644 --- a/android/app/src/main/kotlin/com/snapwish/daodaoshi/snap_wish/MainActivity.kt +++ b/android/app/src/main/kotlin/com/snapwish/daodaoshi/snap_wish/MainActivity.kt @@ -1,6 +1,69 @@ package com.snapwish.daodaoshi.snap_wish +import android.content.Intent +import android.os.Bundle import io.flutter.embedding.android.FlutterActivity +/// 主活动类 - 处理应用主入口和分享接收 +/// 负责接收来自其他应用的分享意图,特别是图片分享 class MainActivity: FlutterActivity() { + + /// 活动创建时调用 - 处理分享意图 + /// [savedInstanceState] 保存的实例状态 + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // 处理接收到的分享意图 + handleShareIntent(intent) + } + + /// 新意图到达时调用 - 处理前台分享 + /// [intent] 新的意图对象 + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + + // 更新活动意图并处理分享 + setIntent(intent) + handleShareIntent(intent) + } + + /// 处理分享意图 - 验证并处理接收到的分享数据 + /// [intent] 接收到的意图对象 + private fun handleShareIntent(intent: Intent?) { + intent?.let { + val action = it.action + val type = it.type + + // 验证是否为有效的分享意图 + if ((action == Intent.ACTION_SEND || action == Intent.ACTION_SEND_MULTIPLE) && type != null) { + + // 只处理图片类型的分享 + if (type.startsWith("image/")) { + android.util.Log.d("InspoSnap", "接收到图片分享意图: $action, 类型: $type") + + // 记录详细的分享信息用于调试 + when (action) { + Intent.ACTION_SEND -> { + val uri = it.getParcelableExtra(Intent.EXTRA_STREAM) + android.util.Log.d("InspoSnap", "单张图片分享: ${uri?.toString() ?: "无URI"}") + } + Intent.ACTION_SEND_MULTIPLE -> { + val uris = it.getParcelableArrayListExtra(Intent.EXTRA_STREAM) + android.util.Log.d("InspoSnap", "多张图片分享: ${uris?.size ?: 0} 张") + } + else -> { + android.util.Log.w("InspoSnap", "未知的分享类型: $action") + } + } + + // 分享数据将通过receive_sharing_intent插件传递给Flutter端 + // 这里只需要记录日志,具体的处理在Flutter端完成 + } else { + android.util.Log.w("InspoSnap", "跳过非图片类型的分享: $type") + } + } else { + android.util.Log.d("InspoSnap", "非分享意图或类型为空: $action, $type") + } + } + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index fe33829..918fe6c 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -45,5 +45,36 @@ UIApplicationSupportsIndirectInputEvents + + + + + - + \ No newline at end of file diff --git a/lib/data/datasources/share/share_intent_datasource.dart b/lib/data/datasources/share/share_intent_datasource.dart new file mode 100644 index 0000000..4339870 --- /dev/null +++ b/lib/data/datasources/share/share_intent_datasource.dart @@ -0,0 +1,348 @@ +import 'dart:async'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import '../../../core/utils/logger.dart'; + +/// 分享接收数据源 - 处理系统分享接收功能 +/// 负责接收来自其他应用的图片分享,支持单张和多张图片 +/// 优化批量处理和性能,支持大量图片分享场景 +class ShareIntentDataSource { + /// 分享接收流控制器 - 管理分享数据的异步流 + final StreamController> _sharingStreamController = + StreamController>.broadcast(); + + /// 当前接收到的分享文件列表 - 临时存储接收到的文件 + List _currentSharedFiles = []; + + /// 分享接收流 - 外部订阅分享事件 + Stream> get sharingStream => _sharingStreamController.stream; + + /// 是否正在处理分享 - 防止重复处理 + bool _isProcessingShare = false; + + /// 批量处理配置 - 控制批量处理的参数 + static const int _maxBatchSize = 20; // 最大批量处理数量 + static const Duration _batchProcessingDelay = Duration(milliseconds: 100); // 批量处理延迟 + + /// 分享文件队列 - 用于批量处理分享文件 + final List _shareQueue = []; + + /// 批量处理定时器 - 控制批量处理的时机 + Timer? _batchProcessingTimer; + + + /// 初始化分享接收 - 设置分享监听器 + /// 在应用启动时调用,监听系统分享事件 + Future initShareReceiving() async { + try { + Logger.info('初始化分享接收功能...'); + + // 监听前台分享接收(应用运行时) + ReceiveSharingIntent.instance.getMediaStream().listen( + _handleSharedMedia, + onError: _handleShareError, + ); + + // 获取初始分享数据(应用从分享启动时) + final initialSharedMedia = await ReceiveSharingIntent.instance.getInitialMedia(); + if (initialSharedMedia.isNotEmpty) { + Logger.info('检测到应用启动时的分享数据: ${initialSharedMedia.length} 个文件'); + _handleSharedMedia(initialSharedMedia); + } + + Logger.info('分享接收功能初始化完成'); + } catch (e) { + Logger.error('分享接收初始化失败', error: e); + throw ShareIntentException('初始化分享接收功能失败: $e'); + } + } + + /// 处理接收到的媒体文件 - 验证并处理分享的图片文件 + /// 支持多张图片的批量处理,优化性能和用户体验 + /// [sharedFiles] 接收到的分享文件列表 + void _handleSharedMedia(List sharedFiles) { + if (_isProcessingShare) { + Logger.warning('正在处理其他分享,将新文件加入队列'); + _addToShareQueue(sharedFiles); + return; + } + + try { + _isProcessingShare = true; + Logger.info('接收到分享文件: ${sharedFiles.length} 个'); + + // 大量文件分批处理,避免UI卡顿 + if (sharedFiles.length > _maxBatchSize) { + Logger.info('文件数量超过$_maxBatchSize个,启用分批处理'); + _processShareFilesInBatches(sharedFiles); + return; + } + + // 普通数量的文件直接处理 + _processShareFiles(sharedFiles); + + } catch (e) { + Logger.error('处理分享文件失败', error: e); + _sharingStreamController.addError( + ShareIntentException('处理分享文件失败: $e'), + ); + } finally { + _isProcessingShare = false; + // 处理队列中的剩余文件 + _processShareQueue(); + } + } + + /// 处理分享文件 - 处理普通数量的分享文件 + /// [sharedFiles] 要处理的分享文件列表 + void _processShareFiles(List sharedFiles) { + // 验证分享文件 + final validFiles = _validateSharedFiles(sharedFiles); + if (validFiles.isEmpty) { + Logger.warning('没有有效的图片文件'); + _sharingStreamController.addError( + ShareIntentException('未找到有效的图片文件'), + ); + return; + } + + // 更新当前分享文件 + _currentSharedFiles = validFiles; + + // 记录详细的文件信息 + Logger.info('处理文件详情:'); + Logger.info(' 有效文件数: ${validFiles.length}'); + Logger.info(' 总文件数: ${sharedFiles.length}'); + Logger.info(' 跳过的文件数: ${sharedFiles.length - validFiles.length}'); + + for (final file in validFiles) { + Logger.info('分享文件:'); + Logger.info(' 路径: ${file.path}'); + Logger.info(' 类型: ${file.type}'); + if (file.thumbnail != null) { + Logger.info(' 缩略图: ${file.thumbnail}'); + } + if (file.duration != null) { + Logger.info(' 持续时间: ${file.duration}'); + } + } + + // 通知监听器新的分享数据 + _sharingStreamController.add(validFiles); + Logger.info('分享文件处理完成,已通知监听器'); + } + + /// 分批处理分享文件 - 处理大量分享文件 + /// [sharedFiles] 要分批处理的分享文件列表 + void _processShareFilesInBatches(List sharedFiles) { + Logger.info('开始分批处理${sharedFiles.length}个分享文件'); + + final batches = _createBatches(sharedFiles, _maxBatchSize); + Logger.info('创建${batches.length}个批次,每批最多$_maxBatchSize个文件'); + + // 处理第一批文件(立即处理) + if (batches.isNotEmpty) { + _processShareFiles(batches.first); + } + + // 将其余批次加入队列,稍后处理 + if (batches.length > 1) { + for (int i = 1; i < batches.length; i++) { + _addToShareQueue(batches[i]); + } + Logger.info('已将${batches.length - 1}个批次加入处理队列'); + } + } + + /// 创建文件批次 - 将文件列表分成指定大小的批次 + /// [files] 文件列表 + /// [batchSize] 每批的大小 + /// 返回批次列表 + List> _createBatches(List files, int batchSize) { + final batches = >[]; + + for (int i = 0; i < files.length; i += batchSize) { + final end = (i + batchSize < files.length) ? i + batchSize : files.length; + batches.add(files.sublist(i, end)); + } + + return batches; + } + + /// 添加文件到分享队列 - 将文件加入待处理队列 + /// [files] 要加入队列的文件列表 + void _addToShareQueue(List files) { + Logger.info('添加${files.length}个文件到分享队列'); + _shareQueue.addAll(files); + + // 设置批量处理定时器 + _scheduleBatchProcessing(); + } + + /// 调度批量处理 - 设置定时器处理队列中的文件 + void _scheduleBatchProcessing() { + // 取消现有的定时器 + _batchProcessingTimer?.cancel(); + + // 设置新的定时器 + _batchProcessingTimer = Timer(_batchProcessingDelay, () { + _processShareQueue(); + }); + + Logger.info('已调度批量处理定时器'); + } + + /// 处理分享队列 - 处理队列中的待处理文件 + void _processShareQueue() { + if (_shareQueue.isEmpty) { + Logger.debug('分享队列为空,无需处理'); + return; + } + + if (_isProcessingShare) { + Logger.debug('正在处理其他分享,稍后重试'); + _scheduleBatchProcessing(); // 重新调度 + return; + } + + try { + Logger.info('开始处理分享队列,剩余${_shareQueue.length}个文件'); + + // 从队列中取出最多maxBatchSize个文件 + final batchSize = _shareQueue.length > _maxBatchSize ? _maxBatchSize : _shareQueue.length; + final batch = _shareQueue.sublist(0, batchSize); + _shareQueue.removeRange(0, batchSize); + + // 处理这批文件 + _processShareFiles(batch); + + // 如果队列中还有文件,继续调度处理 + if (_shareQueue.isNotEmpty) { + Logger.info('队列中还有${_shareQueue.length}个文件,继续调度'); + _scheduleBatchProcessing(); + } + + } catch (e) { + Logger.error('处理分享队列失败', error: e); + } + } + + + /// 验证分享文件 - 过滤有效的图片文件 + /// [files] 所有接收到的文件 + /// 返回有效的图片文件列表 + List _validateSharedFiles(List files) { + final validFiles = []; + + for (final file in files) { + try { + // 检查文件是否存在 + if (!file.path.startsWith('/')) { + Logger.warning('文件路径无效: ${file.path}'); + continue; + } + + // 检查文件类型(只处理图片) + if (file.type != SharedMediaType.image) { + Logger.warning('跳过非图片文件: ${file.type}'); + continue; + } + + // 检查文件扩展名 + final path = file.path.toLowerCase(); + final validExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.heic', '.heif']; + final hasValidExtension = validExtensions.any((ext) => path.endsWith(ext)); + + if (!hasValidExtension) { + Logger.warning('文件扩展名不支持: ${file.path}'); + continue; + } + + validFiles.add(file); + Logger.info('验证通过的文件: ${file.path}'); + } catch (e) { + Logger.error('验证文件失败: ${file.path}', error: e); + } + } + + return validFiles; + } + + /// 处理分享错误 - 统一处理分享过程中的错误 + /// [error] 分享错误信息 + void _handleShareError(dynamic error) { + Logger.error('分享接收错误', error: error); + _sharingStreamController.addError( + ShareIntentException('接收分享失败: $error'), + ); + } + + /// 获取当前分享文件 - 获取最近接收到的分享文件 + /// 返回当前分享文件列表,可能为空 + List getCurrentSharedFiles() { + return List.from(_currentSharedFiles); + } + + /// 清除当前分享数据 - 清理已处理的分享文件 + /// 通常在分享处理完成后调用 + void clearCurrentShare() { + Logger.info('清除当前分享数据'); + _currentSharedFiles.clear(); + } + + /// 重置分享接收状态 - 重置所有分享相关状态 + /// 在错误恢复或重新初始化时调用 + void reset() { + Logger.info('重置分享接收状态'); + _currentSharedFiles.clear(); + _isProcessingShare = false; + } + + /// 检查是否有待处理的分享 - 判断是否有未处理的分享文件 + /// 返回是否有待处理的分享 + bool hasPendingShare() { + return _currentSharedFiles.isNotEmpty; + } + + /// 获取分享文件数量 - 获取当前分享文件的数量 + /// 返回分享文件数量 + int getShareFileCount() { + return _currentSharedFiles.length; + } + + /// 释放资源 - 清理分享接收相关资源 + /// 在应用退出时调用 + Future dispose() async { + Logger.info('释放分享接收资源'); + + // 取消批量处理定时器 + _batchProcessingTimer?.cancel(); + _batchProcessingTimer = null; + + // 清空分享队列 + _shareQueue.clear(); + + // 关闭分享流控制器 + await _sharingStreamController.close(); + + // 清空当前分享文件 + _currentSharedFiles.clear(); + + Logger.info('分享接收资源已释放'); + } +} + +/// 分享接收异常 - 分享接收过程中的自定义异常 +class ShareIntentException implements Exception { + /// 异常消息 + final String message; + + /// 异常原因(可选) + final dynamic cause; + + ShareIntentException(this.message, {this.cause}); + + @override + String toString() { + return cause != null ? '$message: $cause' : message; + } +} \ No newline at end of file diff --git a/lib/data/repositories/share_repository_impl.dart b/lib/data/repositories/share_repository_impl.dart new file mode 100644 index 0000000..272e4bb --- /dev/null +++ b/lib/data/repositories/share_repository_impl.dart @@ -0,0 +1,300 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:path/path.dart' as path; +import 'package:image/image.dart' as img; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import '../../domain/entities/inspiration_image.dart'; +import '../../domain/repositories/share_repository.dart'; +import '../datasources/share/share_intent_datasource.dart'; +import '../../../core/utils/logger.dart'; +import '../../../core/utils/image_utils.dart'; +import '../../../core/utils/path_utils.dart'; + +/// 分享仓库实现类 - 实现分享功能的业务逻辑 +/// 负责处理分享接收、图片保存和相关的业务逻辑 +class ShareRepositoryImpl implements ShareRepository { + /// 分享数据源 - 处理系统分享接收 + final ShareIntentDataSource _shareDataSource; + + /// 图片工具类 - 处理图片压缩和格式转换(已使用静态方法) + // final ImageUtils _imageUtils; // 静态类,不需要实例 + + /// 文件工具类 - 处理文件存储(已使用静态方法) + // final FileUtils _fileUtils; // 静态类,不需要实例 + + /// 分享数据流订阅 - 管理分享数据流监听 + StreamSubscription>? _shareSubscription; + + @override + Stream>? get sharingStream => _shareDataSource.sharingStream; + + ShareRepositoryImpl({ + required ShareIntentDataSource shareDataSource, + // required ImageUtils imageUtils, // 静态类,不需要传入 + // required FileUtils fileUtils, // 静态类,不需要传入 + }) : _shareDataSource = shareDataSource; + + /// 初始化分享接收 - 设置分享监听器 + /// 在应用启动时调用,开始监听系统分享事件 + @override + Future initializeShareReceiving() async { + try { + Logger.info('初始化分享仓库...'); + + // 初始化分享数据源 + await _shareDataSource.initShareReceiving(); + + // 订阅分享数据流 + _shareSubscription = _shareDataSource.sharingStream.listen( + _handleSharedImages, + onError: _handleShareError, + ); + + Logger.info('分享仓库初始化完成'); + } catch (e) { + Logger.error('分享仓库初始化失败', error: e); + throw Exception('初始化分享功能失败: $e'); + } + } + + /// 处理接收到的分享图片 - 验证并准备保存分享图片 + /// [sharedFiles] 接收到的分享文件列表 + Future _handleSharedImages(List sharedFiles) async { + try { + Logger.info('开始处理分享图片: ${sharedFiles.length} 张'); + + if (sharedFiles.isEmpty) { + Logger.warning('分享文件列表为空'); + return; + } + + // 验证并处理每个分享文件 + final processedImages = []; + + for (final file in sharedFiles) { + try { + final image = await _processSharedFile(file); + if (image != null) { + processedImages.add(image); + } + } catch (e) { + Logger.error('处理分享文件失败: ${file.path}', error: e); + // 继续处理其他文件,不中断整个流程 + } + } + + if (processedImages.isNotEmpty) { + Logger.info('分享图片处理完成: ${processedImages.length} 张'); + // 这里可以通知UI层有新的分享图片需要处理 + // 例如通过事件总线或状态管理 + } else { + Logger.warning('没有成功处理的分享图片'); + } + + } catch (e) { + Logger.error('处理分享图片失败', error: e); + throw Exception('处理分享图片失败: $e'); + } + } + + /// 处理单个分享文件 - 处理单个分享图片文件 + /// [sharedFile] 分享文件对象 + /// 返回处理后的灵感图片实体,失败时返回null + Future _processSharedFile(SharedMediaFile sharedFile) async { + try { + Logger.info('处理分享文件: ${sharedFile.path}'); + + // 验证文件路径 + final filePath = sharedFile.path; + if (filePath.isEmpty || !filePath.startsWith('/')) { + Logger.error('文件路径无效: $filePath'); + return null; + } + + // 验证文件是否存在 + final file = File(filePath); + if (!await file.exists()) { + Logger.error('文件不存在: $filePath'); + return null; + } + + // 获取文件信息 + final fileStat = await file.stat(); + final fileSize = fileStat.size; + + if (fileSize == 0) { + Logger.error('文件大小为0: $filePath'); + return null; + } + + // 获取文件扩展名和MIME类型 + final fileExtension = path.extension(filePath).substring(1); // 移除点号 + final mimeType = _getMimeType(fileExtension); + + Logger.info('文件信息 - 大小: $fileSize bytes, 类型: $mimeType, 扩展名: $fileExtension'); + + // 生成唯一ID和存储路径 + final imageId = PathUtils.generateUniqueId(); + final storagePath = await _getStoragePath(); + final originalFileName = path.basename(filePath); + + // 创建目标文件路径 + final targetFileName = '$imageId${path.extension(filePath)}'; + final targetFilePath = path.join(storagePath, targetFileName); + + // 复制文件到应用存储目录 + await file.copy(targetFilePath); + Logger.info('文件已复制到: $targetFilePath'); + + // 生成缩略图 + String? thumbnailPath; + try { + // 生成缩略图文件名 + final thumbnailFileName = PathUtils.generateThumbnailFileName(targetFileName); + final thumbnailPathDir = PathUtils.getParentDirectory(targetFilePath); + final thumbnailFullPath = path.join(thumbnailPathDir, thumbnailFileName); + + thumbnailPath = await ImageUtils.generateThumbnail( + imagePath: targetFilePath, + targetPath: thumbnailFullPath, + maxSize: 500, + quality: 85, + ); + Logger.info('缩略图生成完成: $thumbnailPath'); + } catch (e) { + Logger.error('生成缩略图失败', error: e); + // 缩略图失败不影响主流程 + } + + // 获取图片尺寸信息 + int? width; + int? height; + try { + // 读取图片文件获取尺寸信息 + final imageFile = File(targetFilePath); + final imageBytes = await imageFile.readAsBytes(); + + // 使用image库解码图片获取尺寸 + final decodedImage = img.decodeImage(imageBytes); + if (decodedImage != null) { + width = decodedImage.width; + height = decodedImage.height; + Logger.info('图片尺寸: ${width}x$height'); + } else { + Logger.warning('无法解码图片获取尺寸'); + } + } catch (e) { + Logger.error('获取图片尺寸失败', error: e); + // 尺寸获取失败不影响主流程 + } + + // 创建灵感图片实体 + final inspirationImage = InspirationImage( + id: imageId, + filePath: targetFilePath, + thumbnailPath: thumbnailPath ?? targetFilePath, + folderId: null, // 默认无文件夹 + tags: const [], // 初始无标签 + note: null, // 初始无备注 + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + originalName: originalFileName, + fileSize: fileSize, + mimeType: mimeType, + width: width, + height: height, + isFavorite: false, + ); + + Logger.info('分享文件处理完成: $imageId'); + return inspirationImage; + + } catch (e) { + Logger.error('处理分享文件失败: ${sharedFile.path}', error: e); + return null; + } + } + + /// 获取存储路径 - 获取图片存储目录路径 + /// 返回基于日期的存储路径 + Future _getStoragePath() async { + final storagePath = await PathUtils.buildImageStoragePath( + fileName: 'temp', // 临时文件名,后面会替换 + ); + // 获取目录路径(去掉文件名) + final dirPath = PathUtils.getParentDirectory(storagePath); + + // 确保目录存在 + await Directory(dirPath).create(recursive: true); + return dirPath; + } + + /// 获取MIME类型 - 根据文件扩展名获取MIME类型 + /// [extension] 文件扩展名(不含点) + /// 返回对应的MIME类型 + String _getMimeType(String extension) { + final mimeTypes = { + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'png': 'image/png', + 'gif': 'image/gif', + 'webp': 'image/webp', + 'bmp': 'image/bmp', + 'heic': 'image/heic', + 'heif': 'image/heif', + }; + + return mimeTypes[extension.toLowerCase()] ?? 'image/jpeg'; + } + + /// 处理分享错误 - 统一处理分享过程中的错误 + /// [error] 分享错误信息 + void _handleShareError(dynamic error) { + Logger.error('分享接收错误', error: error); + // 这里可以通知UI层显示错误提示 + // 例如通过事件总线或状态管理 + } + + /// 获取待处理的分享文件 - 获取当前待处理的分享文件 + /// 返回待处理的分享文件列表 + @override + List getPendingShareFiles() { + return _shareDataSource.getCurrentSharedFiles(); + } + + /// 是否有待处理的分享 - 检查是否有待处理的分享文件 + /// 返回是否有待处理文件 + @override + bool hasPendingShare() { + return _shareDataSource.hasPendingShare(); + } + + /// 清除当前分享数据 - 清除已处理的分享数据 + /// 在分享处理完成后调用 + @override + void clearCurrentShare() { + _shareDataSource.clearCurrentShare(); + } + + /// 获取分享文件数量 - 获取当前分享文件数量 + /// 返回分享文件数量 + @override + int getShareFileCount() { + return _shareDataSource.getShareFileCount(); + } + + /// 释放资源 - 清理分享接收相关资源 + /// 在应用退出时调用 + @override + Future dispose() async { + Logger.info('释放分享仓库资源'); + + // 取消分享数据流订阅 + await _shareSubscription?.cancel(); + _shareSubscription = null; + + // 释放分享数据源 + await _shareDataSource.dispose(); + } +} + diff --git a/lib/domain/repositories/share_repository.dart b/lib/domain/repositories/share_repository.dart new file mode 100644 index 0000000..71300c0 --- /dev/null +++ b/lib/domain/repositories/share_repository.dart @@ -0,0 +1,35 @@ +import 'dart:async'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; + +/// 分享功能仓库接口 - 定义分享相关的业务逻辑 +/// 提供分享接收、处理和状态管理的核心功能 +abstract class ShareRepository { + /// 分享数据流 - 用于监听接收到的分享文件 + /// 返回分享文件的流,当有新分享时发出数据 + Stream>? get sharingStream; + + /// 初始化分享接收 - 设置分享监听器 + /// 在应用启动时调用,开始监听系统分享事件 + /// 返回初始化操作是否成功 + Future initializeShareReceiving(); + + /// 获取待处理的分享文件 - 获取当前待处理的分享文件 + /// 返回待处理的分享文件列表,可能为空 + List getPendingShareFiles(); + + /// 是否有待处理的分享 - 检查是否有待处理的分享文件 + /// 返回是否有待处理文件 + bool hasPendingShare(); + + /// 清除当前分享数据 - 清除已处理的分享数据 + /// 在分享处理完成后调用 + void clearCurrentShare(); + + /// 获取分享文件数量 - 获取当前分享文件数量 + /// 返回分享文件数量 + int getShareFileCount(); + + /// 释放资源 - 清理分享接收相关资源 + /// 在应用退出时调用 + Future dispose(); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 8524eb0..95ba2b8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,17 +9,17 @@ import 'presentation/app_widget.dart'; void main() async { // 确保Flutter绑定已初始化 WidgetsFlutterBinding.ensureInitialized(); - + // 初始化日志系统 Logger.setLogLevel(LogLevel.info); Logger.info('应用启动 - ${AppConstants.appName} v${AppConstants.appVersion}'); - + // 捕获并记录未处理的异常 FlutterError.onError = (FlutterErrorDetails details) { Logger.logException('Flutter Error', details.exception, stackTrace: details.stack); FlutterError.presentError(details); }; - + // 运行应用 runApp( const ProviderScope( diff --git a/lib/presentation/app_widget.dart b/lib/presentation/app_widget.dart index 57e8213..f5ac683 100644 --- a/lib/presentation/app_widget.dart +++ b/lib/presentation/app_widget.dart @@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart'; import '../core/constants/app_constants.dart'; import '../core/theme/app_theme.dart'; import '../core/utils/logger.dart'; +import 'providers/share_provider.dart'; +import 'widgets/share_test_widget.dart'; /// 主应用组件 /// 负责配置MaterialApp和全局设置 @@ -70,18 +72,39 @@ class AppWidget extends ConsumerWidget { /// 应用主页组件 /// 作为占位符,后续会替换为实际的主页 -class AppHomePage extends StatefulWidget { +class AppHomePage extends ConsumerStatefulWidget { const AppHomePage({super.key}); @override - State createState() => _AppHomePageState(); + ConsumerState createState() => _AppHomePageState(); } -class _AppHomePageState extends State { +class _AppHomePageState extends ConsumerState { @override void initState() { super.initState(); Logger.debug('AppHomePage初始化'); + + // 初始化分享接收功能 + _initializeShareReceiving(); + } + + /// 初始化分享接收功能 + void _initializeShareReceiving() { + try { + Logger.info('开始初始化分享接收功能...'); + + // 延迟执行,避免在widget构建期间修改provider + Future.delayed(Duration.zero, () { + // 获取分享提供者并初始化分享接收 + final shareNotifier = ref.read(shareProvider.notifier); + shareNotifier.refreshShareStatus(); + }); + + Logger.info('分享接收功能初始化完成'); + } catch (e) { + Logger.error('初始化分享接收功能失败', error: e); + } } @override @@ -137,7 +160,11 @@ class _AppHomePageState extends State { ), textAlign: TextAlign.center, ), - const SizedBox(height: 48), + const SizedBox(height: 32), + + // 分享测试组件 + const ShareTestWidget(), + const SizedBox(height: 32), // 开始使用按钮 ElevatedButton.icon( diff --git a/lib/presentation/providers/share_provider.dart b/lib/presentation/providers/share_provider.dart new file mode 100644 index 0000000..8682e64 --- /dev/null +++ b/lib/presentation/providers/share_provider.dart @@ -0,0 +1,403 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import '../../domain/repositories/share_repository.dart'; +import '../../data/datasources/share/share_intent_datasource.dart'; +import '../../data/repositories/share_repository_impl.dart'; +import '../../core/utils/logger.dart'; + +/// 分享状态类 - 管理分享相关的UI状态 +/// 包含分享文件列表、处理状态、错误信息等 +class ShareState { + /// 待处理的分享文件列表 + final List pendingFiles; + + /// 是否正在处理分享 + final bool isProcessing; + + /// 处理错误信息 + final String? error; + + /// 是否显示分享界面 + final bool showShareUI; + + /// 当前处理的批次索引 + final int currentBatchIndex; + + /// 总批次数 + final int totalBatches; + + /// 是否正在批量处理 + final bool isBatchProcessing; + + /// 是否有待处理的分享 + bool get hasPendingShare => pendingFiles.isNotEmpty; + + /// 构造函数 - 创建分享状态实例 + const ShareState({ + this.pendingFiles = const [], + this.isProcessing = false, + this.error, + this.showShareUI = false, + this.currentBatchIndex = 0, + this.totalBatches = 0, + this.isBatchProcessing = false, + }); + + /// 复制构造函数 - 创建状态副本并支持字段更新 + ShareState copyWith({ + List? pendingFiles, + bool? isProcessing, + String? error, + bool? showShareUI, + int? currentBatchIndex, + int? totalBatches, + bool? isBatchProcessing, + }) { + return ShareState( + pendingFiles: pendingFiles ?? this.pendingFiles, + isProcessing: isProcessing ?? this.isProcessing, + error: error, + showShareUI: showShareUI ?? this.showShareUI, + currentBatchIndex: currentBatchIndex ?? this.currentBatchIndex, + totalBatches: totalBatches ?? this.totalBatches, + isBatchProcessing: isBatchProcessing ?? this.isBatchProcessing, + ); + } +} + +/// 分享状态Notifier - 管理分享状态和业务逻辑 +class ShareNotifier extends StateNotifier { + /// 分享仓库 - 处理分享相关的业务逻辑 + final ShareRepository _shareRepository; + + /// 分享数据流订阅 - 监听分享数据变化 + StreamSubscription>? _shareSubscription; + + ShareNotifier(this._shareRepository) : super(const ShareState()) { + _initializeShareListening(); + } + + /// 初始化分享监听 - 设置分享数据流监听 + void _initializeShareListening() { + try { + Logger.info('初始化分享状态监听...'); + + // 监听分享数据流 + _shareSubscription = _shareRepository.sharingStream?.listen( + handleSharedFiles, + onError: (error) { + Logger.error('分享数据流错误', error: error); + state = state.copyWith(error: '分享接收错误: $error'); + }, + ); + + // 检查当前是否有待处理的分享 + _checkPendingShares(); + + Logger.info('分享状态监听初始化完成'); + } catch (e) { + Logger.error('初始化分享监听失败', error: e); + state = state.copyWith(error: '初始化分享功能失败: $e'); + } + } + + /// 检查待处理的分享 - 检查当前是否有待处理的分享文件 + void _checkPendingShares() { + try { + final pendingFiles = _shareRepository.getPendingShareFiles(); + final hasShares = _shareRepository.hasPendingShare(); + + Logger.info('检查待处理分享: ${pendingFiles.length} 个文件'); + + if (hasShares && pendingFiles.isNotEmpty) { + state = state.copyWith( + pendingFiles: pendingFiles, + showShareUI: true, + error: null, + ); + Logger.info('发现待处理分享,显示分享界面'); + } else { + state = state.copyWith( + pendingFiles: [], + showShareUI: false, + error: null, + ); + } + } catch (e) { + Logger.error('检查待处理分享失败', error: e); + state = state.copyWith(error: '检查分享状态失败: $e'); + } + } + + /// 处理接收到的分享文件 - 处理新的分享数据 + /// [sharedFiles] 接收到的分享文件列表 + Future handleSharedFiles(List sharedFiles) async { + if (sharedFiles.isEmpty) { + Logger.warning('接收到的分享文件列表为空'); + return; + } + + try { + Logger.info('处理新的分享文件: ${sharedFiles.length} 个'); + + state = state.copyWith( + pendingFiles: sharedFiles, + showShareUI: true, + isProcessing: false, + error: null, + ); + + // 记录详细的文件信息 + for (final file in sharedFiles) { + Logger.info('分享文件: ${file.path}, 类型: ${file.type}'); + } + + } catch (e) { + Logger.error('处理分享文件失败', error: e); + state = state.copyWith( + error: '处理分享文件失败: $e', + isProcessing: false, + ); + } + } + + /// 开始保存分享图片 - 开始批量保存分享图片,支持大量文件分批保存 + /// [folderId] 目标文件夹ID,可为空 + /// [tags] 要添加的标签列表,可为空 + /// [note] 备注内容,可为空 + Future startSavingSharedImages({ + String? folderId, + List? tags, + String? note, + }) async { + if (state.pendingFiles.isEmpty) { + Logger.warning('没有待保存的分享图片'); + return; + } + + try { + Logger.info('开始保存分享图片,共${state.pendingFiles.length}个文件'); + + // 初始化批量处理状态 + final totalFiles = state.pendingFiles.length; + const batchSize = 10; // 每批处理10个文件 + final totalBatches = (totalFiles / batchSize).ceil(); + + state = state.copyWith( + isProcessing: true, + isBatchProcessing: totalBatches > 1, + totalBatches: totalBatches, + currentBatchIndex: 0, + error: null, + ); + + if (totalBatches > 1) { + Logger.info('启用批量保存模式,共$totalBatches批,每批$batchSize个文件'); + await _batchSaveSharedImages(folderId, tags, note, batchSize); + } else { + Logger.info('普通保存模式,直接处理$totalFiles个文件'); + await _simulateSaveProcess(folderId, tags, note); + } + + // 保存完成后清除分享数据 + _shareRepository.clearCurrentShare(); + + state = state.copyWith( + pendingFiles: [], + isProcessing: false, + isBatchProcessing: false, + showShareUI: false, + currentBatchIndex: 0, + totalBatches: 0, + error: null, + ); + + Logger.info('分享图片保存完成'); + + } catch (e) { + Logger.error('保存分享图片失败', error: e); + state = state.copyWith( + isProcessing: false, + isBatchProcessing: false, + error: '保存图片失败: $e', + ); + } + } + + /// 批量保存分享图片 - 分批处理大量分享图片 + /// [folderId] 目标文件夹ID + /// [tags] 标签列表 + /// [note] 备注内容 + /// [batchSize] 每批处理的大小 + Future _batchSaveSharedImages( + String? folderId, + List? tags, + String? note, + int batchSize, + ) async { + final batches = _createBatches(state.pendingFiles, batchSize); + + Logger.info('开始分批保存,共${batches.length}批'); + + for (int i = 0; i < batches.length; i++) { + final batch = batches[i]; + final batchNumber = i + 1; + + Logger.info('处理第$batchNumber批,共${batch.length}个文件'); + + // 更新当前批次状态 + state = state.copyWith(currentBatchIndex: batchNumber); + + // 模拟处理当前批次 + await _simulateBatchSaveProcess(batch, folderId, tags, note, batchNumber); + + // 批次间短暂延迟,避免系统过载 + if (i < batches.length - 1) { + await Future.delayed(const Duration(milliseconds: 200)); + } + } + + Logger.info('批量保存完成'); + } + + /// 创建文件批次 - 将文件列表分成指定大小的批次 + /// [files] 文件列表 + /// [batchSize] 每批的大小 + /// 返回批次列表 + List> _createBatches(List files, int batchSize) { + final batches = >[]; + + for (int i = 0; i < files.length; i += batchSize) { + final end = (i + batchSize < files.length) ? i + batchSize : files.length; + batches.add(files.sublist(i, end)); + } + + return batches; + } + + /// 模拟批量保存过程 - 处理单个批次的保存 + /// [batch] 当前批次的文件 + /// [folderId] 目标文件夹ID + /// [tags] 标签列表 + /// [note] 备注内容 + /// [batchNumber] 批次编号 + Future _simulateBatchSaveProcess( + List batch, + String? folderId, + List? tags, + String? note, + int batchNumber, + ) async { + // 模拟保存过程,实际实现中会调用真实的保存逻辑 + await Future.delayed(const Duration(seconds: 1)); + + Logger.info('第$batchNumber批次保存完成,处理了${batch.length}个文件'); + } + + /// 模拟保存过程 - 临时模拟保存过程(后续替换为真实逻辑) + /// [folderId] 目标文件夹ID + /// [tags] 标签列表 + /// [note] 备注内容 + Future _simulateSaveProcess( + String? folderId, + List? tags, + String? note, + ) async { + // 模拟保存过程 + await Future.delayed(const Duration(seconds: 2)); + + Logger.info('模拟保存完成 - 文件夹: $folderId, 标签: $tags, 备注: $note'); + } + + /// 取消分享保存 - 取消当前的分享保存操作 + void cancelShareSaving() { + Logger.info('取消分享保存'); + + // 清除当前分享数据 + _shareRepository.clearCurrentShare(); + + state = const ShareState( + pendingFiles: [], + isProcessing: false, + showShareUI: false, + error: null, + ); + } + + /// 关闭分享界面 - 关闭分享处理界面 + void closeShareUI() { + Logger.info('关闭分享界面'); + + state = state.copyWith( + showShareUI: false, + error: null, + ); + } + + /// 清除错误状态 - 清除当前的错误信息 + void clearError() { + if (state.error != null) { + state = state.copyWith(error: null); + } + } + + /// 刷新分享状态 - 重新检查分享状态 + void refreshShareStatus() { + Logger.info('刷新分享状态'); + _checkPendingShares(); + } + + /// 获取分享文件数量 - 获取当前待处理的分享文件数量 + int getShareFileCount() { + return _shareRepository.getShareFileCount(); + } + + /// 释放资源 - 清理分享相关资源 + @override + Future dispose() async { + Logger.info('释放分享状态资源'); + + // 调用父类的dispose方法(StateNotifier的dispose不是异步的) + super.dispose(); + + // 取消分享数据流订阅 + await _shareSubscription?.cancel(); + _shareSubscription = null; + + // 释放分享仓库 + await _shareRepository.dispose(); + } +} + +/// 分享数据源Provider - 提供分享数据源实例 +final shareDataSourceProvider = Provider((ref) { + return ShareIntentDataSource(); +}); + +/// 图片工具Provider - 提供图片工具实例 +/// ImageUtils是静态工具类,不需要实例化,直接提供工具引用 +final imageUtilsProvider = Provider((ref) { + return Object(); // 占位Provider,实际使用ImageUtils静态方法 +}); + +/// 文件工具Provider - 提供文件工具实例 +/// FileUtils是静态工具类,不需要实例化,直接提供工具引用 +final fileUtilsProvider = Provider((ref) { + return Object(); // 占位Provider,实际使用FileUtils静态方法 +}); + +/// 分享仓库Provider - 提供分享仓库实例 +final shareRepositoryProvider = Provider((ref) { + final shareDataSource = ref.read(shareDataSourceProvider); + // ImageUtils和FileUtils是静态工具类,直接传入null占位 + return ShareRepositoryImpl( + shareDataSource: shareDataSource, + ); +}); + +/// 分享状态Provider - 管理分享相关的UI状态 +final shareProvider = StateNotifierProvider((ref) { + final shareRepository = ref.read(shareRepositoryProvider); + return ShareNotifier(shareRepository); +}); \ No newline at end of file diff --git a/lib/presentation/widgets/loading_widgets.dart b/lib/presentation/widgets/loading_widgets.dart index 89db90f..f872640 100644 --- a/lib/presentation/widgets/loading_widgets.dart +++ b/lib/presentation/widgets/loading_widgets.dart @@ -642,7 +642,7 @@ class SkeletonList extends StatelessWidget { itemBuilder: (context, index) { return Padding( padding: EdgeInsets.only(bottom: itemSpacing), - child: Container( + child: SizedBox( height: itemHeight, child: Row( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/presentation/widgets/share_test_widget.dart b/lib/presentation/widgets/share_test_widget.dart new file mode 100644 index 0000000..5b89f45 --- /dev/null +++ b/lib/presentation/widgets/share_test_widget.dart @@ -0,0 +1,422 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import '../../core/utils/logger.dart'; +import '../providers/share_provider.dart'; + +/// 分享测试组件 - 用于验证分享接收功能 +/// 显示当前分享状态和待处理文件信息 +class ShareTestWidget extends ConsumerWidget { + const ShareTestWidget({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final shareState = ref.watch(shareProvider); + + return Card( + margin: const EdgeInsets.all(16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // 标题 + Text( + '分享接收测试', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + + // 分享状态显示 + _buildShareStatus(context, shareState), + const SizedBox(height: 16), + + // 批量处理进度显示 + if (shareState.isBatchProcessing) ...[ + _buildBatchProgress(context, shareState), + const SizedBox(height: 8), + _buildBatchInfo(context, shareState), + const SizedBox(height: 16), + ], + + // 待处理文件列表 + _buildPendingFilesList(context, shareState), + const SizedBox(height: 16), + + // 操作按钮 + _buildActionButtons(context, ref, shareState), + + // 错误信息显示 + if (shareState.error != null) ...[ + const SizedBox(height: 16), + _buildErrorDisplay(context, shareState.error!, ref), + ], + ], + ), + ), + ); + } + + /// 构建分享状态显示 + Widget _buildShareStatus(BuildContext context, ShareState shareState) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '当前状态:', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + shareState.isProcessing + ? Icons.sync + : shareState.hasPendingShare + ? Icons.notifications_active + : Icons.check_circle, + color: shareState.isProcessing + ? Colors.orange + : shareState.hasPendingShare + ? Colors.blue + : Colors.green, + ), + const SizedBox(width: 8), + Text( + shareState.isProcessing + ? '处理中...' + : shareState.hasPendingShare + ? '有待处理分享' + : '无待处理分享', + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + const SizedBox(height: 4), + Text( + '文件数量: ${shareState.pendingFiles.length}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ); + } + + /// 构建待处理文件列表 + Widget _buildPendingFilesList(BuildContext context, ShareState shareState) { + if (shareState.pendingFiles.isEmpty) { + return Text( + '暂无待处理文件', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '待处理文件:', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + ...shareState.pendingFiles.map((file) => _buildFileItem(context, file)), + ], + ); + } + + /// 构建文件项 + Widget _buildFileItem(BuildContext context, SharedMediaFile file) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + _getFileIcon(file.type), + size: 24, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _getFileName(file.path), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + file.path, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + '类型: ${_getFileType(file.type)}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + ], + ), + ); + } + + /// 构建操作按钮 + Widget _buildActionButtons(BuildContext context, WidgetRef ref, ShareState shareState) { + return Row( + children: [ + ElevatedButton.icon( + onPressed: shareState.hasPendingShare && !shareState.isProcessing + ? () => _handleSaveShare(ref) + : null, + icon: const Icon(Icons.save), + label: const Text('保存分享'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + const SizedBox(width: 12), + OutlinedButton.icon( + onPressed: shareState.hasPendingShare && !shareState.isProcessing + ? () => _handleClearShare(ref) + : null, + icon: const Icon(Icons.clear), + label: const Text('清除'), + ), + const SizedBox(width: 12), + OutlinedButton.icon( + onPressed: !shareState.isProcessing + ? () => _handleRefreshShare(ref) + : null, + icon: const Icon(Icons.refresh), + label: const Text('刷新'), + ), + ], + ); + } + + /// 构建错误信息显示 + Widget _buildErrorDisplay(BuildContext context, String error, WidgetRef ref) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + error, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + IconButton( + onPressed: () => _handleClearError(ref), + icon: Icon( + Icons.close, + color: Theme.of(context).colorScheme.error, + ), + ), + ], + ), + ); + } + + /// 构建批量处理进度显示 + Widget _buildBatchProgress(BuildContext context, ShareState shareState) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '批量处理进度:', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: LinearProgressIndicator( + value: shareState.totalBatches > 0 + ? shareState.currentBatchIndex / shareState.totalBatches + : 0, + backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + ), + ), + const SizedBox(width: 12), + Text( + '${shareState.currentBatchIndex}/${shareState.totalBatches}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + '第 ${shareState.currentBatchIndex} 批,共 ${shareState.totalBatches} 批', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ); + } + + /// 构建批量处理信息 + Widget _buildBatchInfo(BuildContext context, ShareState shareState) { + final remainingBatches = shareState.totalBatches - shareState.currentBatchIndex; + final progressPercent = shareState.totalBatches > 0 + ? (shareState.currentBatchIndex / shareState.totalBatches * 100).toStringAsFixed(1) + : '0.0'; + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Theme.of(context).colorScheme.primary.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + children: [ + Icon( + Icons.batch_prediction, + color: Theme.of(context).colorScheme.primary, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '批量保存模式', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 2), + Text( + '进度: $progressPercent% (${shareState.currentBatchIndex}/${shareState.totalBatches} 批次)', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + if (remainingBatches > 0) ...[ + const SizedBox(height: 2), + Text( + '剩余 $remainingBatches 批次待处理', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ], + ), + ), + ], + ), + ); + } + + /// 获取文件图标 + IconData _getFileIcon(SharedMediaType type) { + switch (type) { + case SharedMediaType.image: + return Icons.image; + case SharedMediaType.video: + return Icons.videocam; + case SharedMediaType.file: + return Icons.insert_drive_file; + default: + return Icons.description; + } + } + + /// 获取文件类型描述 + String _getFileType(SharedMediaType type) { + switch (type) { + case SharedMediaType.image: + return '图片'; + case SharedMediaType.video: + return '视频'; + case SharedMediaType.file: + return '文件'; + default: + return '未知'; + } + } + + /// 获取文件名 + String _getFileName(String filePath) { + return filePath.split('/').last; + } + + /// 处理保存分享 + void _handleSaveShare(WidgetRef ref) { + Logger.info('用户点击保存分享'); + final shareNotifier = ref.read(shareProvider.notifier); + shareNotifier.startSavingSharedImages(); + } + + /// 处理清除分享 + void _handleClearShare(WidgetRef ref) { + Logger.info('用户点击清除分享'); + final shareNotifier = ref.read(shareProvider.notifier); + shareNotifier.cancelShareSaving(); + } + + /// 处理刷新分享 + void _handleRefreshShare(WidgetRef ref) { + Logger.info('用户点击刷新分享'); + final shareNotifier = ref.read(shareProvider.notifier); + shareNotifier.refreshShareStatus(); + } + + /// 处理清除错误 + void _handleClearError(WidgetRef ref) { + final shareNotifier = ref.read(shareProvider.notifier); + shareNotifier.clearError(); + } +} \ No newline at end of file