TF-3454 Download all attachments as ZIP

This commit is contained in:
DatDang
2025-02-07 14:38:53 +07:00
committed by Dat H. Pham
parent 636df94458
commit f66e1460e4
34 changed files with 1175 additions and 100 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.1167 4.35833L15.9583 2.95833C15.7333 2.675 15.3917 2.5 15 2.5H5C4.60833 2.5 4.26667 2.675 4.03333 2.95833L2.88333 4.35833C2.64167 4.64167 2.5 5.01667 2.5 5.41667V15.8333C2.5 16.75 3.25 17.5 4.16667 17.5H15.8333C16.75 17.5 17.5 16.75 17.5 15.8333V5.41667C17.5 5.01667 17.3583 4.64167 17.1167 4.35833ZM5.2 4.16667H14.8L15.475 4.975H4.53333L5.2 4.16667ZM4.16667 15.8333V6.66667H15.8333V15.8333H4.16667ZM11.2083 8.33333H8.79167V10.8333H6.66667L10 14.1667L13.3333 10.8333H11.2083V8.33333Z" fill="#55687D"/>
</svg>

After

Width:  |  Height:  |  Size: 618 B

@@ -31,6 +31,7 @@ class SupportedPreviewFileTypes {
'application/vnd.ms-powerpoint'];
static const zipMimeTypes = [
'application/zip',
'application/x-tar',
'application/x-gtar',
'application/x-gzip',
@@ -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;
+6
View File
@@ -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
@@ -201,4 +201,24 @@ abstract class EmailDataSource {
Future<void> storePreviewEMLContentToSessionStorage(EMLPreviewer emlPreviewer);
Future<EMLPreviewer> getPreviewEMLContentInMemory(String keyStored);
Future<void> downloadAllAttachmentsForWeb(
AccountId accountId,
EmailId emailId,
String baseDownloadAllUrl,
String outputFileName,
AccountRequest accountRequest,
DownloadTaskId taskId,
StreamController<Either<Failure, Success>> onReceiveController,
{CancelToken? cancelToken}
);
Future<DownloadedResponse> exportAllAttachments(
AccountId accountId,
EmailId emailId,
String baseDownloadAllUrl,
String outputFileName,
AccountRequest accountRequest,
{CancelToken? cancelToken}
);
}
@@ -540,4 +540,46 @@ class EmailDataSourceImpl extends EmailDataSource {
Future<EMLPreviewer> getPreviewEMLContentInMemory(String keyStored) {
throw UnimplementedError();
}
@override
Future<void> downloadAllAttachmentsForWeb(
AccountId accountId,
EmailId emailId,
String baseDownloadAllUrl,
String outputFileName,
AccountRequest accountRequest,
DownloadTaskId taskId,
StreamController<Either<Failure, Success>> onReceiveController,
{CancelToken? cancelToken}
) => Future.sync(() async {
return await emailAPI.downloadAllAttachmentsForWeb(
accountId,
emailId,
baseDownloadAllUrl,
outputFileName,
accountRequest,
taskId,
onReceiveController,
cancelToken: cancelToken,
);
}).catchError(_exceptionThrower.throwException);
@override
Future<DownloadedResponse> 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);
}
@@ -563,4 +563,23 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource {
Future<EMLPreviewer> getPreviewEMLContentInMemory(String keyStored) {
throw UnimplementedError();
}
@override
Future<void> downloadAllAttachmentsForWeb(
AccountId accountId,
EmailId emailId,
String baseDownloadAllUrl,
String outputFileName,
AccountRequest accountRequest,
DownloadTaskId taskId,
StreamController<Either<Failure, Success>> onReceiveController,
{CancelToken? cancelToken}
) {
throw UnimplementedError();
}
@override
Future<DownloadedResponse> exportAllAttachments(AccountId accountId, EmailId emailId, String baseDownloadAllUrl, String outputFileName, AccountRequest accountRequest, {CancelToken? cancelToken}) {
throw UnimplementedError();
}
}
@@ -254,4 +254,23 @@ class EmailLocalStorageDataSourceImpl extends EmailDataSource {
Future<void> markAsForwarded(Session session, AccountId accountId, List<EmailId> emailIds) {
throw UnimplementedError();
}
@override
Future<void> downloadAllAttachmentsForWeb(
AccountId accountId,
EmailId emailId,
String baseDownloadAllUrl,
String outputFileName,
AccountRequest accountRequest,
DownloadTaskId taskId,
StreamController<Either<Failure, Success>> onReceiveController,
{CancelToken? cancelToken}
) {
throw UnimplementedError();
}
@override
Future<DownloadedResponse> exportAllAttachments(AccountId accountId, EmailId emailId, String baseDownloadAllUrl, String outputFileName, AccountRequest accountRequest, {CancelToken? cancelToken}) {
throw UnimplementedError();
}
}
@@ -252,4 +252,23 @@ class EmailSessionStorageDatasourceImpl extends EmailDataSource {
Future<void> markAsForwarded(Session session, AccountId accountId, List<EmailId> emailIds) {
throw UnimplementedError();
}
@override
Future<void> downloadAllAttachmentsForWeb(
AccountId accountId,
EmailId emailId,
String baseDownloadAllUrl,
String outputFileName,
AccountRequest accountRequest,
DownloadTaskId taskId,
StreamController<Either<Failure, Success>> onReceiveController,
{CancelToken? cancelToken}
) {
throw UnimplementedError();
}
@override
Future<DownloadedResponse> exportAllAttachments(AccountId accountId, EmailId emailId, String baseDownloadAllUrl, String outputFileName, AccountRequest accountRequest, {CancelToken? cancelToken}) {
throw UnimplementedError();
}
}
@@ -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<void> downloadAllAttachmentsForWeb(
AccountId accountId,
EmailId emailId,
String baseDownloadAllUrl,
String outputFileName,
AccountRequest accountRequest,
DownloadTaskId taskId,
StreamController<Either<Failure, Success>> 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<DownloadedResponse> 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<EmailId> emailIdsSuccess,
Map<Id, SetError> mapErrors,
@@ -530,4 +530,41 @@ class EmailRepositoryImpl extends EmailRepository {
htmlContent,
configuration);
}
@override
Future<void> downloadAllAttachmentsForWeb(
AccountId accountId,
EmailId emailId,
String baseDownloadAllUrl,
String outputFileName,
AccountRequest accountRequest,
DownloadTaskId taskId,
StreamController<Either<Failure, Success>> onReceiveController,
{CancelToken? cancelToken}
) => emailDataSource[DataSourceType.network]!.downloadAllAttachmentsForWeb(
accountId,
emailId,
baseDownloadAllUrl,
outputFileName,
accountRequest,
taskId,
onReceiveController,
cancelToken: cancelToken,
);
@override
Future<DownloadedResponse> exportAllAttachments(
AccountId accountId,
EmailId emailId,
String baseDownloadAllUrl,
String outputFileName,
AccountRequest accountRequest, {
CancelToken? cancelToken,
}) => emailDataSource[DataSourceType.network]!.exportAllAttachments(
accountId,
emailId,
baseDownloadAllUrl,
outputFileName,
accountRequest,
);
}
@@ -199,4 +199,24 @@ abstract class EmailRepository {
Future<String> sanitizeHtmlContent(
String htmlContent,
TransformConfiguration configuration);
Future<void> downloadAllAttachmentsForWeb(
AccountId accountId,
EmailId emailId,
String baseDownloadAllUrl,
String outputFileName,
AccountRequest accountRequest,
DownloadTaskId taskId,
StreamController<Either<Failure, Success>> onReceiveController,
{CancelToken? cancelToken}
);
Future<DownloadedResponse> exportAllAttachments(
AccountId accountId,
EmailId emailId,
String baseDownloadAllUrl,
String outputFileName,
AccountRequest accountRequest,
{CancelToken? cancelToken}
);
}
@@ -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<Object> 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<Object?> get props => [taskId, fileName, progress, downloaded, total];
}
class DownloadAllAttachmentsForWebSuccess extends UIState {
DownloadAllAttachmentsForWebSuccess({required this.taskId});
final DownloadTaskId taskId;
@override
List<Object> get props => [taskId];
}
class DownloadAllAttachmentsForWebFailure extends FeatureFailure {
DownloadAllAttachmentsForWebFailure({super.exception, required this.taskId});
final DownloadTaskId taskId;
@override
List<Object> get props => [exception, taskId];
}
@@ -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<Object> get props => [downloadedResponse];
}
class ExportAllAttachmentsFailure extends FeatureFailure {
ExportAllAttachmentsFailure({super.exception});
}
@@ -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<Either<Failure, Success>> execute(
AccountId accountId,
EmailId emailId,
String baseDownloadAllUrl,
Attachment attachment,
DownloadTaskId taskId,
StreamController<Either<Failure, Success>> 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));
}
}
}
@@ -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<Either<Failure, Success>> 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));
}
}
}
@@ -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<ParseEmailByBlobIdInteractor>(),
Get.find<PreviewEmailFromEmlFileInteractor>(),
Get.find<GetHtmlContentFromAttachmentInteractor>(),
Get.find<DownloadAllAttachmentsForWebInteractor>(),
Get.find<ExportAllAttachmentsInteractor>(),
));
}
@@ -168,6 +172,16 @@ class EmailBindings extends BaseBindings {
Get.lazyPut(() => PreviewEmailFromEmlFileInteractor(Get.find<EmailRepository>()));
IdentityInteractorsBindings().dependencies();
Get.lazyPut(() => GetHtmlContentFromAttachmentInteractor(Get.find<DownloadAttachmentForWebInteractor>()));
Get.lazyPut(() => DownloadAllAttachmentsForWebInteractor(
Get.find<EmailRepository>(),
Get.find<AccountRepository>(),
Get.find<AuthenticationOIDCRepository>(),
Get.find<CredentialRepository>()));
Get.lazyPut(() => ExportAllAttachmentsInteractor(
Get.find<EmailRepository>(),
Get.find<AccountRepository>(),
Get.find<AuthenticationOIDCRepository>(),
Get.find<CredentialRepository>()));
}
@override
@@ -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<void> _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);
@@ -339,6 +339,8 @@ class EmailView extends GetWidget<SingleEmailController> {
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();
@@ -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;
@@ -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);
}
@@ -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),
);
}
}
@@ -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
)
],
@@ -33,7 +33,7 @@ class AttachmentListBottomSheetBuilder {
_statusBarHeight = Get.statusBarHeight / MediaQuery.of(_context).devicePixelRatio;
}
void onDownloadAllButtonAction(OnDownloadAllButtonAction onDownloadAllButtonAction) {
void onDownloadAllButtonAction(OnDownloadAllButtonAction? onDownloadAllButtonAction) {
_onDownloadAllButtonAction = onDownloadAllButtonAction;
}
@@ -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
)
],
@@ -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<Widget> 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<Widget> 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,
],
);
}
}
@@ -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,
),
]
),
],
),
);
@@ -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<CapabilityIdentifier, CapabilityProperties Function(Map<String, dynamic>)> customMapCapabilitiesConverter = {
linagoraContactSupportCapability: ContactSupportCapability.deserialize,
tmailContactCapabilityIdentifier: AutocompleteCapability.deserialize,
linagoraDownloadAllCapability: DownloadAllCapability.deserialize,
};
Map<String, dynamic> 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<DownloadAllCapability>(
accountId,
linagoraDownloadAllCapability,
);
}
}
@@ -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)}';
}
}
}
@@ -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<String, dynamic> json) => _$DownloadAllCapabilityFromJson(json);
Map<String, dynamic> toJson() => _$DownloadAllCapabilityToJson(this);
factory DownloadAllCapability.deserialize(Map<String, dynamic> json) => _$DownloadAllCapabilityFromJson(json);
@override
List<Object?> get props => [endpoint];
}
+9 -1
View File
@@ -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"
+4
View File
@@ -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
@@ -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<DioClient>(),
MockSpec<GetHtmlContentFromAttachmentInteractor>(),
MockSpec<TwakeAppManager>(),
MockSpec<DownloadAllAttachmentsForWebInteractor>(),
MockSpec<ExportAllAttachmentsInteractor>(),
])
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,
);
});
@@ -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);
});
});
}