From 8f284075b631e167e77990b0d36303963b081c05 Mon Sep 17 00:00:00 2001
From: ddshi <8811906+ddshi@user.noreply.gitee.com>
Date: Wed, 17 Sep 2025 13:32:25 +0800
Subject: [PATCH] =?UTF-8?q?=E5=BB=BA=E7=AB=8B=E5=88=86=E4=BA=AB=E6=9C=BA?=
=?UTF-8?q?=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CLAUDE.md | 30 +-
android/app/src/main/AndroidManifest.xml | 12 +
.../daodaoshi/snap_wish/MainActivity.kt | 63 +++
ios/Runner/Info.plist | 33 +-
.../share/share_intent_datasource.dart | 348 +++++++++++++++
.../repositories/share_repository_impl.dart | 300 +++++++++++++
lib/domain/repositories/share_repository.dart | 35 ++
lib/main.dart | 6 +-
lib/presentation/app_widget.dart | 35 +-
.../providers/share_provider.dart | 403 +++++++++++++++++
lib/presentation/widgets/loading_widgets.dart | 2 +-
.../widgets/share_test_widget.dart | 422 ++++++++++++++++++
12 files changed, 1665 insertions(+), 24 deletions(-)
create mode 100644 lib/data/datasources/share/share_intent_datasource.dart
create mode 100644 lib/data/repositories/share_repository_impl.dart
create mode 100644 lib/domain/repositories/share_repository.dart
create mode 100644 lib/presentation/providers/share_provider.dart
create mode 100644 lib/presentation/widgets/share_test_widget.dart
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