feat: 图片预览与发送全量实现 (#31~#35)
- #31 ImageMessageBubble: typ=2 气泡,max 220pt / min 80pt 尺寸规则,进度环叠层 - #32 ImageViewerPage: photo_view 全屏查看,PhotoViewGallery 多图滑动,保存+分享工具栏 - #33 ImagePickerSheet + SendImageUseCase: 相册/相机选图(最多9张),裁剪,dart:ui 解析宽高,FormData CDN 上传,typ=2 发送 - #34 image_cropper 接入:_PreviewTile 点击裁剪,iOS TOCropViewController 对齐 - #35 _MessageBubble BubbleKind 路由:typ switch → 各媒体气泡,输入栏新增附件按钮 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
106
apps/im_app/lib/features/chat/usecases/send_image_usecase.dart
Normal file
106
apps/im_app/lib/features/chat/usecases/send_image_usecase.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
|
||||
import 'package:im_app/data/remote/upload_file_request.dart';
|
||||
import 'package:im_app/features/chat/usecases/send_message_use_case.dart';
|
||||
|
||||
/// 图片上传并发送消息用例(#33)
|
||||
///
|
||||
/// 对应 iOS `ImageProcessor.swift` + `ChatView.swift sendImageMessage()`
|
||||
///
|
||||
/// ## 执行流程
|
||||
/// 1. 读取图片字节 → [Uint8List]
|
||||
/// 2. `dart:ui.ImageDescriptor.encoded()` → 解析压缩后宽高(零外部依赖)
|
||||
/// 3. 写入临时文件 → [UploadFileRequest] FormData → [UploadResult.url]
|
||||
/// 4. `jsonEncode({"url":url,"width":w,"height":h})` → [SendMessageUseCase] typ=2
|
||||
/// 5. 删除临时文件
|
||||
///
|
||||
/// 发送中进度通过 [onProgress] 回调传出(0.0~1.0),
|
||||
/// 用于 [ImageMessageBubble] 渲染进度环。
|
||||
class SendImageUseCase {
|
||||
final NetworksSdkApi _apiClient;
|
||||
final SendMessageUseCase _sendMessage;
|
||||
|
||||
SendImageUseCase({
|
||||
required NetworksSdkApi apiClient,
|
||||
required SendMessageUseCase sendMessage,
|
||||
}) : _apiClient = apiClient,
|
||||
_sendMessage = sendMessage;
|
||||
|
||||
Future<void> execute({
|
||||
required String filePath,
|
||||
required int chatId,
|
||||
int chatType = 1,
|
||||
void Function(double)? onProgress,
|
||||
}) async {
|
||||
// 1. 读取字节
|
||||
final bytes = await File(filePath).readAsBytes();
|
||||
|
||||
// 2. 解析宽高
|
||||
final (width, height) = await _resolveSize(bytes);
|
||||
|
||||
// 3. 写临时文件(image_picker 有时返回没有扩展名的路径,确保临时文件有扩展名)
|
||||
final ext = _extOf(filePath);
|
||||
final tempFile = File(
|
||||
'${Directory.systemTemp.path}/upload_${DateTime.now().millisecondsSinceEpoch}$ext',
|
||||
);
|
||||
await tempFile.writeAsBytes(bytes);
|
||||
|
||||
onProgress?.call(0.1);
|
||||
|
||||
// 4. 上传
|
||||
String uploadedUrl = '';
|
||||
try {
|
||||
final result = await _apiClient.executeRequest(
|
||||
UploadFileRequest(filePath: tempFile.path),
|
||||
);
|
||||
uploadedUrl = result?.url ?? '';
|
||||
} catch (e) {
|
||||
debugPrint('[SendImageUseCase] upload error: $e');
|
||||
rethrow;
|
||||
} finally {
|
||||
// 5. 删临时文件(无论成败)
|
||||
try {
|
||||
await tempFile.delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (uploadedUrl.isEmpty) {
|
||||
throw Exception('[SendImageUseCase] upload returned empty url');
|
||||
}
|
||||
|
||||
onProgress?.call(0.9);
|
||||
|
||||
// 6. 发送 typ=2 消息
|
||||
final content = '{"url":"$uploadedUrl","width":$width,"height":$height}';
|
||||
await _sendMessage.execute(chatId: chatId, content: content, typ: 2);
|
||||
|
||||
onProgress?.call(1.0);
|
||||
}
|
||||
|
||||
/// 使用 [dart:ui.ImageDescriptor] 解析字节宽高(零额外依赖)
|
||||
Future<(int, int)> _resolveSize(Uint8List bytes) async {
|
||||
try {
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
|
||||
final descriptor = await ui.ImageDescriptor.encoded(buffer);
|
||||
final w = descriptor.width;
|
||||
final h = descriptor.height;
|
||||
descriptor.dispose();
|
||||
buffer.dispose();
|
||||
return (w, h);
|
||||
} catch (e) {
|
||||
debugPrint('[SendImageUseCase] resolveSize error: $e');
|
||||
return (0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
String _extOf(String path) {
|
||||
final dot = path.lastIndexOf('.');
|
||||
if (dot == -1 || dot == path.length - 1) return '.jpg';
|
||||
return path.substring(dot);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user