From f66e1460e4ee0d405e961918593164a2d5109bc9 Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 7 Feb 2025 14:38:53 +0700 Subject: [PATCH] TF-3454 Download all attachments as ZIP --- assets/images/ic_download_all.svg | 3 + .../preview/supported_preview_file_types.dart | 1 + .../presentation/resources/image_paths.dart | 1 + ios/Podfile.lock | 6 + .../data/datasource/email_datasource.dart | 20 ++ .../email_datasource_impl.dart | 42 ++++ .../email_hive_cache_datasource_impl.dart | 19 ++ .../email_local_storage_datasource_impl.dart | 19 ++ ...email_session_storage_datasource_impl.dart | 19 ++ .../email/data/network/email_api.dart | 85 +++++++ .../repository/email_repository_impl.dart | 37 +++ .../domain/repository/email_repository.dart | 20 ++ ...ownload_all_attachments_for_web_state.dart | 51 ++++ .../state/export_all_attachments_state.dart | 18 ++ ...ad_all_attachments_for_web_interactor.dart | 83 +++++++ .../export_all_attachments_interactor.dart | 68 ++++++ .../presentation/bindings/email_bindings.dart | 14 ++ .../controller/single_email_controller.dart | 203 +++++++++++++++- .../email/presentation/email_view.dart | 2 + .../attachment/attachment_list_styles.dart | 4 +- .../styles/email_attachments_styles.dart | 7 +- ...attachment_list_action_button_builder.dart | 32 +-- ...chment_list_bottom_sheet_body_builder.dart | 11 +- .../attachment_list_bottom_sheet_builder.dart | 2 +- .../attachment_list_dialog_body_builder.dart | 11 +- .../widgets/attachments_info.dart | 99 ++++++++ .../widgets/email_attachments_widget.dart | 98 ++++---- .../domain/extensions/session_extensions.dart | 21 ++ .../download/download_task_item_widget.dart | 12 +- .../download_all/download_all_capability.dart | 20 ++ pubspec.lock | 10 +- pubspec.yaml | 4 + .../single_email_controller_test.dart | 9 + .../session/session_extensions_test.dart | 224 ++++++++++++++++++ 34 files changed, 1175 insertions(+), 100 deletions(-) create mode 100644 assets/images/ic_download_all.svg create mode 100644 lib/features/email/domain/state/download_all_attachments_for_web_state.dart create mode 100644 lib/features/email/domain/state/export_all_attachments_state.dart create mode 100644 lib/features/email/domain/usecases/download_all_attachments_for_web_interactor.dart create mode 100644 lib/features/email/domain/usecases/export_all_attachments_interactor.dart create mode 100644 lib/features/email/presentation/widgets/attachments_info.dart create mode 100644 model/lib/download_all/download_all_capability.dart diff --git a/assets/images/ic_download_all.svg b/assets/images/ic_download_all.svg new file mode 100644 index 000000000..c296423cc --- /dev/null +++ b/assets/images/ic_download_all.svg @@ -0,0 +1,3 @@ + + + diff --git a/core/lib/domain/preview/supported_preview_file_types.dart b/core/lib/domain/preview/supported_preview_file_types.dart index 99df0bdbf..ede994866 100644 --- a/core/lib/domain/preview/supported_preview_file_types.dart +++ b/core/lib/domain/preview/supported_preview_file_types.dart @@ -31,6 +31,7 @@ class SupportedPreviewFileTypes { 'application/vnd.ms-powerpoint']; static const zipMimeTypes = [ + 'application/zip', 'application/x-tar', 'application/x-gtar', 'application/x-gzip', diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 51206955e..4bf5cc9ac 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -222,6 +222,7 @@ class ImagePaths { String get icDeleteSelection => _getImagePath('ic_delete_selection.svg'); String get icLogoTwakeWelcome => _getImagePath('ic_logo_twake_welcome.svg'); String get icHelp => _getImagePath('ic_help.svg'); + String get icDownloadAll => _getImagePath('ic_download_all.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7e72429bd..8899824d3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -99,6 +99,8 @@ PODS: - UniversalDetector2 (= 2.0.1) - flutter_downloader (0.0.1): - Flutter + - flutter_file_dialog (0.0.1): + - Flutter - flutter_image_compress_common (1.0.0): - Flutter - Mantle @@ -221,6 +223,7 @@ DEPENDENCIES: - flutter_appauth (from `.symlinks/plugins/flutter_appauth/ios`) - flutter_charset_detector_darwin (from `.symlinks/plugins/flutter_charset_detector_darwin/darwin`) - flutter_downloader (from `.symlinks/plugins/flutter_downloader/ios`) + - flutter_file_dialog (from `.symlinks/plugins/flutter_file_dialog/ios`) - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) @@ -297,6 +300,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_charset_detector_darwin/darwin" flutter_downloader: :path: ".symlinks/plugins/flutter_downloader/ios" + flutter_file_dialog: + :path: ".symlinks/plugins/flutter_file_dialog/ios" flutter_image_compress_common: :path: ".symlinks/plugins/flutter_image_compress_common/ios" flutter_inappwebview_ios: @@ -363,6 +368,7 @@ SPEC CHECKSUMS: flutter_appauth: 1ce438877bc111c5d8f42da47729909290624886 flutter_charset_detector_darwin: fb3692d6d72cb6afcce7b0dd1a9516be6e78556e flutter_downloader: b7301ae057deadd4b1650dc7c05375f10ff12c39 + flutter_file_dialog: 4c014a45b105709a27391e266c277d7e588e9299 flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index 65d099dbe..d2150a269 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -201,4 +201,24 @@ abstract class EmailDataSource { Future storePreviewEMLContentToSessionStorage(EMLPreviewer emlPreviewer); Future getPreviewEMLContentInMemory(String keyStored); + + Future downloadAllAttachmentsForWeb( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + AccountRequest accountRequest, + DownloadTaskId taskId, + StreamController> onReceiveController, + {CancelToken? cancelToken} + ); + + Future exportAllAttachments( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + AccountRequest accountRequest, + {CancelToken? cancelToken} + ); } \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index 93b389ba5..804cd3d07 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -540,4 +540,46 @@ class EmailDataSourceImpl extends EmailDataSource { Future getPreviewEMLContentInMemory(String keyStored) { throw UnimplementedError(); } + + @override + Future downloadAllAttachmentsForWeb( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + AccountRequest accountRequest, + DownloadTaskId taskId, + StreamController> onReceiveController, + {CancelToken? cancelToken} + ) => Future.sync(() async { + return await emailAPI.downloadAllAttachmentsForWeb( + accountId, + emailId, + baseDownloadAllUrl, + outputFileName, + accountRequest, + taskId, + onReceiveController, + cancelToken: cancelToken, + ); + }).catchError(_exceptionThrower.throwException); + + @override + Future exportAllAttachments( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + AccountRequest accountRequest, { + CancelToken? cancelToken, + }) => Future.sync(() async { + return await emailAPI.exportAllAttachments( + accountId, + emailId, + baseDownloadAllUrl, + outputFileName, + accountRequest, + cancelToken: cancelToken, + ); + }).catchError(_exceptionThrower.throwException); } \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index f7cb296ea..eff48b11a 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -563,4 +563,23 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { Future getPreviewEMLContentInMemory(String keyStored) { throw UnimplementedError(); } + + @override + Future downloadAllAttachmentsForWeb( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + AccountRequest accountRequest, + DownloadTaskId taskId, + StreamController> onReceiveController, + {CancelToken? cancelToken} + ) { + throw UnimplementedError(); + } + + @override + Future exportAllAttachments(AccountId accountId, EmailId emailId, String baseDownloadAllUrl, String outputFileName, AccountRequest accountRequest, {CancelToken? cancelToken}) { + throw UnimplementedError(); + } } \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart index 6703d114f..e8d9d8a34 100644 --- a/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart @@ -254,4 +254,23 @@ class EmailLocalStorageDataSourceImpl extends EmailDataSource { Future markAsForwarded(Session session, AccountId accountId, List emailIds) { throw UnimplementedError(); } + + @override + Future downloadAllAttachmentsForWeb( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + AccountRequest accountRequest, + DownloadTaskId taskId, + StreamController> onReceiveController, + {CancelToken? cancelToken} + ) { + throw UnimplementedError(); + } + + @override + Future exportAllAttachments(AccountId accountId, EmailId emailId, String baseDownloadAllUrl, String outputFileName, AccountRequest accountRequest, {CancelToken? cancelToken}) { + throw UnimplementedError(); + } } \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart index 426968d11..2aebbc7ca 100644 --- a/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart @@ -252,4 +252,23 @@ class EmailSessionStorageDatasourceImpl extends EmailDataSource { Future markAsForwarded(Session session, AccountId accountId, List emailIds) { throw UnimplementedError(); } + + @override + Future downloadAllAttachmentsForWeb( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + AccountRequest accountRequest, + DownloadTaskId taskId, + StreamController> onReceiveController, + {CancelToken? cancelToken} + ) { + throw UnimplementedError(); + } + + @override + Future exportAllAttachments(AccountId accountId, EmailId emailId, String baseDownloadAllUrl, String outputFileName, AccountRequest accountRequest, {CancelToken? cancelToken}) { + throw UnimplementedError(); + } } \ No newline at end of file diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index d851fd5b2..ae2c40567 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -49,6 +49,7 @@ import 'package:model/download/download_task_id.dart'; import 'package:model/email/attachment.dart'; import 'package:model/email/mark_star_action.dart'; import 'package:model/email/read_actions.dart'; +import 'package:model/extensions/account_id_extensions.dart'; import 'package:model/extensions/email_extension.dart'; import 'package:model/extensions/email_id_extensions.dart'; import 'package:model/extensions/keyword_identifier_extension.dart'; @@ -66,11 +67,13 @@ import 'package:tmail_ui_user/features/email/domain/extensions/email_id_extensio import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/email/domain/model/restore_deleted_message_request.dart'; +import 'package:tmail_ui_user/features/email/domain/state/download_all_attachments_for_web_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; +import 'package:uri/uri.dart'; import 'package:uuid/uuid.dart'; class EmailAPI with HandleSetErrorMixin { @@ -402,6 +405,88 @@ class EmailAPI with HandleSetErrorMixin { return bytesDownloaded; } + Future downloadAllAttachmentsForWeb( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + AccountRequest accountRequest, + DownloadTaskId taskId, + StreamController> onReceiveController, + {CancelToken? cancelToken} + ) async { + final authentication = accountRequest.authenticationType == AuthenticationType.oidc + ? accountRequest.bearerToken + : accountRequest.basicAuth; + final headerParam = _dioClient.getHeaders(); + headerParam[HttpHeaders.authorizationHeader] = authentication; + headerParam[HttpHeaders.acceptHeader] = DioClient.jmapHeader; + + final downloadAllUriTemplate = UriTemplate(Uri.decodeFull(baseDownloadAllUrl)); + final downloadAllUrl = downloadAllUriTemplate.expand({ + 'accountId': accountId.asString, + 'emailId': emailId.asString, + 'name': outputFileName, + }); + final downloadFileName = '$outputFileName.zip'; + + final bytesDownloaded = await _dioClient.get( + downloadAllUrl, + options: Options( + headers: headerParam, + responseType: ResponseType.bytes), + onReceiveProgress: (downloaded, total) { + log('EmailAPI::downloadFileForWeb(): downloaded = $downloaded | total: $total'); + double progress = 0; + if (downloaded > 0 && total >= downloaded) { + progress = (downloaded / total) * 100; + } + log('EmailAPI::downloadFileForWeb(): progress = ${progress.round()}%'); + onReceiveController.add(Right(DownloadingAllAttachmentsForWeb( + taskId, + downloadFileName, + progress, + downloaded, + total > 0 ? total : 0, + ))); + }, + cancelToken: cancelToken, + ); + + _downloadManager.createAnchorElementDownloadFileWeb( + bytesDownloaded, + downloadFileName, + ); + } + + Future exportAllAttachments( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + AccountRequest accountRequest, + {CancelToken? cancelToken} + ) async { + final authentication = accountRequest.authenticationType == AuthenticationType.oidc + ? accountRequest.bearerToken + : accountRequest.basicAuth; + + final downloadAllUriTemplate = UriTemplate(Uri.decodeFull(baseDownloadAllUrl)); + final downloadAllUrl = downloadAllUriTemplate.expand({ + 'accountId': accountId.asString, + 'emailId': emailId.asString, + 'name': outputFileName, + }); + final downloadFileName = '$outputFileName.zip'; + + return _downloadManager.downloadFile( + downloadAllUrl, + getTemporaryDirectory(), + downloadFileName, + authentication, + cancelToken: cancelToken); + } + Future<({ List emailIdsSuccess, Map mapErrors, diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index 5cf29d8d7..b00fdaea5 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -530,4 +530,41 @@ class EmailRepositoryImpl extends EmailRepository { htmlContent, configuration); } + + @override + Future downloadAllAttachmentsForWeb( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + AccountRequest accountRequest, + DownloadTaskId taskId, + StreamController> onReceiveController, + {CancelToken? cancelToken} + ) => emailDataSource[DataSourceType.network]!.downloadAllAttachmentsForWeb( + accountId, + emailId, + baseDownloadAllUrl, + outputFileName, + accountRequest, + taskId, + onReceiveController, + cancelToken: cancelToken, + ); + + @override + Future exportAllAttachments( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + AccountRequest accountRequest, { + CancelToken? cancelToken, + }) => emailDataSource[DataSourceType.network]!.exportAllAttachments( + accountId, + emailId, + baseDownloadAllUrl, + outputFileName, + accountRequest, + ); } \ No newline at end of file diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index 64824ffc8..2496a5ac6 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -199,4 +199,24 @@ abstract class EmailRepository { Future sanitizeHtmlContent( String htmlContent, TransformConfiguration configuration); + + Future downloadAllAttachmentsForWeb( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + AccountRequest accountRequest, + DownloadTaskId taskId, + StreamController> onReceiveController, + {CancelToken? cancelToken} + ); + + Future exportAllAttachments( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + AccountRequest accountRequest, + {CancelToken? cancelToken} + ); } \ No newline at end of file diff --git a/lib/features/email/domain/state/download_all_attachments_for_web_state.dart b/lib/features/email/domain/state/download_all_attachments_for_web_state.dart new file mode 100644 index 000000000..7f63a1f01 --- /dev/null +++ b/lib/features/email/domain/state/download_all_attachments_for_web_state.dart @@ -0,0 +1,51 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/download/download_task_id.dart'; +import 'package:model/email/attachment.dart'; + +class StartDownloadAllAttachmentsForWeb extends UIState { + StartDownloadAllAttachmentsForWeb(this.taskId, this.attachment); + + final DownloadTaskId taskId; + final Attachment attachment; + + @override + List get props => [taskId, attachment]; +} + +class DownloadingAllAttachmentsForWeb extends LoadingState { + DownloadingAllAttachmentsForWeb( + this.taskId, + this.fileName, + this.progress, + this.downloaded, + this.total, + ); + + final DownloadTaskId taskId; + final String fileName; + final double progress; + final int downloaded; + final int total; + + @override + List get props => [taskId, fileName, progress, downloaded, total]; +} + +class DownloadAllAttachmentsForWebSuccess extends UIState { + DownloadAllAttachmentsForWebSuccess({required this.taskId}); + + final DownloadTaskId taskId; + + @override + List get props => [taskId]; +} + +class DownloadAllAttachmentsForWebFailure extends FeatureFailure { + DownloadAllAttachmentsForWebFailure({super.exception, required this.taskId}); + + final DownloadTaskId taskId; + + @override + List get props => [exception, taskId]; +} \ No newline at end of file diff --git a/lib/features/email/domain/state/export_all_attachments_state.dart b/lib/features/email/domain/state/export_all_attachments_state.dart new file mode 100644 index 000000000..6755c77b5 --- /dev/null +++ b/lib/features/email/domain/state/export_all_attachments_state.dart @@ -0,0 +1,18 @@ +import 'package:core/data/network/download/downloaded_response.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class ExportingAllAttachments extends LoadingState {} + +class ExportAllAttachmentsSuccess extends UIState { + ExportAllAttachmentsSuccess(this.downloadedResponse); + + final DownloadedResponse downloadedResponse; + + @override + List get props => [downloadedResponse]; +} + +class ExportAllAttachmentsFailure extends FeatureFailure { + ExportAllAttachmentsFailure({super.exception}); +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/download_all_attachments_for_web_interactor.dart b/lib/features/email/domain/usecases/download_all_attachments_for_web_interactor.dart new file mode 100644 index 000000000..eb928b99d --- /dev/null +++ b/lib/features/email/domain/usecases/download_all_attachments_for_web_interactor.dart @@ -0,0 +1,83 @@ +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:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/account/account_request.dart'; +import 'package:model/account/authentication_type.dart'; +import 'package:model/account/password.dart'; +import 'package:model/download/download_task_id.dart'; +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/state/download_all_attachments_for_web_state.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; + +class DownloadAllAttachmentsForWebInteractor { + const DownloadAllAttachmentsForWebInteractor( + this._emailRepository, + this._accountRepository, + this._authenticationOIDCRepository, + this.credentialRepository, + ); + + final EmailRepository _emailRepository; + final AccountRepository _accountRepository; + final AuthenticationOIDCRepository _authenticationOIDCRepository; + final CredentialRepository credentialRepository; + + Stream> execute( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + Attachment attachment, + DownloadTaskId taskId, + StreamController> onReceiveController, + {CancelToken? cancelToken} + ) async* { + try { + yield Right(StartDownloadAllAttachmentsForWeb( + taskId, + attachment, + )); + onReceiveController.add(Right(StartDownloadAllAttachmentsForWeb( + taskId, + attachment, + ))); + final currentAccount = await _accountRepository.getCurrentAccount(); + AccountRequest accountRequest; + if (currentAccount.authenticationType == AuthenticationType.oidc) { + final tokenOidc = await _authenticationOIDCRepository.getStoredTokenOIDC(currentAccount.id); + accountRequest = AccountRequest.withOidc(token: tokenOidc); + } else { + final authenticationInfoCache = await credentialRepository.getAuthenticationInfoStored(); + accountRequest = AccountRequest.withBasic( + userName: UserName(authenticationInfoCache.username), + password: Password(authenticationInfoCache.password), + ); + } + + await _emailRepository.downloadAllAttachmentsForWeb( + accountId, + emailId, + baseDownloadAllUrl, + attachment.name!, + accountRequest, + taskId, + onReceiveController, + cancelToken: cancelToken + ); + + yield Right(DownloadAllAttachmentsForWebSuccess(taskId: taskId)); + } catch (e) { + logError('DownloadAllAttachmentsForWebInteractor::execute():EXCEPTION: $e'); + yield Left(DownloadAllAttachmentsForWebFailure(exception: e, taskId: taskId)); + } + } +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/export_all_attachments_interactor.dart b/lib/features/email/domain/usecases/export_all_attachments_interactor.dart new file mode 100644 index 000000000..d0ee0ca8a --- /dev/null +++ b/lib/features/email/domain/usecases/export_all_attachments_interactor.dart @@ -0,0 +1,68 @@ +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:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/account/account_request.dart'; +import 'package:model/account/authentication_type.dart'; +import 'package:model/account/password.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/state/export_all_attachments_state.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; + +class ExportAllAttachmentsInteractor { + const ExportAllAttachmentsInteractor( + this._emailRepository, + this._accountRepository, + this._authenticationOIDCRepository, + this.credentialRepository, + ); + + final EmailRepository _emailRepository; + final AccountRepository _accountRepository; + final AuthenticationOIDCRepository _authenticationOIDCRepository; + final CredentialRepository credentialRepository; + + Stream> execute( + AccountId accountId, + EmailId emailId, + String baseDownloadAllUrl, + String outputFileName, + {CancelToken? cancelToken} + ) async* { + try { + yield Right(ExportingAllAttachments()); + final currentAccount = await _accountRepository.getCurrentAccount(); + AccountRequest accountRequest; + if (currentAccount.authenticationType == AuthenticationType.oidc) { + final tokenOidc = await _authenticationOIDCRepository.getStoredTokenOIDC(currentAccount.id); + accountRequest = AccountRequest.withOidc(token: tokenOidc); + } else { + final authenticationInfoCache = await credentialRepository.getAuthenticationInfoStored(); + accountRequest = AccountRequest.withBasic( + userName: UserName(authenticationInfoCache.username), + password: Password(authenticationInfoCache.password), + ); + } + + final result = await _emailRepository.exportAllAttachments( + accountId, + emailId, + baseDownloadAllUrl, + outputFileName, + accountRequest, + cancelToken: cancelToken, + ); + + yield Right(ExportAllAttachmentsSuccess(result)); + } catch (e) { + logError('ExportAllAttachmentsInteractor::execute():EXCEPTION: $e'); + yield Left(ExportAllAttachmentsFailure(exception: e)); + } + } +} \ 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 063398edb..d8d56de82 100644 --- a/lib/features/email/presentation/bindings/email_bindings.dart +++ b/lib/features/email/presentation/bindings/email_bindings.dart @@ -16,8 +16,10 @@ import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/email/data/repository/email_repository_impl.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.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/download_attachment_for_web_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachments_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/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_stored_email_state_interactor.dart'; @@ -80,6 +82,8 @@ class EmailBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), + Get.find(), )); } @@ -168,6 +172,16 @@ class EmailBindings extends BaseBindings { Get.lazyPut(() => PreviewEmailFromEmlFileInteractor(Get.find())); IdentityInteractorsBindings().dependencies(); Get.lazyPut(() => GetHtmlContentFromAttachmentInteractor(Get.find())); + Get.lazyPut(() => DownloadAllAttachmentsForWebInteractor( + Get.find(), + Get.find(), + Get.find(), + Get.find())); + Get.lazyPut(() => ExportAllAttachmentsInteractor( + Get.find(), + Get.find(), + Get.find(), + 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 5692a267f..0f06c92c6 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -13,9 +13,11 @@ import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_file_dialog/flutter_file_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:http_parser/http_parser.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; @@ -55,8 +57,10 @@ import 'package:tmail_ui_user/features/email/domain/state/calendar_event_accept_ import 'package:tmail_ui_user/features/email/domain/state/calendar_event_maybe_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/calendar_event_reject_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/calendar_event_reply_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/download_all_attachments_for_web_state.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/download_attachments_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/export_all_attachments_state.dart'; 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'; @@ -72,6 +76,8 @@ import 'package:tmail_ui_user/features/email/domain/state/send_receipt_to_sender import 'package:tmail_ui_user/features/email/domain/state/store_event_attendance_status_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/unsubscribe_email_state.dart'; 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/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'; @@ -109,6 +115,7 @@ import 'package:tmail_ui_user/features/email/presentation/widgets/html_attachmen import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/pdf_viewer.dart'; import 'package:tmail_ui_user/features/email_previewer/email_previewer_dialog_view.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; +import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; @@ -157,6 +164,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final ParseEmailByBlobIdInteractor _parseEmailByBlobIdInteractor; final PreviewEmailFromEmlFileInteractor _previewEmailFromEmlFileInteractor; final GetHtmlContentFromAttachmentInteractor _getHtmlContentFromAttachmentInteractor; + final DownloadAllAttachmentsForWebInteractor _downloadAllAttachmentsForWebInteractor; + final ExportAllAttachmentsInteractor _exportAllAttachmentsInteractor; CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; SendReceiptToSenderInteractor? _sendReceiptToSenderInteractor; @@ -213,6 +222,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { this._parseEmailByBlobIdInteractor, this._previewEmailFromEmlFileInteractor, this._getHtmlContentFromAttachmentInteractor, + this._downloadAllAttachmentsForWebInteractor, + this._exportAllAttachmentsInteractor, ); @override @@ -240,6 +251,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _handleMarkAsEmailReadCompleted(success.readActions); } else if (success is ExportAttachmentSuccess) { _exportAttachmentSuccessAction(success); + } else if (success is ExportAllAttachmentsSuccess) { + _exportAllAttachmentsSuccessAction(success); } else if (success is MoveToMailboxSuccess) { _moveToMailboxSuccess(success); } else if (success is MarkAsStarEmailSuccess) { @@ -250,6 +263,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _updateAttachmentsViewState(success.attachment.blobId, Right(success)); } else if (success is DownloadingAttachmentForWeb) { _updateAttachmentsViewState(success.attachment.blobId, Right(success)); + } else if (success is DownloadAllAttachmentsForWebSuccess) { + mailboxDashBoardController.deleteDownloadTask(success.taskId); } else if (success is GetAllIdentitiesSuccess) { _getAllIdentitiesSuccess(success); } else if (success is SendReceiptToSenderSuccess) { @@ -298,8 +313,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _downloadAttachmentsFailure(failure); } else if (failure is ExportAttachmentFailure) { _exportAttachmentFailureAction(failure); + } else if (failure is ExportAllAttachmentsFailure) { + _exportAllAttachmentsFailureAction(failure); } else if (failure is DownloadAttachmentForWebFailure) { _downloadAttachmentForWebFailureAction(failure); + } else if (failure is DownloadAllAttachmentsForWebFailure) { + _downloadAllAttachmentsForWebFailure(failure); } else if (failure is ParseCalendarEventFailure) { _handleParseCalendarEventFailure(failure); } else if (failure is GetEmailContentFailure) { @@ -445,15 +464,44 @@ class SingleEmailController extends BaseController with AppLoaderMixin { log('SingleEmailController::DownloadingAttachmentForWeb(): $percent%'); emailSupervisorController.mailboxDashBoardController.updateDownloadTask( - success.taskId, - (currentTask) { - final newTask = currentTask.copyWith( - progress: success.progress, - downloaded: success.downloaded, - total: success.total); + success.taskId, + (currentTask) { + final newTask = currentTask.copyWith( + progress: success.progress, + downloaded: success.downloaded, + total: success.total); - return newTask; - }); + return newTask; + }); + } else if (success is StartDownloadAllAttachmentsForWeb) { + emailSupervisorController.mailboxDashBoardController.addDownloadTask( + DownloadTaskState( + taskId: success.taskId, + attachment: success.attachment, + ), + ); + + if (currentOverlayContext != null && currentContext != null) { + appToast.showToastMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).your_download_has_started, + leadingSVGIconColor: AppColor.primaryColor, + leadingSVGIcon: imagePaths.icDownload); + } + } else if (success is DownloadingAllAttachmentsForWeb) { + final percent = success.progress.round(); + log('SingleEmailController::DownloadingAttachmentForWeb(): $percent%'); + + emailSupervisorController.mailboxDashBoardController.updateDownloadTask( + success.taskId, + (currentTask) { + final newTask = currentTask.copyWith( + progress: success.progress, + downloaded: success.downloaded, + total: success.total); + + return newTask; + }); } }); }); @@ -804,11 +852,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void exportAttachment(BuildContext context, Attachment attachment) { final cancelToken = CancelToken(); - _showDownloadingFileDialog(context, attachment, cancelToken: cancelToken); + _showDownloadingFileDialog(context, attachment.name ?? '', cancelToken: cancelToken); _exportAttachmentAction(attachment, cancelToken); } - void _showDownloadingFileDialog(BuildContext context, Attachment attachment, {CancelToken? cancelToken}) { + void _showDownloadingFileDialog(BuildContext context, String attachmentName, {CancelToken? cancelToken}) { if (cancelToken != null) { showCupertinoDialog( context: context, @@ -816,7 +864,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { PointerInterceptor(child: (DownloadingFileDialogBuilder() ..key(const Key('downloading_file_dialog')) ..title(AppLocalizations.of(context).preparing_to_export) - ..content(AppLocalizations.of(context).downloading_file(attachment.name ?? '')) + ..content(AppLocalizations.of(context).downloading_file(attachmentName)) ..actionText(AppLocalizations.of(context).cancel) ..addCancelDownloadActionClick(() { cancelToken.cancel([AppLocalizations.of(context).user_cancel_download_file]); @@ -830,7 +878,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { PointerInterceptor(child: (DownloadingFileDialogBuilder() ..key(const Key('downloading_file_for_web_dialog')) ..title(AppLocalizations.of(context).preparing_to_save) - ..content(AppLocalizations.of(context).downloading_file(attachment.name ?? ''))) + ..content(AppLocalizations.of(context).downloading_file(attachmentName))) .build())); } } @@ -854,6 +902,36 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } + void exportAllAttachments(BuildContext context, String outputFileName) { + final cancelToken = CancelToken(); + _showDownloadingFileDialog(context, outputFileName, cancelToken: cancelToken); + _exportAllAttachmentsAction(outputFileName, cancelToken); + } + + void _exportAllAttachmentsAction(String outputFileName, CancelToken cancelToken) { + if (accountId == null || session == null) { + consumeState(Stream.value(Left(ExportAllAttachmentsFailure( + exception: NotFoundSessionException(), + )))); + return; + } + + final downloadAllSupported = session!.isDownloadAllSupported(accountId); + if (!downloadAllSupported || _currentEmailId == null) { + consumeState(Stream.value(Left(ExportAllAttachmentsFailure()))); + return; + } + + final baseDownloadAllUrl = session!.getDownloadAllCapability(accountId)!.endpoint!; + consumeState(_exportAllAttachmentsInteractor.execute( + accountId!, + _currentEmailId!, + baseDownloadAllUrl, + outputFileName, + cancelToken: cancelToken, + )); + } + void _exportAttachmentFailureAction(ExportAttachmentFailure failure) { if (failure.exception is! CancelDownloadFileException) { popBack(); @@ -871,6 +949,23 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _openDownloadedPreviewWorkGroupDocument(success.downloadedResponse); } + void _exportAllAttachmentsFailureAction(ExportAllAttachmentsFailure failure) { + if (failure.exception is! CancelDownloadFileException) { + popBack(); + + if (currentOverlayContext != null && currentContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).attachment_download_failed); + } + } + } + + void _exportAllAttachmentsSuccessAction(ExportAllAttachmentsSuccess success) { + popBack(); + _saveDownloadedZipAttachments(success.downloadedResponse.filePath); + } + void _openDownloadedPreviewWorkGroupDocument(DownloadedResponse downloadedResponse) async { log('SingleEmailController::_openDownloadedPreviewWorkGroupDocument(): $downloadedResponse'); if (downloadedResponse.mediaType == null) { @@ -892,6 +987,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } + Future _saveDownloadedZipAttachments(String filePath) async { + final params = SaveFileDialogParams(sourceFilePath: filePath); + await FlutterFileDialog.saveFile(params: params); + } + void downloadAttachmentForWeb(Attachment attachment) { if (accountId != null && session != null) { final generateTaskId = DownloadTaskId(uuid.v4()); @@ -921,6 +1021,65 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } + void downloadAllAttachmentsForWeb(String outputFileName) { + final taskId = DownloadTaskId(uuid.v4()); + final session = this.session; + final accountId = this.accountId; + + if (accountId == null || session == null) { + consumeState(Stream.value(Left(DownloadAllAttachmentsForWebFailure( + exception: NotFoundSessionException(), + taskId: taskId, + )))); + return; + } + + final downloadAllSupported = session.isDownloadAllSupported(accountId); + final emailId = _currentEmailId; + + if (!downloadAllSupported || emailId == null) { + consumeState(Stream.value(Left(DownloadAllAttachmentsForWebFailure( + taskId: taskId, + )))); + return; + } + + final baseDownloadAllUrl = session.getDownloadAllCapability(accountId)!.endpoint!; + final downloadAttachment = Attachment( + name: outputFileName, + type: MediaType('application', 'zip'), + ); + final cancelToken = CancelToken(); + consumeState(_downloadAllAttachmentsForWebInteractor.execute( + accountId, + emailId, + baseDownloadAllUrl, + downloadAttachment, + taskId, + _downloadProgressStateController, + cancelToken: cancelToken, + )); + } + + bool isDownloadAllSupported() { + return session?.isDownloadAllSupported(accountId) ?? false; + } + + bool downloadAllButtonIsEnabled() { + return isDownloadAllSupported() && attachments.length > 1; + } + + void _downloadAllAttachmentsForWebFailure( + DownloadAllAttachmentsForWebFailure failure, + ) { + mailboxDashBoardController.deleteDownloadTask(failure.taskId); + if (currentOverlayContext != null && currentContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).attachment_download_failed); + } + } + void _downloadAttachmentForWebSuccessAction(DownloadAttachmentForWebSuccess success) { log('SingleEmailController::_downloadAttachmentForWebSuccessAction():'); @@ -1675,6 +1834,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { ..onCloseButtonAction(() => popBack()) ..onDownloadAttachmentFileAction((attachment) => handleDownloadAttachmentAction(context, attachment)) ..onViewAttachmentFileAction((attachment) => handleViewAttachmentAction(context, attachment)) + ..onDownloadAllButtonAction(isDownloadAllSupported() + ? () => downloadAllAttachmentsForWeb('TwakeMail-${DateTime.now()}') + : null + ) ).show(); } else { Get.dialog( @@ -1688,6 +1851,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin { onCloseButtonAction: () => popBack(), onDownloadAttachmentFileAction: (attachment) => handleDownloadAttachmentAction(context, attachment), onViewAttachmentFileAction: (attachment) => handleViewAttachmentAction(context, attachment), + onDownloadAllButtonAction: isDownloadAllSupported() + ? () => downloadAllAttachmentsForWeb('TwakeMail-${DateTime.now()}') + : null, ) ), barrierColor: AppColor.colorDefaultCupertinoActionSheet, @@ -1995,6 +2161,19 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } + void handleDownloadAllAttachmentsAction( + BuildContext context, + String outputFileName, + ) { + if (PlatformInfo.isWeb) { + downloadAllAttachmentsForWeb(outputFileName); + } else if (PlatformInfo.isMobile) { + exportAllAttachments(context, outputFileName); + } else { + log('EmailView::handleDownloadAllAttachmentsAction: THE PLATFORM IS SUPPORTED'); + } + } + void handleViewAttachmentAction(BuildContext context, Attachment attachment) { if (PlatformInfo.isWeb && PlatformInfo.isCanvasKit && attachment.isPDFFile) { previewPDFFileAction(context, attachment); diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 7ca9e793f..5b42cae74 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -339,6 +339,8 @@ class EmailView extends GetWidget { downloadAttachmentAction: (attachment) => controller.handleDownloadAttachmentAction(context, attachment), viewAttachmentAction: (attachment) => controller.handleViewAttachmentAction(context, attachment), onTapShowAllAttachmentFile: () => controller.openAttachmentList(context, controller.attachments), + showDownloadAllAttachmentsButton: controller.downloadAllButtonIsEnabled(), + onTapDownloadAllButton: () => controller.handleDownloadAllAttachmentsAction(context, 'TwakeMail-${DateTime.now()}'), ); } else { return const SizedBox.shrink(); diff --git a/lib/features/email/presentation/styles/attachment/attachment_list_styles.dart b/lib/features/email/presentation/styles/attachment/attachment_list_styles.dart index 754740e08..f435dd2a0 100644 --- a/lib/features/email/presentation/styles/attachment/attachment_list_styles.dart +++ b/lib/features/email/presentation/styles/attachment/attachment_list_styles.dart @@ -42,9 +42,9 @@ class AttachmentListStyles { static const Color scrollbarThumbColor = AppColor.colorScrollbarThumbColor; static const Color scrollbarTrackBorderColor = Colors.transparent; static const Color separatorColor = AppColor.colorAttachmentBorder; - static const Color downloadAllButtonColor = AppColor.primaryColor; + static const Color downloadAllButtonColor = Colors.transparent; static const Color cancelButtonColor = Colors.transparent; - static const Color downloadAllButtonTextColor = Colors.white; + static const Color downloadAllButtonTextColor = AppColor.textButtonColor; static const Color cancelButtonTextColor = AppColor.primaryColor; static const Color cancelButtonBorderColor = AppColor.colorButtonBorder; static const Color barrierColor = AppColor.colorDefaultCupertinoActionSheet; diff --git a/lib/features/email/presentation/styles/email_attachments_styles.dart b/lib/features/email/presentation/styles/email_attachments_styles.dart index fa8b2f6e4..843b38ac5 100644 --- a/lib/features/email/presentation/styles/email_attachments_styles.dart +++ b/lib/features/email/presentation/styles/email_attachments_styles.dart @@ -5,16 +5,16 @@ import 'package:flutter/material.dart'; class EmailAttachmentsStyles { static const double headerTextSize = 15; static const double headerIconSize = 20; - static const double headerSpace = 4; + static const double headerSpace = 8; static const double marginHeader = 6; - static const double buttonTextSize = 13; + static const double buttonTextSize = 16; static const double buttonMoreAttachmentsTextSize = 14; static const double buttonBorderRadius = 8; static const double listSpace = 8; static const Color headerTextColor = AppColor.colorTitleHeaderAttachment; static const Color headerIconColor = AppColor.colorAttachmentIcon; - static const Color buttonTextColor = AppColor.primaryColor; + static const Color buttonTextColor = AppColor.colorTextBody; static const Color buttonMoreAttachmentsTextColor = AppColor.colorLabelMoreAttachmentsButton; static const FontWeight headerFontWeight = FontWeight.w400; @@ -24,4 +24,5 @@ class EmailAttachmentsStyles { static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.only(start: 16, end: 16, bottom: 12); static const EdgeInsetsGeometry buttonPadding = EdgeInsets.symmetric(vertical: 8, horizontal: 12); static const EdgeInsetsGeometry moreButtonMargin = EdgeInsetsDirectional.only(bottom: 2, start: 8); + static const attachmentsInfoPadding = EdgeInsetsDirectional.only(end: 8); } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/attachment_list/attachment_list_action_button_builder.dart b/lib/features/email/presentation/widgets/attachment_list/attachment_list_action_button_builder.dart index 04363389d..4241816b7 100644 --- a/lib/features/email/presentation/widgets/attachment_list/attachment_list_action_button_builder.dart +++ b/lib/features/email/presentation/widgets/attachment_list/attachment_list_action_button_builder.dart @@ -1,11 +1,10 @@ +import 'package:core/presentation/views/button/icon_button_web.dart'; import 'package:flutter/material.dart'; -import 'package:tmail_ui_user/features/email/presentation/styles/attachment/attachment_list_styles.dart'; class AttachmentListActionButtonBuilder extends StatelessWidget { final String? name; final TextStyle? textStyle; final Color? bgColor; - final Color? borderColor; final Function? action; const AttachmentListActionButtonBuilder({ @@ -13,32 +12,19 @@ class AttachmentListActionButtonBuilder extends StatelessWidget { this.name, this.textStyle, this.bgColor, - this.borderColor, this.action }); @override Widget build(BuildContext context) { - return InkWell( - onTap: () => action?.call(), - child: Container( - padding: AttachmentListStyles.buttonsPadding, - decoration: BoxDecoration( - color: bgColor, - borderRadius: AttachmentListStyles.buttonRadius, - border: Border.all( - width: borderColor != null ? AttachmentListStyles.buttonBorderWidth : 0, - color: borderColor ?? Colors.transparent - ) - ), - child: Center( - child: Text( - name ?? '', - textAlign: TextAlign.center, - style: textStyle - ), - ), - ), + return buildButtonWrapText( + name ?? '', + radius: 10, + height: 44, + textStyle: textStyle, + bgColor: bgColor, + onTap: () => action?.call(), + padding: const EdgeInsets.symmetric(horizontal: 16), ); } } diff --git a/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_body_builder.dart b/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_body_builder.dart index 6d55c26ff..b545e7ca3 100644 --- a/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_body_builder.dart +++ b/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_body_builder.dart @@ -1,3 +1,4 @@ +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:flutter/material.dart'; @@ -118,13 +119,16 @@ class AttachmentListBottomSheetBodyBuilder extends StatelessWidget { Padding( padding: AttachmentListStyles.actionButtonsRowPadding, child: Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, children: [ if (onDownloadAllButtonAction != null) AttachmentListActionButtonBuilder( name: AppLocalizations.of(context).downloadAll, - bgColor: AttachmentListStyles.downloadAllButtonColor, - textStyle: AttachmentListStyles.downloadAllButtonTextStyle + bgColor: AppColor.primaryColor, + textStyle: AttachmentListStyles.downloadAllButtonTextStyle.copyWith( + color: Colors.white, + ), + action: onDownloadAllButtonAction, ), if (onDownloadAllButtonAction != null && onCancelButtonAction != null) const SizedBox(width: AttachmentListStyles.buttonsSpaceBetween), @@ -132,7 +136,6 @@ class AttachmentListBottomSheetBodyBuilder extends StatelessWidget { AttachmentListActionButtonBuilder( name: AppLocalizations.of(context).close, bgColor: AttachmentListStyles.cancelButtonColor, - borderColor: AttachmentListStyles.cancelButtonBorderColor, textStyle: AttachmentListStyles.cancelButtonTextStyle ) ], diff --git a/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart b/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart index 89779313f..2720f6f5b 100644 --- a/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart +++ b/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart @@ -33,7 +33,7 @@ class AttachmentListBottomSheetBuilder { _statusBarHeight = Get.statusBarHeight / MediaQuery.of(_context).devicePixelRatio; } - void onDownloadAllButtonAction(OnDownloadAllButtonAction onDownloadAllButtonAction) { + void onDownloadAllButtonAction(OnDownloadAllButtonAction? onDownloadAllButtonAction) { _onDownloadAllButtonAction = onDownloadAllButtonAction; } diff --git a/lib/features/email/presentation/widgets/attachment_list/attachment_list_dialog_body_builder.dart b/lib/features/email/presentation/widgets/attachment_list/attachment_list_dialog_body_builder.dart index db7573b44..505baa831 100644 --- a/lib/features/email/presentation/widgets/attachment_list/attachment_list_dialog_body_builder.dart +++ b/lib/features/email/presentation/widgets/attachment_list/attachment_list_dialog_body_builder.dart @@ -1,3 +1,4 @@ +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:flutter/material.dart'; @@ -124,13 +125,16 @@ class AttachmentListDialogBodyBuilder extends StatelessWidget { ), ), Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, children: [ if (onDownloadAllButtonAction != null) AttachmentListActionButtonBuilder( name: AppLocalizations.of(context).downloadAll, - bgColor: AttachmentListStyles.downloadAllButtonColor, - textStyle: AttachmentListStyles.downloadAllButtonTextStyle + bgColor: AppColor.primaryColor, + textStyle: AttachmentListStyles.downloadAllButtonTextStyle.copyWith( + color: Colors.white, + ), + action: onDownloadAllButtonAction, ), if (onDownloadAllButtonAction != null && onCancelButtonAction != null) const SizedBox(width: AttachmentListStyles.buttonsSpaceBetween), @@ -138,7 +142,6 @@ class AttachmentListDialogBodyBuilder extends StatelessWidget { AttachmentListActionButtonBuilder( name: AppLocalizations.of(context).close, bgColor: AttachmentListStyles.cancelButtonColor, - borderColor: AttachmentListStyles.cancelButtonBorderColor, textStyle: AttachmentListStyles.cancelButtonTextStyle ) ], diff --git a/lib/features/email/presentation/widgets/attachments_info.dart b/lib/features/email/presentation/widgets/attachments_info.dart new file mode 100644 index 000000000..91f326cfb --- /dev/null +++ b/lib/features/email/presentation/widgets/attachments_info.dart @@ -0,0 +1,99 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/email_attachments_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class AttachmentsInfo extends StatelessWidget { + const AttachmentsInfo({ + super.key, + required this.imagePaths, + required this.numberOfAttachments, + required this.totalSizeInfo, + required this.responsiveUtils, + this.onTapShowAllAttachmentFile, + this.downloadAllEnabled = false, + this.onTapDownloadAllButton, + }); + + final ImagePaths imagePaths; + final int numberOfAttachments; + final String totalSizeInfo; + final ResponsiveUtils responsiveUtils; + final VoidCallback? onTapShowAllAttachmentFile; + final bool downloadAllEnabled; + final VoidCallback? onTapDownloadAllButton; + + @override + Widget build(BuildContext context) { + List attachmentsAmountAndSize = [ + SvgPicture.asset( + imagePaths.icAttachment, + width: EmailAttachmentsStyles.headerIconSize, + height: EmailAttachmentsStyles.headerIconSize, + colorFilter: EmailAttachmentsStyles.headerIconColor.asFilter(), + fit: BoxFit.fill + ), + const SizedBox(width: EmailAttachmentsStyles.headerSpace), + Text( + AppLocalizations.of(context).titleHeaderAttachment( + numberOfAttachments, + totalSizeInfo, + ), + style: const TextStyle( + fontSize: EmailAttachmentsStyles.headerTextSize, + fontWeight: EmailAttachmentsStyles.headerFontWeight, + color: EmailAttachmentsStyles.headerTextColor + ) + ), + ]; + + final showAllAttachments = (numberOfAttachments > 2) + ? TMailButtonWidget( + text: AppLocalizations.of(context).showAll, + backgroundColor: Colors.transparent, + borderRadius: EmailAttachmentsStyles.buttonBorderRadius, + padding: EmailAttachmentsStyles.buttonPadding, + textStyle: const TextStyle( + fontSize: EmailAttachmentsStyles.buttonTextSize, + color: EmailAttachmentsStyles.buttonTextColor, + fontWeight: EmailAttachmentsStyles.buttonFontWeight + ), + onTapActionCallback: onTapShowAllAttachmentFile, + ) + : const SizedBox.shrink(); + + List downloadAllAttachments = downloadAllEnabled && !responsiveUtils.isMobile(context) + ? [ + const Spacer(), + TMailButtonWidget( + text: AppLocalizations.of(context).downloadAll, + icon: imagePaths.icDownloadAll, + iconAlignment: TextDirection.rtl, + backgroundColor: Colors.transparent, + borderRadius: EmailAttachmentsStyles.buttonBorderRadius, + padding: EmailAttachmentsStyles.buttonPadding, + textStyle: const TextStyle( + fontSize: EmailAttachmentsStyles.buttonTextSize, + color: EmailAttachmentsStyles.buttonTextColor, + fontWeight: EmailAttachmentsStyles.buttonFontWeight + ), + onTapActionCallback: onTapDownloadAllButton, + ), + ] + : const []; + + return Row( + children: [ + ...attachmentsAmountAndSize, + if (!PlatformInfo.isWeb) const Spacer(), + showAllAttachments, + ...downloadAllAttachments, + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/email_attachments_widget.dart b/lib/features/email/presentation/widgets/email_attachments_widget.dart index 06e6566ae..f6868729a 100644 --- a/lib/features/email/presentation/widgets/email_attachments_widget.dart +++ b/lib/features/email/presentation/widgets/email_attachments_widget.dart @@ -1,16 +1,15 @@ import 'package:core/presentation/action/action_callback_define.dart'; -import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/utils/platform_info.dart'; import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:model/email/attachment.dart'; import 'package:model/extensions/list_attachment_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/styles/email_attachments_styles.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_item_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/attachments_info.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/draggable_attachment_item_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -24,6 +23,8 @@ class EmailAttachmentsWidget extends StatelessWidget { final ResponsiveUtils responsiveUtils; final ImagePaths imagePaths; final OnTapActionCallback? onTapShowAllAttachmentFile; + final bool showDownloadAllAttachmentsButton; + final OnTapActionCallback? onTapDownloadAllButton; const EmailAttachmentsWidget({ super.key, @@ -35,6 +36,8 @@ class EmailAttachmentsWidget extends StatelessWidget { this.downloadAttachmentAction, this.viewAttachmentAction, this.onTapShowAllAttachmentFile, + this.showDownloadAllAttachmentsButton = false, + this.onTapDownloadAllButton, }); @override @@ -46,51 +49,14 @@ class EmailAttachmentsWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: Row( - children: [ - const SizedBox(width: EmailAttachmentsStyles.headerSpace), - SvgPicture.asset( - imagePaths.icAttachment, - width: EmailAttachmentsStyles.headerIconSize, - height: EmailAttachmentsStyles.headerIconSize, - colorFilter: EmailAttachmentsStyles.headerIconColor.asFilter(), - fit: BoxFit.fill - ), - const SizedBox(width: EmailAttachmentsStyles.headerSpace), - Expanded( - child: Text( - AppLocalizations.of(context).titleHeaderAttachment( - attachments.length, - filesize(attachments.totalSize, 1) - ), - style: const TextStyle( - fontSize: EmailAttachmentsStyles.headerTextSize, - fontWeight: EmailAttachmentsStyles.headerFontWeight, - color: EmailAttachmentsStyles.headerTextColor - ) - ) - ), - const SizedBox(width: EmailAttachmentsStyles.headerSpace), - ] - ) - ), - if (attachments.length > 2) - TMailButtonWidget( - text: AppLocalizations.of(context).showAll, - backgroundColor: Colors.transparent, - borderRadius: EmailAttachmentsStyles.buttonBorderRadius, - padding: EmailAttachmentsStyles.buttonPadding, - textStyle: const TextStyle( - fontSize: EmailAttachmentsStyles.buttonTextSize, - color: EmailAttachmentsStyles.buttonTextColor, - fontWeight: EmailAttachmentsStyles.buttonFontWeight - ), - onTapActionCallback: onTapShowAllAttachmentFile, - ) - ], + AttachmentsInfo( + imagePaths: imagePaths, + numberOfAttachments: attachments.length, + totalSizeInfo: filesize(attachments.totalSize, 1), + responsiveUtils: responsiveUtils, + onTapShowAllAttachmentFile: onTapShowAllAttachmentFile, + downloadAllEnabled: showDownloadAllAttachmentsButton, + onTapDownloadAllButton: onTapDownloadAllButton, ), const SizedBox(height: EmailAttachmentsStyles.marginHeader), Row( @@ -121,7 +87,7 @@ class EmailAttachmentsWidget extends StatelessWidget { }).toList(), ), ), - if (hideItemsCount > 0) + if (hideItemsCount > 0 && !responsiveUtils.isMobile(context)) TMailButtonWidget( text: AppLocalizations.of(context).moreAttachments(hideItemsCount), backgroundColor: Colors.transparent, @@ -137,6 +103,42 @@ class EmailAttachmentsWidget extends StatelessWidget { ), ] ), + const SizedBox(height: EmailAttachmentsStyles.marginHeader), + if (responsiveUtils.isMobile(context)) + Row( + children: [ + if (hideItemsCount > 0) + TMailButtonWidget( + text: AppLocalizations.of(context).moreAttachments(hideItemsCount), + backgroundColor: Colors.transparent, + borderRadius: EmailAttachmentsStyles.buttonBorderRadius, + padding: EdgeInsets.zero, + margin: EmailAttachmentsStyles.moreButtonMargin, + textStyle: const TextStyle( + fontSize: EmailAttachmentsStyles.buttonMoreAttachmentsTextSize, + color: EmailAttachmentsStyles.buttonMoreAttachmentsTextColor, + fontWeight: EmailAttachmentsStyles.buttonMoreAttachmentsFontWeight, + ), + onTapActionCallback: onTapShowAllAttachmentFile, + ), + const Spacer(), + if (showDownloadAllAttachmentsButton) + TMailButtonWidget( + text: AppLocalizations.of(context).downloadAll, + icon: imagePaths.icDownloadAll, + iconAlignment: TextDirection.rtl, + backgroundColor: Colors.transparent, + borderRadius: EmailAttachmentsStyles.buttonBorderRadius, + padding: EmailAttachmentsStyles.buttonPadding, + textStyle: const TextStyle( + fontSize: EmailAttachmentsStyles.buttonTextSize, + color: EmailAttachmentsStyles.buttonTextColor, + fontWeight: EmailAttachmentsStyles.buttonFontWeight + ), + onTapActionCallback: onTapDownloadAllButton, + ), + ] + ), ], ), ); diff --git a/lib/features/home/domain/extensions/session_extensions.dart b/lib/features/home/domain/extensions/session_extensions.dart index 274132899..9d5feac11 100644 --- a/lib/features/home/domain/extensions/session_extensions.dart +++ b/lib/features/home/domain/extensions/session_extensions.dart @@ -12,18 +12,22 @@ import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:model/download_all/download_all_capability.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/home/data/model/session_hive_obj.dart'; import 'package:tmail_ui_user/features/home/domain/converter/session_account_converter.dart'; import 'package:tmail_ui_user/features/home/domain/converter/session_capabilities_converter.dart'; import 'package:tmail_ui_user/features/home/domain/converter/session_primary_account_converter.dart'; +import 'package:tmail_ui_user/main/error/capability_validator.dart'; extension SessionExtensions on Session { static final CapabilityIdentifier linagoraContactSupportCapability = CapabilityIdentifier(Uri.parse('com:linagora:params:jmap:contact:support')); + static final CapabilityIdentifier linagoraDownloadAllCapability = CapabilityIdentifier(Uri.parse('com:linagora:params:downloadAll')); static final Map)> customMapCapabilitiesConverter = { linagoraContactSupportCapability: ContactSupportCapability.deserialize, tmailContactCapabilityIdentifier: AutocompleteCapability.deserialize, + linagoraDownloadAllCapability: DownloadAllCapability.deserialize, }; Map toJson() { @@ -94,4 +98,21 @@ extension SessionExtensions on Session { return null; } } + + bool isDownloadAllSupported(AccountId? accountId) { + if (accountId == null) return false; + final isSupported = linagoraDownloadAllCapability.isSupported(this, accountId); + if (!isSupported) return false; + + final downloadAllCapability = getDownloadAllCapability(accountId); + return downloadAllCapability?.endpoint?.isNotEmpty ?? false; + } + + DownloadAllCapability? getDownloadAllCapability(AccountId? accountId) { + if (accountId == null) return null; + return getCapabilityProperties( + accountId, + linagoraDownloadAllCapability, + ); + } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart index 4bbc4571f..7dd8d0895 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart @@ -76,8 +76,8 @@ class DownloadTaskItemWidget extends StatelessWidget { DefaultTextStyle( style: const TextStyle(color: Colors.white, fontSize: 12), child: Text( - '${ByteConverter(taskState.downloaded.toDouble()).toHumanReadable(SizeUnit.MB)}/' - '${ByteConverter(taskState.total.toDouble()).toHumanReadable(SizeUnit.MB)}', + '${ByteConverter(taskState.downloaded.toDouble()).toHumanReadable(SizeUnit.MB)}' + '${_getTotalSizeText()}', softWrap: CommonTextStyle.defaultSoftWrap, overflow: CommonTextStyle.defaultTextOverFlow, maxLines: 1, @@ -96,4 +96,12 @@ class DownloadTaskItemWidget extends StatelessWidget { ), ); } + + String _getTotalSizeText() { + if (taskState.total == 0) { + return ''; + } else { + return '/${ByteConverter(taskState.total.toDouble()).toHumanReadable(SizeUnit.MB)}'; + } + } } \ No newline at end of file diff --git a/model/lib/download_all/download_all_capability.dart b/model/lib/download_all/download_all_capability.dart new file mode 100644 index 000000000..fd7f85668 --- /dev/null +++ b/model/lib/download_all/download_all_capability.dart @@ -0,0 +1,20 @@ +import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'download_all_capability.g.dart'; + +@JsonSerializable(includeIfNull: false) +class DownloadAllCapability extends CapabilityProperties { + DownloadAllCapability({this.endpoint}); + + final String? endpoint; + + factory DownloadAllCapability.fromJson(Map json) => _$DownloadAllCapabilityFromJson(json); + + Map toJson() => _$DownloadAllCapabilityToJson(this); + + factory DownloadAllCapability.deserialize(Map json) => _$DownloadAllCapabilityFromJson(json); + + @override + List get props => [endpoint]; +} diff --git a/pubspec.lock b/pubspec.lock index 8913a594b..14b812273 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -775,6 +775,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" + flutter_file_dialog: + dependency: "direct main" + description: + name: flutter_file_dialog + sha256: "9344b8f07be6a1b6f9854b723fb0cf84a8094ba94761af1d213589d3cb087488" + url: "https://pub.dev" + source: hosted + version: "3.0.2" flutter_image_compress: dependency: "direct main" description: @@ -2095,7 +2103,7 @@ packages: source: hosted version: "2.2.2" uri: - dependency: transitive + dependency: "direct main" description: name: uri sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" diff --git a/pubspec.yaml b/pubspec.yaml index af295da46..affb5ab95 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -257,6 +257,10 @@ dependencies: win32: 5.5.1 + uri: 1.0.0 + + flutter_file_dialog: 3.0.2 + dev_dependencies: flutter_test: sdk: flutter 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 5509d2ac1..aaa10a95e 100644 --- a/test/features/email/presentation/controller/single_email_controller_test.dart +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -28,6 +28,8 @@ import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; import 'package:tmail_ui_user/features/email/domain/state/calendar_event_accept_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/parse_calendar_event_state.dart'; 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_html_content_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'; @@ -110,6 +112,8 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), + MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -149,6 +153,9 @@ void main() { final getHtmlContentFromAttachmentInteractor = MockGetHtmlContentFromAttachmentInteractor(); final mockTwakeAppManager = MockTwakeAppManager(); + final downloadAllAttachmentsForWebInteractor = MockDownloadAllAttachmentsForWebInteractor(); + final exportAllAttachmentsInteractor = MockExportAllAttachmentsInteractor(); + late SingleEmailController singleEmailController; final testAccountId = AccountId(Id('123')); @@ -206,6 +213,8 @@ void main() { parseEmailByBlobIdInteractor, previewEmailFromEmlFileInteractor, getHtmlContentFromAttachmentInteractor, + downloadAllAttachmentsForWebInteractor, + exportAllAttachmentsInteractor, ); }); diff --git a/test/features/session/session_extensions_test.dart b/test/features/session/session_extensions_test.dart index abacfc00c..5abd7e3d9 100644 --- a/test/features/session/session_extensions_test.dart +++ b/test/features/session/session_extensions_test.dart @@ -6,6 +6,7 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/download_all/download_all_capability.dart'; import 'package:model/support/contact_support_capability.dart'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; @@ -202,4 +203,227 @@ void main() { expect(result, isNull); }); }); + + group('isDownloadAllSupported::test', () { + test('SHOULD return false WHEN AccountId is null', () { + // Arrange + final session = Session( + {}, + {}, + {}, + UserName(''), + Uri(), + Uri(), + Uri(), + Uri(), + State(''), + ); + + // Act + final result = session.isDownloadAllSupported(null); + + // Assert + expect(result, isFalse); + }); + + test('SHOULD return false WHEN isSupported is false', () { + // Arrange + final session = Session( + {}, + {}, + {}, + UserName(''), + Uri(), + Uri(), + Uri(), + Uri(), + State(''), + ); + + // Act + final result = session.isDownloadAllSupported(AccountFixtures.aliceAccountId); + + // Assert + expect(result, isFalse); + }); + + test('SHOULD return false WHEN isSupported is true but endpoint is null', () { + // Arrange + final session = Session( + {}, + { + AccountFixtures.aliceAccountId: Account( + AccountName('Alice'), + true, + false, + { + SessionExtensions.linagoraDownloadAllCapability: DownloadAllCapability() + }, + ) + }, + {}, + UserName(''), + Uri(), + Uri(), + Uri(), + Uri(), + State(''), + ); + + // Act + final result = session.isDownloadAllSupported(AccountFixtures.aliceAccountId); + + // Assert + expect(result, isFalse); + }); + + test('SHOULD return false WHEN isSupported is true but endpoint is empty', () { + // Arrange + final session = Session( + {}, + { + AccountFixtures.aliceAccountId: Account( + AccountName('Alice'), + true, + false, + { + SessionExtensions.linagoraDownloadAllCapability: DownloadAllCapability(endpoint: '') + }, + ) + }, + {}, + UserName(''), + Uri(), + Uri(), + Uri(), + Uri(), + State(''), + ); + + // Act + final result = session.isDownloadAllSupported(AccountFixtures.aliceAccountId); + + // Assert + expect(result, isFalse); + }); + + test('SHOULD return true WHEN isSupported is true and endpoint is not empty', () { + // Arrange + final session = Session( + {}, + { + AccountFixtures.aliceAccountId: Account( + AccountName('Alice'), + true, + false, + { + SessionExtensions.linagoraDownloadAllCapability: DownloadAllCapability( + endpoint: 'https://example.com') + }, + ) + }, + {}, + UserName(''), + Uri(), + Uri(), + Uri(), + Uri(), + State(''), + ); + + // Act + final result = session.isDownloadAllSupported(AccountFixtures.aliceAccountId); + + // Assert + expect(result, isTrue); + }); + }); + + group('getDownloadAllCapability::test', () { + test('SHOULD return DownloadAllCapability WHEN DownloadAllCapability is available', () { + // Arrange + final session = Session( + {}, + { + AccountFixtures.aliceAccountId: Account( + AccountName('Alice'), + true, + false, + { + SessionExtensions.linagoraDownloadAllCapability: DownloadAllCapability() + }, + ) + }, + {}, + UserName(''), + Uri(), + Uri(), + Uri(), + Uri(), + State(''), + ); + + // Act + final result = session.getDownloadAllCapability(AccountFixtures.aliceAccountId); + + // Assert + expect(result, isNotNull); + }); + test('SHOULD return null WHEN DownloadAllCapability is not available', () { + // Arrange + final session = Session( + {}, + { + AccountFixtures.aliceAccountId: Account( + AccountName('Alice'), + true, + false, + { + SessionExtensions.linagoraDownloadAllCapability: EmptyCapability() + }, + ) + }, + {}, + UserName(''), + Uri(), + Uri(), + Uri(), + Uri(), + State(''), + ); + + // Act + final result = session.getDownloadAllCapability(AccountFixtures.aliceAccountId); + + // Assert + expect(result, isNull); + }); + test('SHOULD return null WHEN DownloadAllCapability is not supported', () { + // Arrange + final session = Session( + {}, + { + AccountFixtures.aliceAccountId: Account( + AccountName('Alice'), + true, + false, + {}, + ) + }, + {}, + UserName(''), + Uri(), + Uri(), + Uri(), + Uri(), + State(''), + ); + + // Act + final result = session.getDownloadAllCapability(AccountFixtures.aliceAccountId); + + // Assert + expect(result, isNull); + }); + }); }