TF-3759 Update style popup context menu for search filter option on search email view

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-06-14 02:12:09 +07:00
committed by Dat H. Pham
parent 506603b44f
commit e3c9200172
9 changed files with 244 additions and 146 deletions
@@ -79,7 +79,7 @@ mixin PopupContextMenuActionMixin {
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(6)), borderRadius: BorderRadius.all(Radius.circular(6)),
), ),
constraints: const BoxConstraints(maxWidth: 300), constraints: const BoxConstraints(maxWidth: 300, minWidth: 178),
items: popupMenuItems, items: popupMenuItems,
); );
} }
@@ -82,6 +82,7 @@ class ProfileSettingMenuOverlay extends StatelessWidget {
}, },
)) ))
.toList(), .toList(),
const SizedBox(height: 8),
], ],
), ),
PositionedDirectional( PositionedDirectional(
@@ -106,9 +106,9 @@ class _FilterMessageButtonState extends State<FilterMessageButton> {
); );
} else { } else {
return Material( return Material(
key: _buttonKey,
type: MaterialType.transparency, type: MaterialType.transparency,
child: InkWell( child: InkWell(
key: _buttonKey,
onTapDown: (details) => _onTapDownAction(context, details), onTapDown: (details) => _onTapDownAction(context, details),
borderRadius: FilterMessageButtonStyle.borderRadius, borderRadius: FilterMessageButtonStyle.borderRadius,
child: childItem, child: childItem,
@@ -11,13 +11,11 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/search_filter_button_style.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/search_filter_button_style.dart';
typedef OnSelectSearchFilterAction = Function( typedef OnSelectSearchFilterAction = Function(
BuildContext context, BuildContext context, QuickSearchFilter searchFilter,
QuickSearchFilter searchFilter, {RelativeRect? buttonPosition});
{RelativeRect? buttonPosition});
typedef OnDeleteSearchFilterAction = Function(QuickSearchFilter searchFilter); typedef OnDeleteSearchFilterAction = Function(QuickSearchFilter searchFilter);
class SearchFilterButton extends StatelessWidget { class SearchFilterButton extends StatefulWidget {
static const int _titleCharactersMaximum = 20; static const int _titleCharactersMaximum = 20;
final QuickSearchFilter searchFilter; final QuickSearchFilter searchFilter;
@@ -55,128 +53,197 @@ class SearchFilterButton extends StatelessWidget {
this.onDeleteSearchFilterAction, this.onDeleteSearchFilterAction,
}); });
@override
State<SearchFilterButton> createState() => _SearchFilterButtonState();
}
class _SearchFilterButtonState extends State<SearchFilterButton> {
final GlobalKey _buttonKey = GlobalKey();
final layerLink = LayerLink();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final filterTitle = searchFilter.getTitle( final filterTitle = widget.searchFilter.getTitle(
context, context,
receiveTimeType: receiveTimeType, receiveTimeType: widget.receiveTimeType,
startDate: startDate, startDate: widget.startDate,
endDate: startDate, endDate: widget.startDate,
sortOrderType: sortOrderType, sortOrderType: widget.sortOrderType,
listAddressOfFrom: listAddressOfFrom, listAddressOfFrom: widget.listAddressOfFrom,
mailbox: mailbox, mailbox: widget.mailbox,
listAddressOfTo: listAddressOfTo, listAddressOfTo: widget.listAddressOfTo,
); );
final deleteButtonWidget = TMailButtonWidget.fromIcon( final deleteButtonWidget = TMailButtonWidget.fromIcon(
icon: imagePaths.icDeleteSelection, icon: widget.imagePaths.icDeleteSelection,
iconSize: SearchFilterButtonStyle.deleteIconSize, iconSize: SearchFilterButtonStyle.deleteIconSize,
iconColor: AppColor.colorTextBody, iconColor: AppColor.colorTextBody,
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
onTapActionCallback: () => onDeleteSearchFilterAction?.call(searchFilter), onTapActionCallback: () => widget.onDeleteSearchFilterAction?.call(
widget.searchFilter,
),
); );
final listComponentsWidget = <Widget>[ final listComponentsWidget = <Widget>[
SvgPicture.asset( SvgPicture.asset(
searchFilter.getIcon(imagePaths, isSelected: isSelected), widget.searchFilter.getIcon(
width: SearchFilterButtonStyle.iconSize, widget.imagePaths,
height: SearchFilterButtonStyle.iconSize, isSelected: widget.isSelected,
colorFilter: searchFilter.getIconColor(isSelected: isSelected).asFilter(), ),
fit: BoxFit.fill width: SearchFilterButtonStyle.iconSize,
), height: SearchFilterButtonStyle.iconSize,
colorFilter: widget.searchFilter
.getIconColor(
isSelected: widget.isSelected,
)
.asFilter(),
fit: BoxFit.fill),
const SizedBox(width: SearchFilterButtonStyle.spaceSize), const SizedBox(width: SearchFilterButtonStyle.spaceSize),
Text( Text(
filterTitle.length > _titleCharactersMaximum filterTitle.length > SearchFilterButton._titleCharactersMaximum
? '${filterTitle.substring(0, _titleCharactersMaximum)}...' ? '${filterTitle.substring(
: filterTitle, 0,
SearchFilterButton._titleCharactersMaximum,
)}...'
: filterTitle,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: SearchFilterButtonStyle.titleStyle, style: SearchFilterButtonStyle.titleStyle,
), ),
if (searchFilter.isArrowDownIconSupported()) if (widget.searchFilter.isArrowDownIconSupported())
Padding( Padding(
padding: SearchFilterButtonStyle.elementPadding, padding: SearchFilterButtonStyle.elementPadding,
child: SvgPicture.asset( child: SvgPicture.asset(
imagePaths.icDropDown, widget.imagePaths.icDropDown,
width: SearchFilterButtonStyle.iconSize, width: SearchFilterButtonStyle.iconSize,
height: SearchFilterButtonStyle.iconSize, height: SearchFilterButtonStyle.iconSize,
colorFilter: AppColor.colorTextBody.asFilter(), colorFilter: AppColor.colorTextBody.asFilter(),
fit: BoxFit.fill fit: BoxFit.fill,
), ),
), ),
]; ];
final childItem = _buildContainerForComponents( if (widget.onSelectSearchFilterAction == null) {
children: [ return _buildContainerForComponents(
...listComponentsWidget, children: [
if (isSelected) deleteButtonWidget, ...listComponentsWidget,
] if (widget.isSelected) deleteButtonWidget,
); ],
if (onSelectSearchFilterAction == null) {
return childItem;
}
if (!searchFilter.isOnTapWithPositionActionSupported(context, responsiveUtils)) {
return Material(
type: MaterialType.transparency,
child: InkWell(
onTap: !searchFilter.isOnTapWithPositionActionSupported(context, responsiveUtils)
? () => _onTapAction(context)
: null,
onTapDown: searchFilter.isOnTapWithPositionActionSupported(context, responsiveUtils) && !isSelected
? (details) => _onTapDownAction(context, details)
: null,
borderRadius: SearchFilterButtonStyle.borderRadius,
child: childItem
),
); );
} } else if (!_isOnTapWithPositionActionSupported()) {
return CompositedTransformTarget(
return _buildContainerForComponents( link: layerLink,
children: [ child: Material(
InkWell( key: _buttonKey,
onTapDown: (details) => _onTapDownAction(context, details), type: MaterialType.transparency,
child: Row( child: InkWell(
mainAxisSize: MainAxisSize.min, onTap: !_isOnTapWithPositionActionSupported()
children: listComponentsWidget ? () => _onTapAction(context)
: null,
onTapDown: _isOnTapWithPositionActionSupported() && !widget.isSelected
? (details) => _onTapDownAction(context, details)
: null,
borderRadius: SearchFilterButtonStyle.borderRadius,
child: _buildContainerForComponents(
children: [
...listComponentsWidget,
if (widget.isSelected) deleteButtonWidget,
],
),
), ),
), ),
if (isSelected) deleteButtonWidget, );
] } else {
return _buildContainerForComponents(
key: _buttonKey,
children: [
InkWell(
onTapDown: (details) => _onTapDownAction(context, details),
child: Row(
mainAxisSize: MainAxisSize.min,
children: listComponentsWidget,
),
),
if (widget.isSelected) deleteButtonWidget,
],
);
}
}
bool _isOnTapWithPositionActionSupported() {
return widget.searchFilter.isOnTapWithPositionActionSupported(
context,
widget.responsiveUtils,
); );
} }
Widget _buildContainerForComponents({required List<Widget> children}) { Widget _buildContainerForComponents({
required List<Widget> children,
Key? key,
}) {
return Container( return Container(
key: key,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: SearchFilterButtonStyle.borderRadius, borderRadius: SearchFilterButtonStyle.borderRadius,
color: backgroundColor ?? searchFilter.getBackgroundColor( color: widget.backgroundColor ??
isSelected: isSelected widget.searchFilter
) .getBackgroundColor(isSelected: widget.isSelected),
), ),
padding: buttonPadding ?? SearchFilterButtonStyle.getButtonPadding(isSelected), padding: widget.buttonPadding ??
SearchFilterButtonStyle.getButtonPadding(
widget.isSelected,
),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: children children: children,
) ),
); );
} }
void _onTapAction(BuildContext context) { void _onTapAction(BuildContext context) {
onSelectSearchFilterAction?.call(context, searchFilter); widget.onSelectSearchFilterAction?.call(context, widget.searchFilter);
} }
void _onTapDownAction(BuildContext context, TapDownDetails details) { void _onTapDownAction(BuildContext context, TapDownDetails details) {
final screenSize = MediaQuery.sizeOf(context); RelativeRect? position;
final offset = details.globalPosition;
final position = RelativeRect.fromLTRB(
offset.dx,
offset.dy,
screenSize.width - offset.dx,
screenSize.height - offset.dy);
onSelectSearchFilterAction?.call(context, searchFilter, buttonPosition: position); final buttonBox =
_buttonKey.currentContext?.findRenderObject() as RenderBox?;
final overlayBox =
Overlay.maybeOf(context)?.context.findRenderObject() as RenderBox?;
if (buttonBox == null || overlayBox == null) {
final screenSize = MediaQuery.sizeOf(context);
final offset = details.globalPosition;
position = RelativeRect.fromLTRB(
offset.dx,
offset.dy,
screenSize.width - offset.dx,
screenSize.height - offset.dy,
);
} else {
final buttonPosition = buttonBox.localToGlobal(
Offset.zero,
ancestor: overlayBox,
);
final buttonSize = buttonBox.size;
position = RelativeRect.fromRect(
Rect.fromLTWH(
buttonPosition.dx + buttonSize.width,
buttonPosition.dy + buttonSize.height + 8,
0,
0,
),
Offset.zero & overlayBox.size,
);
}
widget.onSelectSearchFilterAction?.call(
context,
widget.searchFilter,
buttonPosition: position,
);
} }
} }
@@ -0,0 +1,23 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:tmail_ui_user/features/base/model/popup_menu_item_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class PopupMenuItemDateFilterAction
extends PopupMenuItemActionRequiredSelectedIcon<EmailReceiveTimeType> {
final AppLocalizations appLocalizations;
final ImagePaths imagePaths;
PopupMenuItemDateFilterAction(
super.action,
super.selectedAction,
this.appLocalizations,
this.imagePaths,
);
@override
String get actionName => action.getTitleByAppLocalizations(appLocalizations);
@override
String get selectedIcon => imagePaths.icFilterSelected;
}
@@ -0,0 +1,23 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:tmail_ui_user/features/base/model/popup_menu_item_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class PopupMenuItemSortOrderTypeAction
extends PopupMenuItemActionRequiredSelectedIcon<EmailSortOrderType> {
final AppLocalizations appLocalizations;
final ImagePaths imagePaths;
PopupMenuItemSortOrderTypeAction(
super.action,
super.selectedAction,
this.appLocalizations,
this.imagePaths,
);
@override
String get actionName => action.getTitleByAppLocalizations(appLocalizations);
@override
String get selectedIcon => imagePaths.icFilterSelected;
}
@@ -633,8 +633,6 @@ class SearchEmailController extends BaseController
} }
void selectReceiveTimeQuickSearchFilter(BuildContext context, EmailReceiveTimeType emailReceiveTimeType) { void selectReceiveTimeQuickSearchFilter(BuildContext context, EmailReceiveTimeType emailReceiveTimeType) {
popBack();
if (emailReceiveTimeType == EmailReceiveTimeType.customRange) { if (emailReceiveTimeType == EmailReceiveTimeType.customRange) {
showMultipleViewDateRangePicker( showMultipleViewDateRangePicker(
context, context,
@@ -664,7 +662,6 @@ class SearchEmailController extends BaseController
} }
void selectSortOrderQuickSearchFilter(BuildContext context, EmailSortOrderType sortOrderType) { void selectSortOrderQuickSearchFilter(BuildContext context, EmailSortOrderType sortOrderType) {
popBack();
emailSortOrderType.value = sortOrderType; emailSortOrderType.value = sortOrderType;
_updateSimpleSearchFilter(sortOrderTypeOption: Some(sortOrderType)); _updateSimpleSearchFilter(sortOrderTypeOption: Some(sortOrderType));
_searchEmailAction(context); _searchEmailAction(context);
@@ -12,6 +12,7 @@ import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/model.dart'; import 'package:model/model.dart';
import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.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/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/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart';
@@ -24,13 +25,13 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/se
import 'package:tmail_ui_user/features/search/email/presentation/extension/handle_email_more_action_extension.dart'; import 'package:tmail_ui_user/features/search/email/presentation/extension/handle_email_more_action_extension.dart';
import 'package:tmail_ui_user/features/search/email/presentation/model/context_item_receive_time_type_action.dart'; import 'package:tmail_ui_user/features/search/email/presentation/model/context_item_receive_time_type_action.dart';
import 'package:tmail_ui_user/features/search/email/presentation/model/context_item_sort_order_type_action.dart'; import 'package:tmail_ui_user/features/search/email/presentation/model/context_item_sort_order_type_action.dart';
import 'package:tmail_ui_user/features/search/email/presentation/model/popup_menu_item_date_filter_action.dart';
import 'package:tmail_ui_user/features/search/email/presentation/model/popup_menu_item_sort_order_type_action.dart';
import 'package:tmail_ui_user/features/search/email/presentation/model/search_more_state.dart'; import 'package:tmail_ui_user/features/search/email/presentation/model/search_more_state.dart';
import 'package:tmail_ui_user/features/search/email/presentation/search_email_controller.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_controller.dart';
import 'package:tmail_ui_user/features/search/email/presentation/styles/search_email_view_style.dart'; import 'package:tmail_ui_user/features/search/email/presentation/styles/search_email_view_style.dart';
import 'package:tmail_ui_user/features/search/email/presentation/utils/search_email_utils.dart'; import 'package:tmail_ui_user/features/search/email/presentation/utils/search_email_utils.dart';
import 'package:tmail_ui_user/features/search/email/presentation/widgets/app_bar_selection_mode.dart'; import 'package:tmail_ui_user/features/search/email/presentation/widgets/app_bar_selection_mode.dart';
import 'package:tmail_ui_user/features/search/email/presentation/widgets/email_receive_time_action_tile_widget.dart';
import 'package:tmail_ui_user/features/search/email/presentation/widgets/email_sort_by_action_tile_widget.dart';
import 'package:tmail_ui_user/features/search/email/presentation/widgets/empty_search_email_widget.dart'; import 'package:tmail_ui_user/features/search/email/presentation/widgets/empty_search_email_widget.dart';
import 'package:tmail_ui_user/features/search/email/presentation/widgets/search_email_loading_bar_widget.dart'; import 'package:tmail_ui_user/features/search/email/presentation/widgets/search_email_loading_bar_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';
@@ -39,6 +40,7 @@ import 'package:tmail_ui_user/features/thread/presentation/styles/item_email_til
import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart' import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart'
if (dart.library.html) 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_web_builder.dart'; if (dart.library.html) 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_web_builder.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
class SearchEmailView extends GetWidget<SearchEmailController> class SearchEmailView extends GetWidget<SearchEmailController>
with AppLoaderMixin { with AppLoaderMixin {
@@ -337,18 +339,28 @@ class SearchEmailView extends GetWidget<SearchEmailController>
} }
void _openPopupMenuDateFilter(BuildContext context, RelativeRect position) { void _openPopupMenuDateFilter(BuildContext context, RelativeRect position) {
controller.openPopupMenuAction( final popupMenuItems = EmailReceiveTimeType.values.map((timeType) {
context, return PopupMenuItem(
position, padding: EdgeInsets.zero,
_popupMenuEmailReceiveTimeType( child: PopupMenuItemActionWidget(
context, menuAction: PopupMenuItemDateFilterAction(
controller.emailReceiveTimeType.value, timeType,
(receiveTime) => controller.selectReceiveTimeQuickSearchFilter( controller.emailReceiveTimeType.value,
context, AppLocalizations.of(context),
receiveTime controller.imagePaths,
) ),
) menuActionClick: (menuAction) {
); popBack();
controller.selectReceiveTimeQuickSearchFilter(
context,
menuAction.action,
);
},
),
);
}).toList();
controller.openPopupMenuAction(context, position, popupMenuItems);
} }
void _openContextMenuDateFilter(BuildContext context) { void _openContextMenuDateFilter(BuildContext context) {
@@ -366,6 +378,7 @@ class SearchEmailView extends GetWidget<SearchEmailController>
context: context, context: context,
itemActions: contextMenuActions, itemActions: contextMenuActions,
onContextMenuActionClick: (menuAction) { onContextMenuActionClick: (menuAction) {
popBack();
controller.selectReceiveTimeQuickSearchFilter( controller.selectReceiveTimeQuickSearchFilter(
context, context,
menuAction.action, menuAction.action,
@@ -375,15 +388,28 @@ class SearchEmailView extends GetWidget<SearchEmailController>
} }
void _openPopupMenuSortFilter(BuildContext context, RelativeRect position) { void _openPopupMenuSortFilter(BuildContext context, RelativeRect position) {
controller.openPopupMenuAction( final popupMenuItems = EmailSortOrderType.values.map((sortType) {
context, return PopupMenuItem(
position, padding: EdgeInsets.zero,
_popupMenuEmailSortOrderType( child: PopupMenuItemActionWidget(
context, menuAction: PopupMenuItemSortOrderTypeAction(
controller.emailSortOrderType.value, sortType,
controller.selectSortOrderQuickSearchFilter controller.emailSortOrderType.value,
) AppLocalizations.of(context),
); controller.imagePaths,
),
menuActionClick: (menuAction) {
popBack();
controller.selectSortOrderQuickSearchFilter(
context,
menuAction.action,
);
},
),
);
}).toList();
controller.openPopupMenuAction(context, position, popupMenuItems);
} }
void _openContextMenuSortFilter(BuildContext context) { void _openContextMenuSortFilter(BuildContext context) {
@@ -409,39 +435,6 @@ class SearchEmailView extends GetWidget<SearchEmailController>
); );
} }
List<PopupMenuEntry> _popupMenuEmailReceiveTimeType(
BuildContext context,
EmailReceiveTimeType? receiveTimeSelected,
Function(EmailReceiveTimeType)? onCallBack
) {
return EmailReceiveTimeType.values
.map((timeType) => PopupMenuItem(
padding: EdgeInsets.zero,
child: EmailReceiveTimeActionTileWidget(
receiveTimeSelected: receiveTimeSelected,
receiveTimeType: timeType,
onCallBack: onCallBack
)))
.toList();
}
List<PopupMenuEntry> _popupMenuEmailSortOrderType(
BuildContext context,
EmailSortOrderType? sortTypeSelected,
Function(BuildContext, EmailSortOrderType)? onCallBack
) {
return EmailSortOrderType.values
.map((sortType) => PopupMenuItem(
padding: EdgeInsets.zero,
child: EmailSortByActionTitleWidget(
sortType: sortType,
imagePaths: controller.imagePaths,
onSortOrderSelected: onCallBack,
sortTypeSelected: sortTypeSelected,
),
)).toList();
}
Widget _buildShowAllResultSearchButton(BuildContext context, String textSearch) { Widget _buildShowAllResultSearchButton(BuildContext context, String textSearch) {
return Material( return Material(
color: Colors.transparent, color: Colors.transparent,
@@ -904,13 +904,7 @@ class ThreadController extends BaseController with EmailActionController {
mailboxDashBoardController.listEmailSelected.clear(); mailboxDashBoardController.listEmailSelected.clear();
} }
void closeFilterMessageActionSheet() {
popBack();
}
void filterMessagesAction(FilterMessageOption filterOption) { void filterMessagesAction(FilterMessageOption filterOption) {
popBack();
final newFilterOption = mailboxDashBoardController.filterMessageOption.value == filterOption final newFilterOption = mailboxDashBoardController.filterMessageOption.value == filterOption
? FilterMessageOption.all ? FilterMessageOption.all
: filterOption; : filterOption;