TF-3002 [WEB] Add To/Folder search filters

This commit is contained in:
dab246
2024-09-12 19:31:38 +07:00
committed by Dat H. Pham
parent 34c9c0cd0e
commit 8290025c14
24 changed files with 461 additions and 228 deletions
@@ -251,19 +251,18 @@ class EmailView extends GetWidget<SingleEmailController> {
);
}
EdgeInsetsGeometry _getMarginEmailView(BuildContext context) {
EdgeInsetsGeometry? _getMarginEmailView(BuildContext context) {
if (PlatformInfo.isWeb) {
if (controller.responsiveUtils.isDesktop(context)) {
return const EdgeInsetsDirectional.only(
end: 16,
top: 8,
bottom: 16
);
} else {
return const EdgeInsets.symmetric(vertical: 16);
}
} else {
return EdgeInsets.zero;
return null;
}
}
@@ -118,7 +118,7 @@ class AdvancedFilterController extends BaseController {
searchController.updateFilterEmail(sortOrderOption: searchController.sortOrderFiltered.value.getSortOrder());
searchController.updateFilterEmail(
mailbox: _destinationMailboxSelected,
mailboxOption: optionOf(_destinationMailboxSelected),
subjectOption: optionOf(subjectFilterInputController.text),
emailReceiveTimeType: dateFilterSelectedFormAdvancedSearch.value,
hasAttachment: hasAttachment.value,
@@ -397,8 +397,17 @@ class AdvancedFilterController extends BaseController {
);
} else if (action is ClearDateRangeToAdvancedSearch) {
_updateDateRangeTime(action.receiveTime);
} else if (action is StartSearchEmailAction && action.filter == QuickSearchFilter.from) {
_updateFromField();
} else if (action is StartSearchEmailAction) {
switch(action.filter) {
case QuickSearchFilter.from:
_updateFromField();
break;
case QuickSearchFilter.to:
_updateToField();
break;
default:
break;
}
} else if (action is SearchEmailByFromFieldsAction) {
searchController.clearSearchFilter();
_resetAllToOriginalValue();
@@ -437,8 +446,13 @@ class AdvancedFilterController extends BaseController {
}
void _updateFromField() {
final listEmailAddress = searchEmailFilter.from.map((address) => EmailAddress(null, address)).toList();
listFromEmailAddress = List.from(listEmailAddress);
listFromEmailAddress = List.from(
searchEmailFilter.from.map((address) => EmailAddress(null, address)));
}
void _updateToField() {
listToEmailAddress= List.from(
searchEmailFilter.to.map((address) => EmailAddress(null, address)));
}
void onSearchAction(BuildContext context) {
@@ -240,6 +240,7 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
PresentationMailbox? outboxMailbox;
ComposerArguments? composerArguments;
List<Identity>? _identities;
ScrollController? listSearchFilterScrollController;
late StreamSubscription _emailAddressStreamSubscription;
late StreamSubscription _emailContentStreamSubscription;
@@ -297,6 +298,9 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
@override
void onReady() {
if (PlatformInfo.isWeb) {
listSearchFilterScrollController = ScrollController();
}
_registerPendingEmailAddress();
_registerPendingEmailContents();
_registerPendingFileInfo();
@@ -1679,6 +1683,60 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
}
}
Future<void> selectToSearchFilter() async {
if (accountId.value == null || sessionCurrent == null) return;
final contactArgument = ContactArguments(
accountId.value!,
sessionCurrent!,
searchController.searchEmailFilter.value.to);
final newContact = await DialogRouter.pushGeneralDialog(
routeName: AppRoutes.contact,
arguments: contactArgument);
if (newContact is EmailAddress) {
searchController.updateFilterEmail(toOption: Some({newContact.emailAddress}));
dispatchAction(StartSearchEmailAction(filter: QuickSearchFilter.to));
}
}
Future<void> selectFolderSearchFilter() async {
if (accountId.value == null || sessionCurrent == null) return;
final mailboxIdSelected = searchController.mailboxFiltered?.id;
final destinationArgument = DestinationPickerArguments(
accountId.value!,
MailboxActions.select,
sessionCurrent!,
mailboxIdSelected: mailboxIdSelected);
final destinationMailbox = PlatformInfo.isWeb
? await DialogRouter.pushGeneralDialog(
routeName: AppRoutes.destinationPicker,
arguments: destinationArgument)
: await push(
AppRoutes.destinationPicker,
arguments: destinationArgument);
if (destinationMailbox is! PresentationMailbox) return;
if (destinationMailbox != PresentationMailbox.unifiedMailbox &&
mailboxIdSelected != destinationMailbox.id) {
searchController.updateFilterEmail(
mailboxOption: Some(destinationMailbox),
beforeOption: const None(),
positionOption: searchController.sortOrderFiltered.value.isScrollByPosition()
? const Some(0)
: const None()
);
dispatchAction(StartSearchEmailAction(filter: QuickSearchFilter.folder));
}
}
void selectReceiveTimeQuickSearchFilter(BuildContext context, EmailReceiveTimeType receiveTime) {
log('MailboxDashBoardController::selectReceiveTimeQuickSearchFilter():receiveTime: $receiveTime');
popBack();
@@ -2694,6 +2752,9 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
@override
void onClose() {
if (PlatformInfo.isWeb) {
listSearchFilterScrollController?.dispose();
}
_emailReceiveManager.closeEmailReceiveManagerStream();
if (PlatformInfo.isIOS) {
_iosNotificationManager?.dispose();
@@ -158,7 +158,7 @@ class SearchController extends BaseController with DateRangePickerMixin {
SearchQuery? text,
Option<String>? subjectOption,
Set<String>? notKeyword,
PresentationMailbox? mailbox,
Option<PresentationMailbox>? mailboxOption,
EmailReceiveTimeType? emailReceiveTimeType,
bool? hasAttachment,
Option<UTCDate>? beforeOption,
@@ -173,7 +173,7 @@ class SearchController extends BaseController with DateRangePickerMixin {
text: text,
subjectOption: subjectOption,
notKeyword: notKeyword,
mailbox: mailbox,
mailboxOption: mailboxOption,
emailReceiveTimeType: emailReceiveTimeType,
hasAttachment: hasAttachment,
beforeOption: beforeOption,
@@ -191,6 +191,8 @@ class SearchController extends BaseController with DateRangePickerMixin {
DateTime? get endDateFiltered => searchEmailFilter.value.endDate?.value.toLocal();
PresentationMailbox? get mailboxFiltered => searchEmailFilter.value.mailbox;
bool isSearchActive() =>
searchState.value.searchStatus == SearchStatus.ACTIVE;
@@ -1,4 +1,5 @@
import 'package:core/core.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_svg/flutter_svg.dart';
@@ -6,10 +7,10 @@ import 'package:get/get.dart';
import 'package:model/extensions/presentation_mailbox_extension.dart';
import 'package:model/extensions/username_extension.dart';
import 'package:tmail_ui_user/features/base/widget/popup_item_no_icon_widget.dart';
import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart';
import 'package:tmail_ui_user/features/email/presentation/email_view.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_view_web.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/base_mailbox_dashboard_view.dart';
@@ -19,19 +20,19 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_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/download/download_task_item_widget.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/navigation_bar/navigation_bar_widget.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recover_deleted_message_loading_banner_widget.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/search_filters/search_filter_button.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/vacation/styles/vacation_notification_message_widget_style.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart';
import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_banner_widget.dart';
import 'package:tmail_ui_user/features/search/email/presentation/search_email_view.dart';
import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_view.dart';
import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart';
import 'package:tmail_ui_user/features/thread/presentation/styles/banner_delete_all_spam_emails_styles.dart';
import 'package:tmail_ui_user/features/thread/presentation/styles/banner_empty_trash_styles.dart';
import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/banner_delete_all_spam_emails_widget.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/banner_empty_trash_widget.dart';
@@ -88,22 +89,23 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
))
]),
Expanded(child: Column(children: [
const SizedBox(height: 16),
Obx(() => RecoverDeletedMessageLoadingBannerWidget(
isLoading: controller.isRecoveringDeletedMessage.value,
horizontalLoadingWidget: horizontalLoadingWidget,
responsiveUtils: controller.responsiveUtils,
)),
Obx(() => MarkMailboxAsReadLoadingBanner(
viewState: controller.viewStateMarkAsReadMailbox.value,
)),
const SpamReportBannerWebWidget(),
QuotasBannerWidget(
margin: const EdgeInsetsDirectional.only(end: 16, top: 8),
),
QuotasBannerWidget(),
_buildVacationNotificationMessage(context),
Obx(() {
final presentationMailbox = controller.selectedMailbox.value;
if (controller.isEmptyTrashBannerEnabledOnWeb(context, presentationMailbox)) {
return Padding(
padding: const EdgeInsetsDirectional.only(
top: BannerEmptyTrashStyles.webTopMargin,
end: BannerEmptyTrashStyles.webEndMargin
),
child: BannerEmptyTrashWidget(
onTapAction: () => controller.emptyTrashAction(context)
),
return BannerEmptyTrashWidget(
onTapAction: () => controller.emptyTrashAction(context)
);
} else {
return const SizedBox.shrink();
@@ -112,26 +114,14 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
Obx(() {
final presentationMailbox = controller.selectedMailbox.value;
if (controller.isEmptySpamBannerEnabledOnWeb(context, presentationMailbox)) {
return Padding(
padding: const EdgeInsetsDirectional.only(
top: BannerDeleteAllSpamEmailsStyles.webTopMargin,
end: BannerDeleteAllSpamEmailsStyles.webEndMargin
),
child: BannerDeleteAllSpamEmailsWidget(
onTapAction: () => controller.openDialogEmptySpamFolder(context)
),
return BannerDeleteAllSpamEmailsWidget(
onTapAction: () => controller.openDialogEmptySpamFolder(context)
);
} else {
return const SizedBox.shrink();
}
}),
Obx(() => RecoverDeletedMessageLoadingBannerWidget(
isLoading: controller.isRecoveringDeletedMessage.value,
horizontalLoadingWidget: horizontalLoadingWidget,
responsiveUtils: controller.responsiveUtils,
)),
_buildListButtonQuickSearchFilter(context),
_buildMarkAsMailboxReadLoading(context),
Expanded(child: Obx(() {
switch(controller.dashboardRoute.value) {
case DashboardRoutes.thread:
@@ -223,7 +213,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
Widget _buildThreadViewForWebDesktop(BuildContext context) {
return Container(
margin: const EdgeInsetsDirectional.only(end: 16, top: 8, bottom: 16),
margin: const EdgeInsetsDirectional.only(end: 16, bottom: 16),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
border: Border.all(color: AppColor.colorBorderBodyThread, width: 1),
@@ -380,37 +370,18 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
} else {
return const SizedBox.shrink();
}
}),
const Spacer(),
Obx(() {
if (controller.searchController.isSearchEmailRunning &&
controller.dashboardRoute.value == DashboardRoutes.thread) {
return _buildQuickSearchFilterButton(context, QuickSearchFilter.sortBy);
} else {
return const SizedBox.shrink();
}
})
]);
}
Widget _buildMarkAsMailboxReadLoading(BuildContext context) {
return Obx(() {
final viewState = controller.viewStateMarkAsReadMailbox.value;
return viewState.fold(
(failure) => const SizedBox.shrink(),
(success) {
if (success is MarkAsMailboxReadLoading) {
return Padding(
padding: EdgeInsets.only(
top: controller.responsiveUtils.isDesktop(context) ? 16 : 0,
left: 16,
right: 16,
bottom: controller.responsiveUtils.isDesktop(context) ? 0 : 16),
child: horizontalLoadingWidget);
} else if (success is UpdatingMarkAsMailboxReadState) {
final percent = success.countRead / success.totalUnread;
return Padding(
padding: EdgeInsets.only(
top: controller.responsiveUtils.isDesktop(context) ? 16 : 0,
left: 16,
right: 16,
bottom: controller.responsiveUtils.isDesktop(context) ? 0 : 16),
child: horizontalPercentLoadingWidget(percent));
}
return const SizedBox.shrink();
});
});
]);
}
Widget _buildDownloadTaskStateWidget() {
@@ -450,7 +421,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
return Obx(() {
if (controller.vacationResponse.value?.vacationResponderIsValid == true) {
return VacationNotificationMessageWidget(
margin: const EdgeInsetsDirectional.only(top: 8, end: 16),
margin: VacationNotificationMessageWidgetStyle.bannerMargin,
vacationResponse: controller.vacationResponse.value!,
actionGotoVacationSetting: controller.goToVacationSetting,
actionEndNow: controller.disableVacationResponder);
@@ -465,17 +436,44 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
if (controller.searchController.isSearchEmailRunning &&
controller.dashboardRoute.value == DashboardRoutes.thread) {
return Padding(
padding: const EdgeInsetsDirectional.only(end: 16, top: 8),
padding: const EdgeInsetsDirectional.only(end: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildQuickSearchFilterButton(context, QuickSearchFilter.from),
const SizedBox(width: 8),
_buildQuickSearchFilterButton(context, QuickSearchFilter.dateTime),
const SizedBox(width: 8),
_buildQuickSearchFilterButton(context, QuickSearchFilter.hasAttachment),
const Spacer(),
_buildQuickSearchFilterButton(context, QuickSearchFilter.sortBy),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 45,
child: ScrollbarListView(
scrollBehavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.trackpad
},
scrollbars: false
),
scrollController: controller.listSearchFilterScrollController!,
child: ListView(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
padding: const EdgeInsetsDirectional.only(bottom: 10),
controller: controller.listSearchFilterScrollController!,
children: [
_buildQuickSearchFilterButton(context, QuickSearchFilter.folder),
const SizedBox(width: 8),
_buildQuickSearchFilterButton(context, QuickSearchFilter.from),
const SizedBox(width: 8),
_buildQuickSearchFilterButton(context, QuickSearchFilter.to),
const SizedBox(width: 8),
_buildQuickSearchFilterButton(context, QuickSearchFilter.dateTime),
const SizedBox(width: 8),
_buildQuickSearchFilterButton(context, QuickSearchFilter.hasAttachment),
const SizedBox(width: 8),
],
),
),
)
),
]
),
);
@@ -497,6 +495,8 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
final startDate = controller.searchController.startDateFiltered;
final endDate = controller.searchController.endDateFiltered;
final receiveTimeType = controller.searchController.receiveTimeFiltered;
final mailbox = controller.searchController.mailboxFiltered;
final listAddressOfTo = controller.searchController.searchEmailFilter.value.to;
final isSelected = searchFilter.isSelected(
context,
@@ -504,6 +504,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
sortOrderType);
return SearchFilterButton(
key: Key('${searchFilter.name}_search_filter_button'),
searchFilter: searchFilter,
imagePaths: controller.imagePaths,
isSelected: isSelected,
@@ -512,7 +513,9 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
endDate: endDate,
sortOrderType: sortOrderType,
listAddressOfFrom: listAddressOfFrom,
listAddressOfTo: listAddressOfTo,
userName: userName,
mailbox: mailbox,
onSelectSearchFilterAction: _onSelectSearchFilterAction,
);
});
@@ -540,6 +543,12 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
case QuickSearchFilter.hasAttachment:
controller.selectHasAttachmentSearchFilter();
break;
case QuickSearchFilter.to:
controller.selectToSearchFilter();
break;
case QuickSearchFilter.folder:
controller.selectFolderSearchFilter();
break;
default:
break;
}
@@ -3,6 +3,8 @@ import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:flutter/cupertino.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart';
@@ -14,26 +16,9 @@ enum QuickSearchFilter {
fromMe,
sortBy,
dateTime,
from;
String getName(BuildContext context) {
switch(this) {
case QuickSearchFilter.hasAttachment:
return AppLocalizations.of(context).hasAttachment;
case QuickSearchFilter.last7Days:
return AppLocalizations.of(context).last7Days;
case QuickSearchFilter.fromMe:
return AppLocalizations.of(context).fromMe;
case QuickSearchFilter.sortBy:
return AppLocalizations.of(context).sortBy;
case QuickSearchFilter.dateTime:
return AppLocalizations.of(context).allTime;
case QuickSearchFilter.from:
return AppLocalizations.of(context).from_email_address_prefix;
default:
return '';
}
}
from,
to,
folder;
String getTitle(
BuildContext context, {
@@ -42,7 +27,9 @@ enum QuickSearchFilter {
DateTime? startDate,
DateTime? endDate,
Set<String>? listAddressOfFrom,
UserName? userName
UserName? userName,
Set<String>? listAddressOfTo,
PresentationMailbox? mailbox
}) {
switch(this) {
case QuickSearchFilter.hasAttachment:
@@ -61,17 +48,11 @@ enum QuickSearchFilter {
endDate: endDate
) ?? AppLocalizations.of(context).allTime;
case QuickSearchFilter.from:
if (listAddressOfFrom?.length != 1) {
return AppLocalizations.of(context).from_email_address_prefix;
}
if (userName?.value.isNotEmpty == true && listAddressOfFrom?.first == userName?.value) {
return AppLocalizations.of(context).fromMe;
} else {
return '${AppLocalizations.of(context).from_email_address_prefix} ${listAddressOfFrom?.first}';
}
default:
return '';
return AppLocalizations.of(context).from_email_address_prefix;
case QuickSearchFilter.folder:
return mailbox?.getDisplayName(context) ?? AppLocalizations.of(context).all;
case QuickSearchFilter.to:
return AppLocalizations.of(context).to_email_address_prefix;
}
}
@@ -89,6 +70,10 @@ enum QuickSearchFilter {
return imagePaths.icCalendarSB;
case QuickSearchFilter.from:
return imagePaths.icUserSB;
case QuickSearchFilter.to:
return imagePaths.icUserSB;
case QuickSearchFilter.folder:
return imagePaths.icFolderMailbox;
}
}
@@ -136,6 +121,10 @@ enum QuickSearchFilter {
return searchFilter.emailReceiveTimeType != EmailReceiveTimeType.allTime;
case QuickSearchFilter.from:
return searchFilter.from.isNotEmpty;
case QuickSearchFilter.to:
return searchFilter.to.isNotEmpty;
case QuickSearchFilter.folder:
return searchFilter.mailbox != null;
}
}
@@ -145,5 +134,7 @@ enum QuickSearchFilter {
bool isArrowDownIconSupported() =>
this == QuickSearchFilter.dateTime ||
this == QuickSearchFilter.from ||
this == QuickSearchFilter.to ||
this == QuickSearchFilter.folder ||
this == QuickSearchFilter.sortBy;
}
@@ -60,7 +60,7 @@ class SearchEmailFilter with EquatableMixin, OptionParamMixin {
SearchQuery? text,
Option<String>? subjectOption,
Set<String>? notKeyword,
PresentationMailbox? mailbox,
Option<PresentationMailbox>? mailboxOption,
EmailReceiveTimeType? emailReceiveTimeType,
bool? hasAttachment,
Option<UTCDate>? beforeOption,
@@ -75,7 +75,7 @@ class SearchEmailFilter with EquatableMixin, OptionParamMixin {
text: text ?? this.text,
subject: getOptionParam(subjectOption, subject),
notKeyword: notKeyword ?? this.notKeyword,
mailbox: mailbox ?? this.mailbox,
mailbox: getOptionParam(mailboxOption, mailbox),
emailReceiveTimeType: emailReceiveTimeType ?? this.emailReceiveTimeType,
hasAttachment: hasAttachment ?? this.hasAttachment,
before: getOptionParam(beforeOption, before),
@@ -0,0 +1,5 @@
import 'package:flutter/material.dart';
class MarkMailboxAsReadLoadingBannerStyle {
static const EdgeInsetsGeometry bannerMargin = EdgeInsetsDirectional.only(end: 16, bottom: 8);
}
@@ -0,0 +1,26 @@
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
class RecoverDeletedMessageLoadingBannerStyle {
static EdgeInsetsGeometry getBannerMargin(
BuildContext context,
ResponsiveUtils responsiveUtils
) {
if (PlatformInfo.isWeb) {
if (responsiveUtils.isDesktop(context)) {
return const EdgeInsetsDirectional.only(end: 16, bottom: 8);
} else if (responsiveUtils.isTablet(context)) {
return const EdgeInsetsDirectional.only(bottom: 8, start: 24, end: 24);
} else {
return const EdgeInsetsDirectional.only(bottom: 8, start: 12, end: 12);
}
} else {
if (responsiveUtils.isPortraitTablet(context)) {
return const EdgeInsetsDirectional.only(bottom: 8, start: 32, end: 32);
} else {
return const EdgeInsetsDirectional.only(bottom: 8, start: 16, end: 16);
}
}
}
}
@@ -5,6 +5,7 @@ class SearchFilterButtonStyle {
static const double iconSize = 16;
static const double deleteIconSize = 10;
static const double spaceSize = 4;
static const double buttonSize = 35;
static const EdgeInsetsGeometry buttonPadding = EdgeInsets.symmetric(horizontal: 12, vertical: 8);
static const EdgeInsetsGeometry elementPadding = EdgeInsetsDirectional.only(start: 8);
@@ -0,0 +1,37 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart';
import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/mark_mailbox_as_read_loading_banner_style.dart';
class MarkMailboxAsReadLoadingBanner extends StatelessWidget with AppLoaderMixin {
final Either<Failure, Success> viewState;
const MarkMailboxAsReadLoadingBanner({
super.key,
required this.viewState,
});
@override
Widget build(BuildContext context) {
return viewState.fold(
(failure) => const SizedBox.shrink(),
(success) {
if (success is MarkAsMailboxReadLoading) {
return Padding(
padding: MarkMailboxAsReadLoadingBannerStyle.bannerMargin,
child: horizontalLoadingWidget);
} else if (success is UpdatingMarkAsMailboxReadState) {
final percent = success.countRead / success.totalUnread;
return Padding(
padding: MarkMailboxAsReadLoadingBannerStyle.bannerMargin,
child: horizontalPercentLoadingWidget(percent));
} else {
return const SizedBox.shrink();
}
}
);
}
}
@@ -1,6 +1,7 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/recover_deleted_message_loading_banner_style.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class RecoverDeletedMessageLoadingBannerWidget extends StatelessWidget {
@@ -18,42 +19,38 @@ class RecoverDeletedMessageLoadingBannerWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (isLoading) {
return Padding(
padding: EdgeInsetsDirectional.only(
top: responsiveUtils.isDesktop(context) ? 16 : 0,
end: 16,
bottom: responsiveUtils.isDesktop(context) ? 0 : 16,
return Container(
margin: RecoverDeletedMessageLoadingBannerStyle.getBannerMargin(
context,
responsiveUtils),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: const ShapeDecoration(
color: Colors.white,
shape: RoundedRectangleBorder(
side: BorderSide(
width: 1,
color: AppColor.colorBorderBodyThread,
decoration: const ShapeDecoration(
color: Colors.white,
shape: RoundedRectangleBorder(
side: BorderSide(
width: 1,
color: AppColor.colorBorderBodyThread,
),
borderRadius: BorderRadius.all(Radius.circular(14)),
)
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context).bannerProgressingRecoveryMessage,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w400,
color: AppColor.primaryColor,
),
borderRadius: BorderRadius.all(Radius.circular(14)),
)
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context).bannerProgressingRecoveryMessage,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w400,
color: AppColor.primaryColor,
),
),
const SizedBox(height: 12),
horizontalLoadingWidget
],
),
),
const SizedBox(height: 12),
horizontalLoadingWidget
],
),
);
} else {
@@ -5,6 +5,7 @@ import 'package:core/presentation/views/button/tmail_button_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart';
@@ -17,6 +18,8 @@ typedef OnSelectSearchFilterAction = Function(
class SearchFilterButton extends StatelessWidget {
static const int _titleCharactersMaximum = 20;
final QuickSearchFilter searchFilter;
final bool isSelected;
final ImagePaths imagePaths;
@@ -25,7 +28,9 @@ class SearchFilterButton extends StatelessWidget {
final DateTime? startDate;
final DateTime? endDate;
final Set<String>? listAddressOfFrom;
final Set<String>? listAddressOfTo;
final UserName? userName;
final PresentationMailbox? mailbox;
final Color? backgroundColor;
final OnSelectSearchFilterAction? onSelectSearchFilterAction;
@@ -39,18 +44,33 @@ class SearchFilterButton extends StatelessWidget {
this.startDate,
this.endDate,
this.listAddressOfFrom,
this.listAddressOfTo,
this.userName,
this.mailbox,
this.backgroundColor,
this.onSelectSearchFilterAction,
});
@override
Widget build(BuildContext context) {
final filterTitle = searchFilter.getTitle(
context,
receiveTimeType: receiveTimeType,
startDate: startDate,
endDate: startDate,
sortOrderType: sortOrderType,
listAddressOfFrom: listAddressOfFrom,
userName: userName,
mailbox: mailbox,
listAddressOfTo: listAddressOfTo,
);
final childItem = Container(
decoration: BoxDecoration(
borderRadius: SearchFilterButtonStyle.borderRadius,
color: backgroundColor ?? searchFilter.getBackgroundColor(isFilterSelected: isSelected)),
padding: SearchFilterButtonStyle.buttonPadding,
height: SearchFilterButtonStyle.buttonSize,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -62,14 +82,9 @@ class SearchFilterButton extends StatelessWidget {
fit: BoxFit.fill),
const SizedBox(width: SearchFilterButtonStyle.spaceSize),
Text(
searchFilter.getTitle(
context,
receiveTimeType: receiveTimeType,
startDate: startDate,
endDate: startDate,
sortOrderType: sortOrderType,
listAddressOfFrom: listAddressOfFrom,
userName: userName),
filterTitle.length > _titleCharactersMaximum
? '${filterTitle.substring(0, _titleCharactersMaximum)}...'
: filterTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: SearchFilterButtonStyle.titleStyle,
@@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
class VacationNotificationMessageWidgetStyle {
static const EdgeInsetsGeometry bannerMargin = EdgeInsetsDirectional.only(
end: 16,
bottom: 8);
}
@@ -1,10 +1,10 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
class QuotasBannerStyles {
static const double verticalPadding = 8;
static const double horizontalPadding = 16;
static const double iconPadding = 16;
static const double iconSize = 32;
static const double titleTextSize = 17;
@@ -16,4 +16,29 @@ class QuotasBannerStyles {
static const FontWeight titleFontWeight = FontWeight.w700;
static const FontWeight messageFontWeight = FontWeight.w400;
static const EdgeInsetsGeometry bannerPadding = EdgeInsetsDirectional.symmetric(
horizontal: 16,
vertical: 8,
);
static EdgeInsetsGeometry getBannerMargin(
BuildContext context,
ResponsiveUtils responsiveUtils
) {
if (PlatformInfo.isWeb) {
if (responsiveUtils.isDesktop(context)) {
return const EdgeInsetsDirectional.only(end: 16, bottom: 8);
} else if (responsiveUtils.isTablet(context)) {
return const EdgeInsetsDirectional.only(bottom: 8, start: 24, end: 24);
} else {
return const EdgeInsetsDirectional.only(bottom: 8, start: 12, end: 12);
}
} else {
if (responsiveUtils.isPortraitTablet(context)) {
return const EdgeInsetsDirectional.only(bottom: 8, start: 32, end: 32);
} else {
return const EdgeInsetsDirectional.only(bottom: 8, start: 16, end: 16);
}
}
}
}
@@ -8,28 +8,23 @@ import 'package:tmail_ui_user/features/quotas/presentation/styles/quotas_banner_
class QuotasBannerWidget extends StatelessWidget {
final QuotasController _quotasController = Get.find<QuotasController>();
final EdgeInsetsGeometry? margin;
QuotasBannerWidget({
Key? key,
this.margin,
}) : super(key: key);
QuotasBannerWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(() {
if (_quotasController.octetsQuota.value != null && _quotasController.octetsQuota.value!.allowedDisplayToQuotaBanner) {
final octetQuota = _quotasController.octetsQuota.value!;
final octetQuota = _quotasController.octetsQuota.value;
if (octetQuota != null && octetQuota.allowedDisplayToQuotaBanner) {
return Container(
decoration: BoxDecoration(
color: octetQuota.getQuotaBannerBackgroundColor(),
borderRadius: const BorderRadius.all(Radius.circular(QuotasBannerStyles.borderRadius)),
),
margin: margin,
padding: const EdgeInsetsDirectional.symmetric(
horizontal: QuotasBannerStyles.horizontalPadding,
vertical: QuotasBannerStyles.verticalPadding,
),
margin: QuotasBannerStyles.getBannerMargin(
context,
_quotasController.responsiveUtils),
padding: QuotasBannerStyles.bannerPadding,
child: Row(
children: [
SvgPicture.asset(
@@ -1,5 +1,7 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
class BannerDeleteAllSpamEmailsStyles {
@@ -7,9 +9,6 @@ class BannerDeleteAllSpamEmailsStyles {
static const double buttonBorderRadius = 20;
static const double horizontalPadding = 16;
static const double verticalPadding = 10;
static const double webEndMargin = 16;
static const double webTopMargin = 8;
static const double mobileMargin = 16;
static const double labelTextSize = 13;
static const double buttonTextSize = 17;
static const double iconSize = 20;
@@ -18,4 +17,25 @@ class BannerDeleteAllSpamEmailsStyles {
static const Color borderStrokeColor = AppColor.colorBorderBodyThread;
static const Color labelTextColor = AppColor.colorSubtitle;
static const Color buttonTextColor = AppColor.toastErrorBackgroundColor;
static EdgeInsetsGeometry getBannerMargin(
BuildContext context,
ResponsiveUtils responsiveUtils
) {
if (PlatformInfo.isWeb) {
if (responsiveUtils.isDesktop(context)) {
return const EdgeInsetsDirectional.only(end: 16, bottom: 8);
} else if (responsiveUtils.isTablet(context)) {
return const EdgeInsetsDirectional.only(bottom: 8, start: 24, end: 24);
} else {
return const EdgeInsetsDirectional.only(bottom: 8, start: 12, end: 12);
}
} else {
if (responsiveUtils.isPortraitTablet(context)) {
return const EdgeInsetsDirectional.only(bottom: 8, start: 32, end: 32);
} else {
return const EdgeInsetsDirectional.only(bottom: 8, start: 16, end: 16);
}
}
}
}
@@ -1,5 +1,7 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
class BannerEmptyTrashStyles {
@@ -7,9 +9,6 @@ class BannerEmptyTrashStyles {
static const double buttonBorderRadius = 20;
static const double horizontalPadding = 16;
static const double verticalPadding = 10;
static const double webEndMargin = 16;
static const double webTopMargin = 8;
static const double mobileMargin = 16;
static const double labelTextSize = 13;
static const double buttonTextSize = 17;
static const double iconSize = 20;
@@ -18,4 +17,25 @@ class BannerEmptyTrashStyles {
static const Color borderStrokeColor = AppColor.colorBorderBodyThread;
static const Color labelTextColor = AppColor.colorSubtitle;
static const Color buttonTextColor = AppColor.toastErrorBackgroundColor;
static EdgeInsetsGeometry getBannerMargin(
BuildContext context,
ResponsiveUtils responsiveUtils
) {
if (PlatformInfo.isWeb) {
if (responsiveUtils.isDesktop(context)) {
return const EdgeInsetsDirectional.only(end: 16, bottom: 8);
} else if (responsiveUtils.isTablet(context)) {
return const EdgeInsetsDirectional.only(bottom: 8, start: 24, end: 24);
} else {
return const EdgeInsetsDirectional.only(bottom: 8, start: 12, end: 12);
}
} else {
if (responsiveUtils.isPortraitTablet(context)) {
return const EdgeInsetsDirectional.only(bottom: 8, start: 32, end: 32);
} else {
return const EdgeInsetsDirectional.only(bottom: 8, start: 16, end: 16);
}
}
}
}
@@ -3,11 +3,14 @@ import 'package:core/presentation/extensions/color_extension.dart';
import 'package:flutter/material.dart';
class SpamReportBannerWebStyles {
static const double horizontalMargin = 16;
static const double verticalMargin = 16;
static const double verticalPadding = 8;
static const double horizontalPadding = 16;
static const double borderRadius = 12;
static Color backgroundColor = AppColor.colorSpamReportBannerBackground.withOpacity(0.12);
static const Color strokeBorderColor = AppColor.colorSpamReportBannerStrokeBorder;
static const EdgeInsetsGeometry bannerPadding = EdgeInsetsDirectional.symmetric(
horizontal: 16,
vertical: 8);
static const EdgeInsetsGeometry bannerMargin = EdgeInsetsDirectional.only(
end: 16,
bottom: 8);
}
@@ -0,0 +1,25 @@
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
class ThreadViewStyle {
static EdgeInsetsGeometry getBannerMargin(
BuildContext context,
ResponsiveUtils responsiveUtils
) {
if (PlatformInfo.isWeb) {
if (responsiveUtils.isMobile(context) || responsiveUtils.isTabletLarge(context)) {
return const EdgeInsetsDirectional.only(start: 12, end: 12, bottom: 8);
} else {
return const EdgeInsetsDirectional.only(start: 24, end: 24, bottom: 8);
}
} else {
if (responsiveUtils.isPortraitMobile(context) || responsiveUtils.isLandscapeTablet(context)) {
return const EdgeInsetsDirectional.only(start: 16, end: 16, bottom: 8);
} else {
return const EdgeInsetsDirectional.only(start: 32, end: 32, bottom: 8);
}
}
}
}
@@ -22,10 +22,9 @@ import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.d
import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/loading_more_status.dart';
import 'package:tmail_ui_user/features/thread/presentation/styles/banner_delete_all_spam_emails_styles.dart';
import 'package:tmail_ui_user/features/thread/presentation/styles/banner_empty_trash_styles.dart';
import 'package:tmail_ui_user/features/thread/presentation/styles/item_email_tile_styles.dart';
import 'package:tmail_ui_user/features/thread/presentation/styles/scroll_to_top_button_widget_styles.dart';
import 'package:tmail_ui_user/features/thread/presentation/styles/thread_view_style.dart';
import 'package:tmail_ui_user/features/thread/presentation/thread_controller.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart';
import 'package:tmail_ui_user/features/thread/presentation/widgets/banner_delete_all_spam_emails_widget.dart';
@@ -112,22 +111,26 @@ class ThreadView extends GetWidget<ThreadController>
SearchBarView(
key: const Key('email_search_bar_view'),
imagePaths: controller.imagePaths,
margin: _getBannerMargin(context, controller.responsiveUtils),
margin: ThreadViewStyle.getBannerMargin(
context,
controller.responsiveUtils),
hintTextSearch: AppLocalizations.of(context).search_emails,
onOpenSearchViewAction: controller.goToSearchView
),
SpamReportBannerWidget(
spamReportController: controller.mailboxDashBoardController.spamReportController,
margin: _getBannerMargin(context, controller.responsiveUtils),
),
QuotasBannerWidget(
margin: _getBannerMargin(context, controller.responsiveUtils),
margin: ThreadViewStyle.getBannerMargin(
context,
controller.responsiveUtils),
),
QuotasBannerWidget(),
Obx(() {
final vacation = controller.mailboxDashBoardController.vacationResponse.value;
if (vacation?.vacationResponderIsValid == true) {
return VacationNotificationMessageWidget(
margin: _getBannerMargin(context, controller.responsiveUtils),
margin: ThreadViewStyle.getBannerMargin(
context,
controller.responsiveUtils),
vacationResponse: vacation!,
actionGotoVacationSetting: controller.mailboxDashBoardController.goToVacationSetting,
actionEndNow: controller.mailboxDashBoardController.disableVacationResponder
@@ -145,13 +148,10 @@ class ThreadView extends GetWidget<ThreadController>
Obx(() {
final presentationMailbox = controller.mailboxDashBoardController.selectedMailbox.value;
if (controller.mailboxDashBoardController.isEmptyTrashBannerEnabledOnMobile(context, presentationMailbox)) {
return Padding(
padding: const EdgeInsetsDirectional.symmetric(
horizontal: BannerEmptyTrashStyles.mobileMargin
),
child: BannerEmptyTrashWidget(
onTapAction: () => controller.deleteSelectionEmailsPermanently(context, DeleteActionType.all)
),
return BannerEmptyTrashWidget(
onTapAction: () => controller.deleteSelectionEmailsPermanently(
context,
DeleteActionType.all)
);
} else {
return const SizedBox.shrink();
@@ -160,13 +160,8 @@ class ThreadView extends GetWidget<ThreadController>
Obx(() {
final presentationMailbox = controller.mailboxDashBoardController.selectedMailbox.value;
if (controller.mailboxDashBoardController.isEmptySpamBannerEnabledOnMobile(context, presentationMailbox)) {
return Padding(
padding: const EdgeInsetsDirectional.symmetric(
horizontal: BannerDeleteAllSpamEmailsStyles.mobileMargin
),
child: BannerDeleteAllSpamEmailsWidget(
onTapAction: () => controller.mailboxDashBoardController.openDialogEmptySpamFolder(context)
),
return BannerDeleteAllSpamEmailsWidget(
onTapAction: () => controller.mailboxDashBoardController.openDialogEmptySpamFolder(context)
);
} else {
return const SizedBox.shrink();
@@ -222,22 +217,6 @@ class ThreadView extends GetWidget<ThreadController>
);
}
EdgeInsetsGeometry _getBannerMargin(BuildContext context, ResponsiveUtils responsiveUtils) {
if (PlatformInfo.isWeb) {
if (responsiveUtils.isMobile(context) || responsiveUtils.isTabletLarge(context)) {
return const EdgeInsetsDirectional.only(start: 12, end: 12, bottom: 8);
} else {
return const EdgeInsetsDirectional.only(start: 24, end: 24, bottom: 8);
}
} else {
if (responsiveUtils.isPortraitMobile(context) || responsiveUtils.isLandscapeTablet(context)) {
return const EdgeInsetsDirectional.only(start: 16, end: 16, bottom: 8);
} else {
return const EdgeInsetsDirectional.only(start: 32, end: 32, bottom: 8);
}
}
}
bool _supportVerticalDivider(BuildContext context) {
if (PlatformInfo.isWeb) {
return controller.responsiveUtils.isTabletLarge(context);
@@ -1,9 +1,11 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/thread/presentation/styles/banner_delete_all_spam_emails_styles.dart';
import 'package:tmail_ui_user/features/thread/presentation/styles/banner_empty_trash_styles.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class BannerDeleteAllSpamEmailsWidget extends StatelessWidget {
@@ -15,6 +17,8 @@ class BannerDeleteAllSpamEmailsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final imagePaths = Get.find<ImagePaths>();
final responsiveUtils = Get.find<ResponsiveUtils>();
return Container(
decoration: const ShapeDecoration(
color: BannerDeleteAllSpamEmailsStyles.backgroundColor,
@@ -26,6 +30,7 @@ class BannerDeleteAllSpamEmailsWidget extends StatelessWidget {
borderRadius: BorderRadius.all(Radius.circular(BannerDeleteAllSpamEmailsStyles.borderRadius)),
),
),
margin: BannerEmptyTrashStyles.getBannerMargin(context, responsiveUtils),
child: Material(
color: Colors.transparent,
child: InkWell(
@@ -1,5 +1,6 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
@@ -15,6 +16,8 @@ class BannerEmptyTrashWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final imagePaths = Get.find<ImagePaths>();
final responsiveUtils = Get.find<ResponsiveUtils>();
return Container(
decoration: const ShapeDecoration(
color: BannerEmptyTrashStyles.backgroundColor,
@@ -26,6 +29,7 @@ class BannerEmptyTrashWidget extends StatelessWidget {
borderRadius: BorderRadius.all(Radius.circular(BannerEmptyTrashStyles.borderRadius)),
),
),
margin: BannerEmptyTrashStyles.getBannerMargin(context, responsiveUtils),
child: Material(
color: Colors.transparent,
child: InkWell(
@@ -21,20 +21,12 @@ class SpamReportBannerWebWidget extends StatelessWidget {
return Obx(() {
if (spamReportController.spamReportState.value == SpamReportState.disabled
|| spamReportController.presentationSpamMailbox.value == null) {
return const SizedBox(
height: 8,
);
return const SizedBox.shrink();
}
return Container(
margin: const EdgeInsetsDirectional.only(
end: SpamReportBannerWebStyles.horizontalMargin,
top: SpamReportBannerWebStyles.verticalMargin,
),
margin: SpamReportBannerWebStyles.bannerMargin,
width: double.infinity,
padding: const EdgeInsetsDirectional.symmetric(
horizontal: SpamReportBannerWebStyles.horizontalPadding,
vertical: SpamReportBannerWebStyles.verticalPadding,
),
padding: SpamReportBannerWebStyles.bannerPadding,
decoration: ShapeDecoration(
color: SpamReportBannerWebStyles.backgroundColor,
shape: const RoundedRectangleBorder(