Files
xianyan/lib/features/discover/providers/chat_attachment_provider.dart
Developer 9ea8d3d606 chore: 汇总批量提交的功能优化与bug修复
本次提交包含多项迭代优化和问题修复:
1. 新增缩略图图片组件、数字格式化工具类,补充多语言翻译类型与本地化支持
2. 优化底部导航栏主题色统一使用动态accent色值
3. 修复多处图表动画、路由跳转、API请求相关问题
4. 简化服务器公告文案,调整默认分屏状态为关闭
5. 新增安卓/iOS桌面快捷方式配置
6. 重构多处状态管理类使用SafeNotifierInit统一异常保护
7. 替换硬编码蓝色为主题色,更新版本号获取方式为动态读取
8. 优化缓存预加载逻辑,移除无用代码
9. 调整默认设置项,优化用户体验细节
2026-05-31 12:24:05 +08:00

412 lines
13 KiB
Dart

/// ============================================================
/// 闲言APP — 聊天附件Provider
/// 创建时间: 2026-05-08
/// 更新时间: 2026-05-30
/// 作用: 附件选择/预览/发送状态管理
/// 上次更新: 使用SafeNotifierInit统一异常保护
/// ============================================================
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:file_picker/file_picker.dart';
import 'package:image_picker/image_picker.dart';
import 'package:xianyan/core/storage/database/app_database.dart';
import 'package:xianyan/core/utils/logger.dart';
import 'package:xianyan/core/utils/safe_init_mixin.dart';
import 'package:xianyan/features/discover/services/chat_attachment_service.dart';
import 'package:xianyan/features/discover/services/chat_file_service.dart';
import 'package:xianyan/features/discover/services/chat_message_service.dart';
class PendingAttachment {
const PendingAttachment({
required this.fileName,
required this.fileSize,
required this.fileType,
this.filePath,
this.thumbnailPath,
this.width,
this.height,
this.durationMs,
});
final String fileName;
final int fileSize;
final String fileType;
final String? filePath;
final String? thumbnailPath;
final int? width;
final int? height;
final int? durationMs;
bool get isImage => ChatFileService.isImageType(fileType);
bool get isVideo => ChatFileService.isVideoType(fileType);
bool get isAudio => ChatFileService.isAudioType(fileType);
String get sizeText => ChatFileService.formatFileSize(fileSize);
}
class ChatAttachmentState {
const ChatAttachmentState({
this.pendingAttachments = const [],
this.isSending = false,
this.attachments = const [],
this.totalSize = 0,
});
final List<PendingAttachment> pendingAttachments;
final bool isSending;
final List<ChatAttachment> attachments;
final int totalSize;
String get totalSizeText => ChatFileService.formatFileSize(totalSize);
bool get hasPending => pendingAttachments.isNotEmpty;
ChatAttachmentState copyWith({
List<PendingAttachment>? pendingAttachments,
bool? isSending,
List<ChatAttachment>? attachments,
int? totalSize,
}) {
return ChatAttachmentState(
pendingAttachments: pendingAttachments ?? this.pendingAttachments,
isSending: isSending ?? this.isSending,
attachments: attachments ?? this.attachments,
totalSize: totalSize ?? this.totalSize,
);
}
}
class ChatAttachmentNotifier extends Notifier<ChatAttachmentState>
with SafeNotifierInit {
@override
ChatAttachmentState build() {
safeNotifierInit(_init, label: 'ChatAttachmentNotifier');
return const ChatAttachmentState();
}
ChatAttachmentNotifier(this.conversationId);
final String conversationId;
Future<void> _init() async {
await loadAttachments();
}
/// 加载会话附件列表
Future<void> loadAttachments() async {
try {
final atts = await ChatAttachmentService.getByConversation(
conversationId,
);
final size = await ChatFileService.getConversationFilesSize(
conversationId,
);
state = state.copyWith(attachments: atts, totalSize: size);
} catch (e) {
Log.e('附件列表加载失败', e);
}
}
/// 选择图片(相册)
Future<void> pickImageFromGallery() async {
try {
final picker = ImagePicker();
final images = await picker.pickMultiImage();
for (final image in images) {
final file = File(image.path);
final size = await file.length();
final result = await ChatFileService.saveImage(
conversationId: conversationId,
sourceFile: file,
fileName: image.name,
);
state = state.copyWith(
pendingAttachments: [
...state.pendingAttachments,
PendingAttachment(
fileName: image.name,
fileSize: size,
fileType: 'image/${_getExtension(image.name)}',
filePath: result.filePath,
thumbnailPath: result.thumbnailPath,
width: result.width,
height: result.height,
),
],
);
}
Log.d('已选择${images.length}张图片');
} catch (e) {
Log.e('选择图片失败', e);
}
}
/// 选择图片(拍照)
Future<void> pickImageFromCamera() async {
try {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.camera);
if (image == null) return;
final file = File(image.path);
final size = await file.length();
final result = await ChatFileService.saveImage(
conversationId: conversationId,
sourceFile: file,
fileName: image.name,
);
state = state.copyWith(
pendingAttachments: [
...state.pendingAttachments,
PendingAttachment(
fileName: image.name,
fileSize: size,
fileType: 'image/${_getExtension(image.name)}',
filePath: result.filePath,
thumbnailPath: result.thumbnailPath,
width: result.width,
height: result.height,
),
],
);
Log.d('拍照图片已添加');
} catch (e) {
Log.e('拍照失败', e);
}
}
/// 选择视频
Future<void> pickVideo() async {
try {
final result = await FilePicker.pickFiles(type: FileType.video);
if (result == null) return;
final platformFile = result.files.first;
if (platformFile.path == null) return;
final file = File(platformFile.path!);
final size = await file.length();
final savedPath = await ChatFileService.saveFile(
conversationId: conversationId,
sourceFile: file,
fileName: platformFile.name,
);
state = state.copyWith(
pendingAttachments: [
...state.pendingAttachments,
PendingAttachment(
fileName: platformFile.name,
fileSize: size,
fileType: 'video/${_getExtension(platformFile.name)}',
filePath: savedPath,
),
],
);
Log.d('视频已选择: ${platformFile.name}');
} catch (e) {
Log.e('选择视频失败', e);
}
}
/// 选择文件
Future<void> pickFile() async {
try {
final result = await FilePicker.pickFiles();
if (result == null) return;
for (final platformFile in result.files) {
if (platformFile.path == null) continue;
final file = File(platformFile.path!);
final size = await file.length();
final savedPath = await ChatFileService.saveFile(
conversationId: conversationId,
sourceFile: file,
fileName: platformFile.name,
);
state = state.copyWith(
pendingAttachments: [
...state.pendingAttachments,
PendingAttachment(
fileName: platformFile.name,
fileSize: size,
fileType: _getMimeType(platformFile.name),
filePath: savedPath,
),
],
);
}
Log.d('已选择${result.files.length}个文件');
} catch (e) {
Log.e('选择文件失败', e);
}
}
/// 移除待发送附件
void removePendingAttachment(int index) {
final updated = [...state.pendingAttachments];
if (index >= 0 && index < updated.length) {
final removed = updated.removeAt(index);
if (removed.filePath != null) {
ChatFileService.deleteFile(removed.filePath!);
}
state = state.copyWith(pendingAttachments: updated);
}
}
/// 清空待发送附件
void clearPendingAttachments() {
for (final att in state.pendingAttachments) {
if (att.filePath != null) {
ChatFileService.deleteFile(att.filePath!);
}
}
state = state.copyWith(pendingAttachments: []);
}
/// 确认发送附件(创建消息+附件记录)
Future<void> commitAttachments(String messageId) async {
state = state.copyWith(isSending: true);
try {
for (final pending in state.pendingAttachments) {
if (pending.isImage) {
await ChatAttachmentService.saveImageAttachment(
messageId: messageId,
conversationId: conversationId,
fileName: pending.fileName,
filePath: pending.filePath!,
fileSize: pending.fileSize,
thumbnailPath: pending.thumbnailPath,
width: pending.width,
height: pending.height,
);
} else {
await ChatAttachmentService.saveFileAttachment(
messageId: messageId,
conversationId: conversationId,
fileName: pending.fileName,
filePath: pending.filePath!,
fileSize: pending.fileSize,
mimeType: pending.fileType,
);
}
}
state = state.copyWith(pendingAttachments: [], isSending: false);
await loadAttachments();
Log.d('附件发送完成: ${state.pendingAttachments.length}');
} catch (e) {
Log.e('附件发送失败', e);
state = state.copyWith(isSending: false);
}
}
/// 发送所有待发送附件(自动创建消息)
Future<void> sendPendingAttachments() async {
if (state.pendingAttachments.isEmpty) return;
state = state.copyWith(isSending: true);
try {
for (final pending in state.pendingAttachments) {
if (pending.isImage) {
final record = await ChatMessageService.sendImage(
conversationId: conversationId,
content: pending.fileName,
meta: {
'fileName': pending.fileName,
'fileSize': pending.fileSize,
'filePath': pending.filePath,
'thumbnailPath': pending.thumbnailPath,
'width': pending.width,
'height': pending.height,
},
);
await ChatAttachmentService.saveImageAttachment(
messageId: record.id,
conversationId: conversationId,
fileName: pending.fileName,
filePath: pending.filePath!,
fileSize: pending.fileSize,
thumbnailPath: pending.thumbnailPath,
width: pending.width,
height: pending.height,
);
} else {
final record = await ChatMessageService.sendFile(
conversationId: conversationId,
content: pending.fileName,
meta: {
'fileName': pending.fileName,
'fileSize': pending.fileSize,
'filePath': pending.filePath,
'mimeType': pending.fileType,
},
);
await ChatAttachmentService.saveFileAttachment(
messageId: record.id,
conversationId: conversationId,
fileName: pending.fileName,
filePath: pending.filePath!,
fileSize: pending.fileSize,
mimeType: pending.fileType,
);
}
}
state = state.copyWith(pendingAttachments: [], isSending: false);
await loadAttachments();
Log.d('所有待发送附件已发送');
} catch (e) {
Log.e('发送待发送附件失败', e);
state = state.copyWith(isSending: false);
}
}
/// 删除附件
Future<void> deleteAttachment(String messageId) async {
await ChatAttachmentService.deleteByMessage(messageId);
await loadAttachments();
}
/// 删除会话所有附件
Future<void> deleteAllAttachments() async {
await ChatAttachmentService.deleteByConversation(conversationId);
await loadAttachments();
}
String _getExtension(String fileName) {
final dotIndex = fileName.lastIndexOf('.');
if (dotIndex >= 0 && dotIndex < fileName.length - 1) {
return fileName.substring(dotIndex + 1).toLowerCase();
}
return 'jpg';
}
String _getMimeType(String fileName) {
final ext = _getExtension(fileName);
const mimeMap = {
'pdf': 'application/pdf',
'doc': 'application/msword',
'docx':
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls': 'application/vnd.ms-excel',
'xlsx':
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'ppt': 'application/vnd.ms-powerpoint',
'pptx':
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'txt': 'text/plain',
'zip': 'application/zip',
'rar': 'application/x-rar-compressed',
'mp3': 'audio/mpeg',
'mp4': 'video/mp4',
'wav': 'audio/wav',
'avi': 'video/x-msvideo',
'mov': 'video/quicktime',
};
return mimeMap[ext] ?? 'application/octet-stream';
}
}
final chatAttachmentProvider =
NotifierProvider.family<
ChatAttachmentNotifier,
ChatAttachmentState,
String
>((conversationId) => ChatAttachmentNotifier(conversationId));