TF-4233 Show popup menu when click three dot on label item

This commit is contained in:
dab246
2026-01-05 15:20:38 +07:00
committed by Dat H. Pham
parent f9ff9ecde7
commit 0302e09981
12 changed files with 438 additions and 37 deletions
@@ -0,0 +1,20 @@
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:labels/model/label.dart';
import 'package:tmail_ui_user/features/labels/presentation/label_controller.dart';
import 'package:tmail_ui_user/features/labels/presentation/models/label_action_type.dart';
extension HandleLabelActionTypeExtension on LabelController {
void handleLabelActionType({
required LabelActionType actionType,
required AccountId? accountId,
Label? label,
}) {
switch (actionType) {
case LabelActionType.create:
openCreateNewLabelModal(accountId);
break;
case LabelActionType.edit:
break;
}
}
}
@@ -16,6 +16,7 @@ import 'package:tmail_ui_user/features/labels/domain/state/get_all_label_state.d
import 'package:tmail_ui_user/features/labels/domain/usecases/create_new_label_interactor.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/get_all_label_interactor.dart';
import 'package:tmail_ui_user/features/labels/presentation/label_interactor_bindings.dart';
import 'package:tmail_ui_user/features/labels/presentation/mixin/label_context_menu_mixin.dart';
import 'package:tmail_ui_user/features/labels/presentation/widgets/create_new_label_modal.dart';
import 'package:tmail_ui_user/features/manage_account/domain/state/get_label_setting_state.dart';
import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_label_setting_state_interactor.dart';
@@ -24,7 +25,7 @@ import 'package:tmail_ui_user/main/exceptions/logic_exception.dart';
import 'package:tmail_ui_user/main/routes/dialog_router.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
class LabelController extends BaseController {
class LabelController extends BaseController with LabelContextMenuMixin {
final labels = <Label>[].obs;
final labelListExpandMode = Rx(ExpandMode.EXPAND);
final isLabelSettingEnabled = RxBool(false);
@@ -0,0 +1,71 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:flutter/material.dart';
import 'package:labels/model/label.dart';
import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart';
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart';
import 'package:tmail_ui_user/features/labels/presentation/models/label_action_type.dart';
import 'package:tmail_ui_user/features/labels/presentation/models/popup_menu_item_label_action.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/utils/labels/label_method_action_define.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
mixin LabelContextMenuMixin on PopupContextMenuActionMixin {
Future<void> openLabelContextMenuAction(
BuildContext context,
ImagePaths imagePaths,
Label label,
RelativeRect position,
OnLabelActionTypeCallback onLabelActionTypeCallback,
) async {
final listLabelAction = [LabelActionType.edit];
final popupMenuItems = listLabelAction
.map((actionType) => _buildLabelPopupMenuItem(
label,
actionType,
AppLocalizations.of(context),
imagePaths,
onLabelActionTypeCallback,
))
.toList();
return openPopupMenuAction(
context,
position,
popupMenuItems,
);
}
PopupMenuItem _buildLabelPopupMenuItem(
Label label,
LabelActionType actionType,
AppLocalizations appLocalizations,
ImagePaths imagePaths,
OnLabelActionTypeCallback onLabelActionTypeCallback,
) {
return PopupMenuItem(
padding: EdgeInsets.zero,
child: PopupMenuItemActionWidget(
menuAction: PopupMenuItemLabelAction(
actionType,
appLocalizations,
imagePaths,
),
menuActionClick: (menuAction) => _onPerformLabelActionType(
label,
actionType,
onLabelActionTypeCallback,
),
),
);
}
void _onPerformLabelActionType(
Label label,
LabelActionType actionType,
OnLabelActionTypeCallback onLabelActionTypeCallback,
) {
popBack();
onLabelActionTypeCallback(label, actionType);
}
}
@@ -0,0 +1,46 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
enum LabelActionType {
create,
edit;
String getContextMenuTitle(AppLocalizations appLocalizations) {
switch (this) {
case LabelActionType.create:
return appLocalizations.create;
case LabelActionType.edit:
return appLocalizations.edit;
}
}
String getModalTitle(AppLocalizations appLocalizations) {
switch (this) {
case LabelActionType.create:
return appLocalizations.createANewLabel;
case LabelActionType.edit:
return appLocalizations.updateTheLabelName;
}
}
String getContextMenuIcon(ImagePaths imagePaths) {
switch (this) {
case LabelActionType.create:
return imagePaths.icAddNewFolder;
case LabelActionType.edit:
return imagePaths.icRenameMailbox;
}
}
Color getPopupMenuTitleColor() => Colors.black;
Color getPopupMenuIconColor() => AppColor.steelGrayA540;
Color getContextMenuIconColor() =>
AppColor.gray424244.withValues(alpha: 0.72);
Color getContextMenuTitleColor() =>
AppColor.gray424244.withValues(alpha: 0.9);
}
@@ -0,0 +1,32 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/base/model/popup_menu_item_action.dart';
import 'package:tmail_ui_user/features/labels/presentation/models/label_action_type.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class PopupMenuItemLabelAction
extends PopupMenuItemActionRequiredIcon<LabelActionType> {
final AppLocalizations appLocalizations;
final ImagePaths imagePaths;
PopupMenuItemLabelAction(
super.action,
this.appLocalizations,
this.imagePaths,
);
@override
String get actionIcon => action.getContextMenuIcon(imagePaths);
@override
String get actionName => action.getContextMenuTitle(appLocalizations);
@override
Color get actionIconColor => action.getPopupMenuIconColor();
@override
Color get actionNameColor => action.getPopupMenuTitleColor();
@override
String get hoverIcon => imagePaths.icThumbsUp;
}
@@ -10,6 +10,9 @@ import 'package:model/extensions/session_extension.dart';
import 'package:model/mailbox/expand_mode.dart';
import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart';
import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart';
import 'package:tmail_ui_user/features/labels/presentation/extensions/handle_label_action_type_extension.dart';
import 'package:tmail_ui_user/features/labels/presentation/models/label_action_type.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/handle_label_action_type_extension.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/handle_mailbox_action_extension.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/open_app_grid_extension.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/toggle_expand_folders_extension.dart';
@@ -263,6 +266,9 @@ abstract class BaseMailboxView extends GetWidget<MailboxController>
Widget buildListMailbox(BuildContext context) {
final isDesktop = controller.responsiveUtils.isDesktop(context);
final isMobileResponsive = controller
.responsiveUtils
.isScreenWithShortestSide(context);
return SingleChildScrollView(
controller: controller.mailboxListScrollController,
@@ -342,7 +348,7 @@ abstract class BaseMailboxView extends GetWidget<MailboxController>
: const Offstage(),
)),
buildLabelsBar(context, isDesktop),
buildLabelsList(context, isDesktop),
buildLabelsList(context, isDesktop, isMobileResponsive),
]),
);
}
@@ -373,7 +379,10 @@ abstract class BaseMailboxView extends GetWidget<MailboxController>
countLabels: countLabels,
onToggleLabelListState: labelController.toggleLabelListState,
onAddNewLabel: () =>
labelController.openCreateNewLabelModal(accountId),
labelController.handleLabelActionType(
actionType: LabelActionType.create,
accountId: accountId,
),
);
} else {
return const SizedBox.shrink();
@@ -381,17 +390,19 @@ abstract class BaseMailboxView extends GetWidget<MailboxController>
});
}
Widget buildLabelsList(BuildContext context, bool isDesktop) {
Widget buildLabelsList(
BuildContext context,
bool isDesktop,
bool isMobileResponsive,
) {
return Obx(() {
final isLabelAvailable = controller
.mailboxDashBoardController
.isLabelAvailable;
final dashboardController = controller.mailboxDashBoardController;
final labelController =
controller.mailboxDashBoardController.labelController;
final isLabelAvailable = dashboardController.isLabelAvailable;
final selectedMailbox = controller.mailboxDashBoardController
.selectedMailbox.value;
final labelController = dashboardController.labelController;
final selectedMailbox = dashboardController.selectedMailbox.value;
Id? labelIdSelected;
if (selectedMailbox is PresentationLabelMailbox) {
labelIdSelected = selectedMailbox.id.id;
@@ -409,10 +420,18 @@ abstract class BaseMailboxView extends GetWidget<MailboxController>
imagePaths: controller.imagePaths,
isDesktop: isDesktop,
labelIdSelected: labelIdSelected,
isMobileResponsive: isMobileResponsive,
onOpenLabelCallback: (label) => controller.openMailbox(
context,
PresentationLabelMailbox.initial(label),
),
onOpenContextMenu: (label, position) =>
dashboardController.openLabelContextMenuAction(
context,
controller.imagePaths,
label,
position,
),
)
: const Offstage(),
);
@@ -0,0 +1,41 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:labels/model/label.dart';
import 'package:tmail_ui_user/features/labels/presentation/extensions/handle_label_action_type_extension.dart';
import 'package:tmail_ui_user/features/labels/presentation/models/label_action_type.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
extension HandleLabelActionTypeExtension on MailboxDashBoardController {
Future<void> openLabelContextMenuAction(
BuildContext context,
ImagePaths imagePaths,
Label label,
RelativeRect position,
) async {
if (PlatformInfo.isWeb) {
isPopupMenuOpened.value = true;
}
return labelController
.openLabelContextMenuAction(
context,
imagePaths,
label,
position,
_onPerformLabelActionType,
)
.whenComplete(() {
if (PlatformInfo.isWeb) {
isPopupMenuOpened.value = false;
}
});
}
void _onPerformLabelActionType(Label label, LabelActionType actionType) {
labelController.handleLabelActionType(
actionType: actionType,
accountId: accountId.value,
label: label,
);
}
}
@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:labels/labels.dart';
import 'package:tmail_ui_user/features/labels/presentation/models/label_action_type.dart';
typedef OnOpenLabelContextMenuAction = Future<void> Function(
Label label,
RelativeRect position,
);
typedef OnLabelActionTypeCallback = void Function(
Label label,
LabelActionType actionType,
);
@@ -1,20 +1,25 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/views/button/tmail_button_widget.dart';
import 'package:flutter/material.dart';
import 'package:labels/extensions/label_extension.dart';
import 'package:labels/model/label.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/styles/mailbox_item_widget_styles.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/styles/trailing_mailbox_item_widget_styles.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/utils/labels/label_method_action_define.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/widgets/labels/label_icon_widget.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/widgets/labels/label_name_widget.dart';
typedef OnOpenLabelCallback = void Function(Label label);
class LabelListItem extends StatelessWidget {
class LabelListItem extends StatefulWidget {
final Label label;
final ImagePaths imagePaths;
final bool isDesktop;
final bool isSelected;
final bool isMobileResponsive;
final OnOpenLabelCallback onOpenLabelCallback;
final OnOpenLabelContextMenuAction? onOpenContextMenu;
const LabelListItem({
super.key,
@@ -23,11 +28,30 @@ class LabelListItem extends StatelessWidget {
required this.onOpenLabelCallback,
this.isDesktop = false,
this.isSelected = false,
this.isMobileResponsive = false,
this.onOpenContextMenu,
});
@override
Widget build(BuildContext context) {
final borderRadius = BorderRadius.all(
State<LabelListItem> createState() => _LabelListItemState();
}
class _LabelListItemState extends State<LabelListItem> {
bool _isContextMenuVisible = false;
bool _isItemHovered = false;
late final BorderRadius _borderRadius;
late final EdgeInsetsGeometry _itemPadding;
late final double _itemHeight;
late final double _iconSpacing;
@override
void initState() {
super.initState();
final isDesktop = widget.isDesktop;
_borderRadius = BorderRadius.all(
Radius.circular(
isDesktop
? MailboxItemWidgetStyles.borderRadius
@@ -35,41 +59,57 @@ class LabelListItem extends StatelessWidget {
),
);
_itemPadding = EdgeInsetsDirectional.symmetric(
horizontal: isDesktop
? MailboxItemWidgetStyles.itemPadding
: MailboxItemWidgetStyles.mobileItemPadding,
);
_itemHeight = isDesktop
? MailboxItemWidgetStyles.height
: MailboxItemWidgetStyles.mobileHeight;
_iconSpacing = isDesktop
? MailboxItemWidgetStyles.labelIconSpace
: MailboxItemWidgetStyles.mobileLabelIconSpace;
}
bool get _isMenuButtonVisible => _isContextMenuVisible || _isItemHovered;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: borderRadius,
onTap: () => onOpenLabelCallback(label),
borderRadius: _borderRadius,
onHover: _handleHoverChanged,
onTap: () => widget.onOpenLabelCallback(widget.label),
child: Container(
height: _itemHeight,
padding: _itemPadding,
decoration: BoxDecoration(
borderRadius: borderRadius,
borderRadius: _borderRadius,
color: _backgroundColorItem,
),
padding: EdgeInsetsDirectional.symmetric(
horizontal: isDesktop
? MailboxItemWidgetStyles.itemPadding
: MailboxItemWidgetStyles.mobileItemPadding,
),
height: isDesktop
? MailboxItemWidgetStyles.height
: MailboxItemWidgetStyles.mobileHeight,
child: Row(
children: [
LabelIconWidget(
icon: imagePaths.icTag,
color: label.backgroundColor,
padding: EdgeInsetsDirectional.only(
end: isDesktop
? MailboxItemWidgetStyles.labelIconSpace
: MailboxItemWidgetStyles.mobileLabelIconSpace,
),
_LabelIcon(
icon: widget.imagePaths.icTag,
color: widget.label.backgroundColor,
spacing: _iconSpacing,
),
Expanded(
child: LabelNameWidget(
name: label.safeDisplayName,
isDesktop: isDesktop,
name: widget.label.safeDisplayName,
isDesktop: widget.isDesktop,
),
),
_ContextMenuButton(
visible: _isMenuButtonVisible,
backgroundColor: _menuButtonBackgroundColor(context),
imagePaths: widget.imagePaths,
onTap: _handleMenuTap,
),
],
),
),
@@ -78,5 +118,90 @@ class LabelListItem extends StatelessWidget {
}
Color get _backgroundColorItem =>
isSelected ? AppColor.blue100 : Colors.transparent;
widget.isSelected ? AppColor.blue100 : Colors.transparent;
Color _menuButtonBackgroundColor(BuildContext context) {
if (_isContextMenuVisible) {
return Theme.of(context).colorScheme.outline.withValues(alpha: 0.08);
}
return TrailingMailboxItemWidgetStyles.menuIconBackgroundColor;
}
void _handleHoverChanged(bool hovered) {
if (_isItemHovered == hovered) return;
setState(() {
_isItemHovered = hovered;
});
}
void _setContextMenuVisible(bool visible) {
if (_isContextMenuVisible == visible) return;
setState(() {
_isContextMenuVisible = visible;
});
}
void _handleMenuTap(RelativeRect position) {
if (!widget.isMobileResponsive) {
_setContextMenuVisible(true);
}
widget.onOpenContextMenu?.call(widget.label, position).whenComplete(() {
if (mounted && !widget.isMobileResponsive) {
_setContextMenuVisible(false);
}
});
}
}
class _LabelIcon extends StatelessWidget {
final String icon;
final Color? color;
final double spacing;
const _LabelIcon({
required this.icon,
required this.color,
required this.spacing,
});
@override
Widget build(BuildContext context) {
return LabelIconWidget(
icon: icon,
color: color,
padding: EdgeInsetsDirectional.only(end: spacing),
);
}
}
class _ContextMenuButton extends StatelessWidget {
final bool visible;
final Color backgroundColor;
final ImagePaths imagePaths;
final ValueChanged<RelativeRect> onTap;
const _ContextMenuButton({
required this.visible,
required this.backgroundColor,
required this.imagePaths,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Offstage(
offstage: !visible,
child: TMailButtonWidget.fromIcon(
margin: TrailingMailboxItemWidgetStyles.menuIconMargin,
icon: imagePaths.icMoreVertical,
iconSize: TrailingMailboxItemWidgetStyles.menuIconSize,
padding: TrailingMailboxItemWidgetStyles.menuIconPadding,
backgroundColor: backgroundColor,
onTapActionAtPositionCallback: onTap,
),
);
}
}
@@ -2,6 +2,7 @@ import 'package:core/presentation/resources/image_paths.dart';
import 'package:flutter/material.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:labels/model/label.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/utils/labels/label_method_action_define.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/widgets/labels/label_list_item.dart';
class LabelListView extends StatelessWidget {
@@ -10,6 +11,8 @@ class LabelListView extends StatelessWidget {
final bool isDesktop;
final Id? labelIdSelected;
final OnOpenLabelCallback onOpenLabelCallback;
final bool isMobileResponsive;
final OnOpenLabelContextMenuAction? onOpenContextMenu;
const LabelListView({
super.key,
@@ -18,6 +21,8 @@ class LabelListView extends StatelessWidget {
required this.onOpenLabelCallback,
this.isDesktop = false,
this.labelIdSelected,
this.isMobileResponsive = false,
this.onOpenContextMenu,
});
@override
@@ -36,6 +41,8 @@ class LabelListView extends StatelessWidget {
isSelected: label.id == labelIdSelected,
isDesktop: isDesktop,
onOpenLabelCallback: onOpenLabelCallback,
isMobileResponsive: isMobileResponsive,
onOpenContextMenu: onOpenContextMenu,
);
},
);
+13 -1
View File
@@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-19T17:48:08.806042",
"@@last_modified": "2026-01-20T09:45:15.272706",
"initializing_data": "Initializing data...",
"@initializing_data": {
"type": "text",
@@ -5353,5 +5353,17 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"editLabel": "Edit label",
"@editLabel": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"updateTheLabelName": "Update the label name",
"@updateTheLabelName": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
@@ -5680,4 +5680,18 @@ class AppLocalizations {
name: 'allLabels',
);
}
String get editLabel {
return Intl.message(
'Edit label',
name: 'editLabel',
);
}
String get updateTheLabelName {
return Intl.message(
'Update the label name',
name: 'updateTheLabelName',
);
}
}