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();
}
void clearFocusRecipients() {
toAddressFocusNode?.unfocus();
ccAddressFocusNode?.unfocus();
bccAddressFocusNode?.unfocus();
replyToAddressFocusNode?.unfocus();
}
void clearFocusSubject() {
subjectEmailInputFocusNode?.unfocus();
}
void _closeSuggestionBox() {
if (toEmailAddressController.text.isEmpty) {
keyToEmailTagEditor.currentState?.closeSuggestionBox();
@@ -233,7 +233,8 @@ class EmailView extends GetWidget<SingleEmailController> {
List<String>? 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<SingleEmailController> {
controller.mailboxDashBoardController.mapMailboxById,
),
)),
if (!isMobile)
if (!isMobileResponsive)
const SizedBox(height: 16),
Obx(() => MailUnsubscribedBanner(
presentationEmail: controller.currentEmail,
@@ -295,7 +296,7 @@ class EmailView extends GetWidget<SingleEmailController> {
Obx(() => EmailViewLoadingBarWidget(
viewState: controller.emailLoadedViewState.value
)),
if (!isMobile)
if (!isMobileResponsive)
_buildAttachmentsList(context),
if (calendarEvent != null)
Column(
@@ -440,7 +441,7 @@ class EmailView extends GetWidget<SingleEmailController> {
height: 5,
color: Colors.transparent,
),
if (isMobile)
if (isMobileResponsive)
_buildAttachmentsList(context),
],
);
@@ -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<Attachment> 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);
@@ -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),
@@ -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,
@@ -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<Attachment> 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,
+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": {
"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": {}
}
}
}
+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 {
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],
);
}
}
@@ -57,7 +57,7 @@ void main() {
attachments: attachments,
isMobile: false,
);
expect(result, attachments.sublist(0, 3));
expect(result, attachments.sublist(0, 2));
});
test(