diff --git a/core/lib/core.dart b/core/lib/core.dart index 4734f686a..d133674b3 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -8,12 +8,16 @@ export 'presentation/extensions/list_extensions.dart'; export 'domain/extensions/datetime_extension.dart'; export 'presentation/extensions/html_extension.dart'; +// Exceptions +export 'domain/exceptions/download_file_exception.dart'; + // Utils export 'presentation/utils/theme_utils.dart'; export 'presentation/utils/responsive_utils.dart'; export 'presentation/utils/keyboard_utils.dart'; export 'presentation/utils/style_utils.dart'; export 'presentation/utils/app_toast.dart'; +export 'data/utils/device_manager.dart'; // Views export 'presentation/views/text/slogan_builder.dart'; diff --git a/core/lib/data/network/dio_client.dart b/core/lib/data/network/dio_client.dart index 9b36e4a47..a2f79e3cc 100644 --- a/core/lib/data/network/dio_client.dart +++ b/core/lib/data/network/dio_client.dart @@ -1,6 +1,8 @@ import 'package:dio/dio.dart'; class DioClient { + static const jmapHeader = 'application/json;jmapVersion=rfc-8621'; + final Dio _dio; DioClient(this._dio); diff --git a/core/lib/data/utils/device_manager.dart b/core/lib/data/utils/device_manager.dart new file mode 100644 index 000000000..b1492a345 --- /dev/null +++ b/core/lib/data/utils/device_manager.dart @@ -0,0 +1,15 @@ +import 'dart:async'; +import 'dart:core'; + +import 'package:device_info/device_info.dart'; + +class DeviceManager { + final DeviceInfoPlugin _deviceInfoPlugin; + + DeviceManager(this._deviceInfoPlugin); + + Future isNeedRequestStoragePermissionOnAndroid() async { + final androidInfo = await _deviceInfoPlugin.androidInfo; + return androidInfo.version.sdkInt <= 28; + } +} \ No newline at end of file diff --git a/core/lib/domain/exceptions/download_file_exception.dart b/core/lib/domain/exceptions/download_file_exception.dart new file mode 100644 index 000000000..8eedec41f --- /dev/null +++ b/core/lib/domain/exceptions/download_file_exception.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; + +abstract class DownloadFileException with EquatableMixin implements Exception { + final String message; + + DownloadFileException(this.message); + + @override + String toString() => message; + + @override + List get props => [message]; +} + +class CommonDownloadFileException extends DownloadFileException { + CommonDownloadFileException(message) : super(message); + + @override + List get props => [message]; +} + +class CancelDownloadFileException extends DownloadFileException { + CancelDownloadFileException(cancelMessage) : super(cancelMessage); + + @override + List get props => [message]; +} + +class DeviceNotSupportedException extends DownloadFileException { + DeviceNotSupportedException() : super('This device is not supported, please try on Android or iOS'); +} \ No newline at end of file diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 8c41bfecf..728821a48 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -53,6 +53,9 @@ dependencies: # GetX get: 4.1.4 + # device_info + device_info: 2.0.2 + dev_dependencies: flutter_test: sdk: flutter diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index b58b87f91..3a144dc2b 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -117,7 +117,7 @@ class ComposerController extends BaseController { return null; } - Tuple3, List, Session>? getContentEmailQuoted() { + Tuple3, List, Session>? getContentEmailQuoted() { if (composerArguments.value != null) { final emailContent = composerArguments.value!.emailContent; final session = composerArguments.value!.session; @@ -196,10 +196,11 @@ class ComposerController extends BaseController { final messageContent = contentEmail.value1.first; final attachmentInlines = contentEmail.value2; final session = contentEmail.value3; + final baseDownloadUrl = session.getDownloadUrl(); final accountId = session.accounts.keys.first; final message = (attachmentInlines.isNotEmpty && messageContent.hasImageInlineWithCid()) - ? '${messageContent.getContentHasInlineAttachment(session, accountId, attachmentInlines)}' + ? '${messageContent.getContentHasInlineAttachment(baseDownloadUrl, accountId, attachmentInlines)}' : '${messageContent.content}'; trustAsHtml = htmlMessagePurifier.purifyHtmlMessage(message, allowAttributes: {'style', 'input', 'form'}) diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index 35cadbdb2..e01373b93 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -1,6 +1,6 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:model/email/read_actions.dart'; +import 'package:model/model.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; abstract class EmailRepository { @@ -9,4 +9,11 @@ abstract class EmailRepository { Future sendEmail(AccountId accountId, EmailRequest emailRequest); Future markAsRead(AccountId accountId, EmailId emailId, ReadActions readAction); + + Future> downloadAttachments( + List attachments, + AccountId accountId, + String baseDownloadUrl, + AccountRequest accountRequest + ); } \ No newline at end of file diff --git a/lib/features/email/domain/state/download_attachments_state.dart b/lib/features/email/domain/state/download_attachments_state.dart new file mode 100644 index 000000000..3e7f51e98 --- /dev/null +++ b/lib/features/email/domain/state/download_attachments_state.dart @@ -0,0 +1,20 @@ +import 'package:core/core.dart'; +import 'package:model/model.dart'; + +class DownloadAttachmentsSuccess extends UIState { + final List taskIds; + + DownloadAttachmentsSuccess(this.taskIds); + + @override + List get props => [taskIds]; +} + +class DownloadAttachmentsFailure extends FeatureFailure { + final exception; + + DownloadAttachmentsFailure(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 new file mode 100644 index 000000000..83ad42c00 --- /dev/null +++ b/lib/features/email/domain/usecases/download_attachments_interactor.dart @@ -0,0 +1,38 @@ +import 'package:core/core.dart'; +import 'package:model/model.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/state/download_attachments_state.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; + +class DownloadAttachmentsInteractor { + final EmailRepository emailRepository; + final CredentialRepository credentialRepository; + + DownloadAttachmentsInteractor(this.emailRepository, this.credentialRepository); + + Future> execute( + List attachments, + AccountId accountId, + String baseDownloadUrl + ) async { + try { + final taskIds = await Future.wait( + [credentialRepository.getUserName(), credentialRepository.getPassword()], + eagerError: true + ).then((List responses) async { + final accountRequest = AccountRequest(responses.first, responses.last); + return await emailRepository.downloadAttachments( + attachments, + accountId, + baseDownloadUrl, + accountRequest); + }); + + return Right(DownloadAttachmentsSuccess(taskIds)); + } catch (e) { + return Left(DownloadAttachmentsFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/home/presentation/home_controller.dart b/lib/features/home/presentation/home_controller.dart index 75ffe38cd..d1efae48f 100644 --- a/lib/features/home/presentation/home_controller.dart +++ b/lib/features/home/presentation/home_controller.dart @@ -1,4 +1,6 @@ import 'package:core/core.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_credential_state.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_credential_interactor.dart'; @@ -14,9 +16,18 @@ class HomeController extends GetxController { @override void onReady() { super.onReady(); + _initFlutterDownloader(); _getCredentialAction(); } + void _initFlutterDownloader() { + FlutterDownloader + .initialize(debug: kDebugMode) + .then((_) => FlutterDownloader.registerCallback(downloadCallback)); + } + + static void downloadCallback(String id, DownloadTaskStatus status, int progress) {} + void _getCredentialAction() async { await _getCredentialInteractor.execute() .then((response) => response.fold( diff --git a/model/lib/download/download_task_id.dart b/model/lib/download/download_task_id.dart new file mode 100644 index 000000000..dad65de30 --- /dev/null +++ b/model/lib/download/download_task_id.dart @@ -0,0 +1,10 @@ +import 'package:equatable/equatable.dart'; + +class DownloadTaskId with EquatableMixin { + final String taskId; + + DownloadTaskId(this.taskId); + + @override + List get props => [taskId]; +} \ No newline at end of file diff --git a/model/lib/email/attachment.dart b/model/lib/email/attachment.dart new file mode 100644 index 000000000..139a0b718 --- /dev/null +++ b/model/lib/email/attachment.dart @@ -0,0 +1,41 @@ + +import 'package:equatable/equatable.dart'; +import 'package:http_parser/http_parser.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:uri/uri.dart'; + +class Attachment with EquatableMixin { + + final PartId? partId; + final Id? blobId; + final UnsignedInt? size; + final String? name; + final MediaType? type; + final String? cid; + + Attachment( + this.partId, + this.blobId, + this.size, + this.name, + this.type, + this.cid + ); + + String getDownloadUrl(String baseDownloadUrl, AccountId accountId) { + final downloadUriTemplate = UriTemplate('$baseDownloadUrl'); + final downloadUri = downloadUriTemplate.expand({ + 'accountId' : '${accountId.id.value}', + 'blobId' : '${blobId?.value}', + 'name' : '$name', + 'type' : '${type?.mimeType}', + }); + return Uri.decodeFull(downloadUri); + } + + @override + List get props => [partId, blobId, size, name, type, cid]; +} \ No newline at end of file diff --git a/model/lib/extensions/session_extension.dart b/model/lib/extensions/session_extension.dart index 93eda7f80..6e93ad672 100644 --- a/model/lib/extensions/session_extension.dart +++ b/model/lib/extensions/session_extension.dart @@ -1,16 +1,12 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:uri/uri.dart'; extension SessionExtension on Session { - - String getDownloadUrl(String accountId, String blobId, String name, String type) { - final downloadUriTemplate = UriTemplate('${downloadUrl.origin}'); - return downloadUriTemplate.expand({ - 'accountId' : '$accountId', - 'blobId' : '$blobId', - 'name' : '$name', - 'type' : '$type', - }); + String getDownloadUrl() { + var baseUrl = '${downloadUrl.origin}${downloadUrl.path}'; + if (baseUrl.endsWith('/')) { + baseUrl = baseUrl.substring(0, baseUrl.length - 1); + } + return Uri.decodeFull(baseUrl); } } \ No newline at end of file diff --git a/model/lib/model.dart b/model/lib/model.dart index ed401eef9..cd2dea0a3 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -23,6 +23,7 @@ export 'email/email_action_type.dart'; export 'email/presentation_email_address.dart'; export 'email/email_address_cache.dart'; export 'email/read_actions.dart'; +export 'email/attachment.dart'; // Extensions export 'extensions/email_address_extension.dart'; @@ -34,3 +35,6 @@ export 'extensions/user_profile_extension.dart'; export 'extensions/presentation_email_extension.dart'; export 'extensions/keyword_identifier_extension.dart'; export 'extensions/presentation_mailbox_extension.dart'; + +// Download +export 'download/download_task_id.dart'; \ No newline at end of file diff --git a/model/pubspec.yaml b/model/pubspec.yaml index cd6a1d4f0..ef8a5b102 100644 --- a/model/pubspec.yaml +++ b/model/pubspec.yaml @@ -52,6 +52,9 @@ dependencies: # json_annotation json_annotation: 4.0.1 + # http_parser + http_parser: 4.0.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 398a5dc29..446ddae4e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -95,6 +95,21 @@ dependencies: # uuid uuid: 3.0.4 + # flutter_downloader + flutter_downloader: 1.7.0 + + # external_path + external_path: 1.0.1 + + # path_provider + path_provider: 2.0.3 + + # device_info + device_info: 2.0.2 + + # permission_handler + permission_handler: 8.1.6 + dev_dependencies: flutter_test: sdk: flutter