From 3645fe58f9e14748e66c655872cee1e70df41eb6 Mon Sep 17 00:00:00 2001 From: DatDang Date: Wed, 19 Feb 2025 16:22:00 +0700 Subject: [PATCH] TF-3450 Add image attachment preview --- .../get_image_data_from_attachment_state.dart | 31 ++++++++++ ...image_data_from_attachment_interactor.dart | 62 +++++++++++++++++++ .../presentation/bindings/email_bindings.dart | 5 ++ .../controller/single_email_controller.dart | 32 +++++++++- .../extensions/attachment_extension.dart | 2 + .../email/presentation/utils/email_utils.dart | 2 + .../single_email_controller_test.dart | 4 ++ 7 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 lib/features/email/domain/state/get_image_data_from_attachment_state.dart create mode 100644 lib/features/email/domain/usecases/get_image_data_from_attachment_interactor.dart diff --git a/lib/features/email/domain/state/get_image_data_from_attachment_state.dart b/lib/features/email/domain/state/get_image_data_from_attachment_state.dart new file mode 100644 index 000000000..a78c7b14d --- /dev/null +++ b/lib/features/email/domain/state/get_image_data_from_attachment_state.dart @@ -0,0 +1,31 @@ +import 'dart:typed_data'; + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/email/attachment.dart'; + +class GettingImageDataFromAttachment extends LoadingState { + GettingImageDataFromAttachment({required this.attachment}); + + final Attachment attachment; + + @override + List get props => [attachment]; +} + +class GetImageDataFromAttachmentSuccess extends UIState { + GetImageDataFromAttachmentSuccess({ + required this.attachment, + required this.data, + }); + + final Attachment attachment; + final Uint8List data; + + @override + List get props => [attachment, data]; +} + +class GetImageDataFromAttachmentFailure extends FeatureFailure { + GetImageDataFromAttachmentFailure({super.exception}); +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/get_image_data_from_attachment_interactor.dart b/lib/features/email/domain/usecases/get_image_data_from_attachment_interactor.dart new file mode 100644 index 000000000..af578794b --- /dev/null +++ b/lib/features/email/domain/usecases/get_image_data_from_attachment_interactor.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:model/download/download_task_id.dart'; +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_image_data_from_attachment_state.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; + +class GetImageDataFromAttachmentInteractor { + const GetImageDataFromAttachmentInteractor(this._downloadAttachmentForWebInteractor); + + final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor; + + Stream> execute( + DownloadTaskId taskId, + Attachment attachment, + AccountId accountId, + String baseDownloadUrl, + {CancelToken? cancelToken,} + ) async* { + final onProgressStream = StreamController>(); + try { + yield Right(GettingImageDataFromAttachment(attachment: attachment)); + final downloadState = await _downloadAttachmentForWebInteractor.execute( + taskId, + attachment, + accountId, + baseDownloadUrl, + onProgressStream, + cancelToken: cancelToken, + ).last; + + yield downloadState.fold( + (failure) { + return Left(GetImageDataFromAttachmentFailure( + exception: failure is FeatureFailure ? failure.exception : null, + )); + }, + (success) { + if (success is! DownloadAttachmentForWebSuccess) { + return Left(GetImageDataFromAttachmentFailure()); + } + + return Right(GetImageDataFromAttachmentSuccess( + attachment: attachment, + data: success.bytes, + )); + }, + ); + } catch (e) { + logError('GetImageDataFromAttachmentInteractor::execute(): $e'); + yield Left(GetImageDataFromAttachmentFailure(exception: e)); + } + onProgressStream.close(); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/bindings/email_bindings.dart b/lib/features/email/presentation/bindings/email_bindings.dart index d8d56de82..9ea8ba9d0 100644 --- a/lib/features/email/presentation/bindings/email_bindings.dart +++ b/lib/features/email/presentation/bindings/email_bindings.dart @@ -22,6 +22,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment import 'package:tmail_ui_user/features/email/domain/usecases/export_all_attachments_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/export_attachment_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/get_image_data_from_attachment_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_stored_email_state_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart'; @@ -84,6 +85,7 @@ class EmailBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } @@ -182,6 +184,9 @@ class EmailBindings extends BaseBindings { Get.find(), Get.find(), Get.find())); + Get.lazyPut(() => GetImageDataFromAttachmentInteractor( + Get.find(), + )); } @override diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index f4e604980..9b10376a2 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -64,6 +64,7 @@ import 'package:tmail_ui_user/features/email/domain/state/export_all_attachments import 'package:tmail_ui_user/features/email/domain/state/export_attachment_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_html_content_from_attachment_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_image_data_from_attachment_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; @@ -78,6 +79,7 @@ import 'package:tmail_ui_user/features/email/domain/state/unsubscribe_email_stat import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_accept_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_all_attachments_for_web_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/export_all_attachments_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/get_image_data_from_attachment_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/maybe_calendar_event_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_reject_interactor.dart'; @@ -141,6 +143,8 @@ import 'package:tmail_ui_user/main/routes/navigation_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; +import 'package:twake_previewer_flutter/core/previewer_options/options/top_bar_options.dart'; +import 'package:twake_previewer_flutter/twake_image_previewer/twake_image_previewer.dart'; class SingleEmailController extends BaseController with AppLoaderMixin { @@ -167,6 +171,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final GetHtmlContentFromAttachmentInteractor _getHtmlContentFromAttachmentInteractor; final DownloadAllAttachmentsForWebInteractor _downloadAllAttachmentsForWebInteractor; final ExportAllAttachmentsInteractor _exportAllAttachmentsInteractor; + final GetImageDataFromAttachmentInteractor _getImageDataFromAttachmentInteractor; CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; SendReceiptToSenderInteractor? _sendReceiptToSenderInteractor; @@ -225,6 +230,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { this._getHtmlContentFromAttachmentInteractor, this._downloadAllAttachmentsForWebInteractor, this._exportAllAttachmentsInteractor, + this._getImageDataFromAttachmentInteractor, ); @override @@ -302,6 +308,17 @@ class SingleEmailController extends BaseController with AppLoaderMixin { )); } else if (success is GettingHtmlContentFromAttachment) { _updateAttachmentsViewState(success.attachment.blobId, Right(success)); + } else if (success is GettingImageDataFromAttachment) { + _updateAttachmentsViewState(success.attachment.blobId, Right(success)); + } else if (success is GetImageDataFromAttachmentSuccess) { + _updateAttachmentsViewState(success.attachment.blobId, Right(success)); + Get.dialog(TwakeImagePreviewer( + bytes: success.data, + topBarOptions: TopBarOptions( + title: success.attachment.generateFileName(), + onClose: popBack, + ), + )); } } @@ -2202,7 +2219,20 @@ class SingleEmailController extends BaseController with AppLoaderMixin { customTextTransformers: [const StandardizeHtmlSanitizingTransformers()], ), )); - }else { + } else if (attachment.isImage) { + final attachmentEvaluation = evaluateAttachment(attachment); + if (!attachmentEvaluation.canDownloadAttachment) { + consumeState(Stream.value(Left(GetImageDataFromAttachmentFailure()))); + return; + } + + consumeState(_getImageDataFromAttachmentInteractor.execute( + DownloadTaskId(uuid.v4()), + attachment, + attachmentEvaluation.accountId!, + attachmentEvaluation.downloadUrl!, + )); + } else { handleDownloadAttachmentAction(context, attachment); } } diff --git a/lib/features/email/presentation/extensions/attachment_extension.dart b/lib/features/email/presentation/extensions/attachment_extension.dart index 46281d395..8c0a5835d 100644 --- a/lib/features/email/presentation/extensions/attachment_extension.dart +++ b/lib/features/email/presentation/extensions/attachment_extension.dart @@ -40,4 +40,6 @@ extension AttachmentExtension on Attachment { DownloadTaskId get downloadTaskId => DownloadTaskId(blobId!.value); bool get isHTMLFile => type?.isHTMLFile(fileName: name) ?? false; + + bool get isImage => type?.isImageFile() ?? false; } \ No newline at end of file diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index c1209b97e..32b315499 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -18,6 +18,7 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/attachment.dart'; import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_html_content_from_attachment_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_image_data_from_attachment_state.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart'; import 'package:tmail_ui_user/features/email/presentation/styles/attachment/attachment_item_widget_style.dart'; import 'package:tmail_ui_user/features/email/presentation/styles/email_attachments_styles.dart'; @@ -76,6 +77,7 @@ class EmailUtils { (success) { return success is DownloadAttachmentForWebSuccess || success is GetHtmlContentFromAttachmentSuccess + || success is GetImageDataFromAttachmentSuccess || success is IdleDownloadAttachmentForWeb; }) ?? false; } diff --git a/test/features/email/presentation/controller/single_email_controller_test.dart b/test/features/email/presentation/controller/single_email_controller_test.dart index a6e38d6b7..f8051e23e 100644 --- a/test/features/email/presentation/controller/single_email_controller_test.dart +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -31,6 +31,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_acce import 'package:tmail_ui_user/features/email/domain/usecases/download_all_attachments_for_web_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/export_all_attachments_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_html_content_from_attachment_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/get_image_data_from_attachment_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/maybe_calendar_event_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_reject_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; @@ -116,6 +117,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -157,6 +159,7 @@ void main() { final downloadAllAttachmentsForWebInteractor = MockDownloadAllAttachmentsForWebInteractor(); final exportAllAttachmentsInteractor = MockExportAllAttachmentsInteractor(); + final getImageDataFromAttachmentInteractor = MockGetImageDataFromAttachmentInteractor(); late SingleEmailController singleEmailController; @@ -217,6 +220,7 @@ void main() { getHtmlContentFromAttachmentInteractor, downloadAllAttachmentsForWebInteractor, exportAllAttachmentsInteractor, + getImageDataFromAttachmentInteractor, ); });