diff --git a/assets/images/ic_file_audio.svg b/assets/images/ic_file_audio.svg new file mode 100644 index 0000000000..327498f859 --- /dev/null +++ b/assets/images/ic_file_audio.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_file_code.svg b/assets/images/ic_file_code.svg new file mode 100644 index 0000000000..beb4a9e7ad --- /dev/null +++ b/assets/images/ic_file_code.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_file_default.svg b/assets/images/ic_file_default.svg new file mode 100644 index 0000000000..3f232a301d --- /dev/null +++ b/assets/images/ic_file_default.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/ic_file_doc.svg b/assets/images/ic_file_doc.svg new file mode 100644 index 0000000000..18872ff39d --- /dev/null +++ b/assets/images/ic_file_doc.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/ic_file_docx.svg b/assets/images/ic_file_docx.svg deleted file mode 100644 index f809ecc458..0000000000 --- a/assets/images/ic_file_docx.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/images/ic_file_epup.svg b/assets/images/ic_file_epup.svg deleted file mode 100644 index 7d561c8e15..0000000000 --- a/assets/images/ic_file_epup.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/images/ic_file_excel.svg b/assets/images/ic_file_excel.svg new file mode 100644 index 0000000000..3c23703c25 --- /dev/null +++ b/assets/images/ic_file_excel.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/ic_file_image.svg b/assets/images/ic_file_image.svg new file mode 100644 index 0000000000..9225299c95 --- /dev/null +++ b/assets/images/ic_file_image.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_file_odp.svg b/assets/images/ic_file_odp.svg new file mode 100644 index 0000000000..4f0f554644 --- /dev/null +++ b/assets/images/ic_file_odp.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/images/ic_file_ods.svg b/assets/images/ic_file_ods.svg new file mode 100644 index 0000000000..8549b1c996 --- /dev/null +++ b/assets/images/ic_file_ods.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/images/ic_file_odt.svg b/assets/images/ic_file_odt.svg new file mode 100644 index 0000000000..a3335dac50 --- /dev/null +++ b/assets/images/ic_file_odt.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/ic_file_pdf.svg b/assets/images/ic_file_pdf.svg index f404dee744..7f3d8061f5 100644 --- a/assets/images/ic_file_pdf.svg +++ b/assets/images/ic_file_pdf.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + diff --git a/assets/images/ic_file_png.svg b/assets/images/ic_file_png.svg deleted file mode 100644 index d1d36d1e86..0000000000 --- a/assets/images/ic_file_png.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/images/ic_file_power_point.svg b/assets/images/ic_file_power_point.svg new file mode 100644 index 0000000000..6fd5020135 --- /dev/null +++ b/assets/images/ic_file_power_point.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/ic_file_pptx.svg b/assets/images/ic_file_pptx.svg deleted file mode 100644 index 0c80e98296..0000000000 --- a/assets/images/ic_file_pptx.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/images/ic_file_text.svg b/assets/images/ic_file_text.svg new file mode 100644 index 0000000000..94daeda89e --- /dev/null +++ b/assets/images/ic_file_text.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/ic_file_video.svg b/assets/images/ic_file_video.svg new file mode 100644 index 0000000000..2c875d9e0f --- /dev/null +++ b/assets/images/ic_file_video.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_file_xlsx.svg b/assets/images/ic_file_xlsx.svg deleted file mode 100644 index 25738a203d..0000000000 --- a/assets/images/ic_file_xlsx.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/images/ic_file_zip.svg b/assets/images/ic_file_zip.svg index 09f67c4253..f21fb433d7 100644 --- a/assets/images/ic_file_zip.svg +++ b/assets/images/ic_file_zip.svg @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + diff --git a/core/lib/core.dart b/core/lib/core.dart index 1f4ea1a5e1..26e13b64f2 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -17,6 +17,7 @@ export 'presentation/extensions/tap_down_details_extension.dart'; export 'presentation/extensions/map_extensions.dart'; export 'presentation/extensions/either_view_state_extension.dart'; export 'presentation/extensions/media_type_extension.dart'; +export 'presentation/extensions/scroll_controller_extension.dart'; // Exceptions export 'domain/exceptions/download_file_exception.dart'; diff --git a/core/lib/domain/preview/supported_preview_file_types.dart b/core/lib/domain/preview/supported_preview_file_types.dart index 3dee945644..5a8b23a030 100644 --- a/core/lib/domain/preview/supported_preview_file_types.dart +++ b/core/lib/domain/preview/supported_preview_file_types.dart @@ -1,60 +1,8 @@ class SupportedPreviewFileTypes { - static const imageMimeTypes = [ - 'image/bmp', - 'image/jpeg', - 'image/gif', - 'image/webp', - 'image/png', - 'image/svg+xml', - 'image/x-icon', - 'image/tiff', - 'image/heif', - 'image/avif' - ]; - - static const textMimeTypes = [ - 'text/plain', - 'text/markdown', - ]; - static const jsonMimeTypes = [ 'application/json', ]; - static const docMimeTypes = [ - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.oasis.opendocument.text', - 'application/vnd.oasis.opendocument.text-template', - 'application/vnd.oasis.opendocument.text-web', - 'application/vnd.oasis.opendocument.text-master', - 'application/msword', - 'application/vnd.ms-works']; - - static const rtfMimeTypes = ['application/rtf']; - - static const xlsMimeTypes = [ - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.oasis.opendocument.spreadsheet', - 'application/vnd.oasis.opendocument.spreadsheet-template', - 'application/vnd.oasis.opendocument.chart', - 'application/vnd.oasis.opendocument.formula', - 'application/vnd.ms-excel']; - - static const pptMimeTypes = [ - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/vnd.oasis.opendocument.presentation', - 'application/vnd.oasis.opendocument.presentation-template', - 'application/vnd.ms-powerpoint']; - - static const zipMimeTypes = [ - 'application/zip', - 'application/x-tar', - 'application/x-gtar', - 'application/x-gzip', - 'application/x-compressed', - 'application/x-zip-compressed', - 'application/java-archive']; - static const iOSSupportedTypes = { 'text/plain' : 'public.plain-text', 'text/html' : 'public.html', @@ -83,50 +31,4 @@ class SupportedPreviewFileTypes { 'application/vnd.openxmlformats-officedocument.presentationml.presentation' : 'com.microsoft.powerpoint.​ppt', 'application/pdf' : 'com.adobe.pdf', }; - - static const androidSupportedTypes = [ - 'image/bmp', - 'image/jpeg', - 'image/gif', - 'image/png', - 'text/plain', - 'text/html', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/msword', - 'application/vnd.ms-excel', - 'application/vnd.ms-powerpoint', - 'application/vnd.ms-outlook', - 'application/vnd.ms-works', - 'application/vnd.mpohun.certificate', - 'application/vnd.android.package-archive', - 'application/octet-stream', - 'application/x-tar', - 'application/x-gtar', - 'application/x-gzip', - 'application/x-javascript', - 'application/x-compressed', - 'application/x-zip-compressed', - 'application/java-archive', - 'application/pdf', - 'application/rtf', - 'audio/x-mpegurl', - 'video/x-m4v', - 'video/x-ms-asf', - 'video/x-msvideo', - 'audio/x-mpeg', - 'audio/mp4a-latm', - 'video/vnd.mpegurl', - 'video/quicktime', - 'video/mp4', - 'video/3gpp', - 'video/mpeg', - 'audio/mpeg', - 'audio/ogg', - 'audio/x-pn-realaudio', - 'audio/x-wav', - 'audio/x-ms-wma', - 'audio/x-ms-wmv', - ]; } diff --git a/core/lib/presentation/extensions/media_type_extension.dart b/core/lib/presentation/extensions/media_type_extension.dart index 67a6d6bf8a..d6a4b4c452 100644 --- a/core/lib/presentation/extensions/media_type_extension.dart +++ b/core/lib/presentation/extensions/media_type_extension.dart @@ -1,49 +1,18 @@ import 'package:core/data/constants/constant.dart'; import 'package:core/domain/preview/document_uti.dart'; import 'package:core/domain/preview/supported_preview_file_types.dart'; -import 'package:core/presentation/extensions/string_extension.dart'; +import 'package:core/presentation/model/file_category.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:http_parser/http_parser.dart'; extension MediaTypeExtension on MediaType { - bool isAndroidSupportedPreview() => SupportedPreviewFileTypes.androidSupportedTypes.contains(mimeType); - - bool isIOSSupportedPreview() => SupportedPreviewFileTypes.iOSSupportedTypes.containsKey(mimeType); - - bool isImageFile() => SupportedPreviewFileTypes.imageMimeTypes.contains(mimeType); - - bool isDocFile() => SupportedPreviewFileTypes.docMimeTypes.contains(mimeType); - - bool isPowerPointFile() => SupportedPreviewFileTypes.pptMimeTypes.contains(mimeType); - - bool isExcelFile() => SupportedPreviewFileTypes.xlsMimeTypes.contains(mimeType); - - bool isZipFile() => SupportedPreviewFileTypes.zipMimeTypes.contains(mimeType); - - bool isRtfFile() => SupportedPreviewFileTypes.rtfMimeTypes.contains(mimeType); - - bool isTextFile() => SupportedPreviewFileTypes.textMimeTypes.contains(mimeType); - bool isJsonFile() => SupportedPreviewFileTypes.jsonMimeTypes.contains(mimeType); DocumentUti getDocumentUti() => DocumentUti(SupportedPreviewFileTypes.iOSSupportedTypes[mimeType]); String getIcon(ImagePaths imagePaths, {String? fileName}) { - if (isPDFFile(fileName: fileName) == true || isRtfFile()) { - return imagePaths.icFilePdf; - } else if (isDocFile()) { - return imagePaths.icFileDocx; - } else if (isExcelFile()) { - return imagePaths.icFileXlsx; - } else if (isPowerPointFile()) { - return imagePaths.icFilePptx; - } else if (isZipFile()) { - return imagePaths.icFileZip; - } else if (isImageSupportedPreview(fileName: fileName)) { - return imagePaths.icFilePng; - } else { - return imagePaths.icFileEPup; - } + final category = getFileCategory(fileName: fileName); + return category.getIconPath(imagePaths); } bool isPDFFile({required String? fileName}) => @@ -58,10 +27,132 @@ extension MediaTypeExtension on MediaType { (mimeType == Constant.octetStreamMimeType && fileName?.endsWith(Constant.htmlExtension) == true); - bool isImageSupportedPreview({required String? fileName}) => - isImageFile() || - (mimeType == Constant.octetStreamMimeType && - fileName != null && - SupportedPreviewFileTypes.imageMimeTypes - .contains(fileName.imageMimeType)); + FileCategory getFileCategory({String? fileName}) { + String? ext; + + if (fileName != null && fileName.contains('.')) { + ext = fileName.split('.').last.toLowerCase(); + } + + switch (ext) { + case 'pdf': + return FileCategory.pdf; + case 'doc': + case 'docx': + case 'rtf': + return FileCategory.document; + case 'odt': + return FileCategory.documentODT; + case 'xls': + case 'xlsx': + case 'csv': + case 'tsv': + return FileCategory.spreadsheet; + case 'ods': + return FileCategory.spreadsheetODS; + case 'ppt': + case 'pptx': + return FileCategory.presentation; + case 'odp': + return FileCategory.presentationODP; + case 'jpg': + case 'jpeg': + case 'png': + case 'gif': + case 'bmp': + case 'svg': + case 'webp': + case 'heic': + case 'heif': + case 'avif': + case 'tiff': + return FileCategory.image; + case 'mp3': + case 'wav': + case 'ogg': + case 'm4a': + case 'aac': + case 'flac': + return FileCategory.audio; + case 'mp4': + case 'mov': + case 'avi': + case 'mkv': + case 'webm': + case 'wmv': + return FileCategory.video; + case 'zip': + case 'rar': + case '7z': + case 'tar': + case 'gz': + case 'bz2': + return FileCategory.archive; + case 'txt': + case 'md': + case 'log': + return FileCategory.text; + case 'js': + case 'ts': + case 'json': + case 'html': + case 'css': + case 'xml': + case 'java': + case 'kt': + case 'dart': + case 'py': + case 'c': + case 'cpp': + case 'h': + case 'cs': + case 'swift': + case 'go': + case 'rb': + case 'php': + case 'sh': + case 'yml': + case 'yaml': + return FileCategory.code; + case 'apk': + case 'ipa': + case 'exe': + case 'dmg': + return FileCategory.other; + } + + final lowerType = mimeType.toLowerCase(); + + if (lowerType == 'application/pdf') return FileCategory.pdf; + if (lowerType.startsWith('image/')) return FileCategory.image; + if (lowerType.startsWith('audio/')) return FileCategory.audio; + if (lowerType.startsWith('video/')) return FileCategory.video; + if (lowerType.startsWith('text/')) return FileCategory.text; + + if (lowerType.contains('zip') || lowerType.contains('compressed')) { + return FileCategory.archive; + } + + if (lowerType.contains('msword') || + lowerType.contains('wordprocessingml')) { + return FileCategory.document; + } + + if (lowerType.contains('spreadsheetml') || + lowerType.contains('excel') || + lowerType.contains('csv')) { + return FileCategory.spreadsheet; + } + + if (lowerType.contains('presentationml') || + lowerType.contains('powerpoint')) { + return FileCategory.presentation; + } + + if (lowerType == 'application/octet-stream') { + return FileCategory.other; + } + + return FileCategory.other; + } } diff --git a/core/lib/presentation/extensions/scroll_controller_extension.dart b/core/lib/presentation/extensions/scroll_controller_extension.dart new file mode 100644 index 0000000000..e2b2309acd --- /dev/null +++ b/core/lib/presentation/extensions/scroll_controller_extension.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +extension ScrollControllerExtension on ScrollController { + void scrollToBottomWithPadding({required double padding}) { + try { + final maxExtent = position.maxScrollExtent; + + final targetOffset = (maxExtent - padding).clamp(0.0, maxExtent); + + animateTo( + targetOffset, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + } catch (_) {} + } +} diff --git a/core/lib/presentation/model/file_category.dart b/core/lib/presentation/model/file_category.dart new file mode 100644 index 0000000000..e8265b7b90 --- /dev/null +++ b/core/lib/presentation/model/file_category.dart @@ -0,0 +1,51 @@ +import 'package:core/presentation/resources/image_paths.dart'; + +enum FileCategory { + pdf, + document, + documentODT, + spreadsheet, + spreadsheetODS, + presentation, + presentationODP, + image, + video, + audio, + archive, + text, + code, + other; + + String getIconPath(ImagePaths imagePaths) { + switch (this) { + case FileCategory.pdf: + return imagePaths.icFilePdf; + case FileCategory.document: + return imagePaths.icFileDoc; + case FileCategory.documentODT: + return imagePaths.icFileODT; + case FileCategory.spreadsheet: + return imagePaths.icFileExcel; + case FileCategory.spreadsheetODS: + return imagePaths.icFileODS; + case FileCategory.presentation: + return imagePaths.icFilePowerPoint; + case FileCategory.presentationODP: + return imagePaths.icFileODP; + case FileCategory.image: + return imagePaths.icFileImage; + case FileCategory.video: + return imagePaths.icFileVideo; + case FileCategory.audio: + return imagePaths.icFileAudio; + case FileCategory.archive: + return imagePaths.icFileZip; + case FileCategory.text: + return imagePaths.icFileText; + case FileCategory.code: + return imagePaths.icFileCode; + case FileCategory.other: + return imagePaths.icFileDefault; + } + } +} diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 92a2a9a872..24c417b89a 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -101,13 +101,20 @@ class ImagePaths { String get icSelectedSB => _getImagePath('ic_selected_sb.svg'); String get icUserSB => _getImagePath('ic_user_sb.svg'); String get icComposeWeb => _getImagePath('ic_compose_web.svg'); - String get icFileDocx => _getImagePath('ic_file_docx.svg'); + String get icFileDoc => _getImagePath('ic_file_doc.svg'); String get icFileZip => _getImagePath('ic_file_zip.svg'); - String get icFileXlsx => _getImagePath('ic_file_xlsx.svg'); - String get icFilePng => _getImagePath('ic_file_png.svg'); + String get icFileExcel => _getImagePath('ic_file_excel.svg'); + String get icFileImage => _getImagePath('ic_file_image.svg'); String get icFilePdf => _getImagePath('ic_file_pdf.svg'); - String get icFilePptx => _getImagePath('ic_file_pptx.svg'); - String get icFileEPup => _getImagePath('ic_file_epup.svg'); + String get icFilePowerPoint => _getImagePath('ic_file_power_point.svg'); + String get icFileDefault => _getImagePath('ic_file_default.svg'); + String get icFileAudio => _getImagePath('ic_file_audio.svg'); + String get icFileCode => _getImagePath('ic_file_code.svg'); + String get icFileODP => _getImagePath('ic_file_odp.svg'); + String get icFileODS => _getImagePath('ic_file_ods.svg'); + String get icFileODT => _getImagePath('ic_file_odt.svg'); + String get icFileText => _getImagePath('ic_file_text.svg'); + String get icFileVideo => _getImagePath('ic_file_video.svg'); String get icLanguage => _getImagePath('ic_language.svg'); String get icChecked => _getImagePath('ic_checked.svg'); String get icStyleBold => _getImagePath('ic_style_bold.svg'); diff --git a/lib/features/email/presentation/action/email_ui_action.dart b/lib/features/email/presentation/action/email_ui_action.dart index b3e9eb9244..549ae4d71c 100644 --- a/lib/features/email/presentation/action/email_ui_action.dart +++ b/lib/features/email/presentation/action/email_ui_action.dart @@ -83,4 +83,26 @@ class CollapseEmailInThreadDetailAction extends EmailUIAction { @override List get props => [emailId]; +} + +class OpenAttachmentListAction extends EmailUIAction { + OpenAttachmentListAction({ + required this.emailId, + required this.countAttachments, + required this.screenHeight, + this.isDisplayAllAttachments = false, + }); + + final EmailId? emailId; + final int countAttachments; + final bool isDisplayAllAttachments; + final double screenHeight; + + @override + List get props => [ + emailId, + countAttachments, + isDisplayAllAttachments, + screenHeight, + ]; } \ No newline at end of file diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 70fb87716e..f3069f9c3e 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -97,6 +97,7 @@ import 'package:tmail_ui_user/features/email/presentation/bindings/mdn_interacto import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_attendee_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_organizer_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/handle_open_attachment_list_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/update_attendance_status_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/blob_calendar_event.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; @@ -105,8 +106,6 @@ import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscrib import 'package:tmail_ui_user/features/email/presentation/model/eml_previewer.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_dialog_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/html_attachment_previewer.dart'; 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'; @@ -150,7 +149,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final mailboxDashBoardController = Get.find(); final _downloadManager = Get.find(); final _printUtils = Get.find(); - final _attachmentListScrollController = ScrollController(); final GetEmailContentInteractor _getEmailContentInteractor; final MarkAsEmailReadInteractor _markAsEmailReadInteractor; @@ -180,6 +178,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final emailContents = RxnString(); final attachments = [].obs; + final isDisplayAllAttachments = RxBool(false); + final emlPreviewer = [].obs; final blobCalendarEvent = Rxn(); final emailLoadedViewState = Rx>(Right(UIState.idle)); final emailUnsubscribe = Rxn(); @@ -190,10 +190,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final attendanceStatus = Rxn(); final htmlContentViewKey = GlobalKey(); - final ScrollController emailScrollController = ScrollController(); - Identity? _identitySelected; ButtonState? _printEmailButtonState; + GlobalKey? attachmentListKey; + final obxListeners = []; late final EmailActionReactor emailActionReactor; @@ -201,6 +201,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { StreamController>.broadcast(); Stream> get downloadProgressState => _downloadProgressStateController.stream; + ThreadDetailController? get threadDetailController => _threadDetailController; + PresentationEmail? get currentEmail { if (PlatformInfo.isMobile && _threadDetailController?.isThreadDetailEnabled != true) { @@ -248,6 +250,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin { @override void onInit() { + if (PlatformInfo.isWeb) { + attachmentListKey = GlobalKey(); + } _threadDetailController = getBinding(); _injectCalendarEventBindings(session, accountId); _registerObxStreamListener(); @@ -267,8 +272,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void onClose() { _threadDetailController = null; _downloadProgressStateController.close(); - _attachmentListScrollController.dispose(); - emailScrollController.dispose(); CalendarEventInteractorBindings().dispose(); MdnInteractorBindings().dispose(); super.onClose(); @@ -425,6 +428,17 @@ class SingleEmailController extends BaseController with AppLoaderMixin { worker.dispose(); } Get.delete(tag: _currentEmailId!.id.value); + } else if (action is OpenAttachmentListAction) { + if (_currentEmailId == null || action.emailId != _currentEmailId) { + return; + } + jumpToAttachmentList( + emailId: _currentEmailId!, + countAttachments: action.countAttachments, + screenHeight: action.screenHeight, + isDisplayAllAttachments: action.isDisplayAllAttachments, + ); + mailboxDashBoardController.clearEmailUIAction(); } })); @@ -1754,39 +1768,14 @@ class SingleEmailController extends BaseController with AppLoaderMixin { mailboxDashBoardController.openComposer(ComposerArguments.fromEmailAddress(emailAddress)); } - void openAttachmentList(BuildContext context, List attachments) { - final tag = _currentEmailId?.id.value; - if (responsiveUtils.isMobile(context)) { - (AttachmentListBottomSheetBuilder(context, attachments, imagePaths, _attachmentListScrollController, tag) - ..onCloseButtonAction(() => popBack()) - ..onDownloadAttachmentFileAction((attachment) => handleDownloadAttachmentAction(attachment)) - ..onViewAttachmentFileAction((attachment) => handleViewAttachmentAction(context, attachment)) - ..onDownloadAllButtonAction(isDownloadAllSupported() - ? () => downloadAllAttachmentsForWeb('TwakeMail-${DateTime.now()}') - : null - ) - ).show(); - } else { - Get.dialog( - PointerInterceptor( - child: AttachmentListDialogBuilder( - imagePaths: imagePaths, - attachments: attachments, - responsiveUtils: responsiveUtils, - scrollController: _attachmentListScrollController, - backgroundColor: Colors.black.withAlpha(24), - onCloseButtonAction: () => popBack(), - onDownloadAttachmentFileAction: (attachment) => handleDownloadAttachmentAction(attachment), - onViewAttachmentFileAction: (attachment) => handleViewAttachmentAction(context, attachment), - onDownloadAllButtonAction: isDownloadAllSupported() - ? () => downloadAllAttachmentsForWeb('TwakeMail-${DateTime.now()}') - : null, - singleEmailControllerTag: tag, - ) - ), - barrierColor: AppColor.colorDefaultCupertinoActionSheet, - ); - } + void showAllAttachmentsAction() { + isDisplayAllAttachments.value = true; + threadDetailController?.isDisplayAllAttachments = true; + } + + void hideAllAttachmentsAction() { + isDisplayAllAttachments.value = false; + threadDetailController?.isDisplayAllAttachments = false; } void _unsubscribeEmail(BuildContext context, PresentationEmail presentationEmail) { diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 763e395d08..254cd8eb22 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -233,6 +233,8 @@ class EmailView extends GetWidget { List? emailAddressSender, ScrollController? scrollController, }) { + final isMobileResponsive = controller.responsiveUtils.isMobile(context); + return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -286,8 +288,8 @@ class EmailView extends GetWidget { controller.mailboxDashBoardController.mapMailboxById, ), )), - if (!controller.responsiveUtils.isMobile(context)) - const SizedBox(height: 24), + if (!isMobileResponsive) + const SizedBox(height: 16), Obx(() => MailUnsubscribedBanner( presentationEmail: controller.currentEmail, emailUnsubscribe: controller.emailUnsubscribe.value @@ -295,6 +297,8 @@ class EmailView extends GetWidget { Obx(() => EmailViewLoadingBarWidget( viewState: controller.emailLoadedViewState.value )), + if (!isMobileResponsive) + _buildAttachmentsList(context), if (calendarEvent != null) Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -438,44 +442,51 @@ class EmailView extends GetWidget { height: 5, color: Colors.transparent, ), - Obx(() { - if (controller.attachments.isNotEmpty) { - return EmailAttachmentsWidget( - responsiveUtils: controller.responsiveUtils, - attachments: controller.attachments, - imagePaths: controller.imagePaths, - onDragStarted: controller - .mailboxDashBoardController.enableAttachmentDraggableApp, - onDragEnd: (_) { - controller - .mailboxDashBoardController - .disableAttachmentDraggableApp(); - }, - downloadAttachmentAction: (attachment) => - controller.handleDownloadAttachmentAction(attachment), - viewAttachmentAction: (attachment) => - controller.handleViewAttachmentAction( - context, - attachment, - ), - onTapShowAllAttachmentFile: () => controller.openAttachmentList( + if (isMobileResponsive) + _buildAttachmentsList(context), + ], + ); + } + + Widget _buildAttachmentsList(BuildContext context) { + return Obx(() { + if (controller.attachments.isNotEmpty) { + return EmailAttachmentsWidget( + key: PlatformInfo.isWeb && controller.responsiveUtils.isMobile(context) + ? controller.attachmentListKey + : null, + responsiveUtils: controller.responsiveUtils, + attachments: controller.attachments, + imagePaths: controller.imagePaths, + isDisplayAllAttachments: controller.isDisplayAllAttachments.value, + onDragStarted: controller + .mailboxDashBoardController.enableAttachmentDraggableApp, + onDragEnd: (_) { + controller + .mailboxDashBoardController + .disableAttachmentDraggableApp(); + }, + downloadAttachmentAction: (attachment) => + controller.handleDownloadAttachmentAction(attachment), + viewAttachmentAction: (attachment) => + controller.handleViewAttachmentAction( context, - controller.attachments, + attachment, ), - showDownloadAllAttachmentsButton: - controller.downloadAllButtonIsEnabled(), - onTapDownloadAllButton: () => - controller.handleDownloadAllAttachmentsAction( + onTapShowAllAttachmentFile: controller.showAllAttachmentsAction, + onTapHideAllAttachments: controller.hideAllAttachmentsAction, + showDownloadAllAttachmentsButton: + controller.downloadAllButtonIsEnabled(), + onTapDownloadAllButton: () => + controller.handleDownloadAllAttachmentsAction( 'TwakeMail-${DateTime.now()}', ), - singleEmailControllerTag: tag, - ); - } else { - return const SizedBox.shrink(); - } - }), - ], - ); + singleEmailControllerTag: tag, + ); + } else { + return const SizedBox.shrink(); + } + }); } bool _validateDisplayEventActionBanner({ diff --git a/lib/features/email/presentation/extensions/attachment_extension.dart b/lib/features/email/presentation/extensions/attachment_extension.dart index daa22a9352..97509687ab 100644 --- a/lib/features/email/presentation/extensions/attachment_extension.dart +++ b/lib/features/email/presentation/extensions/attachment_extension.dart @@ -1,5 +1,6 @@ import 'package:core/data/constants/constant.dart'; import 'package:core/presentation/extensions/media_type_extension.dart'; +import 'package:core/presentation/model/file_category.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/utils/platform_info.dart'; import 'package:model/download/download_task_id.dart'; @@ -8,7 +9,7 @@ import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; extension AttachmentExtension on Attachment { - String getIcon(ImagePaths imagePaths) => type?.getIcon(imagePaths, fileName: name) ?? imagePaths.icFileEPup; + String getIcon(ImagePaths imagePaths) => type?.getIcon(imagePaths, fileName: name) ?? imagePaths.icFileDefault; bool get isPDFFile => type?.isPDFFile(fileName: name) ?? false; @@ -41,11 +42,9 @@ extension AttachmentExtension on Attachment { bool get isHTMLFile => type?.isHTMLFile(fileName: name) ?? false; - bool get isImage => type?.isImageSupportedPreview(fileName: name) ?? false; + bool get isImage => type?.getFileCategory(fileName: name) == FileCategory.image; - bool get isText => (type?.isTextFile() ?? false) - || name?.endsWith('.txt') == true - || name?.endsWith('.md') == true; + bool get isText => type?.getFileCategory(fileName: name) == FileCategory.text; bool get isJson => (type?.isJsonFile() ?? false) || name?.endsWith('.json') == true; diff --git a/lib/features/email/presentation/extensions/handle_open_attachment_list_extension.dart b/lib/features/email/presentation/extensions/handle_open_attachment_list_extension.dart new file mode 100644 index 0000000000..8680d44412 --- /dev/null +++ b/lib/features/email/presentation/extensions/handle_open_attachment_list_extension.dart @@ -0,0 +1,54 @@ +import 'package:core/presentation/extensions/scroll_controller_extension.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; + +extension HandleOpenAttachmentListExtension on SingleEmailController { + void jumpToAttachmentList({ + required EmailId emailId, + required int countAttachments, + required double screenHeight, + bool isDisplayAllAttachments = false, + }) { + final scrollController = threadDetailController?.scrollController; + if (scrollController == null || !scrollController.hasClients) { + logError( + '$runtimeType::jumpToAttachmentList(): scrollController is null'); + return; + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (PlatformInfo.isWeb && attachmentListKey != null) { + final context = attachmentListKey!.currentContext; + if (context == null) return; + + Scrollable.ensureVisible( + context, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + } else { + final emailContentHeight = + (screenHeight - 180).clamp(0.0, screenHeight).toDouble(); + + final totalItems = isDisplayAllAttachments + ? countAttachments + 1 + : EmailUtils.maxMobileVisibleAttachments + 1; + + final totalAttachmentsHeight = + totalItems * EmailUtils.attachmentItemHeight + + (totalItems - 1) * EmailUtils.attachmentItemSpacing; + + log('$runtimeType::jumpToAttachmentList(): totalAttachmentsHeight: $totalAttachmentsHeight, emailContentHeight: $emailContentHeight'); + scrollController.scrollToBottomWithPadding( + padding: totalAttachmentsHeight < emailContentHeight + ? 0 + : emailContentHeight, + ); + } + }); + } +} diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index 6202be5bbe..82ab96c612 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -2,6 +2,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/mail/mail_address.dart'; +import 'package:flutter/material.dart'; import 'package:get/get_utils/src/get_utils/get_utils.dart'; import 'package:dartz/dartz.dart'; import 'package:http_parser/http_parser.dart'; @@ -22,8 +23,11 @@ import 'package:tmail_ui_user/main/routes/route_utils.dart'; class EmailUtils { static const double desktopItemMaxWidth = 260; - static const double desktopMoreButtonMaxWidth = 70; - static const double maxMobileVisibleAttachments = 3; + static const double desktopMoreButtonMaxWidth = 150; + static const double attachmentItemSpacing = 8; + static const double attachmentItemHeight = 36; + static const double attachmentIcon = 20; + static const int maxMobileVisibleAttachments = 3; EmailUtils._(); @@ -242,26 +246,71 @@ class EmailUtils { required double maxWidth, required List attachments, required bool isMobile, + int maxVisibleAttachments = EmailUtils.maxMobileVisibleAttachments, + double attachmentItemWidth = EmailUtils.desktopItemMaxWidth, + double attachmentItemSpacing = EmailUtils.attachmentItemSpacing, + double showMoreButtonMaxWidth = EmailUtils.desktopMoreButtonMaxWidth, + double attachmentIcon = EmailUtils.attachmentIcon, }) { if (attachments.isEmpty) return []; if (isMobile) { - return attachments.length <= maxMobileVisibleAttachments + return attachments.length <= maxVisibleAttachments ? attachments - : attachments.sublist(0, 3); + : attachments.sublist(0, maxVisibleAttachments); } - final displayedCount = maxWidth ~/ desktopItemMaxWidth; - if (displayedCount == attachments.length) { + final totalNeededWidth = attachments.length * attachmentItemWidth + + (attachments.length - 1) * attachmentItemSpacing; + if (totalNeededWidth <= maxWidth) { return attachments; - } else { - final int possibleDisplayedCount = - ((maxWidth - desktopMoreButtonMaxWidth) ~/ desktopItemMaxWidth) - .clamp(0, attachments.length); + } + + final availableWidth = maxWidth - + showMoreButtonMaxWidth - + attachmentIcon - + attachmentItemSpacing * 4; + + log('EmailUtils::getAttachmentDisplayed: availableWidth = $availableWidth, maxWidth = $maxWidth, showMoreButtonMaxWidth = $showMoreButtonMaxWidth, attachmentIcon = $attachmentIcon, attachmentItemSpacing = $attachmentItemSpacing'); + double usedWidth = 0; + int visibleCount = 0; + + for (int i = 0; i < attachments.length; i++) { + final nextWidth = + attachmentItemWidth + (i > 0 ? attachmentItemSpacing : 0); + if (usedWidth + nextWidth <= availableWidth) { + usedWidth += nextWidth; + visibleCount++; + } else { + break; + } + } - return possibleDisplayedCount == 0 - ? [attachments.first] - : attachments.sublist(0, possibleDisplayedCount); + if (visibleCount == 0) visibleCount = 1; + + return attachments.sublist(0, visibleCount); + } + + static double estimateTextWidth({ + required BuildContext context, + required String text, + TextStyle? textStyle, + Locale? locale, + }) { + try { + final textPainter = TextPainter( + text: TextSpan( + text: text, + style: textStyle ?? DefaultTextStyle.of(context).style, + locale: locale, + ), + maxLines: 1, + textDirection: TextDirection.ltr, + )..layout(); + + return textPainter.width; + } catch (e) { + return desktopMoreButtonMaxWidth; } } diff --git a/lib/features/email/presentation/widgets/attachment_item_widget.dart b/lib/features/email/presentation/widgets/attachment_item_widget.dart index 532ce26aeb..0014554d88 100644 --- a/lib/features/email/presentation/widgets/attachment_item_widget.dart +++ b/lib/features/email/presentation/widgets/attachment_item_widget.dart @@ -4,7 +4,6 @@ import 'package:core/presentation/utils/theme_utils.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/container/tmail_container_widget.dart'; import 'package:core/presentation/views/text/middle_ellipsis_text.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'; @@ -69,26 +68,15 @@ class AttachmentItemWidget extends StatelessWidget { ), ); - final attachmentTitleWithEndDots = Text( - attachment.generateFileName(), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: ThemeUtils.textStyleM3LabelLarge( - color: AppColor.m3SurfaceBackground, - ), - ); - final bodyItemWidget = Row( children: [ isLoading ? loadingIndicator : attachmentIcon, const SizedBox(width: 8), Expanded( - child: PlatformInfo.isCanvasKit - ? attachmentTitleWithMiddleDots - : attachmentTitleWithEndDots, + child: attachmentTitleWithMiddleDots, ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsetsDirectional.only(start: 8, end: 3), child: Text( filesize(attachment.size?.value), maxLines: 1, @@ -97,10 +85,10 @@ class AttachmentItemWidget extends StatelessWidget { ), ), TMailButtonWidget.fromIcon( - icon: imagePaths.icFileDownload, + icon: imagePaths.icDownloadAttachment, backgroundColor: Colors.transparent, padding: const EdgeInsets.all(5), - iconColor: AppColor.steelGrayA540, + iconColor: AppColor.primaryColor, iconSize: 20, onTapActionCallback: isLoading ? null : () => _onTapDownloadAction(attachment), @@ -109,7 +97,7 @@ class AttachmentItemWidget extends StatelessWidget { ); return TMailContainerWidget( - height: 36, + height: EmailUtils.attachmentItemHeight, borderRadius: 8, border: Border.all(color: AppColor.m3Tertiary70), padding: const EdgeInsets.symmetric(horizontal: 8), diff --git a/lib/features/email/presentation/widgets/attachment_list/attachment_list_action_button_builder.dart b/lib/features/email/presentation/widgets/attachment_list/attachment_list_action_button_builder.dart deleted file mode 100644 index 4241816b7e..0000000000 --- a/lib/features/email/presentation/widgets/attachment_list/attachment_list_action_button_builder.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:core/presentation/views/button/icon_button_web.dart'; -import 'package:flutter/material.dart'; - -class AttachmentListActionButtonBuilder extends StatelessWidget { - final String? name; - final TextStyle? textStyle; - final Color? bgColor; - final Function? action; - - const AttachmentListActionButtonBuilder({ - super.key, - this.name, - this.textStyle, - this.bgColor, - this.action - }); - - @override - Widget build(BuildContext context) { - return buildButtonWrapText( - name ?? '', - radius: 10, - height: 44, - textStyle: textStyle, - bgColor: bgColor, - onTap: () => action?.call(), - padding: const EdgeInsets.symmetric(horizontal: 16), - ); - } -} diff --git a/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_body_builder.dart b/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_body_builder.dart deleted file mode 100644 index 61a97fb3ab..0000000000 --- a/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_body_builder.dart +++ /dev/null @@ -1,158 +0,0 @@ -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'; -import 'package:model/email/attachment.dart'; -import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:tmail_ui_user/features/email/presentation/styles/attachment/attachment_list_styles.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_item_widget.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_action_button_builder.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_item_widget.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -class AttachmentListBottomSheetBodyBuilder extends StatelessWidget { - final ImagePaths imagePaths; - final List attachments; - final double statusBarHeight; - final ScrollController scrollController; - final OnDownloadAllButtonAction? onDownloadAllButtonAction; - final OnDownloadAttachmentFileAction? onDownloadAttachmentFileAction; - final OnViewAttachmentFileAction? onViewAttachmentFileAction; - final OnCancelButtonAction? onCancelButtonAction; - final OnCloseButtonAction? onCloseButtonAction; - final String? singleEmailControllerTag; - - const AttachmentListBottomSheetBodyBuilder({ - super.key, - required this.imagePaths, - required this.attachments, - required this.statusBarHeight, - required this.scrollController, - this.onDownloadAllButtonAction, - this.onDownloadAttachmentFileAction, - this.onViewAttachmentFileAction, - this.onCancelButtonAction, - this.onCloseButtonAction, - this.singleEmailControllerTag, - }); - - @override - Widget build(BuildContext context) { - return PointerInterceptor( - child: SafeArea( - top: true, - bottom: false, - left: false, - right: false, - child: GestureDetector( - onTap: () => FocusManager.instance.primaryFocus?.unfocus(), - child: Padding( - padding: EdgeInsets.only(top: statusBarHeight), - child: ClipRRect( - borderRadius: AttachmentListStyles.modalRadius, - child: Scaffold( - appBar: AppBar( - leading: const SizedBox.shrink(), - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - AppLocalizations.of(context).attachmentList, - style: AttachmentListStyles.titleTextStyle - ), - const SizedBox(width: AttachmentListStyles.titleSpace), - Text( - '${attachments.length} ${AppLocalizations.of(context).files}', - style: AttachmentListStyles.subTitleTextStyle - ), - ], - ), - centerTitle: true, - actions: [ - TMailButtonWidget.fromIcon( - icon: imagePaths.icCircleClose, - backgroundColor: Colors.transparent, - iconSize: 28, - margin: const EdgeInsetsDirectional.only(end: 12), - padding: const EdgeInsets.all(5), - onTapActionCallback: onCloseButtonAction, - ) - ], - ), - body: Container( - decoration: AttachmentListStyles.dialogBodyDecorationMobile, - child: Column( - children: [ - Expanded( - child: RawScrollbar( - trackColor: AttachmentListStyles.scrollbarTrackColor, - thumbColor: AttachmentListStyles.scrollbarThumbColor, - radius: AttachmentListStyles.scrollbarThumbRadius, - trackRadius: AttachmentListStyles.scrollbarTrackRadius, - thickness: AttachmentListStyles.scrollbarThickness, - thumbVisibility: true, - trackVisibility: true, - controller: scrollController, - trackBorderColor: AttachmentListStyles.scrollbarTrackBorderColor, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), - child: ListView.separated( - controller: scrollController, - shrinkWrap: true, - physics: const ScrollPhysics(), - itemCount: attachments.length, - itemBuilder: (context, index) { - return AttachmentListItemWidget( - attachment: attachments[index], - downloadAttachmentAction: onDownloadAttachmentFileAction, - viewAttachmentAction: onViewAttachmentFileAction, - singleEmailControllerTag: singleEmailControllerTag, - ); - }, - separatorBuilder: (context, index) { - return const Divider( - color: AttachmentListStyles.separatorColor, - ); - }, - ), - ), - ), - ), - Padding( - padding: AttachmentListStyles.actionButtonsRowPadding, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (onDownloadAllButtonAction != null) - AttachmentListActionButtonBuilder( - name: AppLocalizations.of(context).downloadAll, - bgColor: AppColor.primaryColor, - textStyle: AttachmentListStyles.downloadAllButtonTextStyle.copyWith( - color: Colors.white, - ), - action: onDownloadAllButtonAction, - ), - if (onDownloadAllButtonAction != null && onCancelButtonAction != null) - const SizedBox(width: AttachmentListStyles.buttonsSpaceBetween), - if (onCancelButtonAction != null) - AttachmentListActionButtonBuilder( - name: AppLocalizations.of(context).close, - bgColor: AttachmentListStyles.cancelButtonColor, - textStyle: AttachmentListStyles.cancelButtonTextStyle - ) - ], - ), - ), - const SizedBox(height: AttachmentListStyles.dialogBottomSpace) - ], - ), - ), - ), - ), - ), - ) - ), - ); - } -} diff --git a/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart b/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart deleted file mode 100644 index b30447f3d3..0000000000 --- a/lib/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:core/presentation/resources/image_paths.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:model/email/attachment.dart'; -import 'package:tmail_ui_user/features/email/presentation/styles/attachment/attachment_list_styles.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_item_widget.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_body_builder.dart'; - -typedef OnDownloadAllButtonAction = void Function(); -typedef OnCancelButtonAction = void Function(); -typedef OnCloseButtonAction = void Function(); - -class AttachmentListBottomSheetBuilder { - final BuildContext _context; - final List _attachments; - final ImagePaths _imagePaths; - final ScrollController _scrollController; - final String? _singleEmailControllerTag; - - late double _statusBarHeight; - - OnDownloadAllButtonAction? _onDownloadAllButtonAction; - OnDownloadAttachmentFileAction? _onDownloadAttachmentFileAction; - OnViewAttachmentFileAction? _onViewAttachmentFileAction; - OnCancelButtonAction? _onCancelButtonAction; - OnCloseButtonAction? _onCloseButtonAction; - - AttachmentListBottomSheetBuilder( - this._context, - this._attachments, - this._imagePaths, - this._scrollController, - this._singleEmailControllerTag, - ) { - _statusBarHeight = Get.statusBarHeight / MediaQuery.of(_context).devicePixelRatio; - } - - void onDownloadAllButtonAction(OnDownloadAllButtonAction? onDownloadAllButtonAction) { - _onDownloadAllButtonAction = onDownloadAllButtonAction; - } - - void onDownloadAttachmentFileAction(OnDownloadAttachmentFileAction onDownloadAttachmentFileAction) { - _onDownloadAttachmentFileAction = onDownloadAttachmentFileAction; - } - - void onViewAttachmentFileAction(OnViewAttachmentFileAction onViewAttachmentFileAction) { - _onViewAttachmentFileAction = onViewAttachmentFileAction; - } - - void onCancelButtonAction(OnCancelButtonAction onCancelButtonAction) { - _onCancelButtonAction = onCancelButtonAction; - } - - void onCloseButtonAction(OnCloseButtonAction onCloseButtonAction) { - _onCloseButtonAction = onCloseButtonAction; - } - - Future show() { - return showModalBottomSheet( - context: _context, - isScrollControlled: true, - barrierColor: AttachmentListStyles.barrierColor, - backgroundColor: AttachmentListStyles.modalBackgroundColor, - enableDrag: false, - builder: (context) => AttachmentListBottomSheetBodyBuilder( - imagePaths: _imagePaths, - attachments: _attachments, - statusBarHeight: _statusBarHeight, - scrollController: _scrollController, - onDownloadAllButtonAction: _onDownloadAllButtonAction, - onDownloadAttachmentFileAction: _onDownloadAttachmentFileAction, - onViewAttachmentFileAction: _onViewAttachmentFileAction, - onCancelButtonAction: _onCancelButtonAction, - onCloseButtonAction: _onCloseButtonAction, - singleEmailControllerTag: _singleEmailControllerTag, - ), - ); - } -} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/attachment_list/attachment_list_dialog_body_builder.dart b/lib/features/email/presentation/widgets/attachment_list/attachment_list_dialog_body_builder.dart deleted file mode 100644 index d947762544..0000000000 --- a/lib/features/email/presentation/widgets/attachment_list/attachment_list_dialog_body_builder.dart +++ /dev/null @@ -1,157 +0,0 @@ -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'; -import 'package:model/email/attachment.dart'; -import 'package:tmail_ui_user/features/email/presentation/styles/attachment/attachment_list_styles.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_item_widget.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_action_button_builder.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_item_widget.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -class AttachmentListDialogBodyBuilder extends StatelessWidget { - final BuildContext context; - final ImagePaths imagePaths; - final List attachments; - final ScrollController scrollController; - final double? widthDialog; - final double? heightDialog; - final OnDownloadAllButtonAction? onDownloadAllButtonAction; - final OnDownloadAttachmentFileAction? onDownloadAttachmentFileAction; - final OnViewAttachmentFileAction? onViewAttachmentFileAction; - final OnCancelButtonAction? onCancelButtonAction; - final OnCloseButtonAction? onCloseButtonAction; - final String? singleEmailControllerTag; - - const AttachmentListDialogBodyBuilder({ - super.key, - required this.context, - required this.imagePaths, - required this.attachments, - required this.scrollController, - this.widthDialog, - this.heightDialog, - this.onDownloadAllButtonAction, - this.onDownloadAttachmentFileAction, - this.onViewAttachmentFileAction, - this.onCancelButtonAction, - this.onCloseButtonAction, - this.singleEmailControllerTag, - }); - - @override - Widget build(BuildContext context) { - return Container( - width: widthDialog, - height: heightDialog, - decoration: AttachmentListStyles.dialogBodyDecoration, - child: Column( - children: [ - Container( - padding: AttachmentListStyles.headerPadding, - decoration: AttachmentListStyles.headerDecoration, - child: Row( - children: [ - const SizedBox(width: 50), - Expanded( - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - AppLocalizations.of(context).attachmentList, - style: AttachmentListStyles.titleTextStyle, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: AttachmentListStyles.titleSpace), - Flexible( - child: Text( - '${attachments.length} ${AppLocalizations.of(context).files}', - style: AttachmentListStyles.subTitleTextStyle, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - ) - ), - const SizedBox(width: 8), - TMailButtonWidget.fromIcon( - icon: imagePaths.icCircleClose, - onTapActionCallback: onCloseButtonAction, - iconSize: 28, - backgroundColor: Colors.transparent, - padding: const EdgeInsets.all(5), - ), - const SizedBox(width: 8) - ], - ) - ), - Expanded( - child: RawScrollbar( - trackColor: AttachmentListStyles.scrollbarTrackColor, - thumbColor: AttachmentListStyles.scrollbarThumbColor, - radius: AttachmentListStyles.scrollbarThumbRadius, - trackRadius: AttachmentListStyles.scrollbarTrackRadius, - thickness: AttachmentListStyles.scrollbarThickness, - thumbVisibility: true, - trackVisibility: true, - controller: scrollController, - trackBorderColor: AttachmentListStyles.scrollbarTrackBorderColor, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), - child: ListView.separated( - controller: scrollController, - shrinkWrap: true, - physics: const ScrollPhysics(), - itemCount: attachments.length, - itemBuilder: (context, index) { - return AttachmentListItemWidget( - attachment: attachments[index], - downloadAttachmentAction: onDownloadAttachmentFileAction, - viewAttachmentAction: onViewAttachmentFileAction, - singleEmailControllerTag: singleEmailControllerTag, - ); - }, - separatorBuilder: (context, index) { - return const Divider( - color: AttachmentListStyles.separatorColor, - ); - }, - ), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (onDownloadAllButtonAction != null) - AttachmentListActionButtonBuilder( - name: AppLocalizations.of(context).downloadAll, - bgColor: AppColor.primaryColor, - textStyle: AttachmentListStyles.downloadAllButtonTextStyle.copyWith( - color: Colors.white, - ), - action: onDownloadAllButtonAction, - ), - if (onDownloadAllButtonAction != null && onCancelButtonAction != null) - const SizedBox(width: AttachmentListStyles.buttonsSpaceBetween), - if (onCancelButtonAction != null) - AttachmentListActionButtonBuilder( - name: AppLocalizations.of(context).close, - bgColor: AttachmentListStyles.cancelButtonColor, - textStyle: AttachmentListStyles.cancelButtonTextStyle - ) - ], - ), - const SizedBox(height: AttachmentListStyles.dialogBottomSpace) - ], - ), - ); - } -} diff --git a/lib/features/email/presentation/widgets/attachment_list/attachment_list_dialog_builder.dart b/lib/features/email/presentation/widgets/attachment_list/attachment_list_dialog_builder.dart deleted file mode 100644 index 83188b73df..0000000000 --- a/lib/features/email/presentation/widgets/attachment_list/attachment_list_dialog_builder.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:flutter/material.dart'; -import 'package:model/email/attachment.dart'; -import 'package:tmail_ui_user/features/email/presentation/styles/attachment/attachment_list_styles.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_item_widget.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_dialog_body_builder.dart'; - -class AttachmentListDialogBuilder extends StatelessWidget { - - final ImagePaths imagePaths; - final List attachments; - final ResponsiveUtils responsiveUtils; - final ScrollController scrollController; - - final Color? backgroundColor; - final double? widthDialog; - final double? heightDialog; - final OnDownloadAllButtonAction? onDownloadAllButtonAction; - final OnDownloadAttachmentFileAction? onDownloadAttachmentFileAction; - final OnViewAttachmentFileAction? onViewAttachmentFileAction; - final OnCancelButtonAction? onCancelButtonAction; - final OnCloseButtonAction? onCloseButtonAction; - final String? singleEmailControllerTag; - - const AttachmentListDialogBuilder({ - Key? key, - required this.imagePaths, - required this.attachments, - required this.responsiveUtils, - required this.scrollController, - this.backgroundColor, - this.widthDialog, - this.heightDialog, - this.onDownloadAllButtonAction, - this.onDownloadAttachmentFileAction, - this.onViewAttachmentFileAction, - this.onCancelButtonAction, - this.onCloseButtonAction, - this.singleEmailControllerTag, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Dialog( - key: const ValueKey('attachment_list_dialog'), - shape: AttachmentListStyles.shapeBorder, - insetPadding: responsiveUtils.isDesktop(context) - ? AttachmentListStyles.dialogPaddingWeb - : AttachmentListStyles.dialogPaddingTablet, - alignment: Alignment.center, - backgroundColor: backgroundColor, - child: AttachmentListDialogBodyBuilder( - context: context, - imagePaths: imagePaths, - attachments: attachments, - widthDialog: widthDialog, - heightDialog: heightDialog, - scrollController: scrollController, - onDownloadAllButtonAction: onDownloadAllButtonAction, - onDownloadAttachmentFileAction: onDownloadAttachmentFileAction, - onViewAttachmentFileAction: onViewAttachmentFileAction, - onCancelButtonAction: onCancelButtonAction, - onCloseButtonAction: onCloseButtonAction, - singleEmailControllerTag: singleEmailControllerTag, - ), - ); - } -} diff --git a/lib/features/email/presentation/widgets/attachment_list/attachment_list_item_widget.dart b/lib/features/email/presentation/widgets/attachment_list/attachment_list_item_widget.dart deleted file mode 100644 index 688252c512..0000000000 --- a/lib/features/email/presentation/widgets/attachment_list/attachment_list_item_widget.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/utils/style_utils.dart'; -import 'package:core/presentation/views/button/tmail_button_widget.dart'; -import 'package:core/presentation/views/text/middle_ellipsis_text.dart'; -import 'package:filesize/filesize.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:model/email/attachment.dart'; -import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; -import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; -import 'package:tmail_ui_user/features/email/presentation/styles/attachment/attachment_list_item_widget_styles.dart'; -import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_item_widget.dart'; - -class AttachmentListItemWidget extends StatelessWidget { - - final Attachment attachment; - final OnDownloadAttachmentFileAction? downloadAttachmentAction; - final OnViewAttachmentFileAction? viewAttachmentAction; - final String? singleEmailControllerTag; - - final _imagePaths = Get.find(); - - AttachmentListItemWidget({ - Key? key, - required this.attachment, - this.downloadAttachmentAction, - this.viewAttachmentAction, - this.singleEmailControllerTag, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Obx( - () { - final controller = Get.find(tag: singleEmailControllerTag); - final attachmentsViewState = controller.attachmentsViewState; - bool isLoading = false; - if (attachment.blobId != null) { - isLoading = !EmailUtils.checkingIfAttachmentActionIsEnabled( - attachmentsViewState[attachment.blobId!]); - } - - return Material( - type: MaterialType.transparency, - child: InkWell( - onTap: isLoading ? null : () => (viewAttachmentAction?? downloadAttachmentAction)?.call(attachment), - child: Padding( - padding: AttachmentListItemWidgetStyle.contentPadding, - child: Row( - children: [ - isLoading - ? const SizedBox( - width: AttachmentListItemWidgetStyle.iconSize, - height: AttachmentListItemWidgetStyle.iconSize, - child: CircularProgressIndicator(strokeWidth: 2)) - : SvgPicture.asset(attachment.getIcon(_imagePaths), - width: AttachmentListItemWidgetStyle.iconSize, - height: AttachmentListItemWidgetStyle.iconSize, - fit: BoxFit.fill - ), - const SizedBox(width: AttachmentListItemWidgetStyle.space), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MiddleEllipsisText( - (attachment.name ?? ''), - style: AttachmentListItemWidgetStyle.labelTextStyle, - ), - const SizedBox(height: AttachmentListItemWidgetStyle.fileTitleBottomSpace), - Text( - filesize(attachment.size?.value), - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - style: AttachmentListItemWidgetStyle.sizeLabelTextStyle, - ) - ] - ) - ), - const SizedBox(width: AttachmentListItemWidgetStyle.space), - TMailButtonWidget.fromIcon( - icon: _imagePaths.icDownloadAttachment, - backgroundColor: Colors.transparent, - onTapActionCallback: isLoading - ? null - : () => downloadAttachmentAction?.call(attachment) - ) - ] - ), - ), - ), - ); - } - ); - } -} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/attachments_info.dart b/lib/features/email/presentation/widgets/attachments_info.dart index 416db76875..b200a03346 100644 --- a/lib/features/email/presentation/widgets/attachments_info.dart +++ b/lib/features/email/presentation/widgets/attachments_info.dart @@ -52,79 +52,42 @@ class AttachmentsInfo extends StatelessWidget { style: ThemeUtils.textStyleInter400.copyWith( fontSize: 15, height: 20 / 15, + letterSpacing: -0.24, color: AppColor.gray99A2AD, ), ); - if (responsiveUtils.isMobile(context)) { - return Row( - children: [ - iconAttachment, - Expanded( - child: Padding( - padding: const EdgeInsetsDirectional.only(start: 8, end: 3), - child: titleHeaderAttachment, - ), + return Row( + children: [ + iconAttachment, + Expanded( + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 8, end: 3), + child: titleHeaderAttachment, ), - if (displayShowAll && numberOfAttachments > 3) - _buildShowAllButton(context), - ], - ); - } else { - return Row( - children: [ - iconAttachment, - Expanded( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Padding( - padding: const EdgeInsetsDirectional.only(start: 8, end: 3), - child: titleHeaderAttachment, - ), - ), - if (displayShowAll && numberOfAttachments > 4) - _buildShowAllButton(context), - ], + ), + if (onTapDownloadAllButton != null) + TMailButtonWidget( + text: AppLocalizations.of(context).downloadAll, + icon: !responsiveUtils.isMobile(context) + ? imagePaths.icDownloadAttachment + : imagePaths.icDownloadAll, + iconSize: 20, + iconColor: AppColor.steelGrayA540, + iconAlignment: TextDirection.rtl, + backgroundColor: Colors.transparent, + borderRadius: 5, + mainAxisSize: MainAxisSize.min, + flexibleText: true, + maxLines: 1, + maxWidth: 300, + padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 5), + textStyle: ThemeUtils.textStyleBodyBody1().copyWith( + color: AppColor.steelGray400, ), + onTapActionCallback: onTapDownloadAllButton, ), - if (onTapDownloadAllButton != null) - TMailButtonWidget( - text: AppLocalizations.of(context).archiveAndDownload, - icon: imagePaths.icDownloadAll, - iconSize: 20, - iconColor: AppColor.steelGrayA540, - iconAlignment: TextDirection.rtl, - backgroundColor: Colors.transparent, - borderRadius: 5, - mainAxisSize: MainAxisSize.min, - flexibleText: true, - maxLines: 1, - maxWidth: 300, - padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 5), - textStyle: ThemeUtils.textStyleBodyBody1().copyWith( - color: AppColor.steelGray400, - ), - onTapActionCallback: onTapDownloadAllButton, - ), - ], - ); - } - } - - Widget _buildShowAllButton(BuildContext context) { - return TMailButtonWidget.fromText( - text: AppLocalizations.of(context).showAll, - backgroundColor: Colors.transparent, - textStyle: ThemeUtils.textStyleBodyBody1().copyWith( - color: AppColor.steelGray400, - ), - borderRadius: 5, - maxLines: 1, - maxWidth: 120, - padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 5), - onTapActionCallback: onTapShowAllAttachmentFile, + ], ); } } diff --git a/lib/features/email/presentation/widgets/email_attachments_widget.dart b/lib/features/email/presentation/widgets/email_attachments_widget.dart index 06f8952dca..48dc4e19fb 100644 --- a/lib/features/email/presentation/widgets/email_attachments_widget.dart +++ b/lib/features/email/presentation/widgets/email_attachments_widget.dart @@ -3,8 +3,9 @@ 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/utils/theme_utils.dart'; -import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/presentation/views/dialog/confirm_dialog_button.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:model/email/attachment.dart'; @@ -25,6 +26,7 @@ class EmailAttachmentsWidget extends StatelessWidget { final ResponsiveUtils responsiveUtils; final ImagePaths imagePaths; final OnTapActionCallback? onTapShowAllAttachmentFile; + final OnTapActionCallback? onTapHideAllAttachments; final bool showDownloadAllAttachmentsButton; final bool isDisplayAllAttachments; final OnTapActionCallback? onTapDownloadAllButton; @@ -40,8 +42,9 @@ class EmailAttachmentsWidget extends StatelessWidget { this.downloadAttachmentAction, this.viewAttachmentAction, this.onTapShowAllAttachmentFile, + this.onTapHideAllAttachments, this.showDownloadAllAttachmentsButton = false, - this.isDisplayAllAttachments = true, + this.isDisplayAllAttachments = false, this.onTapDownloadAllButton, this.singleEmailControllerTag, }); @@ -59,35 +62,43 @@ class EmailAttachmentsWidget extends StatelessWidget { : null, ); - if (responsiveUtils.isMobile(context)) { + bool isMobileResponsive = responsiveUtils.isMobile(context); + + if (isMobileResponsive) { final attachmentRecord = _getDisplayedAndHiddenAttachment( context, + isMobileResponsive, responsiveUtils.getDeviceWidth(context), ); - final displayedAttachments = attachmentRecord.displayedAttachments; - final hiddenItemsCount = attachmentRecord.hiddenItemsCount; + + final displayedAttachments = isDisplayAllAttachments + ? attachments + : attachmentRecord.displayedAttachments; + final hiddenItemsCount = isDisplayAllAttachments + ? 0 + : attachmentRecord.hiddenItemsCount; return Padding( - padding: const EdgeInsetsDirectional.only(top: 12, bottom: 24), + padding: const EdgeInsetsDirectional.only( + start: 16, + end: 16, + bottom: 24, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + attachmentHeader, Padding( - padding: const EdgeInsetsDirectional.symmetric(horizontal: 12), - child: attachmentHeader, - ), - Padding( - padding: const EdgeInsetsDirectional.only( - start: 12, - end: 12, - top: 4, - ), + padding: const EdgeInsetsDirectional.only(top: 4), child: Column( children: displayedAttachments.map((attachment) { return AttachmentItemWidget( attachment: attachment, imagePaths: imagePaths, - margin: const EdgeInsets.only(top: 8), + margin: const EdgeInsets.only( + top: EmailUtils.attachmentItemSpacing, + ), + width: EmailUtils.desktopItemMaxWidth, downloadAttachmentAction: downloadAttachmentAction, viewAttachmentAction: viewAttachmentAction, singleEmailControllerTag: singleEmailControllerTag, @@ -95,204 +106,175 @@ class EmailAttachmentsWidget extends StatelessWidget { }).toList(), ), ), - const SizedBox(height: 12), - if (hiddenItemsCount > 0 && showDownloadAllAttachmentsButton) - Padding( - padding: const EdgeInsetsDirectional.symmetric(horizontal: 8), - child: Row( - children: [ - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).moreAttachments( - hiddenItemsCount, - ), - backgroundColor: Colors.transparent, - borderRadius: 5, - maxWidth: 120, - maxLines: 1, - textStyle: ThemeUtils.textStyleM3TitleSmall, - padding: const EdgeInsets.symmetric( - vertical: 3, - horizontal: 5, - ), - onTapActionCallback: onTapShowAllAttachmentFile, - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: TMailButtonWidget( - text: AppLocalizations - .of(context) - .archiveAndDownload, - icon: imagePaths.icDownloadAll, - iconSize: 20, - iconColor: AppColor.steelGrayA540, - iconAlignment: TextDirection.rtl, - backgroundColor: Colors.transparent, - borderRadius: 5, - mainAxisSize: MainAxisSize.min, - maxLines: 1, - flexibleText: true, - padding: const EdgeInsets.symmetric( - vertical: 3, - horizontal: 5, - ), - textStyle: ThemeUtils.textStyleBodyBody1() - .copyWith(color: AppColor.steelGray400), - onTapActionCallback: onTapDownloadAllButton, - ), - ), - ], - ), - ), - ], - ), - ) - else if (hiddenItemsCount > 0) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).moreAttachments( - hiddenItemsCount, - ), - backgroundColor: Colors.transparent, - borderRadius: 5, - maxLines: 1, - textStyle: ThemeUtils.textStyleM3TitleSmall, - padding: const EdgeInsets.symmetric( - vertical: 3, - horizontal: 5, + const SizedBox(height: EmailUtils.attachmentItemSpacing), + if (hiddenItemsCount > 0) + SizedBox( + height: EmailUtils.attachmentItemHeight, + child: ConfirmDialogButton( + icon: imagePaths.icAttachment, + iconSize: 16, + iconColor: AppColor.steelGrayA540, + label: AppLocalizations.of(context).showMoreAttachmentButton( + hiddenItemsCount, + ), + textStyle: ThemeUtils.textStyleBodyBody1( + color: AppColor.steelGray400, + ), + radius: 5, + onTapAction: onTapShowAllAttachmentFile, ), - margin: const EdgeInsetsDirectional.symmetric(horizontal: 8), - onTapActionCallback: onTapShowAllAttachmentFile, - ) - else if (showDownloadAllAttachmentsButton) - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Flexible( - child: TMailButtonWidget( - text: AppLocalizations.of(context).archiveAndDownload, - icon: imagePaths.icDownloadAll, - iconSize: 20, - iconColor: AppColor.steelGrayA540, - iconAlignment: TextDirection.rtl, - backgroundColor: Colors.transparent, - borderRadius: 5, - mainAxisSize: MainAxisSize.min, - maxLines: 1, - flexibleText: true, - padding: const EdgeInsets.symmetric( - vertical: 3, - horizontal: 5, - ), - margin: const EdgeInsetsDirectional.symmetric( - horizontal: 8, - ), - textStyle: ThemeUtils.textStyleBodyBody1().copyWith( - color: AppColor.steelGray400, - ), - onTapActionCallback: onTapDownloadAllButton, - ), - ), - ], + ), + if (isDisplayAllAttachments) + SizedBox( + height: EmailUtils.attachmentItemHeight, + child: ConfirmDialogButton( + icon: imagePaths.icAttachment, + iconSize: 16, + iconColor: AppColor.steelGrayA540, + label: AppLocalizations.of(context).hideAttachmentButton( + attachmentRecord.hiddenItemsCount, + ), + textStyle: ThemeUtils.textStyleBodyBody1( + color: AppColor.steelGray400, + ), + radius: 5, + onTapAction: onTapHideAllAttachments, ), + ), ], ), ); } else { return Padding( padding: const EdgeInsetsDirectional.only( - start: 16, - end: 16, - top: 16, - bottom: 28, + start: 8, + end: 8, + top: 12, + bottom: 16, ), - child: LayoutBuilder( - builder: (context, constraints) { - final attachmentRecord = _getDisplayedAndHiddenAttachment( - context, - constraints.maxWidth, - ); - final displayedAttachments = attachmentRecord.displayedAttachments; - final hiddenItemsCount = attachmentRecord.hiddenItemsCount; + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 12), + child: attachmentHeader, + ), + Padding( + padding: const EdgeInsetsDirectional.only( + start: 12, + end: 12, + top: 12, + ), + child: LayoutBuilder( + builder: (context, constraints) { + final attachmentRecord = _getDisplayedAndHiddenAttachment( + context, + isMobileResponsive, + constraints.maxWidth, + ); + final displayedAttachments = isDisplayAllAttachments + ? attachments + : attachmentRecord.displayedAttachments; + final hiddenItemsCount = isDisplayAllAttachments + ? 0 + : attachmentRecord.hiddenItemsCount; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsetsDirectional.symmetric(horizontal: 12), - child: attachmentHeader, - ), - Padding( - padding: const EdgeInsetsDirectional.only( - start: 12, - end: 12, - top: 12, - ), - child: Wrap( - spacing: 8, - runSpacing: isDisplayAllAttachments ? 8 : 0, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - ...displayedAttachments.map((attachment) { - if (responsiveUtils.isDesktop(context)) { - return DraggableAttachmentItemWidget( - attachment: attachment, - imagePaths: imagePaths, - width: EmailUtils.desktopItemMaxWidth, - onDragStarted: onDragStarted, - onDragEnd: onDragEnd, - downloadAttachmentAction: downloadAttachmentAction, - viewAttachmentAction: viewAttachmentAction, - singleEmailControllerTag: singleEmailControllerTag, - ); - } else { - return AttachmentItemWidget( - attachment: attachment, - imagePaths: imagePaths, - width: EmailUtils.desktopItemMaxWidth, - downloadAttachmentAction: downloadAttachmentAction, - viewAttachmentAction: viewAttachmentAction, - singleEmailControllerTag: singleEmailControllerTag, - ); - } - }).toList(), - if (hiddenItemsCount > 0) - TMailButtonWidget.fromText( - text: '+ $hiddenItemsCount', - backgroundColor: Colors.transparent, - borderRadius: 5, - maxWidth: EmailUtils.desktopMoreButtonMaxWidth, - maxLines: 1, - textStyle: ThemeUtils.textStyleM3TitleSmall, - padding: const EdgeInsets.symmetric( - vertical: 3, - horizontal: 5, + return Wrap( + spacing: EmailUtils.attachmentItemSpacing, + runSpacing: isDisplayAllAttachments + ? EmailUtils.attachmentItemSpacing + : 0, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...displayedAttachments.map((attachment) { + if (PlatformInfo.isWeb && + responsiveUtils.isDesktop(context)) { + return DraggableAttachmentItemWidget( + attachment: attachment, + imagePaths: imagePaths, + width: EmailUtils.desktopItemMaxWidth, + onDragStarted: onDragStarted, + onDragEnd: onDragEnd, + downloadAttachmentAction: downloadAttachmentAction, + viewAttachmentAction: viewAttachmentAction, + singleEmailControllerTag: singleEmailControllerTag, + ); + } else { + return AttachmentItemWidget( + attachment: attachment, + imagePaths: imagePaths, + width: EmailUtils.desktopItemMaxWidth, + downloadAttachmentAction: downloadAttachmentAction, + viewAttachmentAction: viewAttachmentAction, + singleEmailControllerTag: singleEmailControllerTag, + ); + } + }).toList(), + if (hiddenItemsCount > 0) + SizedBox( + height: EmailUtils.attachmentItemHeight, + child: ConfirmDialogButton( + icon: imagePaths.icAttachment, + iconSize: 16, + iconColor: AppColor.steelGrayA540, + label: AppLocalizations.of(context).showMoreAttachmentButton( + hiddenItemsCount, + ), + textStyle: ThemeUtils.textStyleBodyBody1( + color: AppColor.steelGray400, + ), + radius: 5, + onTapAction: onTapShowAllAttachmentFile, + ), ), - onTapActionCallback: onTapShowAllAttachmentFile, - ), - ], - ), - ), - ], - ); - } + if (isDisplayAllAttachments) + SizedBox( + height: EmailUtils.attachmentItemHeight, + child: ConfirmDialogButton( + icon: imagePaths.icAttachment, + iconSize: 16, + iconColor: AppColor.steelGrayA540, + label: AppLocalizations.of(context).hideAttachmentButton( + attachmentRecord.hiddenItemsCount, + ), + textStyle: ThemeUtils.textStyleBodyBody1( + color: AppColor.steelGray400, + ), + radius: 5, + onTapAction: onTapHideAllAttachments, + ), + ), + ], + ); + } + ), + ), + ], ), ); } } ({List displayedAttachments, int hiddenItemsCount}) - _getDisplayedAndHiddenAttachment(BuildContext context, double maxWidth) { - if (isDisplayAllAttachments) { - return (displayedAttachments: attachments, hiddenItemsCount: 0); - } + _getDisplayedAndHiddenAttachment( + BuildContext context, + bool isMobile, + double maxWidth, + ) { + final showMoreButtonMaxWidth = EmailUtils.estimateTextWidth( + context: context, + text: AppLocalizations.of(context).showMoreAttachmentButton(999), + textStyle: ThemeUtils.textStyleBodyBody1( + color: AppColor.steelGray400, + ), + locale: Localizations.localeOf(context), + ); final displayedAttachments = EmailUtils.getAttachmentDisplayed( maxWidth: maxWidth, attachments: attachments, - isMobile: responsiveUtils.isMobile(context), + isMobile: isMobile, + showMoreButtonMaxWidth: showMoreButtonMaxWidth, ); int hiddenItemsCount = attachments.length - displayedAttachments.length; @@ -301,6 +283,7 @@ class EmailAttachmentsWidget extends StatelessWidget { } else if (hiddenItemsCount < 0) { hiddenItemsCount = 0; } + log('EmailAttachmentsWidget::_getDisplayedAndHiddenAttachment: Displayed: ${displayedAttachments.length}, Hidden: $hiddenItemsCount'); return ( displayedAttachments: displayedAttachments, diff --git a/lib/features/email/presentation/widgets/email_receiver_widget.dart b/lib/features/email/presentation/widgets/email_receiver_widget.dart index 8571d26b68..6e9585bd52 100644 --- a/lib/features/email/presentation/widgets/email_receiver_widget.dart +++ b/lib/features/email/presentation/widgets/email_receiver_widget.dart @@ -182,6 +182,7 @@ class _EmailReceiverWidgetState extends State { fontSize: 15 ), backgroundColor: Colors.transparent, + padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 5), onTapActionCallback: () => setState(() => _isDisplayAll = false), ) ] diff --git a/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart b/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart index 90181e4339..96fa5ed2de 100644 --- a/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart +++ b/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart @@ -8,6 +8,7 @@ import 'package:model/email/read_actions.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; +import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/model/context_item_email_action.dart'; import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_email_action.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; @@ -17,6 +18,7 @@ import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_emai import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/parsing_email_opened_properties_extension.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; @@ -217,4 +219,15 @@ extension OnThreadDetailActionClick on ThreadDetailController { return destinationMailbox.id; } + + void onOpenAttachmentListAction(double screenHeight) { + mailboxDashBoardController.dispatchEmailUIAction( + OpenAttachmentListAction( + emailId: currentExpandedEmailId.value, + countAttachments: currentAttachmentsList.length, + screenHeight: screenHeight, + isDisplayAllAttachments: isDisplayAllAttachments, + ), + ); + } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/parsing_email_opened_properties_extension.dart b/lib/features/thread_detail/presentation/extension/parsing_email_opened_properties_extension.dart new file mode 100644 index 0000000000..96b807ffc5 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/parsing_email_opened_properties_extension.dart @@ -0,0 +1,10 @@ +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; + +extension ParsingEmailOpenedPropertiesExtension on ThreadDetailController { + List get currentAttachmentsList => + currentEmailLoaded.value?.attachments ?? []; + + bool get isEmailExpandedHasAttachments => + currentAttachmentsList.isNotEmpty == true; +} diff --git a/lib/features/thread_detail/presentation/thread_detail_controller.dart b/lib/features/thread_detail/presentation/thread_detail_controller.dart index a4316abd1b..f55a507cd2 100644 --- a/lib/features/thread_detail/presentation/thread_detail_controller.dart +++ b/lib/features/thread_detail/presentation/thread_detail_controller.dart @@ -123,6 +123,7 @@ class ThreadDetailController extends BaseController { ScrollController? scrollController; CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; bool loadThreadOnThreadChanged = false; + bool isDisplayAllAttachments = false; AccountId? get accountId => mailboxDashBoardController.accountId.value; Session? get session => mailboxDashBoardController.sessionCurrent; @@ -232,6 +233,7 @@ class ThreadDetailController extends BaseController { currentEmailLoaded.value = null; cachedEmailLoaded.clear(); _threadGetDebouncer.value = null; + isDisplayAllAttachments = false; } @override diff --git a/lib/features/thread_detail/presentation/thread_detail_view.dart b/lib/features/thread_detail/presentation/thread_detail_view.dart index 36da9ee83c..0b5e0479ec 100644 --- a/lib/features/thread_detail/presentation/thread_detail_view.dart +++ b/lib/features/thread_detail/presentation/thread_detail_view.dart @@ -11,19 +11,19 @@ import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action import 'package:tmail_ui_user/features/email/presentation/styles/email_view_app_bar_widget_styles.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_bottom_bar_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_details_email_views.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/on_thread_page_changed.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/parsing_email_opened_properties_extension.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_cupertino_loading_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -import '../../manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; - class ThreadDetailView extends GetWidget { const ThreadDetailView({super.key}); @@ -44,6 +44,11 @@ class ThreadDetailView extends GetWidget { threadDetailCanPermanentlyDelete: controller.threadDetailCanPermanentlyDelete, onThreadActionClick: controller.onThreadDetailActionClick, onThreadMoreActionClick: controller.onThreadDetailMoreActionClick, + onOpenAttachmentListAction: controller.isEmailExpandedHasAttachments + ? () => controller.onOpenAttachmentListAction( + controller.responsiveUtils.getSizeScreenHeight(context), + ) + : null, optionWidgets: [ if (controller.previousAvailable) TMailButtonWidget.fromIcon( @@ -135,8 +140,11 @@ class ThreadDetailView extends GetWidget { itemCount: manager.isThreadDetailEnabled ? manager.availableThreadIds.length : manager.currentDisplayedEmails.length, - itemBuilder: (context, index) { - return SingleChildScrollView(child: threadBody); + itemBuilder: (_, __) { + return SingleChildScrollView( + controller: controller.scrollController, + child: threadBody, + ); }, onPageChanged: controller.onThreadPageChanged, ), diff --git a/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart b/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart index 626c90cb39..2bcc417c14 100644 --- a/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart +++ b/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart @@ -29,6 +29,7 @@ class ThreadDetailAppBar extends StatelessWidget { this.optionWidgets = const [], this.onThreadActionClick, this.onThreadMoreActionClick, + this.onOpenAttachmentListAction, }); final ResponsiveUtils responsiveUtils; @@ -43,9 +44,12 @@ class ThreadDetailAppBar extends StatelessWidget { final List optionWidgets; final OnThreadActionClick? onThreadActionClick; final OnThreadMoreActionClick? onThreadMoreActionClick; + final VoidCallback? onOpenAttachmentListAction; @override Widget build(BuildContext context) { + final isMobileResponsive = responsiveUtils.isMobile(context); + final child = LayoutBuilder( builder: (context, constraints) { Widget backButton = EmailViewBackButton( @@ -55,7 +59,7 @@ class ThreadDetailAppBar extends StatelessWidget { isSearchActivated: isSearchRunning, maxWidth: constraints.maxWidth, ); - if (responsiveUtils.isMobile(context)) { + if (isMobileResponsive) { backButton = Expanded( child: Align( alignment: Alignment.centerLeft, @@ -84,6 +88,14 @@ class ThreadDetailAppBar extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ if (_supportDisplayMailboxNameTitle(context)) backButton, + if (isMobileResponsive && onOpenAttachmentListAction != null) + _ThreadDetailAppBarButton( + icon: imagePaths.icAttachment, + tooltipMessage: AppLocalizations.of(context).attachments, + responsiveUtils: responsiveUtils, + iconColor: EmailViewAppBarWidgetStyles.iconColor, + onTapActionCallback: (_) => onOpenAttachmentListAction?.call(), + ), if (isThreadDetailEnabled && threadActionReady) ...[ _ThreadDetailAppBarButton( icon: threadDetailIsStarred @@ -118,7 +130,7 @@ class ThreadDetailAppBar extends StatelessWidget { : (_) => onThreadActionClick?.call(EmailActionType.moveToTrash), ), ], - if (!responsiveUtils.isMobile(context)) const Spacer(), + if (!isMobileResponsive) const Spacer(), if (AppUtils.getCurrentDirection(context) == TextDirection.rtl) ...optionWidgets.reversed else diff --git a/lib/features/upload/presentation/model/upload_file_state.dart b/lib/features/upload/presentation/model/upload_file_state.dart index acdb5aca03..b3a16cc5a0 100644 --- a/lib/features/upload/presentation/model/upload_file_state.dart +++ b/lib/features/upload/presentation/model/upload_file_state.dart @@ -74,10 +74,10 @@ class UploadFileState with EquatableMixin { if (mediaType == null && file != null) { mediaType = MediaType.parse(file!.mimeType); } - return mediaType?.getIcon(imagePaths, fileName: fileName) ?? imagePaths.icFileEPup; + return mediaType?.getIcon(imagePaths, fileName: fileName) ?? imagePaths.icFileDefault; } catch (e) { logError('UploadFileState::getIcon: Exception: $e'); - return imagePaths.icFileEPup; + return imagePaths.icFileDefault; } } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 66e27e6315..09627c5ced 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2025-09-19T09:56:48.738620", + "@@last_modified": "2025-10-07T12:02:25.245876", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3330,16 +3330,6 @@ "placeholders_order": [], "placeholders": {} }, - "moreAttachments": "+ {count} more", - "@moreAttachments": { - "type": "text", - "placeholders_order": [ - "count" - ], - "placeholders": { - "count": {} - } - }, "attachmentList": "Attachment list", "@attachmentList": { "type": "text", @@ -3940,16 +3930,6 @@ "placeholders_order": [], "placeholders": {} }, - "showMore": "Show more (+{count})", - "@showMore": { - "type": "text", - "placeholders_order": [ - "count" - ], - "placeholders": { - "count": {} - } - }, "showLess": "Show less", "@showLess": { "type": "text", @@ -4887,5 +4867,25 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "showMoreAttachmentButton": "Show +{count} more", + "@showMoreAttachmentButton": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "hideAttachmentButton": "Hide {count}", + "@hideAttachmentButton": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 04de6749e3..f82ee052cb 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3419,14 +3419,6 @@ class AppLocalizations { ); } - String moreAttachments(int count) { - return Intl.message( - '+ $count more', - name: 'moreAttachments', - args: [count] - ); - } - String get attachmentList { return Intl.message( 'Attachment list', @@ -4099,13 +4091,6 @@ class AppLocalizations { ); } - String showMore(int count) { - return Intl.message( - 'Show more (+$count)', - name: 'showMore', - args: [count]); - } - String get showLess { return Intl.message( 'Show less', @@ -5160,4 +5145,20 @@ class AppLocalizations { name: 'spamReportToggleDescription', ); } + + String showMoreAttachmentButton(int count) { + return Intl.message( + 'Show +$count more', + name: 'showMoreAttachmentButton', + args: [count], + ); + } + + String hideAttachmentButton(int count) { + return Intl.message( + 'Hide $count', + name: 'hideAttachmentButton', + args: [count], + ); + } } diff --git a/test/features/email/presentation/get_attachment_displayed_test.dart b/test/features/email/presentation/get_attachment_displayed_test.dart index d820410ec8..d34948e2e3 100644 --- a/test/features/email/presentation/get_attachment_displayed_test.dart +++ b/test/features/email/presentation/get_attachment_displayed_test.dart @@ -57,7 +57,7 @@ void main() { attachments: attachments, isMobile: false, ); - expect(result, attachments.sublist(0, 3)); + expect(result, attachments.sublist(0, 2)); }); test( @@ -83,6 +83,48 @@ void main() { ); expect(result, attachments); }); + + test('returns all if total width <= maxWidth', () { + final attachments = generateAttachments(5); + final result = EmailUtils.getAttachmentDisplayed( + maxWidth: 1400, + attachments: attachments, + isMobile: false, + ); + expect(result.length, attachments.length); + }); + + test('returns some items when total width > maxWidth', () { + final attachments = generateAttachments(5); + final result = EmailUtils.getAttachmentDisplayed( + maxWidth: 600, + attachments: attachments, + isMobile: false, + ); + expect(result.length, 1); + expect(result.first.name, 'A0'); + }); + + test('returns 2 items if enough for 2 + button', () { + final attachments = generateAttachments(5); + final result = EmailUtils.getAttachmentDisplayed( + maxWidth: 800, + attachments: attachments, + isMobile: false, + ); + expect(result.length, 2); + expect(result.map((e) => e.name), ['A0', 'A1']); + }); + + test('returns at least 1 even if not enough for one item + button', () { + final attachments = generateAttachments(5); + final result = EmailUtils.getAttachmentDisplayed( + maxWidth: 100, + attachments: attachments, + isMobile: false, + ); + expect(result.length, 1); + }); }); }); }