建立分享机制
This commit is contained in:
parent
1212c4ea27
commit
8f284075b6
30
CLAUDE.md
30
CLAUDE.md
@ -146,11 +146,11 @@ flutter clean
|
|||||||
- ✅ 错误提示组件:多种错误类型(网络、服务器、权限、数据等),详细错误信息显示
|
- ✅ 错误提示组件:多种错误类型(网络、服务器、权限、数据等),详细错误信息显示
|
||||||
|
|
||||||
#### 📤 Phase 2: 分享功能(2-3天)
|
#### 📤 Phase 2: 分享功能(2-3天)
|
||||||
**任务2.1: 分享接收机制**
|
**任务2.1: 分享接收机制** ✅
|
||||||
- [ ] 配置receive_sharing_intent插件
|
- [x] 配置receive_sharing_intent插件
|
||||||
- [ ] 实现Android分享接收配置
|
- [x] 实现Android分享接收配置
|
||||||
- [ ] 实现iOS分享接收配置
|
- [x] 实现iOS分享接收配置(已添加TODO注释,跳过iOS设备)
|
||||||
- [ ] 处理多张图片接收逻辑
|
- [x] 处理多张图片接收逻辑(支持批量处理和队列管理)
|
||||||
|
|
||||||
**任务2.2: 保存界面UI**
|
**任务2.2: 保存界面UI**
|
||||||
- [ ] 创建半透明模态框界面
|
- [ ] 创建半透明模态框界面
|
||||||
@ -379,18 +379,18 @@ class UserService {
|
|||||||
- [x] Phase 1.2: 数据层架构搭建(4/4)✅
|
- [x] Phase 1.2: 数据层架构搭建(4/4)✅
|
||||||
- [x] Phase 1.3: 核心工具类(4/4)✅
|
- [x] Phase 1.3: 核心工具类(4/4)✅
|
||||||
- [x] Phase 1.4: 基础UI组件(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日
|
**预计完成**:2025年9月19日
|
||||||
**依赖项**:Phase 1.4 完成
|
**依赖项**:Phase 2.1 完成
|
||||||
|
|
||||||
**任务验收标准**:
|
**任务验收标准**:
|
||||||
- 分享接收机制配置完成
|
- 半透明模态框界面创建完成
|
||||||
- Android分享接收配置完成
|
- 图片网格预览组件实现
|
||||||
- iOS分享接收配置完成
|
- 文件夹选择器弹窗完成
|
||||||
- 多张图片接收逻辑处理
|
- 标签输入和选择组件实现
|
||||||
- 分享功能测试通过
|
|
||||||
@ -23,6 +23,18 @@
|
|||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- 分享接收配置 - 接收图片文件 -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<data android:mimeType="image/*"/>
|
||||||
|
</intent-filter>
|
||||||
|
<!-- 多张图片分享接收配置 -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<data android:mimeType="image/*"/>
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
|||||||
@ -1,6 +1,69 @@
|
|||||||
package com.snapwish.daodaoshi.snap_wish
|
package com.snapwish.daodaoshi.snap_wish
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
/// 主活动类 - 处理应用主入口和分享接收
|
||||||
|
/// 负责接收来自其他应用的分享意图,特别是图片分享
|
||||||
class MainActivity: 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<android.net.Uri>(Intent.EXTRA_STREAM)
|
||||||
|
android.util.Log.d("InspoSnap", "单张图片分享: ${uri?.toString() ?: "无URI"}")
|
||||||
|
}
|
||||||
|
Intent.ACTION_SEND_MULTIPLE -> {
|
||||||
|
val uris = it.getParcelableArrayListExtra<android.net.Uri>(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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,5 +45,36 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|
||||||
|
<!-- TODO: 子任务2.1.3 - iOS分享接收配置(跳过,无iOS设备)
|
||||||
|
需要添加以下配置:
|
||||||
|
1. 添加分享扩展(Share Extension)
|
||||||
|
2. 配置NSExtension相关设置
|
||||||
|
3. 设置分享意图过滤器
|
||||||
|
4. 配置应用群组(App Groups)
|
||||||
|
5. 实现分享处理逻辑
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- 分享接收配置 - 需要在Xcode中手动配置分享扩展 -->
|
||||||
|
<!-- <key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionAttributes</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionActivationRule</key>
|
||||||
|
<string>SUBQUERY (
|
||||||
|
extensionItems, $extensionItem,
|
||||||
|
SUBQUERY (
|
||||||
|
$extensionItem.attachments, $attachment,
|
||||||
|
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image"
|
||||||
|
).@count >= 1
|
||||||
|
).@count >= 1</string>
|
||||||
|
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||||
|
<integer>20</integer>
|
||||||
|
</dict>
|
||||||
|
<key>NSExtensionMainStoryboard</key>
|
||||||
|
<string>MainInterface</string>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.share-services</string>
|
||||||
|
</dict> -->
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
348
lib/data/datasources/share/share_intent_datasource.dart
Normal file
348
lib/data/datasources/share/share_intent_datasource.dart
Normal file
@ -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<List<SharedMediaFile>> _sharingStreamController =
|
||||||
|
StreamController<List<SharedMediaFile>>.broadcast();
|
||||||
|
|
||||||
|
/// 当前接收到的分享文件列表 - 临时存储接收到的文件
|
||||||
|
List<SharedMediaFile> _currentSharedFiles = [];
|
||||||
|
|
||||||
|
/// 分享接收流 - 外部订阅分享事件
|
||||||
|
Stream<List<SharedMediaFile>> get sharingStream => _sharingStreamController.stream;
|
||||||
|
|
||||||
|
/// 是否正在处理分享 - 防止重复处理
|
||||||
|
bool _isProcessingShare = false;
|
||||||
|
|
||||||
|
/// 批量处理配置 - 控制批量处理的参数
|
||||||
|
static const int _maxBatchSize = 20; // 最大批量处理数量
|
||||||
|
static const Duration _batchProcessingDelay = Duration(milliseconds: 100); // 批量处理延迟
|
||||||
|
|
||||||
|
/// 分享文件队列 - 用于批量处理分享文件
|
||||||
|
final List<SharedMediaFile> _shareQueue = [];
|
||||||
|
|
||||||
|
/// 批量处理定时器 - 控制批量处理的时机
|
||||||
|
Timer? _batchProcessingTimer;
|
||||||
|
|
||||||
|
|
||||||
|
/// 初始化分享接收 - 设置分享监听器
|
||||||
|
/// 在应用启动时调用,监听系统分享事件
|
||||||
|
Future<void> 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<SharedMediaFile> 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<SharedMediaFile> 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<SharedMediaFile> 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<List<SharedMediaFile>> _createBatches(List<SharedMediaFile> files, int batchSize) {
|
||||||
|
final batches = <List<SharedMediaFile>>[];
|
||||||
|
|
||||||
|
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<SharedMediaFile> 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<SharedMediaFile> _validateSharedFiles(List<SharedMediaFile> files) {
|
||||||
|
final validFiles = <SharedMediaFile>[];
|
||||||
|
|
||||||
|
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<SharedMediaFile> 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<void> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
300
lib/data/repositories/share_repository_impl.dart
Normal file
300
lib/data/repositories/share_repository_impl.dart
Normal file
@ -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<List<SharedMediaFile>>? _shareSubscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<SharedMediaFile>>? get sharingStream => _shareDataSource.sharingStream;
|
||||||
|
|
||||||
|
ShareRepositoryImpl({
|
||||||
|
required ShareIntentDataSource shareDataSource,
|
||||||
|
// required ImageUtils imageUtils, // 静态类,不需要传入
|
||||||
|
// required FileUtils fileUtils, // 静态类,不需要传入
|
||||||
|
}) : _shareDataSource = shareDataSource;
|
||||||
|
|
||||||
|
/// 初始化分享接收 - 设置分享监听器
|
||||||
|
/// 在应用启动时调用,开始监听系统分享事件
|
||||||
|
@override
|
||||||
|
Future<void> 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<void> _handleSharedImages(List<SharedMediaFile> sharedFiles) async {
|
||||||
|
try {
|
||||||
|
Logger.info('开始处理分享图片: ${sharedFiles.length} 张');
|
||||||
|
|
||||||
|
if (sharedFiles.isEmpty) {
|
||||||
|
Logger.warning('分享文件列表为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证并处理每个分享文件
|
||||||
|
final processedImages = <InspirationImage>[];
|
||||||
|
|
||||||
|
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<InspirationImage?> _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<String> _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<SharedMediaFile> getPendingShareFiles() {
|
||||||
|
return _shareDataSource.getCurrentSharedFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 是否有待处理的分享 - 检查是否有待处理的分享文件
|
||||||
|
/// 返回是否有待处理文件
|
||||||
|
@override
|
||||||
|
bool hasPendingShare() {
|
||||||
|
return _shareDataSource.hasPendingShare();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清除当前分享数据 - 清除已处理的分享数据
|
||||||
|
/// 在分享处理完成后调用
|
||||||
|
@override
|
||||||
|
void clearCurrentShare() {
|
||||||
|
_shareDataSource.clearCurrentShare();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取分享文件数量 - 获取当前分享文件数量
|
||||||
|
/// 返回分享文件数量
|
||||||
|
@override
|
||||||
|
int getShareFileCount() {
|
||||||
|
return _shareDataSource.getShareFileCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 释放资源 - 清理分享接收相关资源
|
||||||
|
/// 在应用退出时调用
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
Logger.info('释放分享仓库资源');
|
||||||
|
|
||||||
|
// 取消分享数据流订阅
|
||||||
|
await _shareSubscription?.cancel();
|
||||||
|
_shareSubscription = null;
|
||||||
|
|
||||||
|
// 释放分享数据源
|
||||||
|
await _shareDataSource.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
35
lib/domain/repositories/share_repository.dart
Normal file
35
lib/domain/repositories/share_repository.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
|
|
||||||
|
/// 分享功能仓库接口 - 定义分享相关的业务逻辑
|
||||||
|
/// 提供分享接收、处理和状态管理的核心功能
|
||||||
|
abstract class ShareRepository {
|
||||||
|
/// 分享数据流 - 用于监听接收到的分享文件
|
||||||
|
/// 返回分享文件的流,当有新分享时发出数据
|
||||||
|
Stream<List<SharedMediaFile>>? get sharingStream;
|
||||||
|
|
||||||
|
/// 初始化分享接收 - 设置分享监听器
|
||||||
|
/// 在应用启动时调用,开始监听系统分享事件
|
||||||
|
/// 返回初始化操作是否成功
|
||||||
|
Future<void> initializeShareReceiving();
|
||||||
|
|
||||||
|
/// 获取待处理的分享文件 - 获取当前待处理的分享文件
|
||||||
|
/// 返回待处理的分享文件列表,可能为空
|
||||||
|
List<SharedMediaFile> getPendingShareFiles();
|
||||||
|
|
||||||
|
/// 是否有待处理的分享 - 检查是否有待处理的分享文件
|
||||||
|
/// 返回是否有待处理文件
|
||||||
|
bool hasPendingShare();
|
||||||
|
|
||||||
|
/// 清除当前分享数据 - 清除已处理的分享数据
|
||||||
|
/// 在分享处理完成后调用
|
||||||
|
void clearCurrentShare();
|
||||||
|
|
||||||
|
/// 获取分享文件数量 - 获取当前分享文件数量
|
||||||
|
/// 返回分享文件数量
|
||||||
|
int getShareFileCount();
|
||||||
|
|
||||||
|
/// 释放资源 - 清理分享接收相关资源
|
||||||
|
/// 在应用退出时调用
|
||||||
|
Future<void> dispose();
|
||||||
|
}
|
||||||
@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import '../core/constants/app_constants.dart';
|
import '../core/constants/app_constants.dart';
|
||||||
import '../core/theme/app_theme.dart';
|
import '../core/theme/app_theme.dart';
|
||||||
import '../core/utils/logger.dart';
|
import '../core/utils/logger.dart';
|
||||||
|
import 'providers/share_provider.dart';
|
||||||
|
import 'widgets/share_test_widget.dart';
|
||||||
|
|
||||||
/// 主应用组件
|
/// 主应用组件
|
||||||
/// 负责配置MaterialApp和全局设置
|
/// 负责配置MaterialApp和全局设置
|
||||||
@ -70,18 +72,39 @@ class AppWidget extends ConsumerWidget {
|
|||||||
|
|
||||||
/// 应用主页组件
|
/// 应用主页组件
|
||||||
/// 作为占位符,后续会替换为实际的主页
|
/// 作为占位符,后续会替换为实际的主页
|
||||||
class AppHomePage extends StatefulWidget {
|
class AppHomePage extends ConsumerStatefulWidget {
|
||||||
const AppHomePage({super.key});
|
const AppHomePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AppHomePage> createState() => _AppHomePageState();
|
ConsumerState<AppHomePage> createState() => _AppHomePageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppHomePageState extends State<AppHomePage> {
|
class _AppHomePageState extends ConsumerState<AppHomePage> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
Logger.debug('AppHomePage初始化');
|
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
|
@override
|
||||||
@ -137,7 +160,11 @@ class _AppHomePageState extends State<AppHomePage> {
|
|||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// 分享测试组件
|
||||||
|
const ShareTestWidget(),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
// 开始使用按钮
|
// 开始使用按钮
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
|
|||||||
403
lib/presentation/providers/share_provider.dart
Normal file
403
lib/presentation/providers/share_provider.dart
Normal file
@ -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<SharedMediaFile> 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<SharedMediaFile>? 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<ShareState> {
|
||||||
|
/// 分享仓库 - 处理分享相关的业务逻辑
|
||||||
|
final ShareRepository _shareRepository;
|
||||||
|
|
||||||
|
/// 分享数据流订阅 - 监听分享数据变化
|
||||||
|
StreamSubscription<List<SharedMediaFile>>? _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<void> handleSharedFiles(List<SharedMediaFile> 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<void> startSavingSharedImages({
|
||||||
|
String? folderId,
|
||||||
|
List<String>? 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<void> _batchSaveSharedImages(
|
||||||
|
String? folderId,
|
||||||
|
List<String>? 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<List<SharedMediaFile>> _createBatches(List<SharedMediaFile> files, int batchSize) {
|
||||||
|
final batches = <List<SharedMediaFile>>[];
|
||||||
|
|
||||||
|
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<void> _simulateBatchSaveProcess(
|
||||||
|
List<SharedMediaFile> batch,
|
||||||
|
String? folderId,
|
||||||
|
List<String>? tags,
|
||||||
|
String? note,
|
||||||
|
int batchNumber,
|
||||||
|
) async {
|
||||||
|
// 模拟保存过程,实际实现中会调用真实的保存逻辑
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
Logger.info('第$batchNumber批次保存完成,处理了${batch.length}个文件');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 模拟保存过程 - 临时模拟保存过程(后续替换为真实逻辑)
|
||||||
|
/// [folderId] 目标文件夹ID
|
||||||
|
/// [tags] 标签列表
|
||||||
|
/// [note] 备注内容
|
||||||
|
Future<void> _simulateSaveProcess(
|
||||||
|
String? folderId,
|
||||||
|
List<String>? 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<void> dispose() async {
|
||||||
|
Logger.info('释放分享状态资源');
|
||||||
|
|
||||||
|
// 调用父类的dispose方法(StateNotifier的dispose不是异步的)
|
||||||
|
super.dispose();
|
||||||
|
|
||||||
|
// 取消分享数据流订阅
|
||||||
|
await _shareSubscription?.cancel();
|
||||||
|
_shareSubscription = null;
|
||||||
|
|
||||||
|
// 释放分享仓库
|
||||||
|
await _shareRepository.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 分享数据源Provider - 提供分享数据源实例
|
||||||
|
final shareDataSourceProvider = Provider<ShareIntentDataSource>((ref) {
|
||||||
|
return ShareIntentDataSource();
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 图片工具Provider - 提供图片工具实例
|
||||||
|
/// ImageUtils是静态工具类,不需要实例化,直接提供工具引用
|
||||||
|
final imageUtilsProvider = Provider<Object>((ref) {
|
||||||
|
return Object(); // 占位Provider,实际使用ImageUtils静态方法
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 文件工具Provider - 提供文件工具实例
|
||||||
|
/// FileUtils是静态工具类,不需要实例化,直接提供工具引用
|
||||||
|
final fileUtilsProvider = Provider<Object>((ref) {
|
||||||
|
return Object(); // 占位Provider,实际使用FileUtils静态方法
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 分享仓库Provider - 提供分享仓库实例
|
||||||
|
final shareRepositoryProvider = Provider<ShareRepository>((ref) {
|
||||||
|
final shareDataSource = ref.read(shareDataSourceProvider);
|
||||||
|
// ImageUtils和FileUtils是静态工具类,直接传入null占位
|
||||||
|
return ShareRepositoryImpl(
|
||||||
|
shareDataSource: shareDataSource,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 分享状态Provider - 管理分享相关的UI状态
|
||||||
|
final shareProvider = StateNotifierProvider<ShareNotifier, ShareState>((ref) {
|
||||||
|
final shareRepository = ref.read(shareRepositoryProvider);
|
||||||
|
return ShareNotifier(shareRepository);
|
||||||
|
});
|
||||||
@ -642,7 +642,7 @@ class SkeletonList extends StatelessWidget {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(bottom: itemSpacing),
|
padding: EdgeInsets.only(bottom: itemSpacing),
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|||||||
422
lib/presentation/widgets/share_test_widget.dart
Normal file
422
lib/presentation/widgets/share_test_widget.dart
Normal file
@ -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<Color>(
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user