TF-675 Add UploadController to handle upload attachment
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:core/presentation/extensions/color_extension.dart';
|
||||
import 'package:core/presentation/resources/image_paths.dart';
|
||||
import 'package:core/presentation/state/failure.dart';
|
||||
import 'package:core/presentation/state/success.dart';
|
||||
import 'package:core/presentation/utils/app_toast.dart';
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart';
|
||||
import 'package:model/email/attachment.dart';
|
||||
import 'package:model/extensions/attachment_extension.dart';
|
||||
import 'package:model/extensions/list_attachment_extension.dart';
|
||||
import 'package:model/upload/file_info.dart';
|
||||
import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
|
||||
import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart';
|
||||
import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart';
|
||||
import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart';
|
||||
import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state_list.dart';
|
||||
import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||
|
||||
class UploadController extends GetxController {
|
||||
|
||||
final _imagePaths = Get.find<ImagePaths>();
|
||||
final _appToast = Get.find<AppToast>();
|
||||
final _mailboxDashBoardController = Get.find<MailboxDashBoardController>();
|
||||
|
||||
final UploadAttachmentInteractor _uploadAttachmentInteractor;
|
||||
|
||||
final listUploadAttachments = <UploadFileState>[].obs;
|
||||
|
||||
final StreamGroup<Either<Failure, Success>> _progressUploadStateStreamGroup
|
||||
= StreamGroup<Either<Failure, Success>>.broadcast();
|
||||
|
||||
final UploadFileStateList _uploadingStateFiles = UploadFileStateList();
|
||||
|
||||
UploadController(this._uploadAttachmentInteractor);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
_registerProgressUploadStateStream();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_uploadingStateFiles.clear();
|
||||
_progressUploadStateStreamGroup.close();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void _registerProgressUploadStateStream() {
|
||||
_progressUploadStateStreamGroup.stream.listen(_handleProgressUploadStateStream);
|
||||
}
|
||||
|
||||
void _handleProgressUploadStateStream(Either<Failure, Success> uploadState) {
|
||||
uploadState.fold(
|
||||
(failure) {
|
||||
log('UploadController::_handleProgressUploadStateStream():failure: $failure');
|
||||
if (failure is ErrorAttachmentUploadState) {
|
||||
_uploadingStateFiles.updateElementByUploadTaskId(
|
||||
failure.uploadId,
|
||||
(currentState) => currentState?.copyWith(uploadStatus: UploadFileStatus.uploadFailed));
|
||||
deleteFileUploaded(failure.uploadId);
|
||||
_handleUploadAttachmentsFailure(failure);
|
||||
}
|
||||
},
|
||||
(success) {
|
||||
if (success is UploadingAttachmentUploadState) {
|
||||
log('UploadController::_registerUploadAttachmentsState():uploading[${success.uploadId}]: ${success.progress}');
|
||||
_uploadingStateFiles.updateElementByUploadTaskId(
|
||||
success.uploadId,
|
||||
(currentState) {
|
||||
if (currentState?.uploadStatus.completed ?? false) {
|
||||
return currentState;
|
||||
} else {
|
||||
return currentState?.copyWith(
|
||||
uploadingProgress: (success.progress * 100 / success.total).floor(),
|
||||
uploadStatus: UploadFileStatus.uploading);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
_refreshListUploadAttachmentState();
|
||||
} else if (success is SuccessAttachmentUploadState) {
|
||||
log('UploadController::_handleProgressUploadStateStream():succeed[${success.uploadId}]');
|
||||
_uploadingStateFiles.updateElementByUploadTaskId(
|
||||
success.uploadId,
|
||||
(currentState) {
|
||||
final newState = currentState?.copyWith(
|
||||
uploadingProgress: 100,
|
||||
uploadStatus: UploadFileStatus.succeed,
|
||||
attachment: success.attachment
|
||||
);
|
||||
return newState;
|
||||
},
|
||||
);
|
||||
|
||||
_refreshListUploadAttachmentState();
|
||||
_handleUploadAttachmentsSuccess(success);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void initializeUploadAttachments(List<Attachment> attachments) {
|
||||
listUploadAttachments.value = attachments
|
||||
.map((attachment) => UploadFileState(UploadTaskId(attachment.blobId!.value)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
void deleteFileUploaded(UploadTaskId uploadId) {
|
||||
_uploadingStateFiles.deleteElementByUploadTaskId(uploadId);
|
||||
_refreshListUploadAttachmentState();
|
||||
}
|
||||
|
||||
Future<void> justUploadAttachmentsAction(List<FileInfo> uploadFiles, Uri uploadUri) async {
|
||||
return Future.forEach<FileInfo>(uploadFiles, (uploadFile) async {
|
||||
await _uploadFile(uploadFile, uploadUri);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _uploadFile(FileInfo uploadFile, Uri uploadUri) async {
|
||||
log('UploadAttachmentManager::_uploadFile(): ${uploadFile.fileName}');
|
||||
|
||||
final cancelToken = CancelToken();
|
||||
final uploadAttachment = _uploadAttachmentInteractor.execute(
|
||||
uploadFile, uploadUri,
|
||||
cancelToken: cancelToken);
|
||||
|
||||
_uploadingStateFiles.add(UploadFileState(
|
||||
uploadAttachment.uploadTaskId,
|
||||
file: uploadFile,
|
||||
cancelToken: cancelToken));
|
||||
|
||||
await _progressUploadStateStreamGroup.add(uploadAttachment.progressState);
|
||||
|
||||
_refreshListUploadAttachmentState();
|
||||
}
|
||||
|
||||
void _refreshListUploadAttachmentState() {
|
||||
listUploadAttachments.value =
|
||||
_uploadingStateFiles.uploadingStateFiles.whereNotNull().toList();
|
||||
listUploadAttachments.refresh();
|
||||
}
|
||||
|
||||
List<Attachment> get attachmentsUploaded {
|
||||
if (listUploadAttachments.isEmpty) {
|
||||
return List.empty();
|
||||
}
|
||||
return listUploadAttachments
|
||||
.map((fileState) => fileState.attachment)
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
}
|
||||
|
||||
Set<EmailBodyPart>? generateAttachments() {
|
||||
if (attachmentsUploaded.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return attachmentsUploaded
|
||||
.map((attachment) => attachment.toEmailBodyPart(ContentDisposition.attachment.value))
|
||||
.toSet();
|
||||
}
|
||||
|
||||
void _handleUploadAttachmentsFailure(ErrorAttachmentUploadState failure) {
|
||||
if (currentContext != null && currentOverlayContext != null) {
|
||||
_appToast.showToastWithIcon(currentOverlayContext!,
|
||||
message: AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments,
|
||||
textColor: AppColor.toastErrorBackgroundColor,
|
||||
iconColor: AppColor.toastErrorBackgroundColor,
|
||||
icon: _imagePaths.icAttachment);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleUploadAttachmentsSuccess(SuccessAttachmentUploadState success) {
|
||||
if (currentContext != null && currentOverlayContext != null && _uploadingStateFiles.allSuccess) {
|
||||
_appToast.showToastWithIcon(currentOverlayContext!,
|
||||
message: AppLocalizations.of(currentContext!).attachments_uploaded_successfully,
|
||||
iconColor: AppColor.primaryColor,
|
||||
icon: _imagePaths.icAttachment);
|
||||
}
|
||||
}
|
||||
|
||||
bool hasEnoughMaxAttachmentSize({List<FileInfo>? listFiles}) {
|
||||
final currentTotalAttachmentsSize = attachmentsUploaded.totalSize();
|
||||
log('ComposerController::_validateAttachmentsSize(): $currentTotalAttachmentsSize');
|
||||
num uploadedTotalSize = 0;
|
||||
if (listFiles != null && listFiles.isNotEmpty) {
|
||||
final uploadedListSize = listFiles.map((file) => file.fileSize).toList();
|
||||
uploadedTotalSize = uploadedListSize.reduce((sum, size) => sum + size);
|
||||
log('ComposerController::_validateAttachmentsSize(): uploadedTotalSize: $uploadedTotalSize');
|
||||
}
|
||||
|
||||
final totalSizeReadyToUpload = currentTotalAttachmentsSize + uploadedTotalSize;
|
||||
log('ComposerController::_validateAttachmentsSize(): totalSizeReadyToUpload: $totalSizeReadyToUpload');
|
||||
|
||||
final maxSizeAttachmentsPerEmail = _mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value;
|
||||
if (maxSizeAttachmentsPerEmail != null) {
|
||||
return totalSizeReadyToUpload <= maxSizeAttachmentsPerEmail;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get allUploadAttachmentsCompleted {
|
||||
return listUploadAttachments
|
||||
.every((uploadFile) => uploadFile.uploadStatus.completed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart';
|
||||
|
||||
extension ListUploadFileStateExtension on List<UploadFileState> {
|
||||
|
||||
num get totalSize {
|
||||
if (isNotEmpty) {
|
||||
final currentListSize = map((file) => file.fileSize).toList();
|
||||
final totalSize = currentListSize.reduce((sum, size) => sum + size);
|
||||
return totalSize;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
|
||||
import 'package:core/domain/extensions/media_type_extension.dart';
|
||||
import 'package:core/presentation/resources/image_paths.dart';
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:model/email/attachment.dart';
|
||||
import 'package:model/upload/file_info.dart';
|
||||
import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart';
|
||||
import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart';
|
||||
|
||||
class UploadFileState with EquatableMixin {
|
||||
|
||||
final FileInfo? file;
|
||||
final UploadTaskId uploadTaskId;
|
||||
final UploadFileStatus uploadStatus;
|
||||
final int uploadingProgress;
|
||||
final Attachment? attachment;
|
||||
final CancelToken? cancelToken;
|
||||
|
||||
UploadFileState(this.uploadTaskId, {
|
||||
this.file,
|
||||
this.uploadStatus = UploadFileStatus.waiting,
|
||||
this.uploadingProgress = 0,
|
||||
this.attachment,
|
||||
this.cancelToken,
|
||||
});
|
||||
|
||||
UploadFileState copyWith({
|
||||
UploadTaskId? uploadTaskId,
|
||||
FileInfo? file,
|
||||
UploadFileStatus? uploadStatus,
|
||||
int? uploadingProgress,
|
||||
Attachment? attachment,
|
||||
CancelToken? cancelToken
|
||||
}) {
|
||||
return UploadFileState(
|
||||
uploadTaskId ?? this.uploadTaskId,
|
||||
file: file ?? this.file,
|
||||
uploadStatus: uploadStatus ?? this.uploadStatus,
|
||||
uploadingProgress: uploadingProgress ?? this.uploadingProgress,
|
||||
attachment: attachment ?? this.attachment,
|
||||
cancelToken: cancelToken ?? this.cancelToken
|
||||
);
|
||||
}
|
||||
|
||||
num get fileSize {
|
||||
if (attachment != null) {
|
||||
return attachment?.size?.value ?? 0;
|
||||
} else if (file != null) {
|
||||
return file?.fileSize ?? 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
String get fileName {
|
||||
if (attachment != null) {
|
||||
return attachment?.name ?? '';
|
||||
} else if (file != null) {
|
||||
return file?.fileName ?? '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
double get percentUploading => uploadingProgress / 100;
|
||||
|
||||
String getIcon(ImagePaths imagePaths) {
|
||||
var mediaType = attachment?.type;
|
||||
if (mediaType == null && file != null) {
|
||||
mediaType = MediaType.parse(file!.mimeType);
|
||||
}
|
||||
log('AttachmentExtension::getIcon(): mediaType: $mediaType');
|
||||
if (mediaType == null) {
|
||||
return imagePaths.icFileEPup;
|
||||
}
|
||||
if (mediaType.isDocFile()) {
|
||||
return imagePaths.icFileDocx;
|
||||
} else if (mediaType.isExcelFile()) {
|
||||
return imagePaths.icFileXlsx;
|
||||
} else if (mediaType.isPowerPointFile()) {
|
||||
return imagePaths.icFilePptx;
|
||||
} else if (mediaType.isPdfFile()) {
|
||||
return imagePaths.icFilePdf;
|
||||
} else if (mediaType.isZipFile()) {
|
||||
return imagePaths.icFileZip;
|
||||
} else if (mediaType.isImageFile()) {
|
||||
return imagePaths.icFilePng;
|
||||
}
|
||||
return imagePaths.icFileEPup;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
uploadTaskId,
|
||||
file,
|
||||
uploadStatus,
|
||||
uploadingProgress,
|
||||
attachment
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart';
|
||||
import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart';
|
||||
import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart';
|
||||
|
||||
typedef UpdateFileUploadingState = UploadFileState? Function(UploadFileState? currentState);
|
||||
typedef MatchedState = bool Function(UploadFileState? state);
|
||||
|
||||
class UploadFileStateList {
|
||||
|
||||
final List<UploadFileState?> _uploadingStateFiles = <UploadFileState?>[];
|
||||
|
||||
List<UploadFileState?> get uploadingStateFiles => _uploadingStateFiles.toList(growable: false);
|
||||
|
||||
UploadFileStateList add(UploadFileState element) {
|
||||
_uploadingStateFiles.add(element);
|
||||
return this;
|
||||
}
|
||||
|
||||
UploadFileStateList updateElementBy(
|
||||
MatchedState matchedState,
|
||||
UpdateFileUploadingState updateFileUploadingState,
|
||||
) {
|
||||
final matchIndex = _uploadingStateFiles.indexWhere((element) => matchedState(element));
|
||||
if (matchIndex >= 0) {
|
||||
_uploadingStateFiles[matchIndex] = updateFileUploadingState(_uploadingStateFiles[matchIndex]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
UploadFileStateList updateElementByUploadTaskId(
|
||||
UploadTaskId uploadTaskId,
|
||||
UpdateFileUploadingState updateFileUploadingState,
|
||||
) {
|
||||
return updateElementBy(
|
||||
(state) => state?.uploadTaskId == uploadTaskId,
|
||||
updateFileUploadingState
|
||||
);
|
||||
}
|
||||
|
||||
bool get allSuccess {
|
||||
if (_uploadingStateFiles.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return _uploadingStateFiles
|
||||
.whereNotNull()
|
||||
.every((file) => file.uploadStatus == UploadFileStatus.succeed);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_uploadingStateFiles.clear();
|
||||
}
|
||||
|
||||
void deleteElementByUploadTaskId(UploadTaskId uploadTaskId) {
|
||||
final fileState = _uploadingStateFiles
|
||||
.firstWhereOrNull((fileState) => fileState?.uploadTaskId == uploadTaskId);
|
||||
|
||||
if (fileState != null) {
|
||||
fileState.cancelToken?.cancel();
|
||||
_uploadingStateFiles.remove(fileState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
enum UploadFileStatus {
|
||||
waiting,
|
||||
uploading,
|
||||
uploadFailed,
|
||||
succeed
|
||||
}
|
||||
|
||||
extension UploadFileStatusExtension on UploadFileStatus {
|
||||
bool get completed =>
|
||||
this == UploadFileStatus.uploadFailed || this == UploadFileStatus.succeed;
|
||||
}
|
||||
Reference in New Issue
Block a user