From 6c7331960fbb336ad827557ed49dfca7f57d16c3 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 7 Oct 2025 12:49:35 +0700 Subject: [PATCH] TF-4075 Apply new design for attachments view --- .../presentation/composer_controller.dart | 11 -- .../email/presentation/email_view.dart | 9 +- .../email/presentation/utils/email_utils.dart | 47 ++++++- .../widgets/attachment_item_widget.dart | 20 +-- .../widgets/attachments_info.dart | 6 +- .../widgets/email_attachments_widget.dart | 122 ++++++++++++------ lib/l10n/intl_messages.arb | 42 +++--- lib/main/localizations/app_localizations.dart | 31 ++--- .../get_attachment_displayed_test.dart | 2 +- 9 files changed, 171 insertions(+), 119 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 54d9b93d3..d1ec38363 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1532,17 +1532,6 @@ class ComposerController extends BaseController subjectEmailInputFocusNode?.unfocus(); } - void clearFocusRecipients() { - toAddressFocusNode?.unfocus(); - ccAddressFocusNode?.unfocus(); - bccAddressFocusNode?.unfocus(); - replyToAddressFocusNode?.unfocus(); - } - - void clearFocusSubject() { - subjectEmailInputFocusNode?.unfocus(); - } - void _closeSuggestionBox() { if (toEmailAddressController.text.isEmpty) { keyToEmailTagEditor.currentState?.closeSuggestionBox(); diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 09b4bfdff..883273ddd 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -233,7 +233,8 @@ class EmailView extends GetWidget { List? emailAddressSender, ScrollController? scrollController, }) { - final isMobile = controller.responsiveUtils.isMobile(context); + final isMobileResponsive = controller.responsiveUtils.isMobile(context); + return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -286,7 +287,7 @@ class EmailView extends GetWidget { controller.mailboxDashBoardController.mapMailboxById, ), )), - if (!isMobile) + if (!isMobileResponsive) const SizedBox(height: 16), Obx(() => MailUnsubscribedBanner( presentationEmail: controller.currentEmail, @@ -295,7 +296,7 @@ class EmailView extends GetWidget { Obx(() => EmailViewLoadingBarWidget( viewState: controller.emailLoadedViewState.value )), - if (!isMobile) + if (!isMobileResponsive) _buildAttachmentsList(context), if (calendarEvent != null) Column( @@ -440,7 +441,7 @@ class EmailView extends GetWidget { height: 5, color: Colors.transparent, ), - if (isMobile) + if (isMobileResponsive) _buildAttachmentsList(context), ], ); diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index 015cf9a7e..82ab96c61 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,9 +23,10 @@ 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 desktopMoreButtonMaxWidth = 150; static const double attachmentItemSpacing = 8; static const double attachmentItemHeight = 36; + static const double attachmentIcon = 20; static const int maxMobileVisibleAttachments = 3; EmailUtils._(); @@ -244,30 +246,38 @@ 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, maxMobileVisibleAttachments); + : attachments.sublist(0, maxVisibleAttachments); } - final totalNeededWidth = attachments.length * desktopItemMaxWidth + + final totalNeededWidth = attachments.length * attachmentItemWidth + (attachments.length - 1) * attachmentItemSpacing; if (totalNeededWidth <= maxWidth) { return attachments; } - final availableWidth = - maxWidth - desktopMoreButtonMaxWidth - attachmentItemSpacing; + 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 = - desktopItemMaxWidth + (i > 0 ? attachmentItemSpacing : 0); + attachmentItemWidth + (i > 0 ? attachmentItemSpacing : 0); if (usedWidth + nextWidth <= availableWidth) { usedWidth += nextWidth; visibleCount++; @@ -281,6 +291,29 @@ class EmailUtils { 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; + } + } + static String getDomainByEmailAddress(String emailAddress) { try { MailAddress mailAddress = MailAddress.validateAddress(emailAddress); diff --git a/lib/features/email/presentation/widgets/attachment_item_widget.dart b/lib/features/email/presentation/widgets/attachment_item_widget.dart index 5f3003ed2..0014554d8 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), diff --git a/lib/features/email/presentation/widgets/attachments_info.dart b/lib/features/email/presentation/widgets/attachments_info.dart index 20bdcf43b..b200a0334 100644 --- a/lib/features/email/presentation/widgets/attachments_info.dart +++ b/lib/features/email/presentation/widgets/attachments_info.dart @@ -68,8 +68,10 @@ class AttachmentsInfo extends StatelessWidget { ), if (onTapDownloadAllButton != null) TMailButtonWidget( - text: AppLocalizations.of(context).archiveAndDownload, - icon: imagePaths.icDownloadAll, + text: AppLocalizations.of(context).downloadAll, + icon: !responsiveUtils.isMobile(context) + ? imagePaths.icDownloadAttachment + : imagePaths.icDownloadAll, iconSize: 20, iconColor: AppColor.steelGrayA540, iconAlignment: TextDirection.rtl, diff --git a/lib/features/email/presentation/widgets/email_attachments_widget.dart b/lib/features/email/presentation/widgets/email_attachments_widget.dart index 0695d3413..48dc4e19f 100644 --- a/lib/features/email/presentation/widgets/email_attachments_widget.dart +++ b/lib/features/email/presentation/widgets/email_attachments_widget.dart @@ -62,31 +62,21 @@ class EmailAttachmentsWidget extends StatelessWidget { : null, ); - bool isMobile = responsiveUtils.isMobile(context); + bool isMobileResponsive = responsiveUtils.isMobile(context); - final hideButton = SizedBox( - height: EmailUtils.attachmentItemHeight, - width: isMobile ? double.infinity : null, - child: ConfirmDialogButton( - label: AppLocalizations.of(context).hideAll, - backgroundColor: Theme.of(context).colorScheme.outline.withValues( - alpha: 0.08, - ), - textStyle: ThemeUtils.textStyleM3TitleSmall.copyWith( - color: AppColor.steelGrayA540, - ), - radius: 5, - onTapAction: onTapHideAllAttachments, - ), - ); - - if (isMobile) { + if (isMobileResponsive) { final attachmentRecord = _getDisplayedAndHiddenAttachment( - isMobile, + 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( @@ -108,6 +98,7 @@ class EmailAttachmentsWidget extends StatelessWidget { margin: const EdgeInsets.only( top: EmailUtils.attachmentItemSpacing, ), + width: EmailUtils.desktopItemMaxWidth, downloadAttachmentAction: downloadAttachmentAction, viewAttachmentAction: viewAttachmentAction, singleEmailControllerTag: singleEmailControllerTag, @@ -119,23 +110,37 @@ class EmailAttachmentsWidget extends StatelessWidget { if (hiddenItemsCount > 0) SizedBox( height: EmailUtils.attachmentItemHeight, - width: double.infinity, child: ConfirmDialogButton( - label: AppLocalizations.of(context).moreAttachments( + icon: imagePaths.icAttachment, + iconSize: 16, + iconColor: AppColor.steelGrayA540, + label: AppLocalizations.of(context).showMoreAttachmentButton( hiddenItemsCount, ), - backgroundColor: Theme.of(context).colorScheme.outline.withValues( - alpha: 0.08, - ), - textStyle: ThemeUtils.textStyleM3TitleSmall.copyWith( - color: AppColor.steelGrayA540, + textStyle: ThemeUtils.textStyleBodyBody1( + color: AppColor.steelGray400, ), radius: 5, onTapAction: onTapShowAllAttachmentFile, ), ), if (isDisplayAllAttachments) - hideButton, + 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, + ), + ), ], ), ); @@ -163,11 +168,16 @@ class EmailAttachmentsWidget extends StatelessWidget { child: LayoutBuilder( builder: (context, constraints) { final attachmentRecord = _getDisplayedAndHiddenAttachment( - isMobile, + context, + isMobileResponsive, constraints.maxWidth, ); - final displayedAttachments = attachmentRecord.displayedAttachments; - final hiddenItemsCount = attachmentRecord.hiddenItemsCount; + final displayedAttachments = isDisplayAllAttachments + ? attachments + : attachmentRecord.displayedAttachments; + final hiddenItemsCount = isDisplayAllAttachments + ? 0 + : attachmentRecord.hiddenItemsCount; return Wrap( spacing: EmailUtils.attachmentItemSpacing, @@ -204,19 +214,36 @@ class EmailAttachmentsWidget extends StatelessWidget { SizedBox( height: EmailUtils.attachmentItemHeight, child: ConfirmDialogButton( - label: '+$hiddenItemsCount', - backgroundColor: Theme.of(context).colorScheme.outline.withValues( - alpha: 0.08, + icon: imagePaths.icAttachment, + iconSize: 16, + iconColor: AppColor.steelGrayA540, + label: AppLocalizations.of(context).showMoreAttachmentButton( + hiddenItemsCount, ), - textStyle: ThemeUtils.textStyleM3TitleSmall.copyWith( - color: AppColor.steelGrayA540, + textStyle: ThemeUtils.textStyleBodyBody1( + color: AppColor.steelGray400, ), radius: 5, onTapAction: onTapShowAllAttachmentFile, ), ), if (isDisplayAllAttachments) - hideButton, + 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, + ), + ), ], ); } @@ -229,15 +256,25 @@ class EmailAttachmentsWidget extends StatelessWidget { } ({List displayedAttachments, int hiddenItemsCount}) - _getDisplayedAndHiddenAttachment(bool isMobile, 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: isMobile, + showMoreButtonMaxWidth: showMoreButtonMaxWidth, ); int hiddenItemsCount = attachments.length - displayedAttachments.length; @@ -246,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/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 66e27e631..09627c5ce 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 04de6749e..f82ee052c 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 01c3a8928..d34948e2e 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(