From 0158142e45084d4e37e07204add5bd4ff1e3476f Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 16 Sep 2021 09:46:14 +0700 Subject: [PATCH] TF-32 Add domain layer of export attachment for ios platform --- assets/images/ic_collapse_attachment.svg | 3 + assets/images/ic_download.svg | 3 + assets/images/ic_expand_attachment.svg | 3 + core/lib/core.dart | 3 + .../network/download/download_client.dart | 29 +++++++ .../network/download/download_manager.dart | 76 +++++++++++++++++++ .../extensions/color_extension.dart | 1 + .../presentation/resources/image_paths.dart | 2 + .../downloading_file_dialog_builder.dart | 71 +++++++++++++++++ .../presentation/composer_controller.dart | 1 - .../domain/repository/email_repository.dart | 9 +++ .../domain/state/export_attachment_state.dart | 19 +++++ .../download_attachments_interactor.dart | 8 +- .../export_attachment_interactor.dart | 42 ++++++++++ pubspec.yaml | 3 + 15 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 assets/images/ic_collapse_attachment.svg create mode 100644 assets/images/ic_download.svg create mode 100644 assets/images/ic_expand_attachment.svg create mode 100644 core/lib/data/network/download/download_client.dart create mode 100644 core/lib/data/network/download/download_manager.dart create mode 100644 core/lib/presentation/views/dialog/downloading_file_dialog_builder.dart create mode 100644 lib/features/email/domain/state/export_attachment_state.dart create mode 100644 lib/features/email/domain/usecases/export_attachment_interactor.dart diff --git a/assets/images/ic_collapse_attachment.svg b/assets/images/ic_collapse_attachment.svg new file mode 100644 index 000000000..66bb2c26f --- /dev/null +++ b/assets/images/ic_collapse_attachment.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_download.svg b/assets/images/ic_download.svg new file mode 100644 index 000000000..3b8cae244 --- /dev/null +++ b/assets/images/ic_download.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_expand_attachment.svg b/assets/images/ic_expand_attachment.svg new file mode 100644 index 000000000..e58edcf83 --- /dev/null +++ b/assets/images/ic_expand_attachment.svg @@ -0,0 +1,3 @@ + + + diff --git a/core/lib/core.dart b/core/lib/core.dart index d133674b3..5ddd061ca 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -33,6 +33,7 @@ export 'presentation/views/text/text_form_field_builder.dart'; export 'presentation/views/image/icon_builder.dart'; export 'presentation/views/context_menu/context_menu_action_builder.dart'; export 'presentation/views/context_menu/context_menu_builder.dart'; +export 'presentation/views/dialog/downloading_file_dialog_builder.dart'; // Resources export 'presentation/resources/assets_paths.dart'; @@ -48,6 +49,8 @@ export 'data/network/config/dynamic_url_interceptors.dart'; export 'data/network/config/endpoint.dart'; export 'data/network/config/service_path.dart'; export 'data/network/dio_client.dart'; +export 'data/network/download/download_client.dart'; +export 'data/network/download/download_manager.dart'; // State export 'presentation/state/success.dart'; diff --git a/core/lib/data/network/download/download_client.dart b/core/lib/data/network/download/download_client.dart new file mode 100644 index 000000000..0a3ea69e9 --- /dev/null +++ b/core/lib/data/network/download/download_client.dart @@ -0,0 +1,29 @@ + +import 'dart:io'; + +import 'package:core/core.dart'; +import 'package:dio/dio.dart'; + +class DownloadClient { + + final DioClient _dioClient; + + DownloadClient(this._dioClient); + + Future downloadFile( + String url, + String baseAuth, + CancelToken? cancelToken, + ) async { + final headerParam = _dioClient.getHeaders(); + headerParam[HttpHeaders.authorizationHeader] = baseAuth; + headerParam[HttpHeaders.acceptHeader] = DioClient.jmapHeader; + + final responseBody = await _dioClient.get( + url, + options: Options(headers: headerParam, responseType: ResponseType.stream), + cancelToken: cancelToken); + + return (responseBody as ResponseBody); + } +} \ No newline at end of file diff --git a/core/lib/data/network/download/download_manager.dart b/core/lib/data/network/download/download_manager.dart new file mode 100644 index 000000000..9fa023768 --- /dev/null +++ b/core/lib/data/network/download/download_manager.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:core/core.dart'; +import 'package:core/data/network/download/download_client.dart'; +import 'package:dio/dio.dart'; + +class DownloadManager { + final DownloadClient _downloadClient; + + DownloadManager(this._downloadClient); + + Future downloadFile( + String downloadUrl, + Future directoryToSave, + String filename, + String baseAuth, + {CancelToken? cancelToken} + ) async { + final streamController = StreamController(); + + try { + await Future.wait([ + _downloadClient.downloadFile(downloadUrl, baseAuth, cancelToken), + directoryToSave + ]).then((values) { + final fileStream = (values[0] as ResponseBody).stream; + final tempFilePath = '${(values[1] as Directory).absolute.path}/$filename'; + + final file = File(tempFilePath); + file.createSync(recursive: true); + var randomAccessFile = file.openSync(mode: FileMode.write); + late StreamSubscription subscription; + + subscription = fileStream + .takeWhile((_) => cancelToken == null || !cancelToken.isCancelled) + .listen((data) { + subscription.pause(); + randomAccessFile.writeFrom(data).then((_randomAccessFile) { + randomAccessFile = _randomAccessFile; + subscription.resume(); + if (cancelToken != null && cancelToken.isCancelled) { + streamController.sink.addError(CancelDownloadFileException(cancelToken.cancelError?.message)); + } + }).catchError((error) async { + await subscription.cancel(); + streamController.sink.addError(CommonDownloadFileException(error.toString())); + await streamController.close(); + }); + }, onDone: () async { + await randomAccessFile.close(); + if (cancelToken != null && cancelToken.isCancelled) { + streamController.sink.addError(CancelDownloadFileException(cancelToken.cancelError?.message)); + } else { + streamController.sink.add(tempFilePath); + } + await streamController.close(); + }, onError: (error) async { + await randomAccessFile.close(); + await file.delete(); + streamController.sink.addError(CommonDownloadFileException(error.toString())); + await streamController.close(); + }); + }); + } catch(exception) { + if (cancelToken != null && cancelToken.isCancelled) { + streamController.sink.addError(CancelDownloadFileException(cancelToken.cancelError?.message)); + await streamController.close(); + return streamController.stream.first; + } else { + throw exception; + } + } + return streamController.stream.first; + } +} \ No newline at end of file diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index 47ce8140d..a9722901c 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -55,4 +55,5 @@ extension AppColor on Color { static const toastBackgroundColor = Color(0xFFACAFFF); static const toastSuccessBackgroundColor = Color(0xFF04AA11); static const toastErrorBackgroundColor = Color(0xFFFF5858); + static const backgroundCountAttachment = Color(0x681C1C1C); } diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 7eccb38db..5e7019762 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -37,6 +37,8 @@ class ImagePaths { String get icComposerFileShare => _getImagePath('ic_composer_file_share.svg'); String get icExpandAddress => _getImagePath('ic_expand_address.svg'); String get icSelected => _getImagePath('ic_selected.svg'); + String get icExpandAttachment => _getImagePath('ic_expand_attachment.svg'); + String get icDownload => _getImagePath('ic_download.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/core/lib/presentation/views/dialog/downloading_file_dialog_builder.dart b/core/lib/presentation/views/dialog/downloading_file_dialog_builder.dart new file mode 100644 index 000000000..155c9cb18 --- /dev/null +++ b/core/lib/presentation/views/dialog/downloading_file_dialog_builder.dart @@ -0,0 +1,71 @@ + +import 'package:core/core.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +typedef OnCancelDownloadActionClick = void Function(); + +class DownloadingFileDialogBuilder { + Key? _key; + String _title = ''; + String _content = ''; + String _actionText = ''; + OnCancelDownloadActionClick? _onCancelDownloadActionClick; + + DownloadingFileDialogBuilder(); + + void key(Key key) { + _key = key; + } + + void title(String title) { + _title = title; + } + + void content(String content) { + _content = content; + } + + void actionText(String actionText) { + _actionText = actionText; + } + + void addCancelDownloadActionClick(OnCancelDownloadActionClick onCancelDownloadActionClick) { + _onCancelDownloadActionClick = onCancelDownloadActionClick; + } + + Widget build() { + return CupertinoAlertDialog( + key: _key ?? Key('DownloadingFileBuilder'), + title: Text(_title, style: TextStyle(fontSize: 17.0, color: Colors.black)), + content: Padding( + padding: EdgeInsets.only(top: 16.0, left: 16.0, right: 16.0), + child: Center( + child: Column( + children: [ + SizedBox( + width: 20.0, + height: 20.0, + child: CupertinoActivityIndicator()), + SizedBox(height: 16), + Text( + _content, + style: TextStyle(fontSize: 13.0, color: Colors.black), + softWrap: false, + maxLines: 1) + ], + ), + )), + actions: [ + TextButton( + onPressed: () { + if (_onCancelDownloadActionClick != null) { + _onCancelDownloadActionClick!(); + }}, + child: Text(_actionText, style: TextStyle(fontSize: 17.0, color: AppColor.appColor)), + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 3a144dc2b..9a3ccb6a2 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -25,7 +25,6 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_inter import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/constants/email_constants.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/email_content_extension.dart'; -import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/model/message_content.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index e01373b93..cdb2d25f5 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; @@ -16,4 +17,12 @@ abstract class EmailRepository { String baseDownloadUrl, AccountRequest accountRequest ); + + Future exportAttachment( + Attachment attachment, + AccountId accountId, + String baseDownloadUrl, + AccountRequest accountRequest, + CancelToken cancelToken + ); } \ No newline at end of file diff --git a/lib/features/email/domain/state/export_attachment_state.dart b/lib/features/email/domain/state/export_attachment_state.dart new file mode 100644 index 000000000..b90959cd0 --- /dev/null +++ b/lib/features/email/domain/state/export_attachment_state.dart @@ -0,0 +1,19 @@ +import 'package:core/core.dart'; + +class ExportAttachmentSuccess extends UIState { + final String filePath; + + ExportAttachmentSuccess(this.filePath); + + @override + List get props => [filePath]; +} + +class ExportAttachmentFailure extends FeatureFailure { + final exception; + + ExportAttachmentFailure(this.exception); + + @override + List get props => [exception]; +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/download_attachments_interactor.dart b/lib/features/email/domain/usecases/download_attachments_interactor.dart index 83ad42c00..797d45104 100644 --- a/lib/features/email/domain/usecases/download_attachments_interactor.dart +++ b/lib/features/email/domain/usecases/download_attachments_interactor.dart @@ -12,11 +12,11 @@ class DownloadAttachmentsInteractor { DownloadAttachmentsInteractor(this.emailRepository, this.credentialRepository); - Future> execute( + Stream> execute( List attachments, AccountId accountId, String baseDownloadUrl - ) async { + ) async* { try { final taskIds = await Future.wait( [credentialRepository.getUserName(), credentialRepository.getPassword()], @@ -30,9 +30,9 @@ class DownloadAttachmentsInteractor { accountRequest); }); - return Right(DownloadAttachmentsSuccess(taskIds)); + yield Right(DownloadAttachmentsSuccess(taskIds)); } catch (e) { - return Left(DownloadAttachmentsFailure(e)); + yield Left(DownloadAttachmentsFailure(e)); } } } \ No newline at end of file diff --git a/lib/features/email/domain/usecases/export_attachment_interactor.dart b/lib/features/email/domain/usecases/export_attachment_interactor.dart new file mode 100644 index 000000000..ac3115708 --- /dev/null +++ b/lib/features/email/domain/usecases/export_attachment_interactor.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:core/core.dart'; +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/state/export_attachment_state.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; + +class ExportAttachmentInteractor { + final EmailRepository emailRepository; + final CredentialRepository credentialRepository; + + ExportAttachmentInteractor(this.emailRepository, this.credentialRepository); + + Stream> execute( + Attachment attachment, + AccountId accountId, + String baseDownloadUrl, + CancelToken cancelToken + ) async* { + try { + final filePath = await Future.wait( + [credentialRepository.getUserName(), credentialRepository.getPassword()], + eagerError: true + ).then((List responses) async { + final accountRequest = AccountRequest(responses.first, responses.last); + return await emailRepository.exportAttachment( + attachment, + accountId, + baseDownloadUrl, + accountRequest, + cancelToken); + }); + yield Right(ExportAttachmentSuccess(filePath)); + } catch (exception) { + yield Left(ExportAttachmentFailure(exception)); + } + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 446ddae4e..69b468d8b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -110,6 +110,9 @@ dependencies: # permission_handler permission_handler: 8.1.6 + # share + share: 2.0.4 + dev_dependencies: flutter_test: sdk: flutter