diff --git a/assets/images/ic_arrow_down.svg b/assets/images/ic_arrow_down.svg new file mode 100644 index 000000000..8be30f08d --- /dev/null +++ b/assets/images/ic_arrow_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_filter_web.svg b/assets/images/ic_filter_web.svg new file mode 100644 index 000000000..16f58632c --- /dev/null +++ b/assets/images/ic_filter_web.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_mark_all_as_read.svg b/assets/images/ic_mark_all_as_read.svg new file mode 100644 index 000000000..f8b836a15 --- /dev/null +++ b/assets/images/ic_mark_all_as_read.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_refresh.svg b/assets/images/ic_refresh.svg new file mode 100644 index 000000000..3f5c7a377 --- /dev/null +++ b/assets/images/ic_refresh.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/ic_select_all.svg b/assets/images/ic_select_all.svg new file mode 100644 index 000000000..dd1f0e91a --- /dev/null +++ b/assets/images/ic_select_all.svg @@ -0,0 +1,3 @@ + + + diff --git a/core/lib/core.dart b/core/lib/core.dart index edba7690d..2ff976dff 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -54,6 +54,7 @@ export 'presentation/views/bottom_popup/cupertino_action_sheet_action_builder.da export 'presentation/views/bottom_popup/cupertino_action_sheet_builder.dart'; export 'presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart'; export 'presentation/views/modal_sheets/edit_text_modal_sheet_builder.dart'; +export 'presentation/views/search/search_bar_view.dart'; // Resources export 'presentation/resources/assets_paths.dart'; diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index af93e8f7b..578d60e21 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -98,6 +98,10 @@ extension AppColor on Color { static const colorBottomBarComposer = Color(0x5CEBEDF0); static const colorShadowComposerFullScreen = Color(0x33000000); static const colorCancelButton = Color(0xFFF2F2F2); + static const colorTextButtonHeaderThread = Color(0xFF686E76); + static const colorButtonHeaderThread = Color(0x99EBEDF0); + static const colorBorderBodyThread = Color(0x5CB8C1CC); + static const colorBgDesktop = Color(0xFFF6F6F6); static const mapGradientColor = [ [Color(0xFF21D4FD), Color(0xFFB721FF)], diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 100ed9672..30ddadeeb 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -75,6 +75,11 @@ class ImagePaths { String get icDeleteAttachment => _getImagePath('ic_delete_attachment.svg'); String get icSendMobile => _getImagePath('ic_send_mobile.svg'); String get icSendMobileDisable => _getImagePath('ic_send_mobile_disable.svg'); + String get icArrowDown => _getImagePath('ic_arrow_down.svg'); + String get icFilterWeb => _getImagePath('ic_filter_web.svg'); + String get icMarkAllAsRead => _getImagePath('ic_mark_all_as_read.svg'); + String get icRefresh => _getImagePath('ic_refresh.svg'); + String get icSelectAll => _getImagePath('ic_select_all.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/core/lib/presentation/views/button/button_builder.dart b/core/lib/presentation/views/button/button_builder.dart index 656f88080..c1d33a34f 100644 --- a/core/lib/presentation/views/button/button_builder.dart +++ b/core/lib/presentation/views/button/button_builder.dart @@ -4,10 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; typedef OnPressActionClick = void Function(); +typedef OnPressActionWithPositionClick = void Function(RelativeRect? position); class ButtonBuilder { OnPressActionClick? _onPressActionClick; + OnPressActionWithPositionClick? _onPressActionWithPositionClick; + BuildContext? _context; String? _icon; String? _text; double? _size; @@ -15,12 +18,21 @@ class ButtonBuilder { bool? _isVertical; Key? _key; Color? _iconColor; + Color? _colorButton; TextStyle? _textStyle; + BoxDecoration? _decoration; + Widget? _iconAction; + double? _radiusSplash; + EdgeInsets? _padding; void key(Key key) { _key = key; } + void context(BuildContext context) { + _context = context; + } + void size(double size) { _size = size; } @@ -29,6 +41,26 @@ class ButtonBuilder { _iconColor = color; } + void colorButton(Color color) { + _colorButton = color; + } + + void decoration(BoxDecoration decoration) { + _decoration = decoration; + } + + void addIconAction(Widget icon) { + _iconAction = icon; + } + + void radiusSplash(double? radius) { + _radiusSplash = radius; + } + + void padding(EdgeInsets? padding) { + _padding = padding; + } + void textStyle(TextStyle style) { _textStyle = style; } @@ -48,13 +80,33 @@ class ButtonBuilder { _onPressActionClick = onPressActionClick; } + void addOnPressActionWithPositionClick(OnPressActionWithPositionClick onPressActionClick) { + _onPressActionWithPositionClick = onPressActionClick; + } + Widget build() { return InkWell( - onTap: () => _onPressActionClick?.call(), + onTap: () => _onPressActionClick != null ? _onPressActionClick?.call() : null, + onTapDown: (detail) { + if (_onPressActionWithPositionClick != null && _context != null) { + 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, + ); + _onPressActionWithPositionClick?.call(position); + } + }, + borderRadius: BorderRadius.all(Radius.circular(_radiusSplash ?? 0)), child: Container( key: _key, alignment: Alignment.center, - color: Colors.white, + color: _decoration == null ? _colorButton ?? Colors.white : null, + decoration: _decoration, + padding: _padding ?? EdgeInsets.zero, child: _buildBody() ) ); @@ -64,18 +116,18 @@ class ButtonBuilder { if (_text != null) { return _isVertical! ? Column( - crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ _buildIcon(), _buildText(), + if (_iconAction != null) _iconAction! ]) : Row( - crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ _buildIcon(), _buildText(), + if (_iconAction != null) _iconAction! ]); } else { return _buildIcon(); diff --git a/core/lib/presentation/views/button/icon_button_web.dart b/core/lib/presentation/views/button/icon_button_web.dart index 56db2a026..df16042b3 100644 --- a/core/lib/presentation/views/button/icon_button_web.dart +++ b/core/lib/presentation/views/button/icon_button_web.dart @@ -12,9 +12,15 @@ Widget buildIconWeb({ double? iconSize, }) { return Material( - type: MaterialType.circle, color: Colors.transparent, - child: IconButton(icon: icon, iconSize: iconSize, padding: iconPadding ?? EdgeInsets.all(8.0), tooltip: tooltip, onPressed: onTap) + shape: CircleBorder(), + child: IconButton( + icon: icon, + iconSize: iconSize, + padding: iconPadding ?? EdgeInsets.all(8.0), + splashRadius: 20, + tooltip: tooltip, + onPressed: onTap) ); } diff --git a/core/lib/presentation/views/image/avatar_builder.dart b/core/lib/presentation/views/image/avatar_builder.dart index c2fd27eba..01dda7973 100644 --- a/core/lib/presentation/views/image/avatar_builder.dart +++ b/core/lib/presentation/views/image/avatar_builder.dart @@ -57,40 +57,26 @@ class AvatarBuilder { _onTapAvatarActionClick!(); }}, child: Container( - key: _key, - width: _size ?? 40, - height: _size ?? 40, - padding: EdgeInsets.zero, - margin: EdgeInsets.zero, - alignment: Alignment.center, - child: Stack( - children: [ - Container( - width: _size ?? 40, - height: _size ?? 40, - padding: EdgeInsets.zero, - margin: EdgeInsets.zero, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular((_size ?? 40) * 0.5), - border: Border.all(color: Colors.transparent), - boxShadow: _boxShadows ?? [], - gradient: _avatarColors?.isNotEmpty == true - ? LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - tileMode: TileMode.decal, - colors: _avatarColors ?? []) + key: _key, + width: _size ?? 40, + height: _size ?? 40, + padding: EdgeInsets.zero, + margin: EdgeInsets.zero, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular((_size ?? 40) * 0.5), + border: Border.all(color: Colors.transparent), + boxShadow: _boxShadows ?? [], + gradient: _avatarColors?.isNotEmpty == true + ? LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, tileMode: TileMode.decal, colors: _avatarColors ?? []) : null, - color: _bgColor ?? AppColor.avatarColor - ), - child: Text( - '${_text ?? ''}', - style: _textStyle ?? TextStyle(fontSize: 20, color: _textColor ?? AppColor.avatarTextColor, fontWeight: FontWeight.w500) - ) - ), - ], - )), + color: _bgColor ?? AppColor.avatarColor + ), + child: Text( + '${_text ?? ''}', + style: _textStyle ?? TextStyle(fontSize: 20, color: _textColor ?? AppColor.avatarTextColor, fontWeight: FontWeight.w500) + ) + ), ); } } \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/search_bar_thread_view_widget.dart b/core/lib/presentation/views/search/search_bar_view.dart similarity index 86% rename from lib/features/thread/presentation/widgets/search_bar_thread_view_widget.dart rename to core/lib/presentation/views/search/search_bar_view.dart index 040b89d0b..753943d88 100644 --- a/lib/features/thread/presentation/widgets/search_bar_thread_view_widget.dart +++ b/core/lib/presentation/views/search/search_bar_view.dart @@ -5,7 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart'; typedef OnOpenSearchViewAction = Function(); -class SearchBarThreadViewWidget { +class SearchBarView { OnOpenSearchViewAction? _onOpenSearchViewAction; final ImagePaths _imagePaths; @@ -14,8 +14,9 @@ class SearchBarThreadViewWidget { EdgeInsets? _padding; EdgeInsets? _margin; String? _hintTextSearch; + double? _maxSizeWidth; - SearchBarThreadViewWidget(this._imagePaths); + SearchBarView(this._imagePaths); void addOnOpenSearchViewAction(OnOpenSearchViewAction onOpenSearchViewAction) { _onOpenSearchViewAction = onOpenSearchViewAction; @@ -37,18 +38,24 @@ class SearchBarThreadViewWidget { _hintTextSearch = text; } + void maxSizeWidth(double? size) { + _maxSizeWidth = size; + } + Widget build() { - return GestureDetector( + return InkWell( onTap: () => _onOpenSearchViewAction?.call(), + borderRadius: BorderRadius.all(Radius.circular(10)), child: Container( key: Key('search_bar_widget'), alignment: Alignment.topCenter, height: _heightSearchBar ?? 40, + width: _maxSizeWidth ?? double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: AppColor.colorBgSearchBar), padding: _padding ?? EdgeInsets.zero, - margin: _margin ?? EdgeInsets.only(left: 16, right: 16, bottom: 10), + margin: _margin ?? EdgeInsets.zero, child: MediaQuery( data: MediaQueryData(padding: EdgeInsets.zero), child: Row( diff --git a/core/lib/presentation/views/text/slogan_builder.dart b/core/lib/presentation/views/text/slogan_builder.dart index a4ae89481..dcaf2fc4b 100644 --- a/core/lib/presentation/views/text/slogan_builder.dart +++ b/core/lib/presentation/views/text/slogan_builder.dart @@ -3,67 +3,78 @@ import 'package:flutter_svg/flutter_svg.dart'; /// A builder which builds a reusable slogan widget. /// This contains the logo and the slogan text. -/// The elements are arranged in a column. +/// The elements are arranged in a column or row. class SloganBuilder { + + final bool arrangedByHorizontal; + Key? _key; String? _text; TextStyle? _textStyle; TextAlign? _textAlign; String? _logoSVG; String? _logo; + double? _sizeLogo; - SloganBuilder key(Key key) { + SloganBuilder({this.arrangedByHorizontal = false}); + + void key(Key key) { _key = key; - return this; } - SloganBuilder setSloganText(String text) { + void setSloganText(String text) { _text = text; - return this; } - SloganBuilder setSloganTextStyle(TextStyle textStyle) { + void setSloganTextStyle(TextStyle textStyle) { _textStyle = textStyle; - return this; } - SloganBuilder setSloganTextAlign(TextAlign textAlign) { + void setSloganTextAlign(TextAlign textAlign) { _textAlign = textAlign; - return this; } - SloganBuilder setLogo(String logo) { + void setLogo(String logo) { _logo = logo; - return this; } - SloganBuilder setLogoSVG(String logoSVG) { + void setLogoSVG(String logoSVG) { _logoSVG = logoSVG; - return this; + } + + void setSizeLogo(double? size) { + _sizeLogo = size; } Widget build() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ + if (!arrangedByHorizontal) { + return Column(children: [ _logoApp(), Padding( padding: EdgeInsets.only(top: 16, left: 16, right: 16), child: Text(_text ?? '', key: _key, style: _textStyle, textAlign: _textAlign), ), - ], - ); + ]); + } else { + return Row(children: [ + _logoApp(), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Text(_text ?? '', key: _key, style: _textStyle, textAlign: _textAlign), + ), + ]); + } } Widget _logoApp() { if (_logoSVG != null) { - return SvgPicture.asset(_logoSVG!, width: 150, height: 150); + return SvgPicture.asset(_logoSVG!, width: _sizeLogo ?? 150, height: _sizeLogo ?? 150); } else if (_logo != null) { return Image( image: AssetImage(_logo!), fit: BoxFit.fill, - width: 150, - height: 150, + width: _sizeLogo ?? 150, + height: _sizeLogo ?? 150, alignment: Alignment.center); } return SizedBox.shrink(); diff --git a/lib/features/login/presentation/login_view.dart b/lib/features/login/presentation/login_view.dart index e5e2e506b..8e50e7909 100644 --- a/lib/features/login/presentation/login_view.dart +++ b/lib/features/login/presentation/login_view.dart @@ -55,11 +55,11 @@ class LoginView extends GetWidget { } Widget _buildSlogan(BuildContext context) { - return SloganBuilder() - .setSloganText(AppLocalizations.of(context).login_text_slogan) - .setSloganTextAlign(TextAlign.center) - .setSloganTextStyle(TextStyle(color: AppColor.primaryColor, fontSize: 16, fontWeight: FontWeight.w700)) - .setLogo(imagePaths.icLogoTMail) + return (SloganBuilder() + ..setSloganText(AppLocalizations.of(context).login_text_slogan) + ..setSloganTextAlign(TextAlign.center) + ..setSloganTextStyle(TextStyle(color: AppColor.primaryColor, fontSize: 16, fontWeight: FontWeight.w700)) + ..setLogo(imagePaths.icLogoTMail)) .build(); } diff --git a/lib/features/mailbox/presentation/mailbox_view.dart b/lib/features/mailbox/presentation/mailbox_view.dart index 478e9ce7a..cb2923c7d 100644 --- a/lib/features/mailbox/presentation/mailbox_view.dart +++ b/lib/features/mailbox/presentation/mailbox_view.dart @@ -10,52 +10,65 @@ import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_fold import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_tile_builder.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/widgets/user_information_widget_builder.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/search_app_bar_widget.dart'; -import 'package:tmail_ui_user/features/thread/presentation/widgets/search_bar_thread_view_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class MailboxView extends GetWidget { - final imagePaths = Get.find(); - final responsiveUtils = Get.find(); + final _imagePaths = Get.find(); + final _responsiveUtils = Get.find(); @override Widget build(BuildContext context) { + return _buildBodyMailbox(context); + } + + Widget _buildBodyMailbox(BuildContext context) { return SafeArea( - top: responsiveUtils.isMobile(context) ? true : false, - bottom: false, - left: false, - right: false, - child: ClipRRect( - borderRadius: BorderRadius.only( - topRight: Radius.circular(responsiveUtils.isMobile(context) ? 14 : 0), - topLeft: Radius.circular(responsiveUtils.isMobile(context) ? 14 : 0)), - child: Drawer( - child: Scaffold( - backgroundColor: Colors.white, - body: Column( - children: [ - _buildHeaderMailbox(context), - Obx(() => controller.isSearchActive() ? _buildInputSearchFormWidget(context) : SizedBox.shrink()), - Expanded(child: Obx(() => Container( - color: controller.isSearchActive() ? Colors.white : AppColor.colorBgMailbox, - child: RefreshIndicator( - color: AppColor.primaryColor, - onRefresh: () async => controller.refreshAllMailbox(), - child: controller.isSearchActive() - ? _buildListMailboxSearched(context, controller.listMailboxSearched) - : _buildListMailbox(context)), - ))), - Obx(() => controller.isSelectionEnabled() - ? Column(children: [ + top: _responsiveUtils.isMobile(context) ? true : false, + bottom: false, + left: false, + right: false, + child: ClipRRect( + borderRadius: BorderRadius.only( + topRight: Radius.circular(_responsiveUtils.isMobile(context) ? 14 : 0), + topLeft: Radius.circular(_responsiveUtils.isMobile(context) ? 14 : 0)), + child: Drawer( + child: Scaffold( + backgroundColor: Colors.white, + body: Column( + children: [ + if (_responsiveUtils.isDesktop(context)) + Padding( + padding: EdgeInsets.only(left: 20, top: 30), + child: (SloganBuilder(arrangedByHorizontal: true) + ..setSloganText(AppLocalizations.of(context).app_name) + ..setSloganTextAlign(TextAlign.center) + ..setSloganTextStyle(TextStyle(color: Colors.black, fontSize: 25, fontWeight: FontWeight.bold)) + ..setSizeLogo(28) + ..setLogo(_imagePaths.icLogoTMail)) + .build()), + _buildHeaderMailbox(context), + Obx(() => controller.isSearchActive() ? _buildInputSearchFormWidget(context) : SizedBox.shrink()), + Expanded(child: Obx(() => Container( + color: controller.isSearchActive() ? Colors.white : AppColor.colorBgMailbox, + child: RefreshIndicator( + color: AppColor.primaryColor, + onRefresh: () async => controller.refreshAllMailbox(), + child: controller.isSearchActive() + ? _buildListMailboxSearched(context, controller.listMailboxSearched) + : _buildListMailbox(context)), + ))), + Obx(() => controller.isSelectionEnabled() + ? Column(children: [ Divider(color: AppColor.lineItemListColor, height: 1, thickness: 0.2), _buildOptionSelectionBottomBar(context) ]) - : SizedBox.shrink()), - ] - ), - ) + : SizedBox.shrink()), + ] + ), + ) + ) ) - ) ); } @@ -66,10 +79,10 @@ class MailboxView extends GetWidget { children: [ Padding( padding: EdgeInsets.only( - top: responsiveUtils.isMobile(context) ? 14 : 30, + top: _responsiveUtils.isMobile(context) ? 14 : 30, bottom: 10), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - !responsiveUtils.isDesktop(context) ? _buildCloseScreenButton(context) : SizedBox(width: 50), + !_responsiveUtils.isDesktop(context) ? _buildCloseScreenButton(context) : SizedBox(width: 50), Obx(() { if (controller.isSearchActive()) { return controller.listMailboxSearched.isNotEmpty @@ -105,7 +118,7 @@ class MailboxView extends GetWidget { child: IconButton( key: Key('mailbox_close_button'), onPressed: () => controller.closeMailboxScreen(context), - icon: SvgPicture.asset(imagePaths.icCloseMailbox, width: 30, height: 30, fit: BoxFit.fill))); + icon: SvgPicture.asset(_imagePaths.icCloseMailbox, width: 30, height: 30, fit: BoxFit.fill))); } Widget _buildAddNewFolderButton(BuildContext context) { @@ -114,7 +127,7 @@ class MailboxView extends GetWidget { child: IconButton( key: Key('create_new_mailbox_button'), onPressed: () => controller.goToCreateNewMailboxView(), - icon: SvgPicture.asset(imagePaths.icAddNewFolder, width: 30, height: 30, fit: BoxFit.fill))); + icon: SvgPicture.asset(_imagePaths.icAddNewFolder, width: 30, height: 30, fit: BoxFit.fill))); } Widget _buildEditMailboxButton(BuildContext context, bool isSelectionEnabled) { @@ -136,7 +149,7 @@ class MailboxView extends GetWidget { return Padding( padding: EdgeInsets.only(left: 16, top: 8.0, right: 16), child: Obx(() => (UserInformationWidgetBuilder( - imagePaths, + _imagePaths, context, controller.mailboxDashBoardController.userProfile.value) ..addOnLogoutAction(() => controller.logoutAction())) @@ -145,8 +158,8 @@ class MailboxView extends GetWidget { Widget _buildSearchBarWidget(BuildContext context) { return Padding( - padding: EdgeInsets.only(top: 12), - child: (SearchBarThreadViewWidget(imagePaths) + padding: EdgeInsets.only(top: 12, bottom: 16, left: 16, right: 16), + child: (SearchBarView(_imagePaths) ..hintTextSearch(AppLocalizations.of(context).hint_search_mailboxes) ..addOnOpenSearchViewAction(() => controller.enableSearch())) .build()); @@ -172,10 +185,10 @@ class MailboxView extends GetWidget { shrinkWrap: true, padding: EdgeInsets.zero, children: [ - Obx(() => (controller.isSelectionEnabled() && responsiveUtils.isMobileDevice(context) && responsiveUtils.isLandscape(context)) + Obx(() => (controller.isSelectionEnabled() && _responsiveUtils.isMobileDevice(context) && _responsiveUtils.isLandscape(context)) ? SizedBox.shrink() : _buildUserInformationWidget(context)), - Obx(() => (controller.isSelectionEnabled() && responsiveUtils.isMobileDevice(context) && responsiveUtils.isLandscape(context)) + Obx(() => (controller.isSelectionEnabled() && _responsiveUtils.isMobileDevice(context) && _responsiveUtils.isLandscape(context)) ? SizedBox.shrink() : _buildLineSpaceUserInformation()), _buildSearchBarWidget(context), @@ -253,7 +266,7 @@ class MailboxView extends GetWidget { isExpanded: mailboxNode.expandMode == ExpandMode.EXPAND, parent: Obx(() => (MailBoxFolderTileBuilder( context, - imagePaths, + _imagePaths, mailboxNode, allSelectMode: controller.currentSelectMode.value) ..addOnOpenMailboxFolderClick((mailboxNode) => controller.openMailbox(context, mailboxNode.item)) @@ -266,7 +279,7 @@ class MailboxView extends GetWidget { padding: EdgeInsets.only(left: 16), child: Obx(() => (MailBoxFolderTileBuilder( context, - imagePaths, + _imagePaths, mailboxNode, allSelectMode: controller.currentSelectMode.value) ..addOnOpenMailboxFolderClick((mailboxNode) => controller.openMailbox(context, mailboxNode.item)) @@ -284,7 +297,7 @@ class MailboxView extends GetWidget { children: [ _buildBackSearchButton(context), Expanded(child: (SearchAppBarWidget( - imagePaths, + _imagePaths, controller.searchQuery.value, controller.searchFocus, controller.searchInputController, @@ -293,7 +306,7 @@ class MailboxView extends GetWidget { ..addPadding(EdgeInsets.zero) ..setMargin(EdgeInsets.only(right: 16)) ..addDecoration(BoxDecoration(borderRadius: BorderRadius.circular(12), color: AppColor.colorBgSearchBar)) - ..addIconClearText(SvgPicture.asset(imagePaths.icClearTextSearch, width: 20, height: 20, fit: BoxFit.fill)) + ..addIconClearText(SvgPicture.asset(_imagePaths.icClearTextSearch, width: 20, height: 20, fit: BoxFit.fill)) ..setHintText(AppLocalizations.of(context).hint_search_mailboxes) ..addOnClearTextSearchAction(() => controller.clearSearchText()) ..addOnTextChangeSearchAction((query) => controller.searchMailbox(query)) @@ -313,7 +326,7 @@ class MailboxView extends GetWidget { child: IconButton( color: AppColor.colorTextButton, icon: SvgPicture.asset( - imagePaths.icBack, + _imagePaths.icBack, width: 20, height: 20, color: AppColor.colorTextButton, @@ -332,7 +345,7 @@ class MailboxView extends GetWidget { primary: false, itemBuilder: (context, index) => Obx(() => (MailboxTileBuilder( - imagePaths, + _imagePaths, listMailbox[index], isSearchActive: controller.isSearchActive(), allSelectMode: controller.currentSelectMode.value, @@ -347,7 +360,7 @@ class MailboxView extends GetWidget { padding: EdgeInsets.only(bottom: 30, top: 5), child: (BottomBarSelectionMailboxWidget( context, - imagePaths, + _imagePaths, controller.listMailboxSelected) ..addOnMailboxActionsClick((actions, listMailboxSelected) => controller.pressMailboxSelectionAction(context, actions, listMailboxSelected))) diff --git a/lib/features/mailbox/presentation/widgets/user_information_widget_builder.dart b/lib/features/mailbox/presentation/widgets/user_information_widget_builder.dart index b04bb6ca7..a9abeca44 100644 --- a/lib/features/mailbox/presentation/widgets/user_information_widget_builder.dart +++ b/lib/features/mailbox/presentation/widgets/user_information_widget_builder.dart @@ -38,9 +38,7 @@ class UserInformationWidgetBuilder { ..textColor(Colors.black) ..addBoxShadows([BoxShadow( color: AppColor.colorShadowBgContentEmail, - spreadRadius: 2, - blurRadius: 2, - offset: Offset(0, 1))]) + spreadRadius: 1, blurRadius: 1, offset: Offset(0, 0.5))]) ..size(GetPlatform.isWeb ? 48 : 56)) .build(), Expanded(child: Column( diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart index 2e0004842..72e375ce4 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart @@ -60,6 +60,7 @@ class MailboxDashBoardController extends ReloadableController { void onReady() { super.onReady(); log('MailboxDashBoardController::onReady()'); + dispatchRoute(AppRoutes.THREAD); _setSessionCurrent(); _getUserProfile(); } @@ -155,13 +156,11 @@ class MailboxDashBoardController extends ReloadableController { void setSelectedMailbox(PresentationMailbox? newPresentationMailbox) { selectedMailbox.value = newPresentationMailbox; - dispatchRoute(AppRoutes.THREAD); } void setNewFirstSelectedMailbox(PresentationMailbox? newPresentationMailbox) { selectedMailbox.firstRebuild = true; selectedMailbox.value = newPresentationMailbox; - dispatchRoute(AppRoutes.THREAD); } void setSelectedEmail(PresentationEmail? newPresentationEmail) { diff --git a/lib/features/thread/presentation/thread_bindings.dart b/lib/features/thread/presentation/thread_bindings.dart index 18cc9d230..ec71a3c0c 100644 --- a/lib/features/thread/presentation/thread_bindings.dart +++ b/lib/features/thread/presentation/thread_bindings.dart @@ -1,5 +1,4 @@ import 'package:core/core.dart'; -import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; @@ -43,15 +42,10 @@ class ThreadBindings extends BaseBindings { @override void bindingsController() { - Get.lazyPut(() => ScrollController()); Get.put(ThreadController( - Get.find(), Get.find(), Get.find(), - Get.find(), Get.find(), - Get.find(), - Get.find(), Get.find(), Get.find(), Get.find(), diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index f7120c549..754bc7d09 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -55,13 +55,13 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class ThreadController extends BaseController { final mailboxDashBoardController = Get.find(); + final _imagePaths = Get.find(); + final _responsiveUtils = Get.find(); + final _appToast = Get.find(); + final GetEmailsInMailboxInteractor _getEmailsInMailboxInteractor; final RefreshAllEmailsInMailboxInteractor _refreshAllEmailsInMailboxInteractor; final MarkAsMultipleEmailReadInteractor _markAsMultipleEmailReadInteractor; - final AppToast _appToast; - final ResponsiveUtils responsiveUtils; - final ImagePaths _imagePaths; - final ScrollController listEmailController; final MoveMultipleEmailToMailboxInteractor _moveMultipleEmailToMailboxInteractor; final MarkAsStarEmailInteractor _markAsStarEmailInteractor; final MarkAsStarMultipleEmailInteractor _markAsStarMultipleEmailInteractor; @@ -81,6 +81,7 @@ class ThreadController extends BaseController { get isLoadingMore => _isLoadingMore; MailboxId? _currentMailboxId; jmap.State? _currentEmailState; + final ScrollController listEmailController = ScrollController(); SearchQuery? get searchQuery => mailboxDashBoardController.searchQuery; @@ -91,13 +92,9 @@ class ThreadController extends BaseController { AccountId? get _accountId => mailboxDashBoardController.accountId.value; ThreadController( - this.responsiveUtils, this._getEmailsInMailboxInteractor, this._refreshAllEmailsInMailboxInteractor, - this.listEmailController, this._markAsMultipleEmailReadInteractor, - this._appToast, - this._imagePaths, this._moveMultipleEmailToMailboxInteractor, this._markAsStarEmailInteractor, this._markAsStarMultipleEmailInteractor, @@ -360,7 +357,7 @@ class ThreadController extends BaseController { void previewEmail(BuildContext context, PresentationEmail presentationEmailSelected) { mailboxDashBoardController.setSelectedEmail(presentationEmailSelected); - if (responsiveUtils.isDesktop(context) || responsiveUtils.isTabletLarge(context)) { + if (_responsiveUtils.isDesktop(context) || _responsiveUtils.isTabletLarge(context)) { mailboxDashBoardController.dispatchRoute(AppRoutes.EMAIL); } else { goToEmail(context); diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index a0b60cbaf..e5eebcc0e 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -12,14 +12,13 @@ import 'package:tmail_ui_user/features/thread/presentation/widgets/bottom_bar_th import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/filter_message_cupertino_action_sheet_action_builder.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/search_app_bar_widget.dart'; -import 'package:tmail_ui_user/features/thread/presentation/widgets/search_bar_thread_view_widget.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/suggestion_box_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class ThreadView extends GetWidget { - final responsiveUtils = Get.find(); - final imagePaths = Get.find(); + final _responsiveUtils = Get.find(); + final _imagePaths = Get.find(); @override Widget build(BuildContext context) { @@ -27,63 +26,72 @@ class ThreadView extends GetWidget { onTap: () => FocusManager.instance.primaryFocus?.unfocus(), child: Scaffold( resizeToAvoidBottomInset: false, - backgroundColor: Colors.white, + backgroundColor: _responsiveUtils.isDesktop(context) ? AppColor.colorBgDesktop : Colors.white, body: SafeArea( - right: responsiveUtils.isMobileDevice(context) && responsiveUtils.isLandscape(context), - left: responsiveUtils.isMobileDevice(context) && responsiveUtils.isLandscape(context), - child: Container( - alignment: Alignment.center, - padding: EdgeInsets.zero, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, + right: _responsiveUtils.isMobileDevice(context) && _responsiveUtils.isLandscape(context), + left: _responsiveUtils.isMobileDevice(context) && _responsiveUtils.isLandscape(context), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildAppBarThread(context), - Obx(() => !controller.isSearchActive() - ? (SearchBarThreadViewWidget(imagePaths) - ..hintTextSearch(AppLocalizations.of(context).hint_search_emails) - ..addOnOpenSearchViewAction(() => controller.enableSearch(context))) - .build() - : SizedBox.shrink()), - Expanded(child: Stack( - children: [ - Container( - alignment: Alignment.center, - padding: EdgeInsets.zero, + if (_responsiveUtils.isDesktop(context)) + Container( color: Colors.white, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildStatusResultSearch(context), - _buildLoadingView(), - Expanded(child: _buildListEmail(context)), - _buildLoadingViewLoadMore() - ] - ) - ), - _buildSuggestionBox(context) - ], - )), + padding: EdgeInsets.only(right: 10, top: 16, bottom: 10), + child: _buildHeader(context)), + Obx(() => controller.isSearchActive() + ? _buildSearchForm(context) + : _responsiveUtils.isDesktop(context) ? SizedBox.shrink() : _buildAppBarNormal(context)), + Obx(() => !controller.isSearchActive() && !_responsiveUtils.isDesktop(context) + ? Padding( + padding: EdgeInsets.only(left: 16, right: 16, bottom: 10), + child: (SearchBarView(_imagePaths) + ..hintTextSearch(AppLocalizations.of(context).hint_search_emails) + ..addOnOpenSearchViewAction(() => controller.enableSearch(context))) + .build()) + : SizedBox.shrink()), + Expanded(child: Container( + color: AppColor.colorBgDesktop, + margin: _responsiveUtils.isDesktop(context) ? EdgeInsets.all(16) : EdgeInsets.zero, + child: Stack( + children: [ + Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: _responsiveUtils.isDesktop(context) ? BorderRadius.circular(20) : null, + border: _responsiveUtils.isDesktop(context) ? Border.all(color: AppColor.colorBorderBodyThread, width: 1) : null, + color: Colors.white), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildStatusResultSearch(context), + _buildLoadingView(), + Expanded(child: _buildListEmail(context)), + _buildLoadingViewLoadMore(), + ] + ) + ), + _buildSuggestionBox(context) + ], + )) + ), Obx(() => controller.isSelectionEnabled() - ? Column(children: [ - Divider(color: AppColor.lineItemListColor, height: 1, thickness: 0.2), - _buildOptionSelectionBottomBar(context) - ]) - : SizedBox.shrink()), + ? Column(children: [ + Divider(color: AppColor.lineItemListColor, height: 1, thickness: 0.2), + _buildOptionSelectionBottomBar(context) + ]) + : SizedBox.shrink()), ] - ) ) ), floatingActionButton: Obx(() => !controller.isSearchActive() && !controller.isSelectionEnabled() ? Container( padding: EdgeInsets.only(bottom: controller.isSelectionEnabled() ? 80 - : responsiveUtils.isMobileDevice(context) ? 0 : 16), + : _responsiveUtils.isMobileDevice(context) ? 0 : 16), child: Align( alignment: Alignment.bottomRight, child: ScrollingFloatingButtonAnimated( - icon: SvgPicture.asset(imagePaths.icCompose, width: 20, height: 20, fit: BoxFit.fill), + icon: SvgPicture.asset(_imagePaths.icCompose, width: 20, height: 20, fit: BoxFit.fill), text: Padding( padding: EdgeInsets.only(right: 10), child: Text( @@ -101,15 +109,82 @@ class ThreadView extends GetWidget { ); } - Widget _buildAppBarThread(BuildContext context) { - return Obx(() { - return Stack( - children: [ - if (!controller.isSearchActive()) _buildAppBarNormal(context), - if (controller.isSearchActive()) _buildSearchForm(context), - ], - ); - }); + Widget _buildHeader(BuildContext context) { + return Row(children: [ + (ButtonBuilder(_imagePaths.icRefresh) + ..key(Key('button_reload_thread')) + ..decoration(BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppColor.colorButtonHeaderThread)) + ..paddingIcon(EdgeInsets.zero) + ..size(16) + ..radiusSplash(10) + ..padding(EdgeInsets.symmetric(horizontal: 8, vertical: 8)) + ..onPressActionClick(() => controller.refreshAllEmail())) + .build(), + SizedBox(width: 16), + (ButtonBuilder(_imagePaths.icSelectAll) + ..key(Key('button_select_all')) + ..decoration(BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppColor.colorButtonHeaderThread)) + ..paddingIcon(EdgeInsets.only(right: 8)) + ..size(16) + ..radiusSplash(10) + ..padding(EdgeInsets.symmetric(horizontal: 12, vertical: 8)) + ..textStyle(TextStyle(fontSize: 12, color: AppColor.colorTextButtonHeaderThread)) + ..onPressActionClick(() => controller.enableSelectionEmail()) + ..text(AppLocalizations.of(context).select_all, isVertical: false)) + .build(), + SizedBox(width: 16), + (ButtonBuilder(_imagePaths.icMarkAllAsRead) + ..key(Key('button_mark_all_as_read')) + ..decoration(BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppColor.colorButtonHeaderThread)) + ..paddingIcon(EdgeInsets.only(right: 8)) + ..size(16) + ..padding(EdgeInsets.symmetric(horizontal: 12, vertical: 8)) + ..radiusSplash(10) + ..textStyle(TextStyle(fontSize: 12, color: AppColor.colorTextButtonHeaderThread)) + ..onPressActionClick(() => {}) + ..text(AppLocalizations.of(context).mark_all_as_read, isVertical: false)) + .build(), + SizedBox(width: 16), + (ButtonBuilder(_imagePaths.icFilterWeb) + ..key(Key('button_filter_messages')) + ..context(context) + ..decoration(BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppColor.colorButtonHeaderThread)) + ..paddingIcon(EdgeInsets.only(right: 8)) + ..size(16) + ..padding(EdgeInsets.symmetric(horizontal: 12, vertical: 8)) + ..radiusSplash(10) + ..textStyle(TextStyle(fontSize: 12, color: AppColor.colorTextButtonHeaderThread)) + ..addIconAction(Padding( + padding: EdgeInsets.only(left: 8), + child: SvgPicture.asset(_imagePaths.icArrowDown, fit: BoxFit.fill))) + ..addOnPressActionWithPositionClick((position) => + controller.openFilterMessagesForTablet( + context, + position, + _popupMenuEmailActionTile(context, controller.filterMessageOption.value))) + ..text(AppLocalizations.of(context).filter_messages, isVertical: false)) + .build(), + SizedBox(width: 16), + Spacer(), + Padding( + padding: EdgeInsets.only(right: 16), + child: (SearchBarView(_imagePaths) + ..hintTextSearch(AppLocalizations.of(context).search_emails) + ..maxSizeWidth(240) + ..addOnOpenSearchViewAction(() => controller.enableSearch(context))) + .build()), + Padding( + padding: EdgeInsets.only(right: 16), + child: (AvatarBuilder() + ..text(controller.mailboxDashBoardController.userProfile.value?.getAvatarText() ?? '') + ..backgroundColor(Colors.white) + ..textColor(Colors.black) + ..addBoxShadows([BoxShadow( + color: AppColor.colorShadowBgContentEmail, + spreadRadius: 1, blurRadius: 1, offset: Offset(0, 0.5))]) + ..size(48)) + .build()), + ]); } Widget _buildOptionSelectionBottomBar(BuildContext context) { @@ -117,8 +192,8 @@ class ThreadView extends GetWidget { padding: EdgeInsets.all(10), child: (BottomBarThreadSelectionWidget( context, - imagePaths, - responsiveUtils, + _imagePaths, + _responsiveUtils, controller.getListEmailSelected()) ..addOnPressEmailSelectionActionClick((actionType, selectionEmail) => controller.pressEmailSelectionAction(context, actionType, selectionEmail))) @@ -127,7 +202,7 @@ class ThreadView extends GetWidget { Widget _buildSearchForm(BuildContext context) { return (SearchAppBarWidget( - imagePaths, + _imagePaths, controller.searchQuery, controller.mailboxDashBoardController.searchFocus, controller.mailboxDashBoardController.searchInputController, @@ -145,8 +220,8 @@ class ThreadView extends GetWidget { Widget _buildAppBarNormal(BuildContext context) { return (AppBarThreadWidgetBuilder( context, - imagePaths, - responsiveUtils, + _imagePaths, + _responsiveUtils, controller.mailboxDashBoardController.selectedMailbox.value, controller.getListEmailSelected(), controller.currentSelectMode.value, @@ -154,7 +229,7 @@ class ThreadView extends GetWidget { ..addOpenListMailboxActionClick(() => controller.openMailboxLeftMenu()) ..addOnEditThreadAction(() => controller.enableSelectionEmail()) ..addOnFilterEmailAction((filterMessageOption, position) => - responsiveUtils.isMobileDevice(context) + _responsiveUtils.isMobileDevice(context) ? controller.openFilterMessagesCupertinoActionSheet( context, _filterMessagesCupertinoActionTile(context, filterMessageOption), @@ -187,17 +262,17 @@ class ThreadView extends GetWidget { Widget _filterMessageWithAttachmentsAction(BuildContext context, FilterMessageOption optionCurrent) { return (FilterMessageCupertinoActionSheetActionBuilder( Key('filter_attachment_action'), - SvgPicture.asset(imagePaths.icAttachment, width: 28, height: 28, fit: BoxFit.fill, color: AppColor.colorTextButton), + SvgPicture.asset(_imagePaths.icAttachment, width: 28, height: 28, fit: BoxFit.fill, color: AppColor.colorTextButton), AppLocalizations.of(context).with_attachments, FilterMessageOption.attachments, optionCurrent: optionCurrent, - iconLeftPadding: responsiveUtils.isMobile(context) + iconLeftPadding: _responsiveUtils.isMobile(context) ? EdgeInsets.only(left: 12, right: 16) : EdgeInsets.only(right: 12), - iconRightPadding: responsiveUtils.isMobile(context) + iconRightPadding: _responsiveUtils.isMobile(context) ? EdgeInsets.only(right: 12) : EdgeInsets.zero, - actionSelected: SvgPicture.asset(imagePaths.icFilterSelected, fit: BoxFit.fill)) + actionSelected: SvgPicture.asset(_imagePaths.icFilterSelected, fit: BoxFit.fill)) ..onActionClick((option) => controller.filterMessagesAction(context, option))) .build(); } @@ -205,17 +280,17 @@ class ThreadView extends GetWidget { Widget _filterMessagesUnreadAction(BuildContext context, FilterMessageOption optionCurrent) { return (FilterMessageCupertinoActionSheetActionBuilder( Key('filter_unread_action'), - SvgPicture.asset(imagePaths.icUnread, width: 28, height: 28, fit: BoxFit.fill), + SvgPicture.asset(_imagePaths.icUnread, width: 28, height: 28, fit: BoxFit.fill), AppLocalizations.of(context).unread, FilterMessageOption.unread, optionCurrent: optionCurrent, - iconLeftPadding: responsiveUtils.isMobile(context) + iconLeftPadding: _responsiveUtils.isMobile(context) ? EdgeInsets.only(left: 12, right: 16) : EdgeInsets.only(right: 12), - iconRightPadding: responsiveUtils.isMobile(context) + iconRightPadding: _responsiveUtils.isMobile(context) ? EdgeInsets.only(right: 12) : EdgeInsets.zero, - actionSelected: SvgPicture.asset(imagePaths.icFilterSelected, fit: BoxFit.fill)) + actionSelected: SvgPicture.asset(_imagePaths.icFilterSelected, fit: BoxFit.fill)) ..onActionClick((option) => controller.filterMessagesAction(context, option))) .build(); } @@ -223,17 +298,17 @@ class ThreadView extends GetWidget { Widget _filterMessageStarredAction(BuildContext context, FilterMessageOption optionCurrent) { return (FilterMessageCupertinoActionSheetActionBuilder( Key('filter_starred_action'), - SvgPicture.asset(imagePaths.icStar, width: 28, height: 28, fit: BoxFit.fill), + SvgPicture.asset(_imagePaths.icStar, width: 28, height: 28, fit: BoxFit.fill), AppLocalizations.of(context).starred, FilterMessageOption.starred, optionCurrent: optionCurrent, - iconLeftPadding: responsiveUtils.isMobile(context) + iconLeftPadding: _responsiveUtils.isMobile(context) ? EdgeInsets.only(left: 12, right: 16) : EdgeInsets.only(right: 12), - iconRightPadding: responsiveUtils.isMobile(context) + iconRightPadding: _responsiveUtils.isMobile(context) ? EdgeInsets.only(right: 12) : EdgeInsets.zero, - actionSelected: SvgPicture.asset(imagePaths.icFilterSelected, fit: BoxFit.fill)) + actionSelected: SvgPicture.asset(_imagePaths.icFilterSelected, fit: BoxFit.fill)) ..onActionClick((option) => controller.filterMessagesAction(context, option))) .build(); } @@ -254,55 +329,46 @@ class ThreadView extends GetWidget { (success) { if (controller.isSearchActive()) { return success is SearchingState - ? Center(child: Padding( - padding: EdgeInsets.only(top: 16, bottom: 16), - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(color: AppColor.primaryColor)))) + ? Padding(padding: EdgeInsets.only(top: 16), child: _loadingWidget) : SizedBox.shrink(); } else { return success is LoadingState - ? Center(child: Padding( - padding: EdgeInsets.only(top: 16, bottom: 16), - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(color: AppColor.primaryColor)))) - : SizedBox.shrink(); + ? Padding(padding: EdgeInsets.only(top: 16), child: _loadingWidget) + : SizedBox.shrink(); } })); } + Widget get _loadingWidget { + return Center(child: Padding( + padding: EdgeInsets.zero, + child: SizedBox( + width: 24, + height: 24, + child: CupertinoActivityIndicator(color: AppColor.colorTextButton)))); + } + Widget _buildLoadingViewLoadMore() { return Obx(() => controller.viewState.value.fold( (failure) => SizedBox.shrink(), (success) { if (controller.isSearchActive()) { return success is SearchingMoreState - ? Center(child: Padding( - padding: EdgeInsets.only(top: 16, bottom: 16), - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(color: AppColor.primaryColor)))) - : SizedBox.shrink(); + ? Padding(padding: EdgeInsets.only(bottom: 16), child: _loadingWidget) + : SizedBox.shrink(); } else { return success is LoadingMoreState - ? Center(child: Padding( - padding: EdgeInsets.only(top: 16, bottom: 16), - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(color: AppColor.primaryColor)))) - : SizedBox.shrink(); + ? Padding(padding: EdgeInsets.only(bottom: 16), child: _loadingWidget) + : SizedBox.shrink(); } })); } Widget _buildListEmail(BuildContext context) { return Container( - margin: EdgeInsets.zero, + margin: _responsiveUtils.isDesktop(context) + ? EdgeInsets.only(top: 16, bottom: 16) + : EdgeInsets.zero, width: double.infinity, height: double.infinity, alignment: Alignment.center, @@ -364,12 +430,10 @@ class ThreadView extends GetWidget { child: ListView.builder( controller: controller.listEmailController, physics: AlwaysScrollableScrollPhysics(), - padding: EdgeInsets.only(top: 16), key: Key('presentation_email_list'), itemCount: listPresentationEmail.length, itemBuilder: (context, index) => Obx(() => (EmailTileBuilder( context, - imagePaths, listPresentationEmail[index], controller.mailboxDashBoardController.selectedMailbox.value?.role, controller.currentSelectMode.value, @@ -382,7 +446,8 @@ class ThreadView extends GetWidget { controller.previewEmail(context, selectedEmail); } }) - ..onSelectEmailAction((selectedEmail) => controller.selectEmail(context, selectedEmail))) + ..onSelectEmailAction((selectedEmail) => controller.selectEmail(context, selectedEmail)) + ..addOnMarkAsStarActionClick((selectedEmail) => controller.markAsStarEmail(selectedEmail))) .build()), ) ); @@ -394,7 +459,7 @@ class ThreadView extends GetWidget { (success) => !(success is LoadingState) && !(success is SearchingState) ? (BackgroundWidgetBuilder(context) ..key(Key('empty_email_background')) - ..image(SvgPicture.asset(imagePaths.icEmptyImageDefault, width: 120, height: 120, fit: BoxFit.fill)) + ..image(SvgPicture.asset(_imagePaths.icEmptyImageDefault, width: 120, height: 120, fit: BoxFit.fill)) ..text(controller.isSearchActive() ? AppLocalizations.of(context).no_emails_matching_your_search : AppLocalizations.of(context).no_emails)) @@ -407,7 +472,7 @@ class ThreadView extends GetWidget { return Obx(() => controller.mailboxDashBoardController.suggestionSearch.isNotEmpty ? (SuggestionBoxWidget( context, - imagePaths, + _imagePaths, controller.mailboxDashBoardController.suggestionSearch) ..addOnSelectedSuggestion((suggestion) => controller.mailboxDashBoardController.searchEmail(context, suggestion))) @@ -423,8 +488,12 @@ class ThreadView extends GetWidget { ? Container( width: double.infinity, margin: EdgeInsets.zero, - color: AppColor.bgStatusResultSearch, padding: EdgeInsets.symmetric(vertical: 12, horizontal: 24), + decoration: BoxDecoration( + borderRadius: _responsiveUtils.isDesktop(context) + ? BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)) + : null, + color: AppColor.bgStatusResultSearch), child: Text( AppLocalizations.of(context).results, style: TextStyle(color: AppColor.baseTextColor, fontSize: 14, fontWeight: FontWeight.w500), diff --git a/lib/features/thread/presentation/widgets/email_tile_builder.dart b/lib/features/thread/presentation/widgets/email_tile_builder.dart index ff8882a69..8e22a4f20 100644 --- a/lib/features/thread/presentation/widgets/email_tile_builder.dart +++ b/lib/features/thread/presentation/widgets/email_tile_builder.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:core/core.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -8,13 +9,17 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/search_status.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnOpenEmailActionClick = void Function(PresentationEmail selectedEmail); typedef OnSelectEmailActionClick = void Function(PresentationEmail selectedEmail); +typedef OnMarkAsStarActionClick = void Function(PresentationEmail selectedEmail); class EmailTileBuilder { - final ImagePaths _imagePaths; + final _responsiveUtils = Get.find(); + final _imagePaths = Get.find(); + final PresentationEmail _presentationEmail; final BuildContext _context; final SelectMode _selectModeAll; @@ -24,10 +29,10 @@ class EmailTileBuilder { OnOpenEmailActionClick? _onOpenEmailActionClick; OnSelectEmailActionClick? _onSelectEmailActionClick; + OnMarkAsStarActionClick? _onMarkAsStarActionClick; EmailTileBuilder( this._context, - this._imagePaths, this._presentationEmail, this._mailboxRole, this._selectModeAll, @@ -43,126 +48,124 @@ class EmailTileBuilder { _onSelectEmailActionClick = onSelectEmailActionClick; } + void addOnMarkAsStarActionClick(OnMarkAsStarActionClick onMarkAsStarActionClick) { + _onMarkAsStarActionClick = onMarkAsStarActionClick; + } + Widget build() { return Theme( - data: ThemeData( - splashColor: Colors.transparent, - highlightColor: Colors.transparent), - child: Container( - key: Key('thread_tile'), - margin: EdgeInsets.zero, + key: Key('thread_tile'), + data: ThemeData(splashColor: Colors.transparent, highlightColor: Colors.transparent), + child: ResponsiveWidget( + responsiveUtils: _responsiveUtils, + mobile: _buildListTile(), + desktop: _buildListTileForDesktop(), + ) + ); + } + + Widget _buildListTile() { + return Container( + color: Colors.white, padding: EdgeInsets.only(bottom: 10, left: 20, right: 6), alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(0), - color: Colors.white), - child: MediaQuery( - data: MediaQueryData(padding: EdgeInsets.zero), - child: Column(children: [ - ListTile( - contentPadding: EdgeInsets.zero, - onTap: () => { - if (_onOpenEmailActionClick != null) { - _onOpenEmailActionClick!(_presentationEmail) - } - }, - onLongPress: () => { - if (_onSelectEmailActionClick != null) { - _onSelectEmailActionClick!(_presentationEmail) - } - }, - leading: Transform( - transform: Matrix4.translationValues(-5.0, 0.0, 0.0), - child: _buildAvatarIcon()), - title: Transform( - transform: Matrix4.translationValues(-8.0, 0.0, 0.0), - child: Row( - children: [ - if (_presentationEmail.isUnReadEmail()) - Padding( + child: Column(children: [ + ListTile( + contentPadding: EdgeInsets.zero, + onTap: () => _onOpenEmailActionClick?.call(_presentationEmail), + onLongPress: () => _onSelectEmailActionClick!.call(_presentationEmail), + leading: Transform( + transform: Matrix4.translationValues(-5.0, 0.0, 0.0), + child: _buildAvatarIcon()), + title: Transform( + transform: Matrix4.translationValues(-8.0, 0.0, 0.0), + child: Row( + children: [ + if (_presentationEmail.isUnReadEmail()) + Padding( padding: EdgeInsets.only(right: 5), child: SvgPicture.asset(_imagePaths.icUnreadStatus, width: 9, height: 9, fit: BoxFit.fill)), - Expanded( - child: _searchStatus == SearchStatus.ACTIVE && _searchQuery != null && _searchQuery!.value.isNotEmpty - ? RichTextBuilder( - _getInformationSender(), - '${_searchQuery!.value}', - TextStyle(fontSize: 15, color: AppColor.colorNameEmail, fontWeight: FontWeight.w600), - TextStyle(fontSize: 15, color: AppColor.colorNameEmail, backgroundColor: AppColor.bgWordSearch, fontWeight: FontWeight.w600)).build() - : Text( - _getInformationSender(), - maxLines: 1, - overflow:TextOverflow.ellipsis, - style: TextStyle(fontSize: 15, color: AppColor.colorNameEmail, fontWeight: FontWeight.w600)) - ), - if (_presentationEmail.hasAttachment == true) - Padding( + Expanded( + child: _searchStatus == SearchStatus.ACTIVE && _searchQuery != null && _searchQuery!.value.isNotEmpty + ? RichTextBuilder( + _getInformationSender(), + '${_searchQuery!.value}', + TextStyle(fontSize: 15, color: AppColor.colorNameEmail, fontWeight: FontWeight.w600), + TextStyle(fontSize: 15, color: AppColor.colorNameEmail, backgroundColor: AppColor.bgWordSearch, fontWeight: FontWeight.w600)).build() + : Text( + _getInformationSender(), + maxLines: 1, + overflow:TextOverflow.ellipsis, + style: TextStyle(fontSize: 15, color: AppColor.colorNameEmail, fontWeight: FontWeight.w600)) + ), + if (_presentationEmail.hasAttachment == true) + Padding( padding: EdgeInsets.only(left: 8), child: (ButtonBuilder(_imagePaths.icAttachment) - ..paddingIcon(EdgeInsets.zero) - ..size(16)) - .build()), - Padding( - padding: EdgeInsets.only(right: 4, left: 8), - child: Text( - '${_presentationEmail.getReceivedAt(Localizations.localeOf(_context).toLanguageTag())}', - maxLines: 1, - overflow:TextOverflow.ellipsis, - style: TextStyle(fontSize: 13, color: AppColor.colorContentEmail))), - (ButtonBuilder(_imagePaths.icChevron) - ..paddingIcon(EdgeInsets.zero) - ..size(16)) + ..paddingIcon(EdgeInsets.zero) + ..size(16)) + .build()), + Padding( + padding: EdgeInsets.only(right: 4, left: 8), + child: Text( + '${_presentationEmail.getReceivedAt(Localizations.localeOf(_context).toLanguageTag())}', + maxLines: 1, + overflow:TextOverflow.ellipsis, + style: TextStyle(fontSize: 13, color: AppColor.colorContentEmail))), + (ButtonBuilder(_imagePaths.icChevron) + ..paddingIcon(EdgeInsets.zero) + ..size(16)) .build(), - ], - ) - ), - subtitle: Transform( - transform: Matrix4.translationValues(-8.0, 8.0, 0.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.zero, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded(child: _searchStatus == SearchStatus.ACTIVE && _searchQuery != null && _searchQuery!.value.isNotEmpty - ? RichTextBuilder( - '${_presentationEmail.getEmailTitle()}', - '${_searchQuery!.value}', - TextStyle(fontSize: 13, color: AppColor.colorNameEmail), - TextStyle(fontSize: 13, backgroundColor: AppColor.bgWordSearch, color: AppColor.colorNameEmail)).build() - : Text( - '${_presentationEmail.getEmailTitle()}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 13, color: AppColor.colorNameEmail)) + ], + ) + ), + subtitle: Transform( + transform: Matrix4.translationValues(-8.0, 8.0, 0.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded(child: _searchStatus == SearchStatus.ACTIVE && _searchQuery != null && _searchQuery!.value.isNotEmpty + ? RichTextBuilder( + '${_presentationEmail.getEmailTitle()}', + '${_searchQuery!.value}', + TextStyle(fontSize: 13, color: AppColor.colorNameEmail), + TextStyle(fontSize: 13, backgroundColor: AppColor.bgWordSearch, color: AppColor.colorNameEmail)).build() + : Text( + '${_presentationEmail.getEmailTitle()}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 13, color: AppColor.colorNameEmail)) + ), + if (_searchStatus == SearchStatus.ACTIVE && _presentationEmail.mailboxName.isNotEmpty) + Container( + margin: EdgeInsets.only(left: 8), + padding: EdgeInsets.only(left: 8, right: 8, top: 3, bottom: 3), + constraints: BoxConstraints(maxWidth: 100), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: AppColor.backgroundCounterMailboxColor), + child: Text( + '${_presentationEmail.mailboxName}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 10, color: AppColor.mailboxTextColor, fontWeight: FontWeight.bold), + ) ), - if (_searchStatus == SearchStatus.ACTIVE && _presentationEmail.mailboxName.isNotEmpty) - Container( - margin: EdgeInsets.only(left: 8), - padding: EdgeInsets.only(left: 8, right: 8, top: 3, bottom: 3), - constraints: BoxConstraints(maxWidth: 100), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: AppColor.backgroundCounterMailboxColor), - child: Text( - '${_presentationEmail.mailboxName}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 10, color: AppColor.mailboxTextColor, fontWeight: FontWeight.bold), - ) - ), - if (_presentationEmail.isFlaggedEmail() ) - (ButtonBuilder(_imagePaths.icStar) - ..paddingIcon(EdgeInsets.zero) - ..size(15)) - .build(), - ], - )), - Padding( + if (_presentationEmail.isFlaggedEmail() ) + (ButtonBuilder(_imagePaths.icStar) + ..paddingIcon(EdgeInsets.zero) + ..size(15)) + .build(), + ], + )), + Padding( padding: EdgeInsets.only(top: 8), child: Row(children: [ Expanded(child: _searchStatus == SearchStatus.ACTIVE && _searchQuery != null && _searchQuery!.value.isNotEmpty @@ -178,27 +181,124 @@ class EmailTileBuilder { style: TextStyle(fontSize: 13, color: AppColor.colorContentEmail)) ), ])) - ], - ) - ), + ], + ) ), - Padding( + ), + Padding( padding: EdgeInsets.only(top: 12, right: 12), child: Divider(color: AppColor.lineItemListColor, height: 1, thickness: 0.2)), - ]), - ) - ) + ]), ); } - Widget _buildAvatarIcon() { + Widget _buildListTileForDesktop() { + return InkWell( + onTap: () => _onOpenEmailActionClick?.call(_presentationEmail), + onLongPress: () => _onSelectEmailActionClick?.call(_presentationEmail), + child: Column(children: [ + Row(children: [ + Container( + padding: EdgeInsets.only(left: 16, right: 16), + alignment: Alignment.center, + child: _presentationEmail.isUnReadEmail() + ? SvgPicture.asset(_imagePaths.icUnreadStatus, width: 9, height: 9, fit: BoxFit.fill) + : SizedBox(width: 9)), + buildIconWeb( + icon: SvgPicture.asset( + _presentationEmail.isFlaggedEmail() ? _imagePaths.icStar : _imagePaths.icUnStar, + width: 20, + height: 20, + fit: BoxFit.fill), + tooltip: _presentationEmail.isFlaggedEmail() ? AppLocalizations.of(_context).starred : AppLocalizations.of(_context).not_starred, + onTap: () => _onMarkAsStarActionClick?.call(_presentationEmail)), + SizedBox(width: 8), + _buildAvatarIcon( + iconSize: 32, + iconSizeSelect: 32, + textStyle: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white), + paddingIconSelect: EdgeInsets.all(5)), + SizedBox(width: 10), + Container(width: 180, child: _isSearchEnabled + ? RichTextBuilder( + _getInformationSender(), + '${_searchQuery!.value}', + TextStyle(fontSize: 15, color: AppColor.colorNameEmail, fontWeight: FontWeight.w600), + TextStyle(fontSize: 15, color: AppColor.colorNameEmail, backgroundColor: AppColor.bgWordSearch, fontWeight: FontWeight.w600)).build() + : Text('${_getInformationSender()}', + maxLines: 1, + overflow: kIsWeb ? null : TextOverflow.ellipsis, + style: TextStyle(fontSize: 15, color: AppColor.colorNameEmail, fontWeight: FontWeight.w600)), + ), + SizedBox(width: 16), + Container(width: 150, child: _isSearchEnabled + ? RichTextBuilder( + '${_presentationEmail.getEmailTitle()}', + '${_searchQuery!.value}', + TextStyle(fontSize: 13, color: AppColor.colorNameEmail, fontWeight: FontWeight.w600), + TextStyle(fontSize: 13, backgroundColor: AppColor.bgWordSearch, color: AppColor.colorNameEmail)).build() + : Text('${_presentationEmail.getEmailTitle()}', + maxLines: 1, + overflow: kIsWeb ? null : TextOverflow.ellipsis, + style: TextStyle(fontSize: 13, color: AppColor.colorNameEmail, fontWeight: FontWeight.w600)) + ), + SizedBox(width: 12), + Expanded(flex: 3, child: _isSearchEnabled + ? RichTextBuilder( + '${_presentationEmail.getPartialContent()}', + '${_searchQuery!.value}', + TextStyle(fontSize: 13, color: AppColor.colorContentEmail, fontWeight: FontWeight.normal), + TextStyle(fontSize: 13, color: AppColor.colorContentEmail, backgroundColor: AppColor.bgWordSearch)).build() + : Text('${_presentationEmail.getPartialContent()}', + maxLines: 1, + overflow: kIsWeb ? null : TextOverflow.ellipsis, + style: TextStyle(fontSize: 13, color: AppColor.colorContentEmail, fontWeight: FontWeight.normal)) + ), + SizedBox(width: 10), + if (_searchStatus == SearchStatus.ACTIVE && _presentationEmail.mailboxName.isNotEmpty) + Container( + margin: EdgeInsets.only(left: 8), + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 3), + constraints: BoxConstraints(maxWidth: 100), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppColor.backgroundCounterMailboxColor), + child: Text('${_presentationEmail.mailboxName}', + maxLines: 1, + overflow: kIsWeb ? null: TextOverflow.ellipsis, + style: TextStyle(fontSize: 10, color: AppColor.mailboxTextColor, fontWeight: FontWeight.bold), + ) + ), + if (_presentationEmail.hasAttachment == true) + Padding(padding: EdgeInsets.only(left: 8), + child: (ButtonBuilder(_imagePaths.icAttachment) + ..paddingIcon(EdgeInsets.zero) + ..size(16)) + .build()), + Padding(padding: EdgeInsets.only(right: 20, left: 8), + child: Text('${_presentationEmail.getReceivedAt(Localizations.localeOf(_context).toLanguageTag())}', + maxLines: 1, + overflow: kIsWeb ? null: TextOverflow.ellipsis, + style: TextStyle(fontSize: 13, color: AppColor.colorContentEmail))), + ]), + Padding( + padding: EdgeInsets.only(top: 10, bottom: 10, left: 70), + child: Divider(color: AppColor.lineItemListColor, height: 1, thickness: 0.2)), + ]), + ); + } + + Widget _buildAvatarIcon({ + double? iconSize, + double? iconSizeSelect, + EdgeInsets? paddingIconSelect, + TextStyle? textStyle + }) { if (_selectModeAll == SelectMode.ACTIVE) { return AnimatedSwitcher( duration: Duration(milliseconds: 600), transitionBuilder: _transitionBuilder, child: (IconBuilder(_presentationEmail.selectMode == SelectMode.ACTIVE ? _imagePaths.icSelected : _imagePaths.icUnSelected) - ..padding(EdgeInsets.all(14)) - ..size(GetPlatform.isWeb ? 48 : 56) + ..padding(paddingIconSelect ?? EdgeInsets.all(14)) + ..size(iconSize ?? (GetPlatform.isWeb ? 48 : 56)) ..addOnTapActionClick(() { if (_selectModeAll == SelectMode.ACTIVE && _onSelectEmailActionClick != null) { _onSelectEmailActionClick!(_presentationEmail); @@ -208,8 +308,9 @@ class EmailTileBuilder { } else { return (AvatarBuilder() ..text('${_presentationEmail.getAvatarText()}') - ..size(GetPlatform.isWeb ? 48 : 56) + ..size(iconSize ?? (GetPlatform.isWeb ? 48 : 56)) ..textColor(Colors.white) + ..addTextStyle(textStyle) ..avatarColor(_presentationEmail.avatarColors)) .build(); } @@ -240,4 +341,6 @@ class EmailTileBuilder { } return '${_presentationEmail.getSenderName()}'; } + + bool get _isSearchEnabled => _searchStatus == SearchStatus.ACTIVE && _searchQuery != null && _searchQuery!.value.isNotEmpty; } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index acade40bd..0fae87c33 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -816,4 +816,43 @@ class AppLocalizations { name: 'message_confirm_send_email'); } + String get app_name { + return Intl.message( + 'Tmail', + name: 'app_name'); + } + + String get search_emails { + return Intl.message( + 'Search emails', + name: 'search_emails'); + } + + String get select_all { + return Intl.message( + 'Select all', + name: 'select_all', + ); + } + + String get mark_all_as_read { + return Intl.message( + 'Mark all as read', + name: 'mark_all_as_read', + ); + } + + String get filter_messages { + return Intl.message( + 'Filter messages', + name: 'filter_messages', + ); + } + + String get not_starred { + return Intl.message( + 'Not starred', + name: 'not_starred' + ); + } } \ No newline at end of file