TF-4075 Apply new design for attachments view

This commit is contained in:
dab246
2025-10-07 12:49:35 +07:00
committed by Dat H. Pham
parent 7057648b28
commit 6c7331960f
9 changed files with 171 additions and 119 deletions
@@ -1532,17 +1532,6 @@ class ComposerController extends BaseController
subjectEmailInputFocusNode?.unfocus(); subjectEmailInputFocusNode?.unfocus();
} }
void clearFocusRecipients() {
toAddressFocusNode?.unfocus();
ccAddressFocusNode?.unfocus();
bccAddressFocusNode?.unfocus();
replyToAddressFocusNode?.unfocus();
}
void clearFocusSubject() {
subjectEmailInputFocusNode?.unfocus();
}
void _closeSuggestionBox() { void _closeSuggestionBox() {
if (toEmailAddressController.text.isEmpty) { if (toEmailAddressController.text.isEmpty) {
keyToEmailTagEditor.currentState?.closeSuggestionBox(); keyToEmailTagEditor.currentState?.closeSuggestionBox();
@@ -233,7 +233,8 @@ class EmailView extends GetWidget<SingleEmailController> {
List<String>? emailAddressSender, List<String>? emailAddressSender,
ScrollController? scrollController, ScrollController? scrollController,
}) { }) {
final isMobile = controller.responsiveUtils.isMobile(context); final isMobileResponsive = controller.responsiveUtils.isMobile(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -286,7 +287,7 @@ class EmailView extends GetWidget<SingleEmailController> {
controller.mailboxDashBoardController.mapMailboxById, controller.mailboxDashBoardController.mapMailboxById,
), ),
)), )),
if (!isMobile) if (!isMobileResponsive)
const SizedBox(height: 16), const SizedBox(height: 16),
Obx(() => MailUnsubscribedBanner( Obx(() => MailUnsubscribedBanner(
presentationEmail: controller.currentEmail, presentationEmail: controller.currentEmail,
@@ -295,7 +296,7 @@ class EmailView extends GetWidget<SingleEmailController> {
Obx(() => EmailViewLoadingBarWidget( Obx(() => EmailViewLoadingBarWidget(
viewState: controller.emailLoadedViewState.value viewState: controller.emailLoadedViewState.value
)), )),
if (!isMobile) if (!isMobileResponsive)
_buildAttachmentsList(context), _buildAttachmentsList(context),
if (calendarEvent != null) if (calendarEvent != null)
Column( Column(
@@ -440,7 +441,7 @@ class EmailView extends GetWidget<SingleEmailController> {
height: 5, height: 5,
color: Colors.transparent, color: Colors.transparent,
), ),
if (isMobile) if (isMobileResponsive)
_buildAttachmentsList(context), _buildAttachmentsList(context),
], ],
); );
@@ -2,6 +2,7 @@ import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/state/success.dart';
import 'package:core/utils/app_logger.dart'; import 'package:core/utils/app_logger.dart';
import 'package:core/utils/mail/mail_address.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:get/get_utils/src/get_utils/get_utils.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
@@ -22,9 +23,10 @@ import 'package:tmail_ui_user/main/routes/route_utils.dart';
class EmailUtils { class EmailUtils {
static const double desktopItemMaxWidth = 260; static const double desktopItemMaxWidth = 260;
static const double desktopMoreButtonMaxWidth = 70; static const double desktopMoreButtonMaxWidth = 150;
static const double attachmentItemSpacing = 8; static const double attachmentItemSpacing = 8;
static const double attachmentItemHeight = 36; static const double attachmentItemHeight = 36;
static const double attachmentIcon = 20;
static const int maxMobileVisibleAttachments = 3; static const int maxMobileVisibleAttachments = 3;
EmailUtils._(); EmailUtils._();
@@ -244,30 +246,38 @@ class EmailUtils {
required double maxWidth, required double maxWidth,
required List<Attachment> attachments, required List<Attachment> attachments,
required bool isMobile, 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 (attachments.isEmpty) return [];
if (isMobile) { if (isMobile) {
return attachments.length <= maxMobileVisibleAttachments return attachments.length <= maxVisibleAttachments
? attachments ? attachments
: attachments.sublist(0, maxMobileVisibleAttachments); : attachments.sublist(0, maxVisibleAttachments);
} }
final totalNeededWidth = attachments.length * desktopItemMaxWidth + final totalNeededWidth = attachments.length * attachmentItemWidth +
(attachments.length - 1) * attachmentItemSpacing; (attachments.length - 1) * attachmentItemSpacing;
if (totalNeededWidth <= maxWidth) { if (totalNeededWidth <= maxWidth) {
return attachments; return attachments;
} }
final availableWidth = final availableWidth = maxWidth -
maxWidth - desktopMoreButtonMaxWidth - attachmentItemSpacing; showMoreButtonMaxWidth -
attachmentIcon -
attachmentItemSpacing * 4;
log('EmailUtils::getAttachmentDisplayed: availableWidth = $availableWidth, maxWidth = $maxWidth, showMoreButtonMaxWidth = $showMoreButtonMaxWidth, attachmentIcon = $attachmentIcon, attachmentItemSpacing = $attachmentItemSpacing');
double usedWidth = 0; double usedWidth = 0;
int visibleCount = 0; int visibleCount = 0;
for (int i = 0; i < attachments.length; i++) { for (int i = 0; i < attachments.length; i++) {
final nextWidth = final nextWidth =
desktopItemMaxWidth + (i > 0 ? attachmentItemSpacing : 0); attachmentItemWidth + (i > 0 ? attachmentItemSpacing : 0);
if (usedWidth + nextWidth <= availableWidth) { if (usedWidth + nextWidth <= availableWidth) {
usedWidth += nextWidth; usedWidth += nextWidth;
visibleCount++; visibleCount++;
@@ -281,6 +291,29 @@ class EmailUtils {
return attachments.sublist(0, visibleCount); 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) { static String getDomainByEmailAddress(String emailAddress) {
try { try {
MailAddress mailAddress = MailAddress.validateAddress(emailAddress); MailAddress mailAddress = MailAddress.validateAddress(emailAddress);
@@ -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/button/tmail_button_widget.dart';
import 'package:core/presentation/views/container/tmail_container_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/presentation/views/text/middle_ellipsis_text.dart';
import 'package:core/utils/platform_info.dart';
import 'package:filesize/filesize.dart'; import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.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( final bodyItemWidget = Row(
children: [ children: [
isLoading ? loadingIndicator : attachmentIcon, isLoading ? loadingIndicator : attachmentIcon,
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: PlatformInfo.isCanvasKit child: attachmentTitleWithMiddleDots,
? attachmentTitleWithMiddleDots
: attachmentTitleWithEndDots,
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsetsDirectional.only(start: 8, end: 3),
child: Text( child: Text(
filesize(attachment.size?.value), filesize(attachment.size?.value),
maxLines: 1, maxLines: 1,
@@ -97,10 +85,10 @@ class AttachmentItemWidget extends StatelessWidget {
), ),
), ),
TMailButtonWidget.fromIcon( TMailButtonWidget.fromIcon(
icon: imagePaths.icFileDownload, icon: imagePaths.icDownloadAttachment,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(5),
iconColor: AppColor.steelGrayA540, iconColor: AppColor.primaryColor,
iconSize: 20, iconSize: 20,
onTapActionCallback: onTapActionCallback:
isLoading ? null : () => _onTapDownloadAction(attachment), isLoading ? null : () => _onTapDownloadAction(attachment),
@@ -68,8 +68,10 @@ class AttachmentsInfo extends StatelessWidget {
), ),
if (onTapDownloadAllButton != null) if (onTapDownloadAllButton != null)
TMailButtonWidget( TMailButtonWidget(
text: AppLocalizations.of(context).archiveAndDownload, text: AppLocalizations.of(context).downloadAll,
icon: imagePaths.icDownloadAll, icon: !responsiveUtils.isMobile(context)
? imagePaths.icDownloadAttachment
: imagePaths.icDownloadAll,
iconSize: 20, iconSize: 20,
iconColor: AppColor.steelGrayA540, iconColor: AppColor.steelGrayA540,
iconAlignment: TextDirection.rtl, iconAlignment: TextDirection.rtl,
@@ -62,31 +62,21 @@ class EmailAttachmentsWidget extends StatelessWidget {
: null, : null,
); );
bool isMobile = responsiveUtils.isMobile(context); bool isMobileResponsive = responsiveUtils.isMobile(context);
final hideButton = SizedBox( if (isMobileResponsive) {
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) {
final attachmentRecord = _getDisplayedAndHiddenAttachment( final attachmentRecord = _getDisplayedAndHiddenAttachment(
isMobile, context,
isMobileResponsive,
responsiveUtils.getDeviceWidth(context), 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( return Padding(
padding: const EdgeInsetsDirectional.only( padding: const EdgeInsetsDirectional.only(
@@ -108,6 +98,7 @@ class EmailAttachmentsWidget extends StatelessWidget {
margin: const EdgeInsets.only( margin: const EdgeInsets.only(
top: EmailUtils.attachmentItemSpacing, top: EmailUtils.attachmentItemSpacing,
), ),
width: EmailUtils.desktopItemMaxWidth,
downloadAttachmentAction: downloadAttachmentAction, downloadAttachmentAction: downloadAttachmentAction,
viewAttachmentAction: viewAttachmentAction, viewAttachmentAction: viewAttachmentAction,
singleEmailControllerTag: singleEmailControllerTag, singleEmailControllerTag: singleEmailControllerTag,
@@ -119,23 +110,37 @@ class EmailAttachmentsWidget extends StatelessWidget {
if (hiddenItemsCount > 0) if (hiddenItemsCount > 0)
SizedBox( SizedBox(
height: EmailUtils.attachmentItemHeight, height: EmailUtils.attachmentItemHeight,
width: double.infinity,
child: ConfirmDialogButton( child: ConfirmDialogButton(
label: AppLocalizations.of(context).moreAttachments( icon: imagePaths.icAttachment,
iconSize: 16,
iconColor: AppColor.steelGrayA540,
label: AppLocalizations.of(context).showMoreAttachmentButton(
hiddenItemsCount, hiddenItemsCount,
), ),
backgroundColor: Theme.of(context).colorScheme.outline.withValues( textStyle: ThemeUtils.textStyleBodyBody1(
alpha: 0.08, color: AppColor.steelGray400,
),
textStyle: ThemeUtils.textStyleM3TitleSmall.copyWith(
color: AppColor.steelGrayA540,
), ),
radius: 5, radius: 5,
onTapAction: onTapShowAllAttachmentFile, onTapAction: onTapShowAllAttachmentFile,
), ),
), ),
if (isDisplayAllAttachments) 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( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final attachmentRecord = _getDisplayedAndHiddenAttachment( final attachmentRecord = _getDisplayedAndHiddenAttachment(
isMobile, context,
isMobileResponsive,
constraints.maxWidth, constraints.maxWidth,
); );
final displayedAttachments = attachmentRecord.displayedAttachments; final displayedAttachments = isDisplayAllAttachments
final hiddenItemsCount = attachmentRecord.hiddenItemsCount; ? attachments
: attachmentRecord.displayedAttachments;
final hiddenItemsCount = isDisplayAllAttachments
? 0
: attachmentRecord.hiddenItemsCount;
return Wrap( return Wrap(
spacing: EmailUtils.attachmentItemSpacing, spacing: EmailUtils.attachmentItemSpacing,
@@ -204,19 +214,36 @@ class EmailAttachmentsWidget extends StatelessWidget {
SizedBox( SizedBox(
height: EmailUtils.attachmentItemHeight, height: EmailUtils.attachmentItemHeight,
child: ConfirmDialogButton( child: ConfirmDialogButton(
label: '+$hiddenItemsCount', icon: imagePaths.icAttachment,
backgroundColor: Theme.of(context).colorScheme.outline.withValues( iconSize: 16,
alpha: 0.08, iconColor: AppColor.steelGrayA540,
label: AppLocalizations.of(context).showMoreAttachmentButton(
hiddenItemsCount,
), ),
textStyle: ThemeUtils.textStyleM3TitleSmall.copyWith( textStyle: ThemeUtils.textStyleBodyBody1(
color: AppColor.steelGrayA540, color: AppColor.steelGray400,
), ),
radius: 5, radius: 5,
onTapAction: onTapShowAllAttachmentFile, onTapAction: onTapShowAllAttachmentFile,
), ),
), ),
if (isDisplayAllAttachments) 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<Attachment> displayedAttachments, int hiddenItemsCount}) ({List<Attachment> displayedAttachments, int hiddenItemsCount})
_getDisplayedAndHiddenAttachment(bool isMobile, double maxWidth) { _getDisplayedAndHiddenAttachment(
if (isDisplayAllAttachments) { BuildContext context,
return (displayedAttachments: attachments, hiddenItemsCount: 0); 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( final displayedAttachments = EmailUtils.getAttachmentDisplayed(
maxWidth: maxWidth, maxWidth: maxWidth,
attachments: attachments, attachments: attachments,
isMobile: isMobile, isMobile: isMobile,
showMoreButtonMaxWidth: showMoreButtonMaxWidth,
); );
int hiddenItemsCount = attachments.length - displayedAttachments.length; int hiddenItemsCount = attachments.length - displayedAttachments.length;
@@ -246,6 +283,7 @@ class EmailAttachmentsWidget extends StatelessWidget {
} else if (hiddenItemsCount < 0) { } else if (hiddenItemsCount < 0) {
hiddenItemsCount = 0; hiddenItemsCount = 0;
} }
log('EmailAttachmentsWidget::_getDisplayedAndHiddenAttachment: Displayed: ${displayedAttachments.length}, Hidden: $hiddenItemsCount'); log('EmailAttachmentsWidget::_getDisplayedAndHiddenAttachment: Displayed: ${displayedAttachments.length}, Hidden: $hiddenItemsCount');
return ( return (
displayedAttachments: displayedAttachments, displayedAttachments: displayedAttachments,
+21 -21
View File
@@ -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": "Initializing data...",
"@initializing_data": { "@initializing_data": {
"type": "text", "type": "text",
@@ -3330,16 +3330,6 @@
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
}, },
"moreAttachments": "+ {count} more",
"@moreAttachments": {
"type": "text",
"placeholders_order": [
"count"
],
"placeholders": {
"count": {}
}
},
"attachmentList": "Attachment list", "attachmentList": "Attachment list",
"@attachmentList": { "@attachmentList": {
"type": "text", "type": "text",
@@ -3940,16 +3930,6 @@
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
}, },
"showMore": "Show more (+{count})",
"@showMore": {
"type": "text",
"placeholders_order": [
"count"
],
"placeholders": {
"count": {}
}
},
"showLess": "Show less", "showLess": "Show less",
"@showLess": { "@showLess": {
"type": "text", "type": "text",
@@ -4887,5 +4867,25 @@
"type": "text", "type": "text",
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
},
"showMoreAttachmentButton": "Show +{count} more",
"@showMoreAttachmentButton": {
"type": "text",
"placeholders_order": [
"count"
],
"placeholders": {
"count": {}
}
},
"hideAttachmentButton": "Hide {count}",
"@hideAttachmentButton": {
"type": "text",
"placeholders_order": [
"count"
],
"placeholders": {
"count": {}
}
} }
} }
+16 -15
View File
@@ -3419,14 +3419,6 @@ class AppLocalizations {
); );
} }
String moreAttachments(int count) {
return Intl.message(
'+ $count more',
name: 'moreAttachments',
args: [count]
);
}
String get attachmentList { String get attachmentList {
return Intl.message( return Intl.message(
'Attachment list', '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 { String get showLess {
return Intl.message( return Intl.message(
'Show less', 'Show less',
@@ -5160,4 +5145,20 @@ class AppLocalizations {
name: 'spamReportToggleDescription', 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],
);
}
} }
@@ -57,7 +57,7 @@ void main() {
attachments: attachments, attachments: attachments,
isMobile: false, isMobile: false,
); );
expect(result, attachments.sublist(0, 3)); expect(result, attachments.sublist(0, 2));
}); });
test( test(