建立分享机制

This commit is contained in:
ddshi 2025-09-17 13:32:25 +08:00
parent 1212c4ea27
commit 8f284075b6
12 changed files with 1665 additions and 24 deletions

View File

@ -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: 保存界面UI0/4
### 🎯 当前任务详情
**任务编号**2.1
**任务名称**分享接收机制
**任务编号**2.2
**任务名称**保存界面UI
**任务状态**:待开始
**预计完成**2025年9月18
**依赖项**Phase 1.4 完成
**预计完成**2025年9月19
**依赖项**Phase 2.1 完成
**任务验收标准**
- 分享接收机制配置完成
- Android分享接收配置完成
- iOS分享接收配置完成
- 多张图片接收逻辑处理
- 分享功能测试通过
- 半透明模态框界面创建完成
- 图片网格预览组件实现
- 文件夹选择器弹窗完成
- 标签输入和选择组件实现

View File

@ -23,6 +23,18 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</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>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View File

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

View File

@ -45,5 +45,36 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<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>
</plist>
</plist>

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

View 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();
}
}

View 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();
}

View File

@ -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(

View File

@ -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<AppHomePage> createState() => _AppHomePageState();
ConsumerState<AppHomePage> createState() => _AppHomePageState();
}
class _AppHomePageState extends State<AppHomePage> {
class _AppHomePageState extends ConsumerState<AppHomePage> {
@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<AppHomePage> {
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
const SizedBox(height: 32),
//
const ShareTestWidget(),
const SizedBox(height: 32),
// 使
ElevatedButton.icon(

View 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);
});

View File

@ -642,7 +642,7 @@ class SkeletonList extends StatelessWidget {
itemBuilder: (context, index) {
return Padding(
padding: EdgeInsets.only(bottom: itemSpacing),
child: Container(
child: SizedBox(
height: itemHeight,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,

View 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();
}
}