From 6bbebd8e673644929783c66c2f71f0e264e43641 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 6 Oct 2025 19:17:25 +0700 Subject: [PATCH] TF-4075 Change attachments location (cherry picked from commit ab23dac45d9dcf7c5e057f8e16974b3ed8205771) --- assets/images/ic_file_audio.svg | 4 + assets/images/ic_file_code.svg | 4 + assets/images/ic_file_default.svg | 10 + assets/images/ic_file_doc.svg | 5 + assets/images/ic_file_docx.svg | 4 - assets/images/ic_file_epup.svg | 4 - assets/images/ic_file_excel.svg | 5 + assets/images/ic_file_image.svg | 4 + assets/images/ic_file_odp.svg | 15 + assets/images/ic_file_ods.svg | 13 + assets/images/ic_file_odt.svg | 8 + assets/images/ic_file_pdf.svg | 12 +- assets/images/ic_file_png.svg | 4 - assets/images/ic_file_power_point.svg | 5 + assets/images/ic_file_pptx.svg | 5 - assets/images/ic_file_text.svg | 7 + assets/images/ic_file_video.svg | 4 + assets/images/ic_file_xlsx.svg | 4 - assets/images/ic_file_zip.svg | 14 +- .../preview/supported_preview_file_types.dart | 98 ------ .../extensions/media_type_extension.dart | 171 ++++++++--- .../lib/presentation/model/file_category.dart | 51 ++++ .../presentation/resources/image_paths.dart | 17 +- .../email/presentation/email_view.dart | 84 ++--- .../extensions/attachment_extension.dart | 9 +- .../email/presentation/utils/email_utils.dart | 39 ++- .../widgets/attachments_info.dart | 93 ++---- .../widgets/email_attachments_widget.dart | 286 ++++++------------ .../widgets/email_receiver_widget.dart | 1 + .../presentation/model/upload_file_state.dart | 4 +- .../get_attachment_displayed_test.dart | 42 +++ 31 files changed, 540 insertions(+), 486 deletions(-) create mode 100644 assets/images/ic_file_audio.svg create mode 100644 assets/images/ic_file_code.svg create mode 100644 assets/images/ic_file_default.svg create mode 100644 assets/images/ic_file_doc.svg delete mode 100644 assets/images/ic_file_docx.svg delete mode 100644 assets/images/ic_file_epup.svg create mode 100644 assets/images/ic_file_excel.svg create mode 100644 assets/images/ic_file_image.svg create mode 100644 assets/images/ic_file_odp.svg create mode 100644 assets/images/ic_file_ods.svg create mode 100644 assets/images/ic_file_odt.svg delete mode 100644 assets/images/ic_file_png.svg create mode 100644 assets/images/ic_file_power_point.svg delete mode 100644 assets/images/ic_file_pptx.svg create mode 100644 assets/images/ic_file_text.svg create mode 100644 assets/images/ic_file_video.svg delete mode 100644 assets/images/ic_file_xlsx.svg create mode 100644 core/lib/presentation/model/file_category.dart diff --git a/assets/images/ic_file_audio.svg b/assets/images/ic_file_audio.svg new file mode 100644 index 000000000..327498f85 --- /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 000000000..beb4a9e7a --- /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 000000000..3f232a301 --- /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 000000000..18872ff39 --- /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 f809ecc45..000000000 --- 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 7d561c8e1..000000000 --- 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 000000000..3c23703c2 --- /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 000000000..9225299c9 --- /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 000000000..4f0f55464 --- /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 000000000..8549b1c99 --- /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 000000000..a3335dac5 --- /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 f404dee74..7f3d8061f 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 d1d36d1e8..000000000 --- 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 000000000..6fd502013 --- /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 0c80e9829..000000000 --- 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 000000000..94daeda89 --- /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 000000000..2c875d9e0 --- /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 25738a203..000000000 --- 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 09f67c425..f21fb433d 100644 --- a/assets/images/ic_file_zip.svg +++ b/assets/images/ic_file_zip.svg @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + diff --git a/core/lib/domain/preview/supported_preview_file_types.dart b/core/lib/domain/preview/supported_preview_file_types.dart index 3dee94564..5a8b23a03 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 67a6d6bf8..d6a4b4c45 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/model/file_category.dart b/core/lib/presentation/model/file_category.dart new file mode 100644 index 000000000..e8265b7b9 --- /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 92a2a9a87..24c417b89 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/email_view.dart b/lib/features/email/presentation/email_view.dart index 9d090e1f9..dc7889a8b 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -233,6 +233,7 @@ class EmailView extends GetWidget { List? emailAddressSender, ScrollController? scrollController, }) { + final isMobile = controller.responsiveUtils.isMobile(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -285,8 +286,8 @@ class EmailView extends GetWidget { controller.mailboxDashBoardController.mapMailboxById, ), )), - if (!controller.responsiveUtils.isMobile(context)) - const SizedBox(height: 24), + if (!isMobile) + const SizedBox(height: 16), Obx(() => MailUnsubscribedBanner( presentationEmail: controller.currentEmail, emailUnsubscribe: controller.emailUnsubscribe.value @@ -294,6 +295,8 @@ class EmailView extends GetWidget { Obx(() => EmailViewLoadingBarWidget( viewState: controller.emailLoadedViewState.value )), + if (!isMobile) + _buildAttachmentsList(context), if (calendarEvent != null) Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -437,46 +440,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( - context, - controller.attachments, - ), - showDownloadAllAttachmentsButton: - controller.downloadAllButtonIsEnabled(), - onTapDownloadAllButton: () => - controller.handleDownloadAllAttachmentsAction( - 'TwakeMail-${DateTime.now()}', - ), - singleEmailControllerTag: tag, - ); - } else { - return const SizedBox.shrink(); - } - }), + if (isMobile) + _buildAttachmentsList(context), ], ); } + Widget _buildAttachmentsList(BuildContext context) { + return 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( + context, + controller.attachments, + ), + showDownloadAllAttachmentsButton: + controller.downloadAllButtonIsEnabled(), + onTapDownloadAllButton: () => + controller.handleDownloadAllAttachmentsAction( + 'TwakeMail-${DateTime.now()}', + ), + singleEmailControllerTag: tag, + ); + } else { + return const SizedBox.shrink(); + } + }); + } + bool _validateDisplayEventActionBanner({ required BuildContext context, required CalendarEvent event, diff --git a/lib/features/email/presentation/extensions/attachment_extension.dart b/lib/features/email/presentation/extensions/attachment_extension.dart index daa22a935..97509687a 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/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index 6202be5bb..0e1350954 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -23,7 +23,8 @@ 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 attachmentItemSpacing = 8; + static const int maxMobileVisibleAttachments = 3; EmailUtils._(); @@ -248,21 +249,35 @@ class EmailUtils { if (isMobile) { return attachments.length <= maxMobileVisibleAttachments ? attachments - : attachments.sublist(0, 3); + : attachments.sublist(0, maxMobileVisibleAttachments); } - final displayedCount = maxWidth ~/ desktopItemMaxWidth; - if (displayedCount == attachments.length) { + final totalNeededWidth = attachments.length * desktopItemMaxWidth + + (attachments.length - 1) * attachmentItemSpacing; + if (totalNeededWidth <= maxWidth) { return attachments; - } else { - final int possibleDisplayedCount = - ((maxWidth - desktopMoreButtonMaxWidth) ~/ desktopItemMaxWidth) - .clamp(0, attachments.length); - - return possibleDisplayedCount == 0 - ? [attachments.first] - : attachments.sublist(0, possibleDisplayedCount); } + + final availableWidth = + maxWidth - desktopMoreButtonMaxWidth - attachmentItemSpacing; + + double usedWidth = 0; + int visibleCount = 0; + + for (int i = 0; i < attachments.length; i++) { + final nextWidth = + desktopItemMaxWidth + (i > 0 ? attachmentItemSpacing : 0); + if (usedWidth + nextWidth <= availableWidth) { + usedWidth += nextWidth; + visibleCount++; + } else { + break; + } + } + + if (visibleCount == 0) visibleCount = 1; + + return attachments.sublist(0, visibleCount); } static String getDomainByEmailAddress(String emailAddress) { diff --git a/lib/features/email/presentation/widgets/attachments_info.dart b/lib/features/email/presentation/widgets/attachments_info.dart index 416db7687..20bdcf43b 100644 --- a/lib/features/email/presentation/widgets/attachments_info.dart +++ b/lib/features/email/presentation/widgets/attachments_info.dart @@ -52,79 +52,40 @@ 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).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, ), - 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 06f8952dc..ac8047e6f 100644 --- a/lib/features/email/presentation/widgets/email_attachments_widget.dart +++ b/lib/features/email/presentation/widgets/email_attachments_widget.dart @@ -1,10 +1,10 @@ import 'package:core/presentation/action/action_callback_define.dart'; -import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/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'; @@ -41,7 +41,7 @@ class EmailAttachmentsWidget extends StatelessWidget { this.viewAttachmentAction, this.onTapShowAllAttachmentFile, this.showDownloadAllAttachmentsButton = false, - this.isDisplayAllAttachments = true, + this.isDisplayAllAttachments = false, this.onTapDownloadAllButton, this.singleEmailControllerTag, }); @@ -68,20 +68,17 @@ class EmailAttachmentsWidget extends StatelessWidget { final hiddenItemsCount = 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( @@ -95,189 +92,106 @@ 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, - ), - 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, - ), - ), - ], + const SizedBox(height: 8), + if (hiddenItemsCount > 0) + SizedBox( + height: 36, + width: double.infinity, + child: ConfirmDialogButton( + label: AppLocalizations.of(context).moreAttachments( + hiddenItemsCount, + ), + backgroundColor: Theme.of(context).colorScheme.outline.withValues( + alpha: 0.08, + ), + textStyle: ThemeUtils.textStyleM3TitleSmall, + radius: 5, + onTapAction: onTapShowAllAttachmentFile, ), + ), ], ), ); } 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, + constraints.maxWidth, + ); + final displayedAttachments = attachmentRecord.displayedAttachments; + final hiddenItemsCount = 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: 36, + child: ConfirmDialogButton( + label: '+$hiddenItemsCount', + backgroundColor: Theme.of(context).colorScheme.outline.withValues( + alpha: 0.08, + ), + textStyle: ThemeUtils.textStyleM3TitleSmall, + radius: 5, + onTapAction: onTapShowAllAttachmentFile, + ), ), - onTapActionCallback: onTapShowAllAttachmentFile, - ), - ], - ), - ), - ], - ); - } + ], + ); + } + ), + ), + ], ), ); } diff --git a/lib/features/email/presentation/widgets/email_receiver_widget.dart b/lib/features/email/presentation/widgets/email_receiver_widget.dart index 8571d26b6..6e9585bd5 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/upload/presentation/model/upload_file_state.dart b/lib/features/upload/presentation/model/upload_file_state.dart index acdb5aca0..b3a16cc5a 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/test/features/email/presentation/get_attachment_displayed_test.dart b/test/features/email/presentation/get_attachment_displayed_test.dart index d820410ec..01c3a8928 100644 --- a/test/features/email/presentation/get_attachment_displayed_test.dart +++ b/test/features/email/presentation/get_attachment_displayed_test.dart @@ -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); + }); }); }); }