TF-4171 Display tag in mailbox list, search list on mobile
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import 'package:core/presentation/extensions/color_extension.dart';
|
import 'package:core/presentation/extensions/color_extension.dart';
|
||||||
import 'package:core/presentation/extensions/hex_color_extension.dart';
|
import 'package:core/presentation/extensions/hex_color_extension.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:jmap_dart_client/jmap/core/id.dart';
|
||||||
|
import 'package:labels/model/hex_color.dart';
|
||||||
import 'package:labels/model/label.dart';
|
import 'package:labels/model/label.dart';
|
||||||
|
|
||||||
extension LabelExtension on Label {
|
extension LabelExtension on Label {
|
||||||
@@ -23,4 +25,18 @@ extension LabelExtension on Label {
|
|||||||
return Colors.white;
|
return Colors.white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label copyWith({
|
||||||
|
Id? id,
|
||||||
|
String? keyword,
|
||||||
|
String? displayName,
|
||||||
|
HexColor? color,
|
||||||
|
}) {
|
||||||
|
return Label(
|
||||||
|
id: id ?? this.id,
|
||||||
|
displayName: displayName ?? this.displayName,
|
||||||
|
keyword: keyword ?? this.keyword,
|
||||||
|
color: color ?? this.color,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:core/presentation/utils/theme_utils.dart';
|
import 'package:core/presentation/utils/theme_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tmail_ui_user/features/labels/presentation/utils/label_utils.dart';
|
||||||
|
|
||||||
class TagWidget extends StatelessWidget {
|
class TagWidget extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
@@ -26,7 +27,7 @@ class TagWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget labelText = Text(
|
Widget labelText = Text(
|
||||||
isTruncateText ? _truncateName(text) : text,
|
isTruncateText ? LabelUtils.truncateLabel(text) : text,
|
||||||
style: ThemeUtils.textStyleContentCaption().copyWith(
|
style: ThemeUtils.textStyleContentCaption().copyWith(
|
||||||
color: textColor,
|
color: textColor,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class LabelUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final remainingCount = total - 1;
|
final remainingCount = total - 1;
|
||||||
final plus = _buildPlusLabel(remainingCount);
|
final plus = buildPlusLabel(remainingCount);
|
||||||
|
|
||||||
double plusWidth = effectiveMeasure(plus);
|
double plusWidth = effectiveMeasure(plus);
|
||||||
if (plusMaxWidth != null) {
|
if (plusMaxWidth != null) {
|
||||||
@@ -97,7 +97,7 @@ class LabelUtils {
|
|||||||
|
|
||||||
// Normal shrink logic
|
// Normal shrink logic
|
||||||
while (fitCount > 0) {
|
while (fitCount > 0) {
|
||||||
final plus = _buildPlusLabel(remaining);
|
final plus = buildPlusLabel(remaining);
|
||||||
|
|
||||||
double plusWidth = effectiveMeasure(plus);
|
double plusWidth = effectiveMeasure(plus);
|
||||||
if (plusMaxWidth != null) {
|
if (plusMaxWidth != null) {
|
||||||
@@ -151,7 +151,7 @@ class LabelUtils {
|
|||||||
plusMaxWidth: plusMaxWidth,
|
plusMaxWidth: plusMaxWidth,
|
||||||
measureWidth: measureWidth,
|
measureWidth: measureWidth,
|
||||||
);
|
);
|
||||||
} catch (_, __) {
|
} catch (_) {
|
||||||
if (tags.isEmpty) {
|
if (tags.isEmpty) {
|
||||||
return (displayed: const [], hiddenCount: 0);
|
return (displayed: const [], hiddenCount: 0);
|
||||||
}
|
}
|
||||||
@@ -186,8 +186,61 @@ class LabelUtils {
|
|||||||
return (fitCount: fit, usedWidth: used);
|
return (fitCount: fit, usedWidth: used);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Label _buildPlusLabel(int n) => Label(
|
static Label buildPlusLabel(int n) => Label(
|
||||||
id: Id(LabelExtension.moreLabelId),
|
id: Id(LabelExtension.moreLabelId),
|
||||||
displayName: n > 999 ? '+999+' : '+$n',
|
displayName: n > 999 ? '+999+' : '+$n',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static ({
|
||||||
|
List<Label> displayed,
|
||||||
|
int hiddenCount,
|
||||||
|
}) computeDisplayedLabelsByMaxDisplay({
|
||||||
|
required List<Label> labels,
|
||||||
|
int maxDisplay = 3,
|
||||||
|
}) {
|
||||||
|
if (labels.isEmpty) {
|
||||||
|
return (displayed: [], hiddenCount: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
final limited =
|
||||||
|
labels.length <= maxDisplay ? labels : labels.take(maxDisplay).toList();
|
||||||
|
|
||||||
|
final hiddenCount =
|
||||||
|
labels.length > maxDisplay ? labels.length - maxDisplay : 0;
|
||||||
|
|
||||||
|
return (displayed: limited, hiddenCount: hiddenCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ({
|
||||||
|
List<Label> displayed,
|
||||||
|
int hiddenCount,
|
||||||
|
}) safeComputeDisplayedLabelsByMaxDisplay({
|
||||||
|
required List<Label> labels,
|
||||||
|
int maxDisplay = 3,
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
return computeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: labels,
|
||||||
|
maxDisplay: maxDisplay,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
if (labels.isEmpty) {
|
||||||
|
return (displayed: const [], hiddenCount: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
displayed: [labels.first],
|
||||||
|
hiddenCount: labels.length - 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String truncateLabel(String name, {int maxLength = 16}) {
|
||||||
|
try {
|
||||||
|
if (name.length <= maxLength) return name;
|
||||||
|
return '${name.substring(0, maxLength - 1)}...';
|
||||||
|
} catch (_) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,26 @@ class LabelTagListWidget extends StatelessWidget {
|
|||||||
final List<Label> tags;
|
final List<Label> tags;
|
||||||
final double horizontalPadding;
|
final double horizontalPadding;
|
||||||
final double itemSpacing;
|
final double itemSpacing;
|
||||||
|
final bool autoWrapTagsByMaxWidth;
|
||||||
|
final bool isDesktop;
|
||||||
|
|
||||||
const LabelTagListWidget({
|
const LabelTagListWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.tags,
|
required this.tags,
|
||||||
this.horizontalPadding = 4,
|
this.horizontalPadding = 4,
|
||||||
this.itemSpacing = 12,
|
this.itemSpacing = 12,
|
||||||
|
this.autoWrapTagsByMaxWidth = false,
|
||||||
|
this.isDesktop = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return autoWrapTagsByMaxWidth
|
||||||
|
? _buildAutoWrapMode()
|
||||||
|
: _buildFixedDisplayMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAutoWrapMode() {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (_, constraints) {
|
builder: (_, constraints) {
|
||||||
final maxWidth = constraints.maxWidth;
|
final maxWidth = constraints.maxWidth;
|
||||||
@@ -25,7 +35,7 @@ class LabelTagListWidget extends StatelessWidget {
|
|||||||
final tagMaxWidth =
|
final tagMaxWidth =
|
||||||
maxWidth - LabelUtils.defaultPlusMaxWidth - itemSpacing;
|
maxWidth - LabelUtils.defaultPlusMaxWidth - itemSpacing;
|
||||||
|
|
||||||
final displayedTagsDataResult = LabelUtils.safeComputeDisplayedTags(
|
final result = LabelUtils.safeComputeDisplayedTags(
|
||||||
tags: tags,
|
tags: tags,
|
||||||
maxWidth: maxWidth,
|
maxWidth: maxWidth,
|
||||||
textStyle: ThemeUtils.textStyleContentCaption(),
|
textStyle: ThemeUtils.textStyleContentCaption(),
|
||||||
@@ -38,7 +48,7 @@ class LabelTagListWidget extends StatelessWidget {
|
|||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: itemSpacing,
|
spacing: itemSpacing,
|
||||||
runSpacing: 6,
|
runSpacing: 6,
|
||||||
children: displayedTagsDataResult.displayed
|
children: result.displayed
|
||||||
.map(
|
.map(
|
||||||
(tagDisplayed) => LabelWidget(
|
(tagDisplayed) => LabelWidget(
|
||||||
label: tagDisplayed,
|
label: tagDisplayed,
|
||||||
@@ -51,4 +61,35 @@ class LabelTagListWidget extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildFixedDisplayMode() {
|
||||||
|
final result = LabelUtils.safeComputeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: tags,
|
||||||
|
maxDisplay: isDesktop ? 3 : 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
final displayedWidgets = result.displayed.map((label) {
|
||||||
|
return LabelWidget(
|
||||||
|
label: label,
|
||||||
|
horizontalPadding: horizontalPadding,
|
||||||
|
isTruncateLabel: true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final plusWidget = (result.hiddenCount > 0)
|
||||||
|
? LabelWidget(
|
||||||
|
label: LabelUtils.buildPlusLabel(result.hiddenCount),
|
||||||
|
horizontalPadding: horizontalPadding,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
spacing: itemSpacing,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: [
|
||||||
|
...displayedWidgets,
|
||||||
|
if (plusWidget != null) plusWidget,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:core/utils/platform_info.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:labels/extensions/label_extension.dart';
|
import 'package:labels/extensions/label_extension.dart';
|
||||||
import 'package:labels/model/label.dart';
|
import 'package:labels/model/label.dart';
|
||||||
@@ -7,22 +8,29 @@ class LabelWidget extends StatelessWidget {
|
|||||||
final Label label;
|
final Label label;
|
||||||
final double horizontalPadding;
|
final double horizontalPadding;
|
||||||
final double? maxWidth;
|
final double? maxWidth;
|
||||||
|
final bool isTruncateLabel;
|
||||||
|
|
||||||
const LabelWidget({
|
const LabelWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.label,
|
required this.label,
|
||||||
this.horizontalPadding = 4,
|
this.horizontalPadding = 4,
|
||||||
this.maxWidth,
|
this.maxWidth,
|
||||||
|
this.isTruncateLabel = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final showTooltip =
|
||||||
|
PlatformInfo.isWeb && label.id?.value != LabelExtension.moreLabelId;
|
||||||
|
|
||||||
return TagWidget(
|
return TagWidget(
|
||||||
text: label.safeDisplayName,
|
text: label.safeDisplayName,
|
||||||
backgroundColor: label.backgroundColor,
|
backgroundColor: label.backgroundColor,
|
||||||
textColor: label.textColor,
|
textColor: label.textColor,
|
||||||
horizontalPadding: horizontalPadding,
|
horizontalPadding: horizontalPadding,
|
||||||
maxWidth: maxWidth,
|
maxWidth: maxWidth,
|
||||||
|
isTruncateText: isTruncateLabel,
|
||||||
|
showTooltip: showTooltip,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart';
|
|||||||
import 'package:tmail_ui_user/features/base/widget/keyboard/keyboard_handler_wrapper.dart';
|
import 'package:tmail_ui_user/features/base/widget/keyboard/keyboard_handler_wrapper.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart';
|
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart';
|
import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart';
|
||||||
|
import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart';
|
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_ai_needs_action_extension.dart';
|
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_ai_needs_action_extension.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart';
|
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart';
|
||||||
@@ -654,7 +655,10 @@ class SearchEmailView extends GetWidget<SearchEmailController>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildListEmailBody(BuildContext context, List<PresentationEmail> listPresentationEmail) {
|
Widget _buildListEmailBody(
|
||||||
|
BuildContext context,
|
||||||
|
List<PresentationEmail> listPresentationEmail,
|
||||||
|
) {
|
||||||
return NotificationListener<ScrollNotification>(
|
return NotificationListener<ScrollNotification>(
|
||||||
key: const Key('search_email_list_notification_listener'),
|
key: const Key('search_email_list_notification_listener'),
|
||||||
onNotification: (ScrollNotification scrollInfo) {
|
onNotification: (ScrollNotification scrollInfo) {
|
||||||
@@ -678,11 +682,15 @@ class SearchEmailView extends GetWidget<SearchEmailController>
|
|||||||
separatorBuilder: (context, index) {
|
separatorBuilder: (context, index) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final isMobile = PlatformInfo.isMobile;
|
final isMobile = PlatformInfo.isMobile;
|
||||||
|
final isLastItem = index == listPresentationEmail.length - 1;
|
||||||
final isInactive =
|
final isInactive =
|
||||||
controller.selectionMode.value == SelectMode.INACTIVE;
|
controller.selectionMode.value == SelectMode.INACTIVE;
|
||||||
|
|
||||||
final dividerColor =
|
final showDivider = !isLastItem;
|
||||||
isMobile ? null : (isInactive ? null : Colors.white);
|
|
||||||
|
final dividerColor = isMobile
|
||||||
|
? null
|
||||||
|
: (showDivider && isInactive ? null : Colors.white);
|
||||||
|
|
||||||
final padding = isMobile
|
final padding = isMobile
|
||||||
? SearchEmailUtils.getPaddingItemListMobile(
|
? SearchEmailUtils.getPaddingItemListMobile(
|
||||||
@@ -694,6 +702,10 @@ class SearchEmailView extends GetWidget<SearchEmailController>
|
|||||||
controller.responsiveUtils,
|
controller.responsiveUtils,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!showDivider) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Divider(color: dividerColor),
|
child: Divider(color: dividerColor),
|
||||||
@@ -720,6 +732,10 @@ class SearchEmailView extends GetWidget<SearchEmailController>
|
|||||||
|
|
||||||
final isAINeedsActionEnabled = dashboardController.isAINeedsActionEnabled;
|
final isAINeedsActionEnabled = dashboardController.isAINeedsActionEnabled;
|
||||||
|
|
||||||
|
final allLabels = dashboardController.labelController.labels;
|
||||||
|
|
||||||
|
final emailLabels = presentationEmail.getLabelList(allLabels);
|
||||||
|
|
||||||
return EmailTileBuilder(
|
return EmailTileBuilder(
|
||||||
presentationEmail: presentationEmail,
|
presentationEmail: presentationEmail,
|
||||||
selectAllMode: controller.selectionMode.value,
|
selectAllMode: controller.selectionMode.value,
|
||||||
@@ -728,6 +744,7 @@ class SearchEmailView extends GetWidget<SearchEmailController>
|
|||||||
isSenderImportantFlagEnabled: isSenderImportantFlagEnabled,
|
isSenderImportantFlagEnabled: isSenderImportantFlagEnabled,
|
||||||
isSearchEmailRunning: true,
|
isSearchEmailRunning: true,
|
||||||
isAINeedsActionEnabled: isAINeedsActionEnabled,
|
isAINeedsActionEnabled: isAINeedsActionEnabled,
|
||||||
|
labels: emailLabels,
|
||||||
padding: SearchEmailViewStyle.getPaddingSearchResultList(
|
padding: SearchEmailViewStyle.getPaddingSearchResultList(
|
||||||
context,
|
context,
|
||||||
controller.responsiveUtils,
|
controller.responsiveUtils,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:model/email/presentation_email.dart';
|
|||||||
import 'package:model/mailbox/presentation_mailbox.dart';
|
import 'package:model/mailbox/presentation_mailbox.dart';
|
||||||
import 'package:model/mailbox/select_mode.dart';
|
import 'package:model/mailbox/select_mode.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/labels/ai_action_tag_widget.dart';
|
import 'package:tmail_ui_user/features/base/widget/labels/ai_action_tag_widget.dart';
|
||||||
|
import 'package:tmail_ui_user/features/labels/presentation/widgets/label_list_widget.dart';
|
||||||
import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart';
|
import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart';
|
||||||
import 'package:tmail_ui_user/features/thread/presentation/mixin/base_email_item_tile.dart';
|
import 'package:tmail_ui_user/features/thread/presentation/mixin/base_email_item_tile.dart';
|
||||||
import 'package:tmail_ui_user/features/thread/presentation/styles/item_email_tile_styles.dart';
|
import 'package:tmail_ui_user/features/thread/presentation/styles/item_email_tile_styles.dart';
|
||||||
@@ -22,6 +23,7 @@ class EmailTileBuilder extends StatelessWidget with BaseEmailItemTile {
|
|||||||
final bool isShowingEmailContent;
|
final bool isShowingEmailContent;
|
||||||
final bool isSenderImportantFlagEnabled;
|
final bool isSenderImportantFlagEnabled;
|
||||||
final bool isAINeedsActionEnabled;
|
final bool isAINeedsActionEnabled;
|
||||||
|
final bool autoWrapTagsByMaxWidth;
|
||||||
final List<Label>? labels;
|
final List<Label>? labels;
|
||||||
final OnPressEmailActionClick? emailActionClick;
|
final OnPressEmailActionClick? emailActionClick;
|
||||||
final OnMoreActionClick? onMoreActionClick;
|
final OnMoreActionClick? onMoreActionClick;
|
||||||
@@ -36,6 +38,7 @@ class EmailTileBuilder extends StatelessWidget with BaseEmailItemTile {
|
|||||||
this.isSearchEmailRunning = false,
|
this.isSearchEmailRunning = false,
|
||||||
this.isSenderImportantFlagEnabled = true,
|
this.isSenderImportantFlagEnabled = true,
|
||||||
this.isAINeedsActionEnabled = true,
|
this.isAINeedsActionEnabled = true,
|
||||||
|
this.autoWrapTagsByMaxWidth = false,
|
||||||
this.mailboxContain,
|
this.mailboxContain,
|
||||||
this.padding,
|
this.padding,
|
||||||
this.isDrag = false,
|
this.isDrag = false,
|
||||||
@@ -128,17 +131,7 @@ class EmailTileBuilder extends StatelessWidget with BaseEmailItemTile {
|
|||||||
)),
|
)),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsetsDirectional.only(top: 2),
|
padding: const EdgeInsetsDirectional.only(top: 2),
|
||||||
child: Row(children: [
|
child: _buildPartialContent(context),
|
||||||
Expanded(child: buildEmailPartialContent(
|
|
||||||
context,
|
|
||||||
presentationEmail,
|
|
||||||
isSearchEmailRunning,
|
|
||||||
searchQuery)),
|
|
||||||
if (_shouldShowAIAction)
|
|
||||||
const AiActionTagWidget(
|
|
||||||
margin: EdgeInsetsDirectional.only(start: 8),
|
|
||||||
),
|
|
||||||
])
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -146,6 +139,94 @@ class EmailTileBuilder extends StatelessWidget with BaseEmailItemTile {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPartialContent(BuildContext context) {
|
||||||
|
final hasLabels = labels?.isNotEmpty == true;
|
||||||
|
final isDesktop = responsiveUtils.isDesktop(context);
|
||||||
|
final isAutoWrap = autoWrapTagsByMaxWidth;
|
||||||
|
|
||||||
|
final hasContent =
|
||||||
|
presentationEmail.getPartialContent().isNotEmpty;
|
||||||
|
|
||||||
|
final partialContent = buildEmailPartialContent(
|
||||||
|
context,
|
||||||
|
presentationEmail,
|
||||||
|
isSearchEmailRunning,
|
||||||
|
searchQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasLabels) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (hasContent)
|
||||||
|
Flexible(child: partialContent),
|
||||||
|
if (_shouldShowAIAction)
|
||||||
|
const AiActionTagWidget(
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasContent) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (_shouldShowAIAction)
|
||||||
|
const AiActionTagWidget(
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 8),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: LabelTagListWidget(
|
||||||
|
tags: labels!,
|
||||||
|
autoWrapTagsByMaxWidth: isAutoWrap,
|
||||||
|
isDesktop: isDesktop,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAutoWrap) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Flexible(child: partialContent),
|
||||||
|
if (_shouldShowAIAction)
|
||||||
|
const AiActionTagWidget(
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 8),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
LabelTagListWidget(
|
||||||
|
tags: labels!,
|
||||||
|
autoWrapTagsByMaxWidth: isAutoWrap,
|
||||||
|
isDesktop: isDesktop,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (_, constraints) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: partialContent),
|
||||||
|
if (_shouldShowAIAction)
|
||||||
|
const AiActionTagWidget(
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 8),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: constraints.maxWidth / 2),
|
||||||
|
child: LabelTagListWidget(
|
||||||
|
tags: labels!,
|
||||||
|
autoWrapTagsByMaxWidth: isAutoWrap,
|
||||||
|
isDesktop: isDesktop,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bool get _shouldShowAIAction =>
|
bool get _shouldShowAIAction =>
|
||||||
isAINeedsActionEnabled && presentationEmail.hasNeedAction;
|
isAINeedsActionEnabled && presentationEmail.hasNeedAction;
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,7 @@ class EmailTileBuilder extends StatefulWidget {
|
|||||||
final bool isShowingEmailContent;
|
final bool isShowingEmailContent;
|
||||||
final bool isSenderImportantFlagEnabled;
|
final bool isSenderImportantFlagEnabled;
|
||||||
final bool isAINeedsActionEnabled;
|
final bool isAINeedsActionEnabled;
|
||||||
|
final bool autoWrapTagsByMaxWidth;
|
||||||
final List<Label>? labels;
|
final List<Label>? labels;
|
||||||
final OnPressEmailActionClick? emailActionClick;
|
final OnPressEmailActionClick? emailActionClick;
|
||||||
final OnMoreActionClick? onMoreActionClick;
|
final OnMoreActionClick? onMoreActionClick;
|
||||||
@@ -43,6 +44,7 @@ class EmailTileBuilder extends StatefulWidget {
|
|||||||
this.searchQuery,
|
this.searchQuery,
|
||||||
this.isSearchEmailRunning = false,
|
this.isSearchEmailRunning = false,
|
||||||
this.isSenderImportantFlagEnabled = true,
|
this.isSenderImportantFlagEnabled = true,
|
||||||
|
this.autoWrapTagsByMaxWidth = false,
|
||||||
this.mailboxContain,
|
this.mailboxContain,
|
||||||
this.padding,
|
this.padding,
|
||||||
this.isDrag = false,
|
this.isDrag = false,
|
||||||
@@ -100,88 +102,78 @@ class _EmailTileBuilderState extends State<EmailTileBuilder> with BaseEmailItem
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(children: [
|
child: Column(
|
||||||
Row(children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
if (!widget.presentationEmail.hasRead)
|
children: [
|
||||||
Padding(
|
Row(children: [
|
||||||
padding: const EdgeInsetsDirectional.only(end: 5),
|
if (!widget.presentationEmail.hasRead)
|
||||||
child: buildIconUnreadStatus(),
|
Padding(
|
||||||
),
|
padding: const EdgeInsetsDirectional.only(end: 5),
|
||||||
Expanded(
|
child: buildIconUnreadStatus(),
|
||||||
child: buildInformationSender(
|
|
||||||
context,
|
|
||||||
widget.presentationEmail,
|
|
||||||
widget.mailboxContain,
|
|
||||||
widget.isSearchEmailRunning,
|
|
||||||
widget.searchQuery
|
|
||||||
)
|
|
||||||
),
|
|
||||||
buildIconAnsweredOrForwarded(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
presentationEmail: widget.presentationEmail
|
|
||||||
),
|
|
||||||
if (widget.presentationEmail.hasAttachment == true)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsetsDirectional.only(start: 8),
|
|
||||||
child: buildIconAttachment(),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsetsDirectional.only(
|
|
||||||
end: 4,
|
|
||||||
start: 8
|
|
||||||
),
|
|
||||||
child: buildDateTime(context, widget.presentationEmail)
|
|
||||||
),
|
|
||||||
buildIconChevron()
|
|
||||||
]),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (widget.presentationEmail.hasCalendarEvent)
|
|
||||||
buildCalendarEventIcon(
|
|
||||||
context: context,
|
|
||||||
presentationEmail: widget.presentationEmail
|
|
||||||
),
|
),
|
||||||
if (widget.presentationEmail.isMarkAsImportant && widget.isSenderImportantFlagEnabled)
|
|
||||||
buildMarkAsImportantIcon(context),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: buildEmailTitle(
|
child: buildInformationSender(
|
||||||
context,
|
context,
|
||||||
widget.presentationEmail,
|
widget.presentationEmail,
|
||||||
|
widget.mailboxContain,
|
||||||
widget.isSearchEmailRunning,
|
widget.isSearchEmailRunning,
|
||||||
widget.searchQuery
|
widget.searchQuery
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
buildMailboxContain(
|
buildIconAnsweredOrForwarded(
|
||||||
context,
|
width: 16,
|
||||||
widget.isSearchEmailRunning,
|
height: 16,
|
||||||
widget.presentationEmail
|
presentationEmail: widget.presentationEmail
|
||||||
),
|
),
|
||||||
if (widget.presentationEmail.hasStarred)
|
if (widget.presentationEmail.hasAttachment == true)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsetsDirectional.only(start: 8),
|
padding: const EdgeInsetsDirectional.only(start: 8),
|
||||||
child: buildIconStar(),
|
child: buildIconAttachment(),
|
||||||
),
|
),
|
||||||
],
|
Padding(
|
||||||
),
|
padding: const EdgeInsetsDirectional.only(
|
||||||
const SizedBox(height: 2),
|
end: 4,
|
||||||
Row(children: [
|
start: 8
|
||||||
Expanded(
|
),
|
||||||
child: buildEmailPartialContent(
|
child: buildDateTime(context, widget.presentationEmail)
|
||||||
context,
|
|
||||||
widget.presentationEmail,
|
|
||||||
widget.isSearchEmailRunning,
|
|
||||||
widget.searchQuery
|
|
||||||
)
|
|
||||||
),
|
|
||||||
if (_shouldShowAIAction)
|
|
||||||
const AiActionTagWidget(
|
|
||||||
margin: EdgeInsetsDirectional.only(start: 8),
|
|
||||||
),
|
),
|
||||||
]),
|
buildIconChevron()
|
||||||
]),
|
]),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (widget.presentationEmail.hasCalendarEvent)
|
||||||
|
buildCalendarEventIcon(
|
||||||
|
context: context,
|
||||||
|
presentationEmail: widget.presentationEmail
|
||||||
|
),
|
||||||
|
if (widget.presentationEmail.isMarkAsImportant && widget.isSenderImportantFlagEnabled)
|
||||||
|
buildMarkAsImportantIcon(context),
|
||||||
|
Expanded(
|
||||||
|
child: buildEmailTitle(
|
||||||
|
context,
|
||||||
|
widget.presentationEmail,
|
||||||
|
widget.isSearchEmailRunning,
|
||||||
|
widget.searchQuery
|
||||||
|
)
|
||||||
|
),
|
||||||
|
buildMailboxContain(
|
||||||
|
context,
|
||||||
|
widget.isSearchEmailRunning,
|
||||||
|
widget.presentationEmail
|
||||||
|
),
|
||||||
|
if (widget.presentationEmail.hasStarred)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.only(start: 8),
|
||||||
|
child: buildIconStar(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
_buildPartialContentMobile(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
@@ -197,6 +189,8 @@ class _EmailTileBuilderState extends State<EmailTileBuilder> with BaseEmailItem
|
|||||||
isDrag: widget.isDrag,
|
isDrag: widget.isDrag,
|
||||||
isSenderImportantFlagEnabled: widget.isSenderImportantFlagEnabled,
|
isSenderImportantFlagEnabled: widget.isSenderImportantFlagEnabled,
|
||||||
shouldShowAIAction: _shouldShowAIAction,
|
shouldShowAIAction: _shouldShowAIAction,
|
||||||
|
autoWrapTagsByMaxWidth: widget.autoWrapTagsByMaxWidth,
|
||||||
|
labels: widget.labels,
|
||||||
padding: widget.padding,
|
padding: widget.padding,
|
||||||
searchQuery: widget.searchQuery,
|
searchQuery: widget.searchQuery,
|
||||||
mailboxContain: widget.mailboxContain,
|
mailboxContain: widget.mailboxContain,
|
||||||
@@ -320,7 +314,7 @@ class _EmailTileBuilderState extends State<EmailTileBuilder> with BaseEmailItem
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
const SizedBox(width: 24),
|
const SizedBox(width: 24),
|
||||||
Expanded(child: _buildSubjectAndContent()),
|
Expanded(child: _buildSubjectAndContent(context)),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
ValueListenableBuilder(
|
ValueListenableBuilder(
|
||||||
valueListenable: _hoverNotifier,
|
valueListenable: _hoverNotifier,
|
||||||
@@ -373,8 +367,9 @@ class _EmailTileBuilderState extends State<EmailTileBuilder> with BaseEmailItem
|
|||||||
return widget.mailboxContain?.isTrash == true || widget.mailboxContain?.isDrafts == true || widget.mailboxContain?.isSpam == true;
|
return widget.mailboxContain?.isTrash == true || widget.mailboxContain?.isDrafts == true || widget.mailboxContain?.isSpam == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSubjectAndContent() {
|
Widget _buildSubjectAndContent(BuildContext context) {
|
||||||
final emailTitle = widget.presentationEmail.getEmailTitle();
|
final emailTitle = widget.presentationEmail.getEmailTitle();
|
||||||
|
final emailPartialContent = widget.presentationEmail.getPartialContent();
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -391,78 +386,40 @@ class _EmailTileBuilderState extends State<EmailTileBuilder> with BaseEmailItem
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (_, constraints) {
|
builder: (_, constraints) {
|
||||||
|
const horizontalPadding = 12.0;
|
||||||
|
|
||||||
final maxWidth = constraints.maxWidth;
|
final maxWidth = constraints.maxWidth;
|
||||||
double horizontalPadding = 12;
|
|
||||||
final maxWidthNoPadding = maxWidth - horizontalPadding * 2;
|
final maxWidthNoPadding = maxWidth - horizontalPadding * 2;
|
||||||
|
|
||||||
final labelTagsMaxWidth = emailTitle.isNotEmpty
|
final hasLabels = widget.labels?.isNotEmpty == true;
|
||||||
? maxWidthNoPadding * 0.4
|
final isAutoWrap = widget.autoWrapTagsByMaxWidth;
|
||||||
: maxWidthNoPadding * 0.5;
|
final hasTitle = emailTitle.isNotEmpty;
|
||||||
|
final hasContent = emailPartialContent.isNotEmpty;
|
||||||
|
|
||||||
final emailTitleMaxWidth = widget.labels?.isNotEmpty == true
|
final labelTagsMaxWidth =
|
||||||
? maxWidthNoPadding * 0.4
|
hasTitle ? maxWidthNoPadding * 0.4 : maxWidthNoPadding * 0.5;
|
||||||
: maxWidthNoPadding * 0.5;
|
|
||||||
|
final emailTitleMaxWidth =
|
||||||
|
hasLabels ? maxWidthNoPadding * 0.4 : maxWidthNoPadding * 0.5;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
if (widget.labels?.isNotEmpty == true) ...[
|
if (hasLabels)
|
||||||
Container(
|
_buildLabelsDesktop(
|
||||||
constraints: BoxConstraints(
|
context: context,
|
||||||
maxWidth: labelTagsMaxWidth,
|
maxWidth: labelTagsMaxWidth,
|
||||||
),
|
horizontalPadding: horizontalPadding,
|
||||||
padding: EdgeInsetsDirectional.only(
|
isAutoWrap: isAutoWrap,
|
||||||
end: horizontalPadding,
|
|
||||||
),
|
|
||||||
child: LabelTagListWidget(tags: widget.labels!),
|
|
||||||
),
|
),
|
||||||
if (emailTitle.isNotEmpty)
|
if (hasTitle)
|
||||||
Container(
|
_buildTitleDesktop(
|
||||||
constraints: BoxConstraints(
|
context: context,
|
||||||
maxWidth: emailTitleMaxWidth,
|
maxWidth: emailTitleMaxWidth,
|
||||||
),
|
horizontalPadding: horizontalPadding,
|
||||||
padding: EdgeInsetsDirectional.only(
|
hasContent: hasContent,
|
||||||
end: horizontalPadding,
|
|
||||||
),
|
|
||||||
child: buildEmailTitle(
|
|
||||||
context,
|
|
||||||
widget.presentationEmail,
|
|
||||||
widget.isSearchEmailRunning,
|
|
||||||
widget.searchQuery,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: buildEmailPartialContent(
|
|
||||||
context,
|
|
||||||
widget.presentationEmail,
|
|
||||||
widget.isSearchEmailRunning,
|
|
||||||
widget.searchQuery,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
] else ...[
|
if (hasContent)
|
||||||
if (emailTitle.isNotEmpty)
|
_buildPartialContentDesktop(context),
|
||||||
Container(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: emailTitleMaxWidth,
|
|
||||||
),
|
|
||||||
padding: EdgeInsetsDirectional.only(
|
|
||||||
end: horizontalPadding,
|
|
||||||
),
|
|
||||||
child: buildEmailTitle(
|
|
||||||
context,
|
|
||||||
widget.presentationEmail,
|
|
||||||
widget.isSearchEmailRunning,
|
|
||||||
widget.searchQuery,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: buildEmailPartialContent(
|
|
||||||
context,
|
|
||||||
widget.presentationEmail,
|
|
||||||
widget.isSearchEmailRunning,
|
|
||||||
widget.searchQuery,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -472,6 +429,165 @@ class _EmailTileBuilderState extends State<EmailTileBuilder> with BaseEmailItem
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildLabelsDesktop({
|
||||||
|
required BuildContext context,
|
||||||
|
required double maxWidth,
|
||||||
|
required double horizontalPadding,
|
||||||
|
required bool isAutoWrap,
|
||||||
|
}) {
|
||||||
|
final labels = widget.labels;
|
||||||
|
if (labels == null || labels.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
final child = LabelTagListWidget(
|
||||||
|
tags: labels,
|
||||||
|
autoWrapTagsByMaxWidth: widget.autoWrapTagsByMaxWidth,
|
||||||
|
isDesktop: responsiveUtils.isDesktop(context),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isAutoWrap) {
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
|
padding: EdgeInsetsDirectional.only(end: horizontalPadding),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(end: horizontalPadding),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTitleDesktop({
|
||||||
|
required BuildContext context,
|
||||||
|
required double maxWidth,
|
||||||
|
required double horizontalPadding,
|
||||||
|
bool hasContent = false,
|
||||||
|
}) {
|
||||||
|
if (hasContent) {
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
|
padding: EdgeInsetsDirectional.only(end: horizontalPadding),
|
||||||
|
child: buildEmailTitle(
|
||||||
|
context,
|
||||||
|
widget.presentationEmail,
|
||||||
|
widget.isSearchEmailRunning,
|
||||||
|
widget.searchQuery,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(end: horizontalPadding),
|
||||||
|
child: buildEmailTitle(
|
||||||
|
context,
|
||||||
|
widget.presentationEmail,
|
||||||
|
widget.isSearchEmailRunning,
|
||||||
|
widget.searchQuery,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPartialContentDesktop(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: buildEmailPartialContent(
|
||||||
|
context,
|
||||||
|
widget.presentationEmail,
|
||||||
|
widget.isSearchEmailRunning,
|
||||||
|
widget.searchQuery,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPartialContentMobile(BuildContext context) {
|
||||||
|
final labels = widget.labels;
|
||||||
|
final hasLabels = labels?.isNotEmpty == true;
|
||||||
|
final hasContent = widget.presentationEmail.getPartialContent().isNotEmpty;
|
||||||
|
final isAutoWrap = widget.autoWrapTagsByMaxWidth;
|
||||||
|
final isDesktop = responsiveUtils.isDesktop(context);
|
||||||
|
|
||||||
|
final partialContent = buildEmailPartialContent(
|
||||||
|
context,
|
||||||
|
widget.presentationEmail,
|
||||||
|
widget.isSearchEmailRunning,
|
||||||
|
widget.searchQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasLabels) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (hasContent)
|
||||||
|
Flexible(child: partialContent),
|
||||||
|
if (_shouldShowAIAction)
|
||||||
|
const AiActionTagWidget(
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasContent) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (_shouldShowAIAction)
|
||||||
|
const AiActionTagWidget(
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 8),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: LabelTagListWidget(
|
||||||
|
tags: labels!,
|
||||||
|
autoWrapTagsByMaxWidth: isAutoWrap,
|
||||||
|
isDesktop: isDesktop,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAutoWrap) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Flexible(child: partialContent),
|
||||||
|
if (_shouldShowAIAction)
|
||||||
|
const AiActionTagWidget(
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 8),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
LabelTagListWidget(
|
||||||
|
tags: labels!,
|
||||||
|
autoWrapTagsByMaxWidth: isAutoWrap,
|
||||||
|
isDesktop: isDesktop,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (_, constraints) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: partialContent),
|
||||||
|
if (_shouldShowAIAction)
|
||||||
|
const AiActionTagWidget(
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 8),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: constraints.maxWidth / 2),
|
||||||
|
child: LabelTagListWidget(
|
||||||
|
tags: labels!,
|
||||||
|
autoWrapTagsByMaxWidth: isAutoWrap,
|
||||||
|
isDesktop: isDesktop,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildAvatarIcon({
|
Widget _buildAvatarIcon({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
bool isHovered = false,
|
bool isHovered = false,
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import 'package:core/presentation/resources/image_paths.dart';
|
|||||||
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:labels/model/label.dart';
|
||||||
import 'package:model/email/email_action_type.dart';
|
import 'package:model/email/email_action_type.dart';
|
||||||
import 'package:model/email/presentation_email.dart';
|
import 'package:model/email/presentation_email.dart';
|
||||||
import 'package:model/extensions/presentation_mailbox_extension.dart';
|
import 'package:model/extensions/presentation_mailbox_extension.dart';
|
||||||
import 'package:model/mailbox/presentation_mailbox.dart';
|
import 'package:model/mailbox/presentation_mailbox.dart';
|
||||||
import 'package:model/mailbox/select_mode.dart';
|
import 'package:model/mailbox/select_mode.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/labels/ai_action_tag_widget.dart';
|
import 'package:tmail_ui_user/features/base/widget/labels/ai_action_tag_widget.dart';
|
||||||
|
import 'package:tmail_ui_user/features/labels/presentation/widgets/label_list_widget.dart';
|
||||||
import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart';
|
import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart';
|
||||||
import 'package:tmail_ui_user/features/thread/presentation/mixin/base_email_item_tile.dart';
|
import 'package:tmail_ui_user/features/thread/presentation/mixin/base_email_item_tile.dart';
|
||||||
import 'package:tmail_ui_user/features/thread/presentation/styles/item_email_tile_styles.dart';
|
import 'package:tmail_ui_user/features/thread/presentation/styles/item_email_tile_styles.dart';
|
||||||
@@ -24,6 +26,8 @@ class WebTabletBodyEmailItemWidget extends StatefulWidget {
|
|||||||
final bool isDrag;
|
final bool isDrag;
|
||||||
final bool isSenderImportantFlagEnabled;
|
final bool isSenderImportantFlagEnabled;
|
||||||
final bool shouldShowAIAction;
|
final bool shouldShowAIAction;
|
||||||
|
final bool autoWrapTagsByMaxWidth;
|
||||||
|
final List<Label>? labels;
|
||||||
final EdgeInsetsGeometry? padding;
|
final EdgeInsetsGeometry? padding;
|
||||||
final SearchQuery? searchQuery;
|
final SearchQuery? searchQuery;
|
||||||
final PresentationMailbox? mailboxContain;
|
final PresentationMailbox? mailboxContain;
|
||||||
@@ -39,6 +43,8 @@ class WebTabletBodyEmailItemWidget extends StatefulWidget {
|
|||||||
required this.isShowingEmailContent,
|
required this.isShowingEmailContent,
|
||||||
required this.isDrag,
|
required this.isDrag,
|
||||||
required this.isSenderImportantFlagEnabled,
|
required this.isSenderImportantFlagEnabled,
|
||||||
|
required this.autoWrapTagsByMaxWidth,
|
||||||
|
required this.labels,
|
||||||
required this.padding,
|
required this.padding,
|
||||||
required this.searchQuery,
|
required this.searchQuery,
|
||||||
required this.mailboxContain,
|
required this.mailboxContain,
|
||||||
@@ -102,6 +108,7 @@ class _WebTabletBodyEmailItemWidgetState
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
if (!widget.presentationEmail.hasRead)
|
if (!widget.presentationEmail.hasRead)
|
||||||
@@ -158,22 +165,7 @@ class _WebTabletBodyEmailItemWidgetState
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Row(
|
_buildPartialContent(context),
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: buildEmailPartialContent(
|
|
||||||
context,
|
|
||||||
widget.presentationEmail,
|
|
||||||
widget.isSearchEmailRunning,
|
|
||||||
widget.searchQuery,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.shouldShowAIAction)
|
|
||||||
const AiActionTagWidget(
|
|
||||||
margin: EdgeInsetsDirectional.only(start: 8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -384,4 +376,92 @@ class _WebTabletBodyEmailItemWidgetState
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPartialContent(BuildContext context) {
|
||||||
|
final labels = widget.labels;
|
||||||
|
final hasLabels = labels?.isNotEmpty == true;
|
||||||
|
final isAutoWrap = widget.autoWrapTagsByMaxWidth;
|
||||||
|
final isDesktop = responsiveUtils.isDesktop(context);
|
||||||
|
|
||||||
|
final hasContent = widget.presentationEmail.getPartialContent().isNotEmpty;
|
||||||
|
|
||||||
|
final partialContent = buildEmailPartialContent(
|
||||||
|
context,
|
||||||
|
widget.presentationEmail,
|
||||||
|
widget.isSearchEmailRunning,
|
||||||
|
widget.searchQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasLabels) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (hasContent)
|
||||||
|
Flexible(child: partialContent),
|
||||||
|
if (widget.shouldShowAIAction)
|
||||||
|
const AiActionTagWidget(
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasContent) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (widget.shouldShowAIAction)
|
||||||
|
const AiActionTagWidget(
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 8),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: LabelTagListWidget(
|
||||||
|
tags: labels!,
|
||||||
|
autoWrapTagsByMaxWidth: isAutoWrap,
|
||||||
|
isDesktop: isDesktop,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAutoWrap) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Flexible(child: partialContent),
|
||||||
|
if (widget.shouldShowAIAction)
|
||||||
|
const AiActionTagWidget(
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 8),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
LabelTagListWidget(
|
||||||
|
tags: labels!,
|
||||||
|
autoWrapTagsByMaxWidth: isAutoWrap,
|
||||||
|
isDesktop: isDesktop,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (_, constraints) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: partialContent),
|
||||||
|
if (widget.shouldShowAIAction)
|
||||||
|
const AiActionTagWidget(
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 8),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: constraints.maxWidth / 2),
|
||||||
|
child: LabelTagListWidget(
|
||||||
|
tags: labels!,
|
||||||
|
autoWrapTagsByMaxWidth: isAutoWrap,
|
||||||
|
isDesktop: isDesktop,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -394,18 +394,6 @@ void main() {
|
|||||||
expect(result.hiddenCount, 2);
|
expect(result.hiddenCount, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('safeComputeDisplayedTags falls back gracefully on exception', () {
|
|
||||||
final result = LabelUtils.safeComputeDisplayedTags(
|
|
||||||
tags: [Label(displayName: 'A'), Label(displayName: 'B')],
|
|
||||||
maxWidth: 100,
|
|
||||||
textStyle: const TextStyle(),
|
|
||||||
measureWidth: (_) => throw Exception('boom'),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.displayed.single.safeDisplayName, 'A');
|
|
||||||
expect(result.hiddenCount, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('respects tagMaxWidth by clamping oversized label width', () {
|
test('respects tagMaxWidth by clamping oversized label width', () {
|
||||||
final tags = [
|
final tags = [
|
||||||
label('SUPER_LONG_TAG_NAME'),
|
label('SUPER_LONG_TAG_NAME'),
|
||||||
@@ -479,4 +467,220 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('LabelUtils.safeComputeDisplayedTags', () {
|
||||||
|
test('should falls back gracefully on exception', () {
|
||||||
|
final result = LabelUtils.safeComputeDisplayedTags(
|
||||||
|
tags: [Label(displayName: 'A'), Label(displayName: 'B')],
|
||||||
|
maxWidth: 100,
|
||||||
|
textStyle: const TextStyle(),
|
||||||
|
measureWidth: (_) => throw Exception('boom'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.displayed.single.safeDisplayName, 'A');
|
||||||
|
expect(result.hiddenCount, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('LabelUtils.computeDisplayedLabelsByMaxDisplay', () {
|
||||||
|
test('returns empty result when labels list is empty', () {
|
||||||
|
final result = LabelUtils.computeDisplayedLabelsByMaxDisplay(labels: []);
|
||||||
|
|
||||||
|
expect(result.displayed, isEmpty);
|
||||||
|
expect(result.hiddenCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns all labels when length <= maxDisplay', () {
|
||||||
|
final labels = [
|
||||||
|
label('A'),
|
||||||
|
label('B'),
|
||||||
|
];
|
||||||
|
|
||||||
|
final result = LabelUtils.computeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: labels,
|
||||||
|
maxDisplay: 3,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.displayed.length, 2);
|
||||||
|
expect(result.displayed.map((e) => e.displayName), ['A', 'B']);
|
||||||
|
expect(result.hiddenCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('limits displayed to maxDisplay when list exceeds maxDisplay', () {
|
||||||
|
final labels = [
|
||||||
|
label('A'),
|
||||||
|
label('B'),
|
||||||
|
label('C'),
|
||||||
|
label('D'),
|
||||||
|
];
|
||||||
|
|
||||||
|
final result = LabelUtils.computeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: labels,
|
||||||
|
maxDisplay: 3,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.displayed.length, 3);
|
||||||
|
expect(result.displayed.map((e) => e.displayName), ['A', 'B', 'C']);
|
||||||
|
expect(result.hiddenCount, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computes hiddenCount correctly for larger list', () {
|
||||||
|
final labels = [
|
||||||
|
label('A'),
|
||||||
|
label('B'),
|
||||||
|
label('C'),
|
||||||
|
label('D'),
|
||||||
|
label('E'),
|
||||||
|
];
|
||||||
|
|
||||||
|
final result = LabelUtils.computeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: labels,
|
||||||
|
maxDisplay: 3,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.displayed.length, 3);
|
||||||
|
expect(result.displayed.map((e) => e.displayName), ['A', 'B', 'C']);
|
||||||
|
expect(result.hiddenCount, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('works correctly when maxDisplay = 1', () {
|
||||||
|
final labels = [
|
||||||
|
label('A'),
|
||||||
|
label('B'),
|
||||||
|
label('C'),
|
||||||
|
];
|
||||||
|
|
||||||
|
final result = LabelUtils.computeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: labels,
|
||||||
|
maxDisplay: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.displayed.length, 1);
|
||||||
|
expect(result.displayed.first.displayName, 'A');
|
||||||
|
expect(result.hiddenCount, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('works correctly when maxDisplay is larger than list size', () {
|
||||||
|
final labels = [
|
||||||
|
label('A'),
|
||||||
|
];
|
||||||
|
|
||||||
|
final result = LabelUtils.computeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: labels,
|
||||||
|
maxDisplay: 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.displayed.length, 1);
|
||||||
|
expect(result.displayed.map((e) => e.displayName), ['A']);
|
||||||
|
expect(result.hiddenCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws RangeError when maxDisplay < 0', () {
|
||||||
|
final labels = [
|
||||||
|
label('A'),
|
||||||
|
label('B'),
|
||||||
|
label('C'),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => LabelUtils.computeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: labels,
|
||||||
|
maxDisplay: -1,
|
||||||
|
),
|
||||||
|
throwsA(isA<RangeError>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('LabelUtils.safeComputeDisplayedLabelsByMaxDisplay', () {
|
||||||
|
test('returns empty result when labels list is empty', () {
|
||||||
|
final result =
|
||||||
|
LabelUtils.safeComputeDisplayedLabelsByMaxDisplay(labels: []);
|
||||||
|
|
||||||
|
expect(result.displayed, isEmpty);
|
||||||
|
expect(result.hiddenCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns normal compute result when no exception occurs', () {
|
||||||
|
final labels = [
|
||||||
|
label('A'),
|
||||||
|
label('B'),
|
||||||
|
label('C'),
|
||||||
|
];
|
||||||
|
|
||||||
|
final result = LabelUtils.safeComputeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: labels,
|
||||||
|
maxDisplay: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.displayed.length, 2);
|
||||||
|
expect(result.displayed.map((e) => e.displayName), ['A', 'B']);
|
||||||
|
expect(result.hiddenCount, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'fallback when computeDisplayedLabelsByMaxDisplay throws and labels empty',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
() => LabelUtils.safeComputeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: [],
|
||||||
|
maxDisplay: -999,
|
||||||
|
),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = LabelUtils.safeComputeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: [],
|
||||||
|
maxDisplay: -999,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.displayed, isEmpty);
|
||||||
|
expect(result.hiddenCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fallback returns first label and hiddenCount when exception occurs',
|
||||||
|
() {
|
||||||
|
final labels = [
|
||||||
|
label('A'),
|
||||||
|
label('B'),
|
||||||
|
label('C'),
|
||||||
|
];
|
||||||
|
|
||||||
|
final result = LabelUtils.safeComputeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: labels,
|
||||||
|
maxDisplay: -1,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.displayed.length, 1);
|
||||||
|
expect(result.displayed.first.displayName, 'A');
|
||||||
|
expect(result.hiddenCount, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fallback works correctly when only one label exists', () {
|
||||||
|
final labels = [label('A')];
|
||||||
|
|
||||||
|
final result = LabelUtils.safeComputeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: labels,
|
||||||
|
maxDisplay: -1,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.displayed.length, 1);
|
||||||
|
expect(result.displayed.first.displayName, 'A');
|
||||||
|
expect(result.hiddenCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normal path works when maxDisplay > list size', () {
|
||||||
|
final labels = [
|
||||||
|
label('A'),
|
||||||
|
];
|
||||||
|
|
||||||
|
final result = LabelUtils.safeComputeDisplayedLabelsByMaxDisplay(
|
||||||
|
labels: labels,
|
||||||
|
maxDisplay: 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.displayed.length, 1);
|
||||||
|
expect(result.hiddenCount, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:tmail_ui_user/features/labels/presentation/utils/label_utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('LabelUtils.truncateLabel', () {
|
||||||
|
test('returns original string when length <= maxLength', () {
|
||||||
|
const name = 'ShortLabel';
|
||||||
|
final result = LabelUtils.truncateLabel(name, maxLength: 16);
|
||||||
|
expect(result, name);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('truncates string when length > maxLength', () {
|
||||||
|
const name = 'ThisIsAVeryLongLabel';
|
||||||
|
final result = LabelUtils.truncateLabel(name, maxLength: 16);
|
||||||
|
expect(result, 'ThisIsAVeryLong...');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('works correctly with exact maxLength boundary', () {
|
||||||
|
const name = '1234567890123456';
|
||||||
|
final result = LabelUtils.truncateLabel(name, maxLength: 16);
|
||||||
|
expect(result, name);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses custom maxLength', () {
|
||||||
|
const name = 'ABCDEFGH';
|
||||||
|
final result = LabelUtils.truncateLabel(name, maxLength: 5);
|
||||||
|
expect(result, 'ABCD...');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles empty string gracefully', () {
|
||||||
|
const name = '';
|
||||||
|
final result = LabelUtils.truncateLabel(name, maxLength: 16);
|
||||||
|
expect(result, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles maxLength = 1 (still truncates safely)', () {
|
||||||
|
const name = 'LongName';
|
||||||
|
final result = LabelUtils.truncateLabel(name, maxLength: 1);
|
||||||
|
expect(result, '...');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user