diff --git a/core/lib/data/network/download/download_manager.dart b/core/lib/data/network/download/download_manager.dart index 52cedc6dd..cf71081c6 100644 --- a/core/lib/data/network/download/download_manager.dart +++ b/core/lib/data/network/download/download_manager.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; - +import 'package:http/http.dart' as http; +import 'package:universal_html/html.dart' as html; import 'package:core/core.dart'; import 'package:dio/dio.dart'; @@ -72,4 +73,31 @@ class DownloadManager { } return streamController.stream.first; } + + Future downloadFileForWeb(String downloadUrl, String filename, String basicAuth) async { + final headerParam = Map(); + headerParam[HttpHeaders.authorizationHeader] = basicAuth; + headerParam[HttpHeaders.acceptHeader] = DioClient.jmapHeader; + + http.Response res = await http.get(Uri.parse(downloadUrl), headers: headerParam); + + if (res.statusCode == 200) { + final blob = html.Blob([res.bodyBytes]); + final url = html.Url.createObjectUrlFromBlob(blob); + final anchor = html.document.createElement('a') as html.AnchorElement + ..href = url + ..style.display = 'none' + ..download = filename; + html.document.body?.children.add(anchor); + + anchor.click(); + + html.document.body?.children.remove(anchor); + html.Url.revokeObjectUrl(url); + + return true; + } + + return false; + } } \ No newline at end of file diff --git a/core/lib/presentation/views/dialog/downloading_file_dialog_builder.dart b/core/lib/presentation/views/dialog/downloading_file_dialog_builder.dart index 607b39ae6..f56c14671 100644 --- a/core/lib/presentation/views/dialog/downloading_file_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/downloading_file_dialog_builder.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; typedef OnCancelDownloadActionClick = void Function(); @@ -57,13 +58,13 @@ class DownloadingFileDialogBuilder { ), )), actions: [ - TextButton( - onPressed: () { - if (_onCancelDownloadActionClick != null) { - _onCancelDownloadActionClick!(); - }}, - child: Text(_actionText, style: TextStyle(fontSize: 17.0, color: AppColor.appColor)), - ) + if (_actionText.isNotEmpty) + Padding( + padding: EdgeInsets.only(bottom: kIsWeb ? 16 : 0, top: kIsWeb ? 16 : 0), + child: TextButton( + onPressed: () => _onCancelDownloadActionClick?.call(), + child: Text(_actionText, style: TextStyle(fontSize: 17.0, color: AppColor.appColor)), + )) ], ); } diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 9ab07a007..c4c58ed69 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -76,6 +76,8 @@ dependencies: # collection collection: 1.15.0 + universal_html: 2.0.8 + dev_dependencies: flutter_test: sdk: flutter diff --git a/env.file b/env.file index 152bde72d..9bf93db70 100644 --- a/env.file +++ b/env.file @@ -1 +1 @@ -SERVER_URL=https://dev.open-paas.org \ No newline at end of file +SERVER_URL=http://localhost \ No newline at end of file diff --git a/lib/features/composer/data/network/composer_api.dart b/lib/features/composer/data/network/composer_api.dart index 46bf98880..4c1cee7ca 100644 --- a/lib/features/composer/data/network/composer_api.dart +++ b/lib/features/composer/data/network/composer_api.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:core/core.dart'; import 'package:dio/dio.dart'; import 'package:model/model.dart'; -import 'package:tmail_ui_user/main/utils/app_logger.dart'; class ComposerAPI { @@ -14,7 +13,6 @@ class ComposerAPI { ComposerAPI(this._dioClient); Future uploadAttachment(UploadRequest uploadRequest) async { - log('uploadAttachment: fileInfo: ${uploadRequest.fileInfo.props}'); final headerParam = _dioClient.getHeaders(); headerParam[HttpHeaders.contentTypeHeader] = uploadRequest.fileInfo.mimeType; headerParam[HttpHeaders.contentLengthHeader] = uploadRequest.fileInfo.fileSize; diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index 5cb07df7b..9c75fb7e1 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -27,6 +27,13 @@ abstract class EmailDataSource { CancelToken cancelToken ); + Future downloadAttachmentForWeb( + Attachment attachment, + AccountId accountId, + String baseDownloadUrl, + AccountRequest accountRequest, + ); + Future> moveToMailbox(AccountId accountId, MoveRequest moveRequest); Future> markAsStar( 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 0f59b42fb..2415c3a18 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -113,4 +113,13 @@ class EmailDataSourceImpl extends EmailDataSource { throw error; }); } + + @override + Future downloadAttachmentForWeb(Attachment attachment, AccountId accountId, String baseDownloadUrl, AccountRequest accountRequest) { + return Future.sync(() async { + return await emailAPI.downloadAttachmentForWeb(attachment, accountId, baseDownloadUrl, accountRequest); + }).catchError((error) { + throw error; + }); + } } \ 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 e6274607e..7965a7fa1 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -202,6 +202,19 @@ class EmailAPI { cancelToken: cancelToken); } + Future downloadAttachmentForWeb( + Attachment attachment, + AccountId accountId, + String baseDownloadUrl, + AccountRequest accountRequest, + ) async { + return _downloadManager.downloadFileForWeb( + attachment.getDownloadUrl(baseDownloadUrl, accountId), + attachment.name ?? '', + accountRequest.basicAuth, + ); + } + Future> moveToMailbox(AccountId accountId, MoveRequest moveRequest) async { final setEmailMethod = SetEmailMethod(accountId) ..addUpdates(moveRequest.emailIds diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index c25646c25..fead87f91 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -103,4 +103,9 @@ class EmailRepositoryImpl extends EmailRepository { Future updateEmailDrafts(AccountId accountId, Email newEmail, EmailId oldEmailId) { return emailDataSource.updateEmailDrafts(accountId, newEmail, oldEmailId); } + + @override + Future downloadAttachmentForWeb(Attachment attachment, AccountId accountId, String baseDownloadUrl, AccountRequest accountRequest) { + return emailDataSource.downloadAttachmentForWeb(attachment, accountId, baseDownloadUrl, 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 7c7295396..ad67c79e6 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -27,6 +27,13 @@ abstract class EmailRepository { CancelToken cancelToken ); + Future downloadAttachmentForWeb( + Attachment attachment, + AccountId accountId, + String baseDownloadUrl, + AccountRequest accountRequest, + ); + Future> moveToMailbox(AccountId accountId, MoveRequest moveRequest); Future> markAsStar( diff --git a/lib/features/email/domain/state/download_attachment_for_web_state.dart b/lib/features/email/domain/state/download_attachment_for_web_state.dart new file mode 100644 index 000000000..eb697f3ac --- /dev/null +++ b/lib/features/email/domain/state/download_attachment_for_web_state.dart @@ -0,0 +1,18 @@ +import 'package:core/core.dart'; + +class DownloadAttachmentForWebSuccess extends UIState { + + DownloadAttachmentForWebSuccess(); + + @override + List get props => []; +} + +class DownloadAttachmentForWebFailure extends FeatureFailure { + final exception; + + DownloadAttachmentForWebFailure(this.exception); + + @override + List get props => [exception]; +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/download_attachment_for_web_interactor.dart b/lib/features/email/domain/usecases/download_attachment_for_web_interactor.dart new file mode 100644 index 000000000..4b72d2359 --- /dev/null +++ b/lib/features/email/domain/usecases/download_attachment_for_web_interactor.dart @@ -0,0 +1,39 @@ +import 'dart:async'; + +import 'package:core/core.dart'; +import 'package:dartz/dartz.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/download_attachment_for_web_state.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; + +class DownloadAttachmentForWebInteractor { + final EmailRepository emailRepository; + final CredentialRepository credentialRepository; + + DownloadAttachmentForWebInteractor(this.emailRepository, this.credentialRepository); + + Stream> execute(Attachment attachment, AccountId accountId, String baseDownloadUrl,) async* { + try { + final result = await Future.wait( + [credentialRepository.getUserName(), credentialRepository.getPassword()], + eagerError: true + ).then((List responses) async { + final accountRequest = AccountRequest(responses.first, responses.last); + return await emailRepository.downloadAttachmentForWeb( + attachment, + accountId, + baseDownloadUrl, + accountRequest); + }); + if (result) { + yield Right(DownloadAttachmentForWebSuccess()); + } else { + yield Left(DownloadAttachmentForWebFailure(null)); + } + } catch (exception) { + yield Left(DownloadAttachmentForWebFailure(exception)); + } + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/email_bindings.dart b/lib/features/email/presentation/email_bindings.dart index c87b35b7f..afa6b844e 100644 --- a/lib/features/email/presentation/email_bindings.dart +++ b/lib/features/email/presentation/email_bindings.dart @@ -10,6 +10,7 @@ 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_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_attachment_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; @@ -38,6 +39,7 @@ class EmailBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } @@ -70,6 +72,10 @@ class EmailBindings extends BaseBindings { )); Get.lazyPut(() => MoveToMailboxInteractor(Get.find())); Get.lazyPut(() => MarkAsStarEmailInteractor(Get.find())); + Get.lazyPut(() => DownloadAttachmentForWebInteractor( + Get.find(), + Get.find(), + )); } @override diff --git a/lib/features/email/presentation/email_controller.dart b/lib/features/email/presentation/email_controller.dart index 394225e91..658e2df6f 100644 --- a/lib/features/email/presentation/email_controller.dart +++ b/lib/features/email/presentation/email_controller.dart @@ -7,14 +7,17 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_request.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_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/move_to_mailbox_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/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/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/export_attachment_interactor.dart'; @@ -43,6 +46,7 @@ class EmailController extends BaseController { final ExportAttachmentInteractor _exportAttachmentInteractor; final MoveToMailboxInteractor _moveToMailboxInteractor; final MarkAsStarEmailInteractor _markAsStarEmailInteractor; + final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor; final emailAddressExpandMode = ExpandMode.COLLAPSE.obs; final attachmentsExpandMode = ExpandMode.COLLAPSE.obs; @@ -60,6 +64,7 @@ class EmailController extends BaseController { this._exportAttachmentInteractor, this._moveToMailboxInteractor, this._markAsStarEmailInteractor, + this._downloadAttachmentForWebInteractor, ); @override @@ -107,6 +112,8 @@ class EmailController extends BaseController { _downloadAttachmentsFailure(failure); } else if (failure is ExportAttachmentFailure) { _exportAttachmentFailureAction(failure); + } else if (failure is DownloadAttachmentForWebFailure) { + _downloadAttachmentForWebFailureAction(failure); } }, (success) { @@ -120,6 +127,8 @@ class EmailController extends BaseController { _moveToMailboxSuccess(success); } else if (success is MarkAsStarEmailSuccess) { _markAsEmailStarSuccess(success); + } else if (success is DownloadAttachmentForWebSuccess) { + _downloadAttachmentForWebSuccessAction(success); } }); } @@ -229,23 +238,35 @@ class EmailController extends BaseController { void exportAttachment(BuildContext context, Attachment attachment) { final cancelToken = CancelToken(); - _showDownloadingFileDialog(context, attachment, cancelToken); + _showDownloadingFileDialog(context, attachment, cancelToken: cancelToken); _exportAttachmentAction(attachment, cancelToken); } - void _showDownloadingFileDialog(BuildContext context, Attachment attachment, CancelToken cancelToken) { - showCupertinoDialog( - context: context, - builder: (_) => (DownloadingFileDialogBuilder() - ..key(Key('downloading_file_dialog')) - ..title(AppLocalizations.of(context).preparing_to_export) - ..content(AppLocalizations.of(context).downloading_file(attachment.name ?? '')) - ..actionText(AppLocalizations.of(context).cancel) - ..addCancelDownloadActionClick(() { - cancelToken.cancel([AppLocalizations.of(context).user_cancel_download_file]); - popBack(); - })) - .build()); + void _showDownloadingFileDialog(BuildContext context, Attachment attachment, {CancelToken? cancelToken}) { + if (cancelToken != null) { + showCupertinoDialog( + context: context, + builder: (_) => + PointerInterceptor(child: (DownloadingFileDialogBuilder() + ..key(Key('downloading_file_dialog')) + ..title(AppLocalizations.of(context).preparing_to_export) + ..content(AppLocalizations.of(context).downloading_file(attachment.name ?? '')) + ..actionText(AppLocalizations.of(context).cancel) + ..addCancelDownloadActionClick(() { + cancelToken.cancel([AppLocalizations.of(context).user_cancel_download_file]); + popBack(); + })) + .build())); + } else { + showCupertinoDialog( + context: context, + builder: (_) => + PointerInterceptor(child: (DownloadingFileDialogBuilder() + ..key(Key('downloading_file_for_web_dialog')) + ..title(AppLocalizations.of(context).preparing_to_save) + ..content(AppLocalizations.of(context).downloading_file(attachment.name ?? ''))) + .build())); + } } void _exportAttachmentAction(Attachment attachment, CancelToken cancelToken) async { @@ -269,6 +290,28 @@ class EmailController extends BaseController { } } + void downloadAttachmentForWeb(Attachment attachment) { + _downloadAttachmentForWebAction(attachment); + } + + void _downloadAttachmentForWebAction(Attachment attachment) async { + final accountId = mailboxDashBoardController.accountId.value; + if (accountId != null && mailboxDashBoardController.sessionCurrent != null) { + final baseDownloadUrl = mailboxDashBoardController.sessionCurrent!.getDownloadUrl(); + consumeState(_downloadAttachmentForWebInteractor.execute(attachment, accountId, baseDownloadUrl)); + } + } + + void _downloadAttachmentForWebFailureAction(Failure failure) { + if (failure is DownloadAttachmentForWebFailure) { + popBack(); + } + } + + void _downloadAttachmentForWebSuccessAction(Success success) async { + popBack(); + } + void openDestinationPickerView(PresentationEmail email) async { final currentMailbox = mailboxDashBoardController.selectedMailbox.value; final accountId = mailboxDashBoardController.accountId.value; diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 8b48cc546..c5d2df202 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:core/core.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -221,10 +222,14 @@ class EmailView extends GetView { ..setExpandMode((countAttachments - 1 == index) ? emailController.attachmentsExpandMode.value : null) ..onExpandAttachmentActionClick(() => emailController.toggleDisplayAttachmentsAction()) ..onDownloadAttachmentFileActionClick((attachment) { - if (Platform.isAndroid) { - emailController.downloadAttachments(context, [attachment]); + if (kIsWeb) { + emailController.downloadAttachmentForWeb(attachment); } else { - emailController.exportAttachment(context, attachment); + if (Platform.isAndroid) { + emailController.downloadAttachments(context, [attachment]); + } else if (Platform.isIOS) { + emailController.exportAttachment(context, attachment); + } } })) .build()) diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index c961c6448..1f1a084cc 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -646,7 +646,7 @@ class AppLocalizations { String get create_new_mailbox_failure { return Intl.message( 'Create new mailbox failure', - name: 'new_mailbox_is_created' + name: 'create_new_mailbox_failure' ); } @@ -729,4 +729,11 @@ class AppLocalizations { name: 'email_address_is_not_in_the_correct_format', ); } + + String get preparing_to_save { + return Intl.message( + 'Preparing to save', + name: 'preparing_to_save' + ); + } } \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 3180f76fa..0eb28f40a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -130,9 +130,6 @@ dependencies: # fk_user_agent fk_user_agent: 2.1.0 - # pointer_interceptor - pointer_interceptor: 0.9.3 - dev_dependencies: flutter_test: sdk: flutter diff --git a/test/features/caching/email_cache_client_test.dart b/test/features/caching/email_cache_client_test.dart index c3c312dba..3e6fb935b 100644 --- a/test/features/caching/email_cache_client_test.dart +++ b/test/features/caching/email_cache_client_test.dart @@ -1,3 +1,5 @@ +@TestOn('vm') + import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test/features/caching/mailbox_cache_client_test.dart b/test/features/caching/mailbox_cache_client_test.dart index de1f281af..6663f6c9b 100644 --- a/test/features/caching/mailbox_cache_client_test.dart +++ b/test/features/caching/mailbox_cache_client_test.dart @@ -1,3 +1,5 @@ +@TestOn('vm') + import 'dart:io'; import 'package:flutter_test/flutter_test.dart';