diff --git a/assets/images/ic_selected.svg b/assets/images/ic_selected.svg new file mode 100644 index 000000000..8cee3bcb1 --- /dev/null +++ b/assets/images/ic_selected.svg @@ -0,0 +1,4 @@ + + + + diff --git a/core/lib/core.dart b/core/lib/core.dart index d45341d83..4734f686a 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -26,6 +26,9 @@ export 'presentation/views/button/button_builder.dart'; export 'presentation/views/image/avatar_builder.dart'; export 'presentation/views/list/sliver_grid_delegate_fixed_height.dart'; export 'presentation/views/text/text_form_field_builder.dart'; +export 'presentation/views/image/icon_builder.dart'; +export 'presentation/views/context_menu/context_menu_action_builder.dart'; +export 'presentation/views/context_menu/context_menu_builder.dart'; // Resources export 'presentation/resources/assets_paths.dart'; diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 427ea123b..7eccb38db 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -36,6 +36,7 @@ class ImagePaths { String get icComposerMenu => _getImagePath('ic_composer_menu.svg'); String get icComposerFileShare => _getImagePath('ic_composer_file_share.svg'); String get icExpandAddress => _getImagePath('ic_expand_address.svg'); + String get icSelected => _getImagePath('ic_selected.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/core/lib/presentation/views/context_menu/context_menu_action_builder.dart b/core/lib/presentation/views/context_menu/context_menu_action_builder.dart new file mode 100644 index 000000000..7b0402cb3 --- /dev/null +++ b/core/lib/presentation/views/context_menu/context_menu_action_builder.dart @@ -0,0 +1,31 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:meta/meta.dart'; + +typedef OnContextMenuActionClick = void Function(T data); + +abstract class ContextMenuActionBuilder { + @protected final Key key; + @protected final SvgPicture actionIcon; + @protected final String actionName; + @protected OnContextMenuActionClick? onContextMenuActionClick; + + ContextMenuActionBuilder( + this.key, + this.actionIcon, + this.actionName); + + void onActionClick(OnContextMenuActionClick onContextMenuActionClick) { + this.onContextMenuActionClick = onContextMenuActionClick; + } + + TextStyle actionTextStyle() { + return TextStyle( + fontSize: 15, + color: AppColor.nameUserColor); + } + + Widget build(); +} \ No newline at end of file diff --git a/core/lib/presentation/views/context_menu/context_menu_builder.dart b/core/lib/presentation/views/context_menu/context_menu_builder.dart new file mode 100644 index 000000000..f6847af96 --- /dev/null +++ b/core/lib/presentation/views/context_menu/context_menu_builder.dart @@ -0,0 +1,100 @@ + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:get/get.dart'; + +typedef OnCloseContextMenuAction = void Function(); + +class ContextMenuBuilder { + + final BuildContext _context; + final List _actionTiles = []; + final bool areTilesHorizontal; + + Widget? _header; + Widget? _footer; + OnCloseContextMenuAction? _onCloseContextMenuAction; + + ContextMenuBuilder( + this._context, + { + this.areTilesHorizontal = false + } + ); + + void addTiles(List tiles) { + _actionTiles.addAll(tiles); + } + + void addHeader(Widget header) { + _header = header; + } + + void addFooter(Widget footer) { + _footer = footer; + } + + void addOnCloseContextMenuAction(OnCloseContextMenuAction onCloseContextMenuAction) { + _onCloseContextMenuAction = onCloseContextMenuAction; + } + + RoundedRectangleBorder _shape() { + return RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.0), + topRight: Radius.circular(20.0))); + } + + BoxDecoration _decoration(BuildContext context) { + return BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(20.0), + topRight: const Radius.circular(20.0))); + } + + void build() { + Get.bottomSheet( + GestureDetector( + onTap: () { + if (_onCloseContextMenuAction != null) { + _onCloseContextMenuAction!(); + }}, + child: SingleChildScrollView( + child: Container( + margin: EdgeInsets.zero, + decoration: _decoration(_context), + child: GestureDetector( + onTap: () => {}, + child: Wrap( + children: [ + _header ?? SizedBox.shrink(), + Divider(), + areTilesHorizontal + ? Row(children: [ + ..._actionTiles, + _actionTiles.isNotEmpty && _footer != null ? Divider() : SizedBox.shrink() + ]) + : Column(children: [ + ..._actionTiles, + _actionTiles.isNotEmpty && _footer != null ? Divider() : SizedBox.shrink() + ]), + _footer != null + ? Padding( + padding: EdgeInsets.only(bottom: 10.0), + child: Wrap(children: [_footer ?? SizedBox.shrink()])) + : SizedBox.shrink(), + ], + ), + ), + ), + ), + ), + useRootNavigator: true, + shape: _shape(), + isScrollControlled: true, + enableDrag: false, + backgroundColor: Colors.transparent + ); + } +} \ No newline at end of file diff --git a/core/lib/presentation/views/image/avatar_builder.dart b/core/lib/presentation/views/image/avatar_builder.dart index e853eda60..e086b6b63 100644 --- a/core/lib/presentation/views/image/avatar_builder.dart +++ b/core/lib/presentation/views/image/avatar_builder.dart @@ -2,12 +2,15 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +typedef OnTapAvatarActionClick = void Function(); + class AvatarBuilder { Key? _key; String? _text; double? _size; String? _iconStatus; Color? _bgColor; + OnTapAvatarActionClick? _onTapAvatarActionClick; AvatarBuilder key(Key key) { _key = key; @@ -34,31 +37,41 @@ class AvatarBuilder { return this; } + AvatarBuilder addOnTapActionClick(OnTapAvatarActionClick onTapAvatarActionClick) { + _onTapAvatarActionClick = onTapAvatarActionClick; + return this; + } + Widget build() { - return Container( - key: _key, - width: _size ?? 40, - height: _size ?? 40, - alignment: Alignment.center, - child: Stack( - children: [ - Container( - width: _size ?? 40, - height: _size ?? 40, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular((_size ?? 40) / 2), - border: Border.all(color: Colors.white), - color: _bgColor ?? AppColor.avatarColor), - child: Text( - '${_text ?? ''}', - style: TextStyle(fontSize: 20, color: AppColor.avatarTextColor, fontWeight: FontWeight.w500))), - if (_iconStatus != null && _iconStatus!.isNotEmpty) - Align( - child: SvgPicture.asset(_iconStatus!, width: 12, height: 12, fit: BoxFit.fill), - alignment: Alignment.bottomRight) - ], - ) + return GestureDetector( + onTap: () { + if (_onTapAvatarActionClick != null) { + _onTapAvatarActionClick!(); + }}, + child: Container( + key: _key, + width: _size ?? 40, + height: _size ?? 40, + alignment: Alignment.center, + child: Stack( + children: [ + Container( + width: _size ?? 40, + height: _size ?? 40, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular((_size ?? 40) / 2), + border: Border.all(color: Colors.white), + color: _bgColor ?? AppColor.avatarColor), + child: Text( + '${_text ?? ''}', + style: TextStyle(fontSize: 20, color: AppColor.avatarTextColor, fontWeight: FontWeight.w500))), + if (_iconStatus != null && _iconStatus!.isNotEmpty) + Align( + child: SvgPicture.asset(_iconStatus!, width: 12, height: 12, fit: BoxFit.fill), + alignment: Alignment.bottomRight) + ], + )), ); } } \ No newline at end of file diff --git a/core/lib/presentation/views/image/icon_builder.dart b/core/lib/presentation/views/image/icon_builder.dart new file mode 100644 index 000000000..ee6fae814 --- /dev/null +++ b/core/lib/presentation/views/image/icon_builder.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +typedef OnPressIconActionClick = void Function(); + +class IconBuilder { + Key? _key; + double? _size; + String? _icon; + OnPressIconActionClick? _onPressIconActionClick; + + IconBuilder(this._icon); + + void key(Key key) { + _key = key; + } + + void size(double size) { + _size = size; + } + + void addOnTapActionClick(OnPressIconActionClick onPressIconActionClick) { + _onPressIconActionClick = onPressIconActionClick; + } + + Widget build() { + return Container( + key: _key, + width: _size ?? 40, + height: _size ?? 40, + alignment: Alignment.center, + padding: EdgeInsets.all(3), + child: IconButton( + padding: EdgeInsets.zero, + iconSize: _size ?? 40, + icon: SvgPicture.asset(_icon!, width: _size ?? 40, height: _size ?? 40, fit: BoxFit.fill), + onPressed: () => { + if (_onPressIconActionClick != null) { + _onPressIconActionClick!() + } + }) + ); + } +} \ No newline at end of file diff --git a/core/pubspec.yaml b/core/pubspec.yaml index a37ae8ed6..8c41bfecf 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -50,6 +50,9 @@ dependencies: # sqflite sqflite: 2.0.0+4 + # GetX + get: 4.1.4 + dev_dependencies: flutter_test: sdk: flutter diff --git a/lib/features/thread/presentation/thread_bindings.dart b/lib/features/thread/presentation/thread_bindings.dart index bd8bd68a6..82adc1713 100644 --- a/lib/features/thread/presentation/thread_bindings.dart +++ b/lib/features/thread/presentation/thread_bindings.dart @@ -1,4 +1,5 @@ import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/thread/data/datasource/thread_datasource.dart'; import 'package:tmail_ui_user/features/thread/data/datasource_impl/thread_datasource_impl.dart'; @@ -16,9 +17,11 @@ class ThreadBindings extends Bindings { Get.lazyPut(() => ThreadRepositoryImpl(Get.find())); Get.lazyPut(() => Get.find()); Get.lazyPut(() => GetEmailsInMailboxInteractor(Get.find())); + Get.lazyPut(() => ScrollController()); Get.put(ThreadController( Get.find(), Get.find(), + Get.find(), )); } } \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 38554daa8..f17f80f09 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -2,6 +2,7 @@ import 'package:core/core.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; @@ -21,7 +22,9 @@ import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants. import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/load_more_state.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/email_context_menu_action_builder.dart'; import 'package:tmail_ui_user/main/actions/email_action.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -30,6 +33,7 @@ class ThreadController extends BaseController { final mailboxDashBoardController = Get.find(); final GetEmailsInMailboxInteractor _getEmailsInMailboxInteractor; final ResponsiveUtils responsiveUtils; + final ScrollController listEmailController; final _properties = Properties({ 'id', 'subject', 'from', 'to', 'cc', 'bcc', 'keywords', 'receivedAt', @@ -38,6 +42,7 @@ class ThreadController extends BaseController { final emailList = [].obs; final loadMoreState = LoadMoreState.IDLE.obs; + final currentSelectMode = SelectMode.INACTIVE.obs; int positionCurrent = 0; int lastGetTotal = 0; @@ -45,7 +50,8 @@ class ThreadController extends BaseController { ThreadController( this.responsiveUtils, - this._getEmailsInMailboxInteractor + this._getEmailsInMailboxInteractor, + this.listEmailController, ); @override @@ -66,8 +72,9 @@ class ThreadController extends BaseController { @override void dispose() { - super.dispose(); mailboxDashBoardController.selectedMailbox.close(); + listEmailController.dispose(); + super.dispose(); } @override @@ -175,13 +182,35 @@ class ThreadController extends BaseController { : SelectMode.INACTIVE; } - void selectEmail(BuildContext context, PresentationEmail presentationEmailSelected) { + void previewEmail(BuildContext context, PresentationEmail presentationEmailSelected) { mailboxDashBoardController.setSelectedEmail(presentationEmailSelected); if (!responsiveUtils.isDesktop(context)) { goToEmail(context); } } + void selectEmail(BuildContext context, PresentationEmail presentationEmailSelected) { + emailList.value = emailList.map((email) => email.id == presentationEmailSelected.id ? email.toggleSelect() : email).toList(); + if (_isUnSelectedAll()) { + currentSelectMode.value = SelectMode.INACTIVE; + } else { + if (currentSelectMode.value == SelectMode.INACTIVE) { + currentSelectMode.value = SelectMode.ACTIVE; + } + } + } + + List getListEmailSelected() => emailList.where((email) => email.selectMode == SelectMode.ACTIVE).toList(); + + bool _isUnSelectedAll() => emailList.every((email) => email.selectMode == SelectMode.INACTIVE); + + bool _isEmailAllRead(List listEmail) => listEmail.every((email) => !email.isUnReadEmail()); + + void cancelSelectEmail() { + emailList.value = emailList.map((email) => email.toSelectedEmail(selectMode: SelectMode.INACTIVE)).toList(); + currentSelectMode.value = SelectMode.INACTIVE; + } + void _updateStateUnReadEmail(EmailId emailId, {required bool unRead}) { final newEmailList = emailList .map((email) => email.id == emailId ? email.markAsReadPresentationEmail(unRead: unRead) : email) @@ -189,6 +218,73 @@ class ThreadController extends BaseController { emailList.value = newEmailList; } + void unreadSelectedEmail(List listEmail) { + popBack(); + } + + void openContextMenuSelectedEmail(BuildContext context, ImagePaths imagePaths, List listEmail) { + (ContextMenuBuilder(context) + ..addTiles(_contextMenuActionList(context, imagePaths, listEmail))) + .build(); + } + + List _contextMenuActionList(BuildContext context, ImagePaths imagePaths, List listEmail) { + return [ + _moveToTrashAction(context, imagePaths, listEmail), + _moveToMailboxAction(context, imagePaths, listEmail), + _markAsSeenAction(context, imagePaths, listEmail), + _markAsFlagAction(context, imagePaths, listEmail), + _moveToSpamAction(context, imagePaths, listEmail), + SizedBox(height: 40), + ]; + } + + Widget _markAsSeenAction(BuildContext context, ImagePaths imagePaths, List listEmail) { + return (EmailContextMenuActionBuilder( + Key('mark_as_seen_context_menu_action'), + SvgPicture.asset(imagePaths.icEyeDisable, width: 24, height: 24, fit: BoxFit.fill), + _isEmailAllRead(listEmail) ? AppLocalizations.of(context).mark_as_unread : AppLocalizations.of(context).mark_as_read, + listEmail) + ..onActionClick((data) => unreadSelectedEmail(data))) + .build(); + } + + Widget _moveToTrashAction(BuildContext context, ImagePaths imagePaths, List listEmail) { + return (EmailContextMenuActionBuilder( + Key('move_to_trash_context_menu_action'), + SvgPicture.asset(imagePaths.icTrash, width: 24, height: 24, fit: BoxFit.fill), + AppLocalizations.of(context).move_to_trash, listEmail) + ..onActionClick((data) => {})) + .build(); + } + + Widget _moveToMailboxAction(BuildContext context, ImagePaths imagePaths, List listEmail) { + return (EmailContextMenuActionBuilder( + Key('move_to_mailbox_context_menu_action'), + SvgPicture.asset(imagePaths.icFolder, width: 24, height: 24, fit: BoxFit.fill), + AppLocalizations.of(context).move_to_mailbox, listEmail) + ..onActionClick((data) => {})) + .build(); + } + + Widget _markAsFlagAction(BuildContext context, ImagePaths imagePaths, List listEmail) { + return (EmailContextMenuActionBuilder( + Key('mark_as_flag_context_menu_action'), + SvgPicture.asset(imagePaths.icFlag, width: 24, height: 24, fit: BoxFit.fill), + AppLocalizations.of(context).mark_as_flag, listEmail) + ..onActionClick((data) => {})) + .build(); + } + + Widget _moveToSpamAction(BuildContext context, ImagePaths imagePaths, List listEmail) { + return (EmailContextMenuActionBuilder( + Key('move_to_spam_context_menu_action'), + SvgPicture.asset(imagePaths.icMailboxSpam, width: 24, height: 24, fit: BoxFit.fill), + AppLocalizations.of(context).move_to_spam, listEmail) + ..onActionClick((data) => {})) + .build(); + } + bool canComposeEmail() => mailboxDashBoardController.sessionCurrent != null && mailboxDashBoardController.userProfile.value != null && mailboxDashBoardController.mapMailboxId.containsKey(PresentationMailbox.roleOutbox); diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index f1aa148d3..d9e5556bf 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -6,6 +6,7 @@ import 'package:model/email/presentation_email.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/load_more_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/thread_controller.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar_thread_select_mode_active_builder.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar_thread_widget_builder.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -30,7 +31,7 @@ class ThreadView extends GetWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildAppBarMailboxListMail(context), + _buildAppBarThread(context), _buildLoadingView(), Expanded(child: _buildListEmail(context)), _buildLoadingViewLoadMore() @@ -47,17 +48,37 @@ class ThreadView extends GetWidget { ); } - Widget _buildAppBarMailboxListMail(BuildContext context) { + Widget _buildAppBarThread(BuildContext context) { + return Obx(() => controller.currentSelectMode.value == SelectMode.ACTIVE + ? _buildAppBarSelectModeActive(context) + : _buildAppBarNormal(context)); + } + + Widget _buildAppBarNormal(BuildContext context) { return Obx(() => Padding( - padding: EdgeInsets.only(left: 15, right: 12), - child: AppBarThreadWidgetBuilder( + padding: EdgeInsets.only(left: 15, right: 12), + child: AppBarThreadWidgetBuilder( context, imagePaths, responsiveUtils, controller.mailboxDashBoardController.selectedMailbox.value, controller.mailboxDashBoardController.userProfile.value) - .onOpenListMailboxActionClick(() => controller.openMailboxLeftMenu()) - .build())); + .onOpenListMailboxActionClick(() => controller.openMailboxLeftMenu()) + .build())); + } + + Widget _buildAppBarSelectModeActive(BuildContext context) { + return Obx(() => Padding( + padding: EdgeInsets.only(left: 15, right: 12), + child: (AppBarThreadSelectModeActiveBuilder( + context, + imagePaths, + controller.getListEmailSelected()) + ..addCloseActionClick(() => controller.cancelSelectEmail()) + ..addRemoveEmailActionClick((listEmail) => {}) + ..addUnreadEmailActionClick((listEmail) => controller.unreadSelectedEmail(listEmail)) + ..addOpenContextMenuActionClick((listEmail) => controller.openContextMenuSelectedEmail(context, imagePaths, listEmail))) + .build())); } Widget _buildLoadingView() { @@ -92,12 +113,16 @@ class ThreadView extends GetWidget { alignment: Alignment.center, padding: EdgeInsets.zero, color: responsiveUtils.isMobile(context) ? AppColor.bgMailboxListMail : Colors.white, - child: RefreshIndicator( - color: AppColor.primaryColor, - onRefresh: () async => controller.refreshGetAllEmailAction(), - child: Obx(() => controller.emailList.isNotEmpty - ? _buildListEmailBody(context, controller.emailList) - : _buildEmptyEmail(context)))); + child: Obx(() => controller.currentSelectMode.value == SelectMode.INACTIVE + ? RefreshIndicator( + color: AppColor.primaryColor, + onRefresh: () async => controller.refreshGetAllEmailAction(), + child: controller.emailList.isNotEmpty + ? _buildListEmailBody(context, controller.emailList) + : _buildEmptyEmail(context)) + : controller.emailList.isNotEmpty + ? _buildListEmailBody(context, controller.emailList) + : _buildEmptyEmail(context))); } Widget _buildListEmailBody(BuildContext context, List listPresentationEmail) { @@ -114,18 +139,21 @@ class ThreadView extends GetWidget { return false; }, child: ListView.builder( + controller: controller.listEmailController, padding: EdgeInsets.only(top: 16), key: Key('presentation_email_list'), itemCount: listPresentationEmail.length, itemBuilder: (context, index) => - Obx(() => EmailTileBuilder( - context, - imagePaths, - controller.getSelectMode(listPresentationEmail[index], controller.mailboxDashBoardController.selectedEmail.value), - listPresentationEmail[index], - responsiveUtils) - .onOpenMailAction(() => controller.selectEmail(context, listPresentationEmail[index])) - .build()) + Obx(() => (EmailTileBuilder( + context, + imagePaths, + controller.getSelectMode(listPresentationEmail[index], controller.mailboxDashBoardController.selectedEmail.value), + listPresentationEmail[index], + responsiveUtils, + controller.currentSelectMode.value) + ..onOpenEmailAction((selectedEmail) => controller.previewEmail(context, selectedEmail)) + ..onSelectEmailAction((selectedEmail) => controller.selectEmail(context, selectedEmail))) + .build()), )); } } diff --git a/lib/features/thread/presentation/widgets/app_bar_thread_select_mode_active_builder.dart b/lib/features/thread/presentation/widgets/app_bar_thread_select_mode_active_builder.dart new file mode 100644 index 000000000..c4b08ab51 --- /dev/null +++ b/lib/features/thread/presentation/widgets/app_bar_thread_select_mode_active_builder.dart @@ -0,0 +1,111 @@ + +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnCloseActionClick = void Function(); +typedef OnUnreadEmailActionClick = void Function(List listEmail); +typedef OnRemoveEmailActionClick = void Function(List listEmail); +typedef OnOpenContextMenuActionClick = void Function(List listEmail); + +class AppBarThreadSelectModeActiveBuilder { + OnCloseActionClick? _onCloseActionClick; + OnUnreadEmailActionClick? _onUnreadEmailActionClick; + OnRemoveEmailActionClick? _onRemoveEmailActionClick; + OnOpenContextMenuActionClick? _onOpenContextMenuActionClick; + + final BuildContext _context; + final ImagePaths _imagePaths; + final List _listEmail; + + AppBarThreadSelectModeActiveBuilder(this._context, this._imagePaths, this._listEmail); + + void addCloseActionClick(OnCloseActionClick onCloseActionClick) { + _onCloseActionClick = onCloseActionClick; + } + + void addUnreadEmailActionClick(OnUnreadEmailActionClick onUnreadEmailActionClick) { + _onUnreadEmailActionClick = onUnreadEmailActionClick; + } + + void addRemoveEmailActionClick(OnRemoveEmailActionClick onRemoveEmailActionClick) { + _onRemoveEmailActionClick = onRemoveEmailActionClick; + } + + void addOpenContextMenuActionClick(OnOpenContextMenuActionClick onOpenContextMenuActionClick) { + _onOpenContextMenuActionClick = onOpenContextMenuActionClick; + } + + Widget build() { + return Container( + key: Key('app_bar_thread_select_mode_active'), + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 8), + color: Colors.white, + child: MediaQuery( + data: MediaQueryData(padding: EdgeInsets.zero), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _buildBackButton(), + Expanded(child: _buildCountItemSelected()), + _buildListOptionButton(), + ] + ) + ) + ); + } + + Widget _buildCountItemSelected() { + return Padding( + padding: EdgeInsets.only(left: 12), + child: Text( + AppLocalizations.of(_context).count_email_selected(_listEmail.length), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 18, color: AppColor.nameUserColor, fontWeight: FontWeight.w500))); + } + + Widget _buildBackButton() { + return ButtonBuilder(_imagePaths.icComposerClose) + .padding(5) + .size(30) + .onPressActionClick(() { + if (_onCloseActionClick != null) { + _onCloseActionClick!(); + }}) + .build(); + } + + Widget _buildListOptionButton() { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ButtonBuilder(_imagePaths.icTrash).key(Key('button_remove_email_selected')) + .onPressActionClick(() { + if (_onRemoveEmailActionClick != null) { + _onRemoveEmailActionClick!(_listEmail); + }}) + .build(), + SizedBox(width: 10), + ButtonBuilder(_imagePaths.icEyeDisable).key(Key('button_unread_email_selected')) + .onPressActionClick(() { + if (_onUnreadEmailActionClick != null) { + _onUnreadEmailActionClick!(_listEmail); + }}) + .build(), + SizedBox(width: 10), + ButtonBuilder(_imagePaths.icComposerMenu).key(Key('button_menu_select_email')) + .onPressActionClick(() { + if (_onOpenContextMenuActionClick != null) { + _onOpenContextMenuActionClick!(_listEmail); + }}) + .build(), + ] + ); + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/email_context_menu_action_builder.dart b/lib/features/thread/presentation/widgets/email_context_menu_action_builder.dart new file mode 100644 index 000000000..a1c55cd88 --- /dev/null +++ b/lib/features/thread/presentation/widgets/email_context_menu_action_builder.dart @@ -0,0 +1,33 @@ + +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:model/model.dart'; + +class EmailContextMenuActionBuilder extends ContextMenuActionBuilder> { + final List _listEmail; + + EmailContextMenuActionBuilder( + Key key, + SvgPicture actionIcon, + String actionName, + this._listEmail + ) : super(key, actionIcon, actionName); + + @override + ListTile build() { + return ListTile( + key: key, + leading: Padding( + padding: EdgeInsets.only(left: 12), + child: actionIcon), + title: Text(actionName, style: actionTextStyle()), + onTap: () { + if (onContextMenuActionClick != null) { + onContextMenuActionClick!(_listEmail); + } + } + ); + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/email_tile_builder.dart b/lib/features/thread/presentation/widgets/email_tile_builder.dart index 0ef744701..5d982639b 100644 --- a/lib/features/thread/presentation/widgets/email_tile_builder.dart +++ b/lib/features/thread/presentation/widgets/email_tile_builder.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import 'dart:ui'; import 'package:core/core.dart'; @@ -5,7 +6,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:model/model.dart'; -typedef OnOpenMailActionClick = void Function(); +typedef OnOpenEmailActionClick = void Function(PresentationEmail selectedEmail); +typedef OnSelectEmailActionClick = void Function(PresentationEmail selectedEmail); class EmailTileBuilder { @@ -14,8 +16,10 @@ class EmailTileBuilder { final PresentationEmail _presentationEmail; final BuildContext _context; final ResponsiveUtils _responsiveUtils; + final SelectMode _selectModeAll; - OnOpenMailActionClick? _onOpenMailActionClick; + OnOpenEmailActionClick? _onOpenEmailActionClick; + OnSelectEmailActionClick? _onSelectEmailActionClick; EmailTileBuilder( this._context, @@ -23,11 +27,15 @@ class EmailTileBuilder { this._selectMode, this._presentationEmail, this._responsiveUtils, + this._selectModeAll, ); - EmailTileBuilder onOpenMailAction(OnOpenMailActionClick onOpenMailActionClick) { - _onOpenMailActionClick = onOpenMailActionClick; - return this; + void onOpenEmailAction(OnOpenEmailActionClick onOpenEmailActionClick) { + _onOpenEmailActionClick = onOpenEmailActionClick; + } + + void onSelectEmailAction(OnSelectEmailActionClick onSelectEmailActionClick) { + _onSelectEmailActionClick = onSelectEmailActionClick; } Widget build() { @@ -41,28 +49,25 @@ class EmailTileBuilder { padding: EdgeInsets.only(top: 10, bottom: 10, left: 25, right: 6), alignment: Alignment.center, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: _selectMode== SelectMode.ACTIVE - ? _responsiveUtils.isDesktop(_context) - ? AppColor.mailboxSelectedBackgroundColor - : _responsiveUtils.isMobile(_context) ? AppColor.bgMailboxListMail : Colors.white - : _responsiveUtils.isMobile(_context) ? AppColor.bgMailboxListMail : Colors.white), + borderRadius: BorderRadius.circular(_selectModeAll == SelectMode.ACTIVE ? 0 : 16), + color: _getBackgroundColorItem()), child: MediaQuery( data: MediaQueryData(padding: EdgeInsets.zero), child: ListTile( contentPadding: EdgeInsets.zero, onTap: () => { - if (_onOpenMailActionClick != null) { - _onOpenMailActionClick!() + if (_onOpenEmailActionClick != null) { + _onOpenEmailActionClick!(_presentationEmail) + } + }, + onLongPress: () => { + if (_onSelectEmailActionClick != null) { + _onSelectEmailActionClick!(_presentationEmail) } }, leading: Transform( transform: Matrix4.translationValues(-10.0, -10.0, 0.0), - child: AvatarBuilder() - .text('${_presentationEmail.getAvatarText()}') - .size(40) - .iconStatus(_imagePaths.icOffline) - .build()), + child: _buildAvatarIcon()), title: Transform( transform: Matrix4.translationValues(-10.0, 0.0, 0.0), child: Row( @@ -131,4 +136,70 @@ class EmailTileBuilder { ) ); } + + Widget _buildAvatarIcon () { + if (_selectModeAll == SelectMode.ACTIVE) { + return AnimatedSwitcher( + duration: Duration(milliseconds: 600), + transitionBuilder: __transitionBuilder, + child: _presentationEmail.selectMode == SelectMode.ACTIVE + ? (IconBuilder(_imagePaths.icSelected) + ..addOnTapActionClick(() { + if (_selectModeAll == SelectMode.ACTIVE && _onSelectEmailActionClick != null) { + _onSelectEmailActionClick!(_presentationEmail); + }})) + .build() + : AvatarBuilder() + .text('${_presentationEmail.getAvatarText()}') + .size(40) + .iconStatus(_imagePaths.icOffline) + .addOnTapActionClick(() { + if (_selectModeAll == SelectMode.ACTIVE && _onSelectEmailActionClick != null) { + _onSelectEmailActionClick!(_presentationEmail); + }}) + .build() + ); + } else { + return AvatarBuilder() + .text('${_presentationEmail.getAvatarText()}') + .size(40) + .iconStatus(_imagePaths.icOffline) + .build(); + } + } + + Widget __transitionBuilder(Widget widget, Animation animation) { + final rotateAnim = Tween(begin: pi, end: 0.0).animate(animation); + return AnimatedBuilder( + animation: rotateAnim, + child: widget, + builder: (context, widget) { + final isUnder = _presentationEmail.selectMode == SelectMode.ACTIVE; + final value = isUnder ? min(rotateAnim.value, pi / 2) : rotateAnim.value; + return Transform( + transform: Matrix4.rotationY(value), + child: widget, + alignment: Alignment.center, + ); + }, + ); + } + + Color _getBackgroundColorItem() { + if (_selectModeAll == SelectMode.ACTIVE) { + if (_presentationEmail.selectMode == SelectMode.ACTIVE) { + return AppColor.mailboxSelectedBackgroundColor; + } else { + return _responsiveUtils.isMobile(_context) ? AppColor.bgMailboxListMail : Colors.white; + } + } else { + if (_responsiveUtils.isMobile(_context)) { + return AppColor.bgMailboxListMail; + } else if (_responsiveUtils.isDesktop(_context)) { + return _selectMode == SelectMode.ACTIVE ? AppColor.mailboxSelectedBackgroundColor : Colors.white; + } else { + return Colors.white; + } + } + } } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index b7c2f879a..3519fd2b1 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -142,5 +142,62 @@ "@prefix_forward_email": { "type": "text", "placeholders": {} + }, + "your_email_being_sent": "Your email being sent...", + "@your_email_being_sent": { + "type": "text", + "placeholders": {} + }, + "your_email_should_have_at_least_one_recipient": "Your email should have at least one recipient", + "@your_email_should_have_at_least_one_recipient": { + "type": "text", + "placeholders": {} + }, + "message_sent": "Message sent", + "@message_sent": { + "type": "text", + "placeholders": {} + }, + "error_message_sent": "Error message sent", + "@error_message_sent": { + "type": "text", + "placeholders": {} + }, + "count_email_selected": "{count} selected", + "@count_email_selected": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "mark_as_unread": "Mark as unread", + "@mark_as_unread": { + "type": "text", + "placeholders": {} + }, + "mark_as_read": "Mark as read", + "@mark_as_read": { + "type": "text", + "placeholders": {} + }, + "move_to_trash": "Move to trash", + "@move_to_trash": { + "type": "text", + "placeholders": {} + }, + "move_to_mailbox": "Move to mailbox", + "@move_to_mailbox": { + "type": "text", + "placeholders": {} + }, + "mark_as_flag": "Star", + "@mark_as_flag": { + "type": "text", + "placeholders": {} + }, + "move_to_spam": "Move to spam", + "@move_to_spam": { + "type": "text", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index b7c2f879a..3519fd2b1 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -142,5 +142,62 @@ "@prefix_forward_email": { "type": "text", "placeholders": {} + }, + "your_email_being_sent": "Your email being sent...", + "@your_email_being_sent": { + "type": "text", + "placeholders": {} + }, + "your_email_should_have_at_least_one_recipient": "Your email should have at least one recipient", + "@your_email_should_have_at_least_one_recipient": { + "type": "text", + "placeholders": {} + }, + "message_sent": "Message sent", + "@message_sent": { + "type": "text", + "placeholders": {} + }, + "error_message_sent": "Error message sent", + "@error_message_sent": { + "type": "text", + "placeholders": {} + }, + "count_email_selected": "{count} selected", + "@count_email_selected": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "mark_as_unread": "Mark as unread", + "@mark_as_unread": { + "type": "text", + "placeholders": {} + }, + "mark_as_read": "Mark as read", + "@mark_as_read": { + "type": "text", + "placeholders": {} + }, + "move_to_trash": "Move to trash", + "@move_to_trash": { + "type": "text", + "placeholders": {} + }, + "move_to_mailbox": "Move to mailbox", + "@move_to_mailbox": { + "type": "text", + "placeholders": {} + }, + "mark_as_flag": "Star", + "@mark_as_flag": { + "type": "text", + "placeholders": {} + }, + "move_to_spam": "Move to spam", + "@move_to_spam": { + "type": "text", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 84ae33843..3a8c15b2e 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2021-09-16T11:55:46.823676", + "@@last_modified": "2021-09-15T10:03:14.927099", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -197,5 +197,62 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "your_email_being_sent": "Your email being sent...", + "@your_email_being_sent": { + "type": "text", + "placeholders": {} + }, + "your_email_should_have_at_least_one_recipient": "Your email should have at least one recipient", + "@your_email_should_have_at_least_one_recipient": { + "type": "text", + "placeholders": {} + }, + "message_sent": "Message sent", + "@message_sent": { + "type": "text", + "placeholders": {} + }, + "error_message_sent": "Error message sent", + "@error_message_sent": { + "type": "text", + "placeholders": {} + }, + "count_email_selected": "{count} selected", + "@count_email_selected": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "mark_as_unread": "Mark as unread", + "@mark_as_unread": { + "type": "text", + "placeholders": {} + }, + "mark_as_read": "Mark as read", + "@mark_as_read": { + "type": "text", + "placeholders": {} + }, + "move_to_trash": "Move to trash", + "@move_to_trash": { + "type": "text", + "placeholders": {} + }, + "move_to_mailbox": "Move to mailbox", + "@move_to_mailbox": { + "type": "text", + "placeholders": {} + }, + "mark_as_flag": "Star", + "@mark_as_flag": { + "type": "text", + "placeholders": {} + }, + "move_to_spam": "Move to spam", + "@move_to_spam": { + "type": "text", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index b7c2f879a..3519fd2b1 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -142,5 +142,62 @@ "@prefix_forward_email": { "type": "text", "placeholders": {} + }, + "your_email_being_sent": "Your email being sent...", + "@your_email_being_sent": { + "type": "text", + "placeholders": {} + }, + "your_email_should_have_at_least_one_recipient": "Your email should have at least one recipient", + "@your_email_should_have_at_least_one_recipient": { + "type": "text", + "placeholders": {} + }, + "message_sent": "Message sent", + "@message_sent": { + "type": "text", + "placeholders": {} + }, + "error_message_sent": "Error message sent", + "@error_message_sent": { + "type": "text", + "placeholders": {} + }, + "count_email_selected": "{count} selected", + "@count_email_selected": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "mark_as_unread": "Mark as unread", + "@mark_as_unread": { + "type": "text", + "placeholders": {} + }, + "mark_as_read": "Mark as read", + "@mark_as_read": { + "type": "text", + "placeholders": {} + }, + "move_to_trash": "Move to trash", + "@move_to_trash": { + "type": "text", + "placeholders": {} + }, + "move_to_mailbox": "Move to mailbox", + "@move_to_mailbox": { + "type": "text", + "placeholders": {} + }, + "mark_as_flag": "Star", + "@mark_as_flag": { + "type": "text", + "placeholders": {} + }, + "move_to_spam": "Move to spam", + "@move_to_spam": { + "type": "text", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index b7c2f879a..3519fd2b1 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -142,5 +142,62 @@ "@prefix_forward_email": { "type": "text", "placeholders": {} + }, + "your_email_being_sent": "Your email being sent...", + "@your_email_being_sent": { + "type": "text", + "placeholders": {} + }, + "your_email_should_have_at_least_one_recipient": "Your email should have at least one recipient", + "@your_email_should_have_at_least_one_recipient": { + "type": "text", + "placeholders": {} + }, + "message_sent": "Message sent", + "@message_sent": { + "type": "text", + "placeholders": {} + }, + "error_message_sent": "Error message sent", + "@error_message_sent": { + "type": "text", + "placeholders": {} + }, + "count_email_selected": "{count} selected", + "@count_email_selected": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "mark_as_unread": "Mark as unread", + "@mark_as_unread": { + "type": "text", + "placeholders": {} + }, + "mark_as_read": "Mark as read", + "@mark_as_read": { + "type": "text", + "placeholders": {} + }, + "move_to_trash": "Move to trash", + "@move_to_trash": { + "type": "text", + "placeholders": {} + }, + "move_to_mailbox": "Move to mailbox", + "@move_to_mailbox": { + "type": "text", + "placeholders": {} + }, + "mark_as_flag": "Star", + "@mark_as_flag": { + "type": "text", + "placeholders": {} + }, + "move_to_spam": "Move to spam", + "@move_to_spam": { + "type": "text", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 558b8f16c..ac6289179 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -227,5 +227,55 @@ class AppLocalizations { name: 'error_message_sent', ); } + + String count_email_selected(int count) { + return Intl.message( + '$count selected', + name: 'count_email_selected', + args: [count] + ); + } + + String get mark_as_unread { + return Intl.message( + 'Mark as unread', + name: 'mark_as_unread', + ); + } + + String get mark_as_read { + return Intl.message( + 'Mark as read', + name: 'mark_as_read', + ); + } + + String get move_to_trash { + return Intl.message( + 'Move to trash', + name: 'move_to_trash', + ); + } + + String get move_to_mailbox { + return Intl.message( + 'Move to mailbox', + name: 'move_to_mailbox', + ); + } + + String get mark_as_flag { + return Intl.message( + 'Star', + name: 'mark_as_flag', + ); + } + + String get move_to_spam { + return Intl.message( + 'Move to spam', + name: 'move_to_spam', + ); + } } diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index 0fb97d9a0..efb91a13d 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -46,4 +46,42 @@ extension PresentationEmailExtension on PresentationEmail { selectMode: selectMode ); } + + PresentationEmail toggleSelect() { + return PresentationEmail( + id, + keywords: keywords, + size: size, + receivedAt: receivedAt, + hasAttachment: hasAttachment, + preview: preview, + subject: subject, + sentAt: sentAt, + from: from, + to: to, + cc: cc, + bcc: bcc, + replyTo: replyTo, + selectMode: selectMode == SelectMode.INACTIVE ? SelectMode.ACTIVE : SelectMode.INACTIVE + ); + } + + PresentationEmail toSelectedEmail({required SelectMode selectMode}) { + return PresentationEmail( + id, + keywords: keywords, + size: size, + receivedAt: receivedAt, + hasAttachment: hasAttachment, + preview: preview, + subject: subject, + sentAt: sentAt, + from: from, + to: to, + cc: cc, + bcc: bcc, + replyTo: replyTo, + selectMode: selectMode + ); + } } \ No newline at end of file