From e3d86c67bded16a7f13ad18534d524807f5a0a20 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 16 Sep 2022 03:21:49 +0700 Subject: [PATCH] TF-933 Add new `SearchEmailView` to handle search all email on Mobile/Tablet --- .../presentation/model/search_more_state.dart | 6 + .../presentation/search_email_bindings.dart | 49 ++ .../presentation/search_email_controller.dart | 623 ++++++++++++++++++ .../presentation/search_email_view.dart | 499 ++++++++++++++ .../utils/search_email_utils.dart | 90 +++ .../widgets/app_bar_selection_mode.dart | 149 +++++ ...email_receive_time_action_tile_widget.dart | 51 ++ ...cupertino_action_sheet_action_builder.dart | 54 ++ 8 files changed, 1521 insertions(+) create mode 100644 lib/features/search/presentation/model/search_more_state.dart create mode 100644 lib/features/search/presentation/search_email_bindings.dart create mode 100644 lib/features/search/presentation/search_email_controller.dart create mode 100644 lib/features/search/presentation/search_email_view.dart create mode 100644 lib/features/search/presentation/utils/search_email_utils.dart create mode 100644 lib/features/search/presentation/widgets/app_bar_selection_mode.dart create mode 100644 lib/features/search/presentation/widgets/email_receive_time_action_tile_widget.dart create mode 100644 lib/features/search/presentation/widgets/email_receive_time_cupertino_action_sheet_action_builder.dart diff --git a/lib/features/search/presentation/model/search_more_state.dart b/lib/features/search/presentation/model/search_more_state.dart new file mode 100644 index 000000000..8af554e37 --- /dev/null +++ b/lib/features/search/presentation/model/search_more_state.dart @@ -0,0 +1,6 @@ + +enum SearchMoreState { + idle, + waiting, + completed; +} \ No newline at end of file diff --git a/lib/features/search/presentation/search_email_bindings.dart b/lib/features/search/presentation/search_email_bindings.dart new file mode 100644 index 000000000..5dfc13661 --- /dev/null +++ b/lib/features/search/presentation/search_email_bindings.dart @@ -0,0 +1,49 @@ + +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/base_bindings.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_all_recent_search_latest_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_recent_search_interactor.dart'; +import 'package:tmail_ui_user/features/search/domain/usecases/refresh_changes_search_email_interactor.dart'; +import 'package:tmail_ui_user/features/search/presentation/search_email_controller.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/search_email_interactor.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/search_more_email_interactor.dart'; + +class SearchEmailBindings extends BaseBindings { + + @override + void bindingsController() { + Get.put(SearchEmailController( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + )); + } + + @override + void bindingsDataSource() { + } + + @override + void bindingsDataSourceImpl() { + } + + @override + void bindingsInteractor() { + } + + @override + void bindingsRepository() { + } + + @override + void bindingsRepositoryImpl() { + } + + void disposeBindings() { + Get.delete(); + } +} \ No newline at end of file diff --git a/lib/features/search/presentation/search_email_controller.dart b/lib/features/search/presentation/search_email_controller.dart new file mode 100644 index 000000000..0e4b73b32 --- /dev/null +++ b/lib/features/search/presentation/search_email_controller.dart @@ -0,0 +1,623 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; +import 'package:debounce_throttle/debounce_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; +import 'package:jmap_dart_client/jmap/core/utc_date.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_comparator.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_comparator_property.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_all_recent_search_latest_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/quick_search_email_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_all_recent_search_latest_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_recent_search_interactor.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/quick_search_filter.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart'; +import 'package:tmail_ui_user/features/search/domain/state/refresh_changes_search_email_state.dart'; +import 'package:tmail_ui_user/features/search/domain/usecases/refresh_changes_search_email_interactor.dart'; +import 'package:tmail_ui_user/features/search/presentation/model/search_more_state.dart'; +import 'package:tmail_ui_user/features/search/presentation/search_email_bindings.dart'; +import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/search_more_email_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/search_email_interactor.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/search_more_email_interactor.dart'; +import 'package:tmail_ui_user/features/thread/presentation/mixin/base_email_action_controller.dart'; +import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class SearchEmailController extends BaseController + with BaseEmailActionController { + + final QuickSearchEmailInteractor _quickSearchEmailInteractor; + final SaveRecentSearchInteractor _saveRecentSearchInteractor; + final GetAllRecentSearchLatestInteractor _getAllRecentSearchLatestInteractor; + final SearchEmailInteractor _searchEmailInteractor; + final SearchMoreEmailInteractor _searchMoreEmailInteractor; + final RefreshChangesSearchEmailInteractor _refreshChangesSearchEmailInteractor; + + final textInputSearchController = TextEditingController(); + final resultSearchScrollController = ScrollController(); + final textInputSearchFocus = FocusNode(); + + final currentSearchText = RxString(''); + final listRecentSearch = RxList(); + final listSuggestionSearch = RxList(); + final listResultSearch = RxList(); + final searchEmailFilter = Rx(SearchEmailFilter()); + final searchIsRunning = RxBool(false); + final emailReceiveTimeType = Rxn(); + final selectionMode = Rx(SelectMode.INACTIVE); + + late Debouncer _deBouncerTime; + late Worker dashBoardViewStateWorker; + late SearchMoreState searchMoreState; + late bool canSearchMore; + + PresentationMailbox? get currentMailbox => mailboxDashBoardController.selectedMailbox.value; + + AccountId? get accountId => mailboxDashBoardController.accountId.value; + + UserProfile? get userProfile => mailboxDashBoardController.userProfile.value; + + SearchQuery? get searchQuery => searchEmailFilter.value.text; + + SearchEmailController( + this._quickSearchEmailInteractor, + this._saveRecentSearchInteractor, + this._getAllRecentSearchLatestInteractor, + this._searchEmailInteractor, + this._searchMoreEmailInteractor, + this._refreshChangesSearchEmailInteractor, + ); + + @override + void onInit() { + super.onInit(); + _initializeDebounceTimeTextSearchChange(); + _initializeTextInputFocus(); + _initWorkerListener(); + } + + @override + void onReady() async { + listRecentSearch.value = await getAllRecentSearchAction(); + textInputSearchFocus.requestFocus(); + searchMoreState = SearchMoreState.idle; + canSearchMore = true; + super.onReady(); + } + + @override + void onData(Either newState) { + super.onData(newState); + newState.fold( + (failure) { + if (failure is SearchEmailFailure) { + _searchEmailsFailure(failure); + } else if (failure is SearchMoreEmailFailure) { + _searchMoreEmailsFailure(failure); + } + }, + (success) { + if (success is SearchEmailSuccess) { + _searchEmailsSuccess(success); + } else if (success is SearchingMoreState) { + searchMoreState = SearchMoreState.waiting; + } else if (success is SearchMoreEmailSuccess) { + _searchMoreEmailsSuccess(success); + } else if (success is RefreshChangesSearchEmailSuccess) { + _refreshChangesSearchEmailsSuccess(success); + } + } + ); + } + + @override + void onDone() {} + + @override + void onError(error) {} + + void _initializeDebounceTimeTextSearchChange() { + _deBouncerTime = Debouncer( + const Duration(milliseconds: 500), + initialValue: ''); + _deBouncerTime.values.listen((value) async { + log('SearchEmailController::_initializeDebounceTimeTextSearchChange(): $value'); + currentSearchText.value = value; + _updateFilterEmail(text: value.isNotEmpty ? SearchQuery(value) : null); + if (value.isNotEmpty && accountId != null) { + listSuggestionSearch.value = await quickSearchEmails(accountId: accountId!); + } else { + listSuggestionSearch.clear(); + } + if (listSuggestionSearch.isEmpty && currentSearchText.isEmpty) { + listRecentSearch.value = await getAllRecentSearchAction(pattern: value); + } + }); + } + + void _initializeTextInputFocus() { + textInputSearchFocus.addListener(() { + if (textInputSearchFocus.hasFocus) { + searchIsRunning.value = false; + } + }); + } + + void _initWorkerListener() { + dashBoardViewStateWorker = ever(mailboxDashBoardController.viewState, (viewState) { + if (viewState is Either) { + viewState.map((success) { + if (success is MarkAsEmailReadSuccess || + success is MoveToMailboxSuccess || + success is MarkAsStarEmailSuccess || + success is DeleteEmailPermanentlySuccess || + success is MarkAsMultipleEmailReadAllSuccess || + success is MarkAsMultipleEmailReadHasSomeEmailFailure || + success is MarkAsStarMultipleEmailAllSuccess || + success is MarkAsStarMultipleEmailHasSomeEmailFailure || + success is MoveMultipleEmailToMailboxAllSuccess || + success is MoveMultipleEmailToMailboxHasSomeEmailFailure || + success is EmptyTrashFolderSuccess || + success is DeleteMultipleEmailsPermanentlyAllSuccess || + success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure + ) { + _refreshEmailChanges(); + } + }); + } + }); + } + + void _refreshEmailChanges() { + if (searchIsRunning.isTrue && accountId != null) { + final limit = listResultSearch.isNotEmpty + ? UnsignedInt(listResultSearch.length) + : ThreadConstants.defaultLimit; + searchEmailFilter.value = searchEmailFilter.value.toSearchEmailFilter(newBefore: null); + + consumeState(_refreshChangesSearchEmailInteractor.execute( + accountId!, + limit: limit, + sort: {} + ..add(EmailComparator(EmailComparatorProperty.receivedAt) + ..setIsAscending(false)), + filter: searchEmailFilter.value.mappingToEmailFilterCondition(), + properties: ThreadConstants.propertiesDefault, + )); + } + } + + void _refreshChangesSearchEmailsSuccess(RefreshChangesSearchEmailSuccess success) { + final resultEmailSearchList = success.emailList + .map((email) => email.toSearchPresentationEmail(mailboxDashBoardController.mapMailbox)) + .toList(); + + final emailsBeforeChanges = listResultSearch; + final emailsAfterChanges = resultEmailSearchList; + final newListEmail = emailsAfterChanges.combine(emailsBeforeChanges); + + listResultSearch.value = newListEmail; + listResultSearch.refresh(); + } + + Future> getAllRecentSearchAction({String pattern = ''}) async { + return _getAllRecentSearchLatestInteractor + .execute(pattern: pattern) + .then((result) => result.fold( + (failure) => [], + (success) => success is GetAllRecentSearchLatestSuccess + ? success.listRecentSearch + : [])); + } + + Future> quickSearchEmails({required AccountId accountId}) async { + return _quickSearchEmailInteractor + .execute(accountId, + limit: UnsignedInt(5), + sort: {}..add( + EmailComparator(EmailComparatorProperty.receivedAt) + ..setIsAscending(false)), + filter: searchEmailFilter.value.mappingToEmailFilterCondition(), + properties: ThreadConstants.propertiesQuickSearch) + .then((result) => result.fold( + (failure) => [], + (success) => success is QuickSearchEmailSuccess + ? success.emailList + : [])); + } + + void saveRecentSearch(RecentSearch recentSearch) { + consumeState(_saveRecentSearchInteractor.execute(recentSearch)); + } + + void _searchEmailAction(BuildContext context) { + FocusScope.of(context).unfocus(); + + if (accountId != null) { + searchIsRunning.value = true; + + consumeState(_searchEmailInteractor.execute( + accountId!, + limit: ThreadConstants.defaultLimit, + sort: {} + ..add(EmailComparator(EmailComparatorProperty.receivedAt) + ..setIsAscending(false)), + filter: searchEmailFilter.value.mappingToEmailFilterCondition(), + properties: ThreadConstants.propertiesDefault, + )); + } + } + + void _searchEmailsSuccess(SearchEmailSuccess success) { + final resultEmailSearchList = success.emailList + .map((email) => email.toSearchPresentationEmail(mailboxDashBoardController.mapMailbox)) + .toList(); + + final emailsBeforeChanges = listResultSearch; + final emailsAfterChanges = resultEmailSearchList; + final newListEmail = emailsAfterChanges.combine(emailsBeforeChanges); + + listResultSearch.value = newListEmail; + + if (resultSearchScrollController.hasClients) { + resultSearchScrollController.animateTo( + 0, + duration: const Duration(milliseconds: 500), + curve: Curves.fastOutSlowIn); + } + } + + void _searchEmailsFailure(SearchEmailFailure failure) { + listResultSearch.clear(); + } + + void searchMoreEmailsAction() { + if (canSearchMore && accountId != null) { + final lastEmail = listResultSearch.last; + _updateFilterEmail(before: lastEmail.receivedAt); + + consumeState(_searchMoreEmailInteractor.execute( + accountId!, + limit: ThreadConstants.defaultLimit, + sort: {} + ..add(EmailComparator(EmailComparatorProperty.receivedAt) + ..setIsAscending(false)), + filter: searchEmailFilter.value.mappingToEmailFilterCondition(), + properties: ThreadConstants.propertiesDefault, + lastEmailId: lastEmail.id + )); + } + } + + void _searchMoreEmailsFailure(SearchMoreEmailFailure failure) { + searchMoreState = SearchMoreState.completed; + } + + void _searchMoreEmailsSuccess(SearchMoreEmailSuccess success) { + if (success.emailList.isNotEmpty) { + final resultEmailSearchList = success.emailList + .map((email) => email.toSearchPresentationEmail(mailboxDashBoardController.mapMailbox)) + .where((email) => !listResultSearch.contains(email)) + .toList(); + listResultSearch.addAll(resultEmailSearchList); + } else { + canSearchMore = false; + } + searchMoreState = SearchMoreState.completed; + } + + void showAllResultSearchAction(BuildContext context, String query) { + setTextInputSearchForm(query); + _updateFilterEmail(text: SearchQuery(query)); + _searchEmailAction(context); + } + + void searchEmailByRecentAction(BuildContext context, RecentSearch recentSearch) { + setTextInputSearchForm(recentSearch.value); + _updateFilterEmail(text: SearchQuery(recentSearch.value)); + _searchEmailAction(context); + } + + void submitSearchAction(BuildContext context, String query) { + _updateFilterEmail(text: SearchQuery(query)); + _searchEmailAction(context); + } + + void selectQuickSearchFilter(BuildContext context, QuickSearchFilter filter) { + _selectQuickSearchFilter(filter); + _searchEmailAction(context); + } + + bool checkQuickSearchFilterSelected(QuickSearchFilter filter) { + switch (filter) { + case QuickSearchFilter.hasAttachment: + return searchEmailFilter.value.hasAttachment == true; + case QuickSearchFilter.last7Days: + if (emailReceiveTimeType.value != null) { + return true; + } + return searchEmailFilter.value.emailReceiveTimeType == EmailReceiveTimeType.last7Days; + case QuickSearchFilter.fromMe: + return userProfile != null && + searchEmailFilter.value.from.contains(userProfile!.email) && + searchEmailFilter.value.from.length == 1; + } + } + + void _selectQuickSearchFilter(QuickSearchFilter filter) { + final filterSelected = checkQuickSearchFilterSelected(filter); + + switch (filter) { + case QuickSearchFilter.hasAttachment: + _updateFilterEmail(hasAttachment: !filterSelected); + break; + case QuickSearchFilter.last7Days: + if (filterSelected) { + _setEmailReceiveTimeType(null); + _updateFilterEmail(emailReceiveTimeType: EmailReceiveTimeType.allTime); + } else { + _setEmailReceiveTimeType(EmailReceiveTimeType.last7Days); + _updateFilterEmail(emailReceiveTimeType: EmailReceiveTimeType.last7Days); + } + break; + case QuickSearchFilter.fromMe: + if (userProfile != null) { + filterSelected + ? searchEmailFilter.value.from.removeWhere((e) => e == userProfile!.email) + : searchEmailFilter.value.from.add(userProfile!.email); + _updateFilterEmail(from: searchEmailFilter.value.from); + } + break; + } + } + + void _setEmailReceiveTimeType(EmailReceiveTimeType? receiveTimeType) { + emailReceiveTimeType.value = receiveTimeType; + } + + void selectReceiveTimeQuickSearchFilter(BuildContext context, EmailReceiveTimeType? emailReceiveTimeType) { + popBack(); + + if (emailReceiveTimeType != null) { + _updateFilterEmail( + emailReceiveTimeType: emailReceiveTimeType, + text: searchQuery == null ? SearchQuery.initial() : searchEmailFilter.value.text); + } else { + _updateFilterEmail( + emailReceiveTimeType: EmailReceiveTimeType.allTime, + text: searchQuery == null ? SearchQuery.initial() : searchEmailFilter.value.text); + } + _setEmailReceiveTimeType(emailReceiveTimeType); + _searchEmailAction(context); + } + + void _updateFilterEmail({ + Set? from, + Set? to, + SearchQuery? text, + String? subject, + Set? hasKeyword, + Set? notKeyword, + PresentationMailbox? mailbox, + EmailReceiveTimeType? emailReceiveTimeType, + bool? hasAttachment, + UTCDate? before, + }) { + searchEmailFilter.value = searchEmailFilter.value.copyWith( + from: from, + to: to, + text: text, + subject: subject, + hasKeyword: hasKeyword, + notKeyword: notKeyword, + mailbox: mailbox, + emailReceiveTimeType: emailReceiveTimeType, + hasAttachment: hasAttachment, + before: before, + ); + } + + void onTextSearchChange(String text) { + _deBouncerTime.value = text; + } + + void setTextInputSearchForm(String value) { + textInputSearchController.text = value; + } + + void clearAllTextInputSearchForm() { + textInputSearchController.clear(); + currentSearchText.value = ''; + listSuggestionSearch.clear(); + textInputSearchFocus.requestFocus(); + } + + void clearAllResultSearch() { + searchIsRunning.value = false; + currentSearchText.value = ''; + listRecentSearch.clear(); + listSuggestionSearch.clear(); + listResultSearch.clear(); + searchEmailFilter.value = SearchEmailFilter(); + } + + void closeSearchView(BuildContext context) { + clearAllTextInputSearchForm(); + clearAllResultSearch(); + FocusScope.of(context).unfocus(); + mailboxDashBoardController.searchController.disableSearch(); + mailboxDashBoardController.dispatchRoute(AppRoutes.THREAD); + SearchEmailBindings().disposeBindings(); + } + + void pressEmailAction( + BuildContext context, + EmailActionType actionType, + PresentationEmail selectedEmail, + {PresentationMailbox? mailboxContain} + ) { + switch(actionType) { + case EmailActionType.preview: + if (mailboxContain?.isDrafts == true) { + editEmail(selectedEmail); + } else { + previewEmail(context, selectedEmail); + } + break; + case EmailActionType.selection: + selectEmail(context, selectedEmail); + break; + case EmailActionType.markAsRead: + markAsEmailRead(selectedEmail, ReadActions.markAsRead); + break; + case EmailActionType.markAsUnread: + markAsEmailRead(selectedEmail, ReadActions.markAsUnread); + break; + case EmailActionType.markAsStarred: + markAsStarEmail(selectedEmail, MarkStarAction.markStar); + break; + case EmailActionType.unMarkAsStarred: + markAsStarEmail(selectedEmail, MarkStarAction.unMarkStar); + break; + case EmailActionType.moveToMailbox: + moveToMailbox(context, selectedEmail); + break; + case EmailActionType.moveToTrash: + moveToTrash(selectedEmail); + break; + case EmailActionType.deletePermanently: + deleteEmailPermanently(context, selectedEmail); + break; + case EmailActionType.moveToSpam: + popBack(); + moveToSpam(selectedEmail); + break; + case EmailActionType.unSpam: + popBack(); + unSpam(selectedEmail); + break; + default: + break; + } + } + + void selectEmail(BuildContext context, PresentationEmail presentationEmailSelected) { + listResultSearch.value = listResultSearch + .map((email) => email.id == presentationEmailSelected.id + ? email.toggleSelect() + : email) + .toList(); + + if (listResultSearch.isAllSelectionInActive) { + selectionMode.value = SelectMode.INACTIVE; + } else { + if (selectionMode.value == SelectMode.INACTIVE) { + selectionMode.value = SelectMode.ACTIVE; + } + } + } + + void cancelSelectionMode(BuildContext context) { + listResultSearch.value = listResultSearch + .map((email) => email.toSelectedEmail(selectMode: SelectMode.INACTIVE)) + .toList(); + selectionMode.value = SelectMode.INACTIVE; + } + + void handleSelectionEmailAction( + BuildContext context, + EmailActionType actionType, + List listEmails + ) { + switch(actionType) { + case EmailActionType.markAsRead: + cancelSelectionMode(context); + markAsReadSelectedMultipleEmail(listEmails, ReadActions.markAsRead); + break; + case EmailActionType.markAsUnread: + cancelSelectionMode(context); + markAsReadSelectedMultipleEmail(listEmails, ReadActions.markAsUnread); + break; + case EmailActionType.markAsStarred: + cancelSelectionMode(context); + markAsStarSelectedMultipleEmail(listEmails, MarkStarAction.markStar); + break; + case EmailActionType.unMarkAsStarred: + cancelSelectionMode(context); + + markAsStarSelectedMultipleEmail(listEmails, MarkStarAction.unMarkStar); + break; + case EmailActionType.moveToMailbox: + cancelSelectionMode(context); + final mailboxContainCurrent = listEmails.getCurrentMailboxContain(mailboxDashBoardController.mapMailbox); + if (mailboxContainCurrent != null) { + moveSelectedMultipleEmailToMailbox(listEmails, mailboxContainCurrent); + } + break; + case EmailActionType.moveToTrash: + cancelSelectionMode(context); + final mailboxContainCurrent = listEmails.getCurrentMailboxContain(mailboxDashBoardController.mapMailbox); + if (mailboxContainCurrent != null) { + moveSelectedMultipleEmailToTrash(listEmails, mailboxContainCurrent); + } + break; + case EmailActionType.deletePermanently: + final mailboxContainCurrent = listEmails.getCurrentMailboxContain(mailboxDashBoardController.mapMailbox); + if (mailboxContainCurrent != null) { + deleteSelectionEmailsPermanently( + context, + DeleteActionType.multiple, + listEmails: listEmails, + mailboxCurrent: mailboxContainCurrent, + onCancelSelectionEmail: () => cancelSelectionMode(context)); + } + break; + case EmailActionType.moveToSpam: + cancelSelectionMode(context); + final mailboxContainCurrent = listEmails.getCurrentMailboxContain(mailboxDashBoardController.mapMailbox); + if (mailboxContainCurrent != null) { + moveSelectedMultipleEmailToSpam(listEmails, mailboxContainCurrent); + } + break; + case EmailActionType.unSpam: + cancelSelectionMode(context); + unSpamSelectedMultipleEmail(listEmails); + break; + default: + break; + } + } + + @override + void onClose() { + textInputSearchController.dispose(); + resultSearchScrollController.dispose(); + _deBouncerTime.cancel(); + dashBoardViewStateWorker.dispose(); + super.onClose(); + } +} \ No newline at end of file diff --git a/lib/features/search/presentation/search_email_view.dart b/lib/features/search/presentation/search_email_view.dart new file mode 100644 index 000000000..60dd30eab --- /dev/null +++ b/lib/features/search/presentation/search_email_view.dart @@ -0,0 +1,499 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/background/background_widget_builder.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/presentation/views/text/text_field_builder.dart'; +import 'package:core/utils/build_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.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/quick_search_filter.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/email_quick_search_item_tile_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recent_search_item_tile_widget.dart'; +import 'package:tmail_ui_user/features/search/presentation/model/search_more_state.dart'; +import 'package:tmail_ui_user/features/search/presentation/search_email_controller.dart'; +import 'package:tmail_ui_user/features/search/presentation/utils/search_email_utils.dart'; +import 'package:tmail_ui_user/features/search/presentation/widgets/app_bar_selection_mode.dart'; +import 'package:tmail_ui_user/features/search/presentation/widgets/email_receive_time_action_tile_widget.dart'; +import 'package:tmail_ui_user/features/search/presentation/widgets/email_receive_time_cupertino_action_sheet_action_builder.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/search_more_email_state.dart'; +import 'package:tmail_ui_user/features/thread/presentation/model/search_status.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.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'; + +class SearchEmailView extends GetWidget + with AppLoaderMixin { + + final _imagePaths = Get.find(); + final _responsiveUtils = Get.find(); + + SearchEmailView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Container( + color: Colors.white, + child: Column(children: [ + Container( + height: 52, + color: Colors.white, + padding: SearchEmailUtils.getPaddingAppBar(context, _responsiveUtils), + child: Obx(() { + if (controller.selectionMode.value == SelectMode.ACTIVE) { + return AppBarSelectionMode( + controller.listResultSearch.listEmailSelected, + controller.mailboxDashBoardController.mapMailbox, + onCancelSelection: () => controller.cancelSelectionMode(context), + onHandleEmailAction: (actionType, listEmails) => + controller.handleSelectionEmailAction(context, actionType, listEmails)); + } else { + return _buildSearchInputForm(context); + } + }) + ), + const Divider(color: AppColor.colorDividerComposer, height: 1), + _buildListSearchFilterAction(context), + _buildLoadingView(), + Expanded(child: Obx(() { + if (controller.searchIsRunning.isFalse) { + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: Column(children: [ + if (controller.currentSearchText.value.isNotEmpty) + _buildShowAllResultSearchButton(context, controller.currentSearchText.value), + if (controller.listSuggestionSearch.isNotEmpty && controller.currentSearchText.isNotEmpty) + _buildListSuggestionSearch(context, controller.listSuggestionSearch) + else if (controller.listRecentSearch.isNotEmpty && controller.listSuggestionSearch.isEmpty) + _buildListRecentSearch(context, controller.listRecentSearch) + ]), + ); + } else { + if (controller.listResultSearch.isNotEmpty) { + return Container( + color: Colors.white, + child: _buildListEmailBody(context, controller.listResultSearch) + ); + } else { + return _buildEmptyEmail(context); + } + } + })), + _buildLoadingViewLoadMore(), + ]), + ), + ), + ); + } + + Widget _buildSearchInputForm(BuildContext context) { + return Row( + children: [ + buildIconWeb( + icon: SvgPicture.asset(_imagePaths.icBack, + width: 18, + height: 18, + color: AppColor.colorTextButton, + fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).back, + onTap: () => controller.closeSearchView(context) + ), + Expanded(child: (TextFieldBuilder() + ..onChange(controller.onTextSearchChange) + ..textInputAction(TextInputAction.search) + ..addController(controller.textInputSearchController) + ..addFocusNode(controller.textInputSearchFocus) + ..textStyle(const TextStyle(color: Colors.black, fontSize: 16)) + ..keyboardType(TextInputType.text) + ..onSubmitted((value) { + final query = value.trim(); + if (query.isNotEmpty) { + controller.saveRecentSearch(RecentSearch.now(query)); + controller.submitSearchAction(context, query); + } + }) + ..maxLines(1) + ..textDecoration(InputDecoration( + contentPadding: const EdgeInsets.all(12), + hintText: AppLocalizations.of(context).search_emails, + fillColor: Colors.redAccent, + hintStyle: const TextStyle( + color: AppColor.loginTextFieldHintColor, + fontSize: 16), + border: InputBorder.none))) + .build()), + Obx(() { + if (controller.currentSearchText.isNotEmpty) { + return + buildIconWeb( + icon: SvgPicture.asset( + _imagePaths.icClearTextSearch, + width: 18, + height: 18, + fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).clearAll, + onTap: controller.clearAllTextInputSearchForm); + } else { + return const SizedBox.shrink(); + } + }) + ] + ); + } + + Widget _buildListSearchFilterAction(BuildContext context) { + return Container( + margin: SearchEmailUtils.getMarginSearchFilterButton(context, _responsiveUtils), + padding: SearchEmailUtils.getPaddingSearchFilterButton(context, _responsiveUtils), + height: 60, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: QuickSearchFilter.values.length, + itemBuilder: (context, index) { + return _buildQuickSearchFilterButton( + context, + QuickSearchFilter.values[index]); + } + ), + ); + } + + Widget _buildQuickSearchFilterButton(BuildContext context, QuickSearchFilter filter) { + return Obx(() { + final filterSelected = controller.checkQuickSearchFilterSelected(filter); + + return Padding( + padding: const EdgeInsets.only(right: 8), + child: InkWell( + onTap: () { + if (filter != QuickSearchFilter.last7Days) { + controller.selectQuickSearchFilter(context, filter); + } else if (_responsiveUtils.isMobile(context)) { + controller.openContextMenuAction( + context, + _emailReceiveTimeCupertinoActionTile( + context, + controller.emailReceiveTimeType.value, + (receiveTime) => controller.selectReceiveTimeQuickSearchFilter(context, receiveTime))); + } + }, + onTapDown: (detail) { + if (filter == QuickSearchFilter.last7Days && + !_responsiveUtils.isMobile(context)) { + final screenSize = MediaQuery.of(context).size; + final offset = detail.globalPosition; + final position = RelativeRect.fromLTRB( + offset.dx, + offset.dy, + screenSize.width - offset.dx, + screenSize.height - offset.dy, + ); + controller.openPopupMenuAction( + context, + position, + _popupMenuEmailReceiveTimeType( + context, + controller.emailReceiveTimeType.value, + (receiveTime) => controller.selectReceiveTimeQuickSearchFilter(context, receiveTime))); + } + }, + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: filter.getBackgroundColor(quickSearchFilterSelected: filterSelected)), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + SvgPicture.asset( + filter.getIcon(_imagePaths, quickSearchFilterSelected: filterSelected), + width: 16, + height: 16, + fit: BoxFit.fill), + const SizedBox(width: 4), + Text( + filter.getTitle(context, receiveTimeType: controller.emailReceiveTimeType.value), + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: filter.getTextStyle(quickSearchFilterSelected: filterSelected), + ), + if (filter == QuickSearchFilter.last7Days) + ... [ + const SizedBox(width: 4), + SvgPicture.asset( + _imagePaths.icChevronDown, + width: 16, + height: 16, + fit: BoxFit.fill), + ] + ]) + ), + ), + ); + }); + } + + List _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 _emailReceiveTimeCupertinoActionTile( + BuildContext context, + EmailReceiveTimeType? receiveTimeSelected, + Function(EmailReceiveTimeType?)? onCallBack + ) { + return EmailReceiveTimeType.values + .map((timeType) => (EmailReceiveTimeCupertinoActionSheetActionBuilder( + timeType.getTitle(context), + timeType, + timeTypeCurrent: receiveTimeSelected, + iconLeftPadding: _responsiveUtils.isMobile(context) + ? const EdgeInsets.only(left: 12, right: 16) + : const EdgeInsets.only(right: 12), + iconRightPadding: _responsiveUtils.isMobile(context) + ? const EdgeInsets.only(right: 12) + : EdgeInsets.zero, + actionSelected: SvgPicture.asset( + _imagePaths.icFilterSelected, + width: 20, + height: 20, + fit: BoxFit.fill)) + ..onActionClick((timeType) => onCallBack?.call(timeType == receiveTimeSelected ? null : timeType))) + .build()) + .toList(); + } + + Widget _buildShowAllResultSearchButton(BuildContext context, String textSearch) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + final query = textSearch.trim(); + if (query.isNotEmpty) { + controller.saveRecentSearch(RecentSearch.now(query)); + controller.showAllResultSearchAction(context, query); + } + }, + child: Padding( + padding: SearchEmailUtils.getPaddingShowAllResultButton(context, _responsiveUtils), + child: Row(children: [ + Text(AppLocalizations.of(context).showingResultsFor, + style: const TextStyle(fontSize: 13.0, + color: AppColor.colorTextButtonHeaderThread, + fontWeight: FontWeight.w500) + ), + const SizedBox(width: 4), + Expanded(child: Text('"${controller.currentSearchText.value}"', + style: const TextStyle( + fontSize: 13.0, + color: Colors.black, + fontWeight: FontWeight.w500))) + ]) + ), + ), + ); + } + + Widget _buildListRecentSearch( + BuildContext context, + List listRecentSearch + ) { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: SearchEmailUtils.getPaddingSearchRecentTitle(context, _responsiveUtils), + child: Text(AppLocalizations.of(context).recent, + style: const TextStyle(fontSize: 13.0, + color: AppColor.colorTextButtonHeaderThread, + fontWeight: FontWeight.w500) + ) + ), + ListView.builder( + shrinkWrap: true, + primary: false, + itemCount: listRecentSearch.length, + itemBuilder: (context, index) { + final recentSearch = listRecentSearch[index]; + return Material( + color: Colors.transparent, + child: InkWell( + child: RecentSearchItemTileWidget( + recentSearch, + contentPadding: SearchEmailUtils.getPaddingListRecentSearch(context, _responsiveUtils)), + onTap: () => controller.searchEmailByRecentAction(context, recentSearch), + ), + ); + }) + ]); + } + + Widget _buildListSuggestionSearch( + BuildContext context, + List listSuggestionSearch + ) { + return ListView.builder( + shrinkWrap: true, + primary: false, + itemCount: listSuggestionSearch.length, + itemBuilder: (context, index) { + return Material( + color: Colors.transparent, + child: InkWell( + child: EmailQuickSearchItemTileWidget( + listSuggestionSearch[index], + controller.currentMailbox, + contentPadding: SearchEmailUtils.getPaddingSearchSuggestionList(context, _responsiveUtils)), + onTap: () => controller.previewEmail(context, listSuggestionSearch[index]), + ), + ); + }); + } + + Widget _buildEmptyEmail(BuildContext context) { + return Obx(() => controller.viewState.value.fold( + (failure) => const SizedBox.shrink(), + (success) => success is! SearchingState + ? (BackgroundWidgetBuilder(context) + ..image(SvgPicture.asset( + _imagePaths.icEmptyImageDefault, + width: 120, + height: 120, + fit: BoxFit.fill)) + ..text(AppLocalizations.of(context).no_emails_matching_your_search)) + .build() + : const SizedBox.shrink()) + ); + } + + Widget _buildListEmailBody(BuildContext context, List listPresentationEmail) { + return NotificationListener( + onNotification: (ScrollNotification scrollInfo) { + if (scrollInfo is ScrollEndNotification + && controller.searchMoreState != SearchMoreState.waiting + && scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) { + controller.searchMoreEmailsAction(); + } + return false; + }, + child: ListView.builder( + controller: controller.resultSearchScrollController, + physics: const AlwaysScrollableScrollPhysics(), + key: const PageStorageKey('list_presentation_email_in_search_view'), + itemExtent: _getItemExtent(context), + itemCount: listPresentationEmail.length, + itemBuilder: (context, index) => Obx(() => (EmailTileBuilder( + context, + listPresentationEmail[index], + controller.selectionMode.value, + SearchStatus.ACTIVE, + controller.searchQuery, + padding: SearchEmailUtils.getPaddingSearchResultList(context, _responsiveUtils), + paddingDivider: SearchEmailUtils.getPaddingDividerSearchResultList(context, _responsiveUtils), + mailboxCurrent: listPresentationEmail[index].findMailboxContain( + controller.mailboxDashBoardController.mapMailbox)) + ..addOnPressEmailActionClick((action, email) => + controller.pressEmailAction( + context, + action, + email, + mailboxContain: email.findMailboxContain( + controller.mailboxDashBoardController.mapMailbox))) + ..addOnMoreActionClick((email, position) => _responsiveUtils.isMobile(context) + ? controller.openContextMenuAction(context, _contextMenuActionTile(context, email)) + : controller.openPopupMenuAction(context, position, _popupMenuActionTile(context, email)))) + .build())) + ); + } + + double? _getItemExtent(BuildContext context) { + if (BuildUtils.isWeb) { + return _responsiveUtils.isDesktop(context) ? 52 : 95; + } else { + return null; + } + } + + Widget _buildLoadingView() { + return Obx(() => controller.viewState.value.fold( + (failure) => const SizedBox.shrink(), + (success) => success is SearchingState + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: loadingWidget) + : const SizedBox.shrink())); + } + + List _contextMenuActionTile(BuildContext context, PresentationEmail email) { + return [ + _markAsEmailSpamOrUnSpamAction(context, email), + ]; + } + + Widget _markAsEmailSpamOrUnSpamAction(BuildContext context, PresentationEmail email) { + final mailboxContain = email.findMailboxContain(controller.mailboxDashBoardController.mapMailbox); + + return (EmailActionCupertinoActionSheetActionBuilder( + const Key('mark_as_spam_or_un_spam_action'), + SvgPicture.asset( + mailboxContain?.isSpam == true ? _imagePaths.icNotSpam : _imagePaths.icSpam, + width: 28, height: 28, fit: BoxFit.fill, color: AppColor.colorTextButton), + mailboxContain?.isSpam == true + ? AppLocalizations.of(context).remove_from_spam + : AppLocalizations.of(context).mark_as_spam, + email, + iconLeftPadding: _responsiveUtils.isMobile(context) + ? const EdgeInsets.only(left: 12, right: 16) + : const EdgeInsets.only(right: 12), + iconRightPadding: _responsiveUtils.isMobile(context) + ? const EdgeInsets.only(right: 12) + : EdgeInsets.zero) + ..onActionClick((email) => controller.pressEmailAction(context, + mailboxContain?.isSpam == true + ? EmailActionType.unSpam + : EmailActionType.moveToSpam, + email))) + .build(); + } + + List _popupMenuActionTile(BuildContext context, PresentationEmail email) { + return [ + PopupMenuItem( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: _markAsEmailSpamOrUnSpamAction(context, email)), + ]; + } + + Widget _buildLoadingViewLoadMore() { + return Obx(() => controller.viewState.value.fold( + (failure) => const SizedBox.shrink(), + (success) { + return success is SearchingMoreState + ? Padding( + padding: const EdgeInsets.only(bottom: 16), + child: loadingWidget) + : const SizedBox.shrink(); + })); + } +} \ No newline at end of file diff --git a/lib/features/search/presentation/utils/search_email_utils.dart b/lib/features/search/presentation/utils/search_email_utils.dart new file mode 100644 index 000000000..9dad12161 --- /dev/null +++ b/lib/features/search/presentation/utils/search_email_utils.dart @@ -0,0 +1,90 @@ + +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/build_utils.dart'; +import 'package:flutter/material.dart'; + +class SearchEmailUtils { + static EdgeInsets getPaddingAppBar(BuildContext context, ResponsiveUtils responsiveUtils) { + if (BuildUtils.isWeb) { + return const EdgeInsets.symmetric(horizontal: 16); + } else { + if (responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsets.symmetric(horizontal: 10); + } else { + return const EdgeInsets.symmetric(horizontal: 32); + } + } + } + + static EdgeInsets getPaddingSearchSuggestionList(BuildContext context, ResponsiveUtils responsiveUtils) { + if (BuildUtils.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsets.symmetric(horizontal: 32, vertical: 12); + } else { + return const EdgeInsets.symmetric(horizontal: 16, vertical: 12); + } + } + + static EdgeInsets getPaddingShowAllResultButton(BuildContext context, ResponsiveUtils responsiveUtils) { + if (BuildUtils.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsets.symmetric(horizontal: 32, vertical: 12); + } else { + return const EdgeInsets.symmetric(horizontal: 16, vertical: 12); + } + } + + static EdgeInsets getPaddingSearchRecentTitle(BuildContext context, ResponsiveUtils responsiveUtils) { + if (BuildUtils.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsets.symmetric(horizontal: 32, vertical: 8); + } else { + return const EdgeInsets.symmetric(horizontal: 16, vertical: 8); + } + } + + static EdgeInsets getPaddingListRecentSearch(BuildContext context, ResponsiveUtils responsiveUtils) { + if (BuildUtils.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsets.symmetric(horizontal: 32, vertical: 12); + } else { + return const EdgeInsets.symmetric(horizontal: 16, vertical: 12); + } + } + + static EdgeInsets getPaddingSearchFilterButton(BuildContext context, ResponsiveUtils responsiveUtils) { + if (BuildUtils.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsets.all(12); + } else { + return const EdgeInsets.symmetric(vertical: 12, horizontal: 16); + } + } + + static EdgeInsets getMarginSearchFilterButton(BuildContext context, ResponsiveUtils responsiveUtils) { + if (BuildUtils.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsets.symmetric(horizontal: 20); + } else { + return EdgeInsets.zero; + } + } + + static EdgeInsets getPaddingSearchResultList(BuildContext context, ResponsiveUtils responsiveUtils) { + if (BuildUtils.isWeb) { + return const EdgeInsets.only(left: 10); + } else { + if (responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsets.symmetric(horizontal: 16, vertical: 5); + } else { + return const EdgeInsets.symmetric(horizontal: 32, vertical: 5); + } + } + } + + static EdgeInsets getPaddingDividerSearchResultList(BuildContext context, ResponsiveUtils responsiveUtils) { + if (BuildUtils.isWeb) { + return const EdgeInsets.symmetric(horizontal: 16); + } else { + if (responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsets.symmetric(horizontal: 16); + } else { + return const EdgeInsets.symmetric(horizontal: 32); + } + } + } +} \ No newline at end of file diff --git a/lib/features/search/presentation/widgets/app_bar_selection_mode.dart b/lib/features/search/presentation/widgets/app_bar_selection_mode.dart new file mode 100644 index 000000000..cadce9f7e --- /dev/null +++ b/lib/features/search/presentation/widgets/app_bar_selection_mode.dart @@ -0,0 +1,149 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/list_presentation_email_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class AppBarSelectionMode extends StatelessWidget { + + final _imagePaths = Get.find(); + + final List listEmail; + final Map mapMailbox; + final Function()? onCancelSelection; + final Function(EmailActionType actionType, List emails)? onHandleEmailAction; + + AppBarSelectionMode( + this.listEmail, + this.mapMailbox, { + Key? key, + this.onCancelSelection, + this.onHandleEmailAction, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final canDeletePermanently = listEmail.isAllCanDeletePermanently(mapMailbox); + final canSpamAndMove = listEmail.isAllCanSpamAndMove(mapMailbox); + final isAllSpam = listEmail.isAllSpam(mapMailbox); + final isAllBelongToTheSameMailbox = listEmail.isAllBelongToTheSameMailbox(mapMailbox); + + return Row(children: [ + buildIconWeb( + icon: SvgPicture.asset(_imagePaths.icCloseComposer, + color: AppColor.colorTextButton, + fit: BoxFit.fill), + minSize: 25, + iconSize: 25, + iconPadding: const EdgeInsets.all(5), + splashRadius: 15, + tooltip: AppLocalizations.of(context).cancel, + onTap: onCancelSelection), + Expanded(child: Text( + AppLocalizations.of(context).count_email_selected(listEmail.length), + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: AppColor.colorTextButton))), + buildIconWeb( + minSize: 25, + iconSize: 25, + iconPadding: const EdgeInsets.all(5), + splashRadius: 15, + icon: SvgPicture.asset( + listEmail.isAllEmailRead + ? _imagePaths.icRead + : _imagePaths.icUnread, + fit: BoxFit.fill), + tooltip: listEmail.isAllEmailRead + ? AppLocalizations.of(context).unread + : AppLocalizations.of(context).read, + onTap: () => onHandleEmailAction?.call( + listEmail.isAllEmailRead + ? EmailActionType.markAsUnread + : EmailActionType.markAsRead, + listEmail)), + const SizedBox(width: 5), + buildIconWeb( + minSize: 25, + iconSize: 25, + iconPadding: const EdgeInsets.all(5), + splashRadius: 15, + icon: SvgPicture.asset( + listEmail.isAllEmailStarred + ? _imagePaths.icUnStar + : _imagePaths.icStar, + fit: BoxFit.fill), + tooltip: listEmail.isAllEmailStarred + ? AppLocalizations.of(context).not_starred + : AppLocalizations.of(context).starred, + onTap: () => onHandleEmailAction?.call( + listEmail.isAllEmailStarred + ? EmailActionType.unMarkAsStarred + : EmailActionType.markAsStarred, + listEmail)), + const SizedBox(width: 5), + if (canSpamAndMove) + ... [ + buildIconWeb( + minSize: 25, + iconSize: 25, + iconPadding: const EdgeInsets.all(5), + splashRadius: 15, + icon: SvgPicture.asset(_imagePaths.icMove, fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).move, + onTap: () => onHandleEmailAction?.call(EmailActionType.moveToMailbox, listEmail)), + const SizedBox(width: 5), + buildIconWeb( + minSize: 25, + iconSize: 25, + iconPadding: const EdgeInsets.all(5), + splashRadius: 15, + icon: SvgPicture.asset(isAllSpam + ? _imagePaths.icNotSpam + : _imagePaths.icSpam, + fit: BoxFit.fill), + tooltip: isAllSpam + ? AppLocalizations.of(context).un_spam + : AppLocalizations.of(context).mark_as_spam, + onTap: () => isAllSpam + ? onHandleEmailAction?.call(EmailActionType.unSpam, listEmail) + : onHandleEmailAction?.call(EmailActionType.moveToSpam, listEmail.listEmailCanSpam(mapMailbox))), + const SizedBox(width: 5), + ], + if (isAllBelongToTheSameMailbox) + ...[ + buildIconWeb( + minSize: 25, + iconSize: 25, + iconPadding: const EdgeInsets.all(5), + splashRadius: 15, + icon: SvgPicture.asset( + canDeletePermanently + ? _imagePaths.icDeleteComposer + : _imagePaths.icDelete, + color: canDeletePermanently + ? AppColor.colorDeletePermanentlyButton + : AppColor.primaryColor, + width: 20, + height: 20, + fit: BoxFit.fill), + tooltip: canDeletePermanently + ? AppLocalizations.of(context).delete_permanently + : AppLocalizations.of(context).move_to_trash, + onTap: () => canDeletePermanently + ? onHandleEmailAction?.call(EmailActionType.deletePermanently, listEmail) + : onHandleEmailAction?.call(EmailActionType.moveToTrash, listEmail)), + const SizedBox(width: 10) + ], + ]); + } +} \ No newline at end of file diff --git a/lib/features/search/presentation/widgets/email_receive_time_action_tile_widget.dart b/lib/features/search/presentation/widgets/email_receive_time_action_tile_widget.dart new file mode 100644 index 000000000..bd310784f --- /dev/null +++ b/lib/features/search/presentation/widgets/email_receive_time_action_tile_widget.dart @@ -0,0 +1,51 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; + +class EmailReceiveTimeActionTileWidget extends StatelessWidget { + + final EmailReceiveTimeType? receiveTimeSelected; + final EmailReceiveTimeType receiveTimeType; + final Function(EmailReceiveTimeType?)? onCallBack; + + const EmailReceiveTimeActionTileWidget({ + Key? key, + this.receiveTimeSelected, + required this.receiveTimeType, + this.onCallBack + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final imagePaths = Get.find(); + + return InkWell( + onTap: () => onCallBack?.call(receiveTimeType == receiveTimeSelected ? null : receiveTimeType), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: SizedBox( + width: 320, + child: Row(children: [ + Expanded(child: Text( + receiveTimeType.getTitle(context), + style: const TextStyle( + fontSize: 17, + color: Colors.black, + fontWeight: FontWeight.normal))), + if (receiveTimeType == receiveTimeSelected) + const SizedBox(width: 12), + if (receiveTimeType == receiveTimeSelected) + SvgPicture.asset( + imagePaths.icFilterSelected, + width: 24, + height: 24, + fit: BoxFit.fill), + ]) + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/search/presentation/widgets/email_receive_time_cupertino_action_sheet_action_builder.dart b/lib/features/search/presentation/widgets/email_receive_time_cupertino_action_sheet_action_builder.dart new file mode 100644 index 000000000..bd5f9204f --- /dev/null +++ b/lib/features/search/presentation/widgets/email_receive_time_cupertino_action_sheet_action_builder.dart @@ -0,0 +1,54 @@ + +import 'package:core/core.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; + +class EmailReceiveTimeCupertinoActionSheetActionBuilder + extends CupertinoActionSheetNoIconBuilder { + + final EmailReceiveTimeType timeType; + final EmailReceiveTimeType? timeTypeCurrent; + final SvgPicture? actionSelected; + final Color? bgColor; + final EdgeInsets? iconLeftPadding; + final EdgeInsets? iconRightPadding; + + EmailReceiveTimeCupertinoActionSheetActionBuilder( + String actionName, + this.timeType, { + Key? key, + this.timeTypeCurrent, + this.actionSelected, + this.bgColor, + this.iconLeftPadding, + this.iconRightPadding, + }) : super(actionName); + + @override + Widget build() { + return Container( + color: bgColor ?? Colors.white, + child: MouseRegion( + cursor: BuildUtils.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, + child: CupertinoActionSheetAction( + key: key, + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + const SizedBox(width: 16), + Expanded(child: Text(actionName, textAlign: TextAlign.left, style: actionTextStyle())), + if (timeTypeCurrent == timeType && actionSelected != null) + Padding( + padding: iconRightPadding ?? const EdgeInsets.only(right: 16), + child: actionSelected!), + ]), + onPressed: () { + if (onCupertinoActionSheetActionClick != null) { + onCupertinoActionSheetActionClick!(timeType); + } + }, + ), + ), + ); + } +} \ No newline at end of file