From ef1d37f7ac170b08949b0aef9fefc8dd50c5d9fa Mon Sep 17 00:00:00 2001 From: DatDang Date: Thu, 20 Feb 2025 10:24:58 +0700 Subject: [PATCH] TF-3450 Add text attachment preview --- .../preview/supported_preview_file_types.dart | 5 ++ .../extensions/media_type_extension.dart | 2 + .../get_text_data_from_attachment_state.dart | 31 ++++++++++ ..._text_data_from_attachment_interactor.dart | 62 +++++++++++++++++++ .../presentation/bindings/email_bindings.dart | 5 ++ .../controller/single_email_controller.dart | 35 +++++++++++ .../extensions/attachment_extension.dart | 4 ++ .../email/presentation/utils/email_utils.dart | 2 + .../single_email_controller_test.dart | 4 ++ 9 files changed, 150 insertions(+) create mode 100644 lib/features/email/domain/state/get_text_data_from_attachment_state.dart create mode 100644 lib/features/email/domain/usecases/get_text_data_from_attachment_interactor.dart diff --git a/core/lib/domain/preview/supported_preview_file_types.dart b/core/lib/domain/preview/supported_preview_file_types.dart index ede994866..3718f612f 100644 --- a/core/lib/domain/preview/supported_preview_file_types.dart +++ b/core/lib/domain/preview/supported_preview_file_types.dart @@ -5,6 +5,11 @@ class SupportedPreviewFileTypes { 'image/gif', 'image/png',]; + static const textMimeTypes = [ + 'text/plain', + 'text/markdown', + ]; + static const docMimeTypes = [ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.oasis.opendocument.text', diff --git a/core/lib/presentation/extensions/media_type_extension.dart b/core/lib/presentation/extensions/media_type_extension.dart index d92f4c6d1..8c57b9985 100644 --- a/core/lib/presentation/extensions/media_type_extension.dart +++ b/core/lib/presentation/extensions/media_type_extension.dart @@ -21,6 +21,8 @@ extension MediaTypeExtension on MediaType { bool isRtfFile() => SupportedPreviewFileTypes.rtfMimeTypes.contains(mimeType); + bool isTextFile() => SupportedPreviewFileTypes.textMimeTypes.contains(mimeType); + DocumentUti getDocumentUti() => DocumentUti(SupportedPreviewFileTypes.iOSSupportedTypes[mimeType]); String getIcon(ImagePaths imagePaths, {String? fileName}) { diff --git a/lib/features/email/domain/state/get_text_data_from_attachment_state.dart b/lib/features/email/domain/state/get_text_data_from_attachment_state.dart new file mode 100644 index 000000000..5cb6081fe --- /dev/null +++ b/lib/features/email/domain/state/get_text_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 GettingTextDataFromAttachment extends LoadingState { + GettingTextDataFromAttachment({required this.attachment}); + + final Attachment attachment; + + @override + List get props => [attachment]; +} + +class GetTextDataFromAttachmentSuccess extends UIState { + GetTextDataFromAttachmentSuccess({ + required this.attachment, + required this.data, + }); + + final Attachment attachment; + final Uint8List data; + + @override + List get props => [attachment, data]; +} + +class GetTextDataFromAttachmentFailure extends FeatureFailure { + GetTextDataFromAttachmentFailure({super.exception}); +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/get_text_data_from_attachment_interactor.dart b/lib/features/email/domain/usecases/get_text_data_from_attachment_interactor.dart new file mode 100644 index 000000000..cc8dd4a82 --- /dev/null +++ b/lib/features/email/domain/usecases/get_text_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_text_data_from_attachment_state.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; + +class GetTextDataFromAttachmentInteractor { + const GetTextDataFromAttachmentInteractor(this._downloadAttachmentForWebInteractor); + + final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor; + + Stream> execute( + DownloadTaskId taskId, + Attachment attachment, + AccountId accountId, + String baseDownloadUrl, + {CancelToken? cancelToken,} + ) async* { + final onProgressStream = StreamController>(); + try { + yield Right(GettingTextDataFromAttachment(attachment: attachment)); + final downloadState = await _downloadAttachmentForWebInteractor.execute( + taskId, + attachment, + accountId, + baseDownloadUrl, + onProgressStream, + cancelToken: cancelToken, + ).last; + + yield downloadState.fold( + (failure) { + return Left(GetTextDataFromAttachmentFailure( + exception: failure is FeatureFailure ? failure.exception : null, + )); + }, + (success) { + if (success is! DownloadAttachmentForWebSuccess) { + return Left(GetTextDataFromAttachmentFailure()); + } + + return Right(GetTextDataFromAttachmentSuccess( + attachment: attachment, + data: success.bytes, + )); + }, + ); + } catch (e) { + logError('GetTextDataFromAttachmentInteractor::execute(): $e'); + yield Left(GetTextDataFromAttachmentFailure(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 9ea8ba9d0..bf231f23d 100644 --- a/lib/features/email/presentation/bindings/email_bindings.dart +++ b/lib/features/email/presentation/bindings/email_bindings.dart @@ -24,6 +24,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/export_attachment_i 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/get_text_data_from_attachment_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'; import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart'; @@ -86,6 +87,7 @@ class EmailBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } @@ -187,6 +189,9 @@ class EmailBindings extends BaseBindings { Get.lazyPut(() => GetImageDataFromAttachmentInteractor( Get.find(), )); + Get.lazyPut(() => GetTextDataFromAttachmentInteractor( + 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 9b10376a2..ff4e0856d 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -65,6 +65,7 @@ import 'package:tmail_ui_user/features/email/domain/state/export_attachment_stat 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/get_text_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'; @@ -80,6 +81,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_image_data_from_attachment_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/get_text_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'; @@ -143,8 +145,11 @@ 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/constants/supported_charset.dart'; import 'package:twake_previewer_flutter/core/previewer_options/options/top_bar_options.dart'; +import 'package:twake_previewer_flutter/core/previewer_options/previewer_options.dart'; import 'package:twake_previewer_flutter/twake_image_previewer/twake_image_previewer.dart'; +import 'package:twake_previewer_flutter/twake_plain_text_previewer/twake_plain_text_previewer.dart'; class SingleEmailController extends BaseController with AppLoaderMixin { @@ -172,6 +177,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final DownloadAllAttachmentsForWebInteractor _downloadAllAttachmentsForWebInteractor; final ExportAllAttachmentsInteractor _exportAllAttachmentsInteractor; final GetImageDataFromAttachmentInteractor _getImageDataFromAttachmentInteractor; + final GetTextDataFromAttachmentInteractor _getTextDataFromAttachmentInteractor; CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; SendReceiptToSenderInteractor? _sendReceiptToSenderInteractor; @@ -231,6 +237,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { this._downloadAllAttachmentsForWebInteractor, this._exportAllAttachmentsInteractor, this._getImageDataFromAttachmentInteractor, + this._getTextDataFromAttachmentInteractor, ); @override @@ -319,6 +326,21 @@ class SingleEmailController extends BaseController with AppLoaderMixin { onClose: popBack, ), )); + } else if (success is GettingTextDataFromAttachment) { + _updateAttachmentsViewState(success.attachment.blobId, Right(success)); + } else if (success is GetTextDataFromAttachmentSuccess) { + _updateAttachmentsViewState(success.attachment.blobId, Right(success)); + Get.dialog(TwakePlainTextPreviewer( + supportedCharset: SupportedCharset.utf8, + bytes: success.data, + previewerOptions: PreviewerOptions( + width: currentContext == null ? 200 : currentContext!.width * 0.8, + ), + topBarOptions: TopBarOptions( + title: success.attachment.generateFileName(), + onClose: popBack, + ), + )); } } @@ -2232,6 +2254,19 @@ class SingleEmailController extends BaseController with AppLoaderMixin { attachmentEvaluation.accountId!, attachmentEvaluation.downloadUrl!, )); + } else if (attachment.isText) { + final attachmentEvaluation = evaluateAttachment(attachment); + if (!attachmentEvaluation.canDownloadAttachment) { + consumeState(Stream.value(Left(GetTextDataFromAttachmentFailure()))); + return; + } + + consumeState(_getTextDataFromAttachmentInteractor.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 8c0a5835d..fcbc1821f 100644 --- a/lib/features/email/presentation/extensions/attachment_extension.dart +++ b/lib/features/email/presentation/extensions/attachment_extension.dart @@ -42,4 +42,8 @@ extension AttachmentExtension on Attachment { bool get isHTMLFile => type?.isHTMLFile(fileName: name) ?? false; bool get isImage => type?.isImageFile() ?? false; + + bool get isText => (type?.isTextFile() ?? false) + || name?.endsWith('.txt') == true + || name?.endsWith('.md') == true; } \ 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 32b315499..ad79dbb80 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -19,6 +19,7 @@ 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/domain/state/get_text_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'; @@ -78,6 +79,7 @@ class EmailUtils { return success is DownloadAttachmentForWebSuccess || success is GetHtmlContentFromAttachmentSuccess || success is GetImageDataFromAttachmentSuccess + || success is GetTextDataFromAttachmentSuccess || 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 f8051e23e..d0bc99d1d 100644 --- a/test/features/email/presentation/controller/single_email_controller_test.dart +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -32,6 +32,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/download_all_attach 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/get_text_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'; @@ -118,6 +119,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -160,6 +162,7 @@ void main() { final downloadAllAttachmentsForWebInteractor = MockDownloadAllAttachmentsForWebInteractor(); final exportAllAttachmentsInteractor = MockExportAllAttachmentsInteractor(); final getImageDataFromAttachmentInteractor = MockGetImageDataFromAttachmentInteractor(); + final getTextDataFromAttachmentInteractor = MockGetTextDataFromAttachmentInteractor(); late SingleEmailController singleEmailController; @@ -221,6 +224,7 @@ void main() { downloadAllAttachmentsForWebInteractor, exportAllAttachmentsInteractor, getImageDataFromAttachmentInteractor, + getTextDataFromAttachmentInteractor, ); });