diff --git a/assets/images/ic_double_arrow_down.svg b/assets/images/ic_double_arrow_down.svg new file mode 100644 index 000000000..b3fec0557 --- /dev/null +++ b/assets/images/ic_double_arrow_down.svg @@ -0,0 +1,8 @@ + + + + diff --git a/assets/images/ic_double_arrow_up.svg b/assets/images/ic_double_arrow_up.svg new file mode 100644 index 000000000..139557c8d --- /dev/null +++ b/assets/images/ic_double_arrow_up.svg @@ -0,0 +1,8 @@ + + + + diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 84c3c56da..2a368bf7f 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -227,6 +227,8 @@ class ImagePaths { String get icSubaddressingDisallow => _getImagePath('ic_subaddressing_disallow.svg'); String get icMarkAsImportant => _getImagePath('ic_mark_as_important.svg'); String get icCloseDialog => _getImagePath('ic_close_dialog.svg'); + String get icDoubleArrowUp => _getImagePath('ic_double_arrow_up.svg'); + String get icDoubleArrowDown => _getImagePath('ic_double_arrow_down.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index cc3e3662d..fcebc5a6d 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -262,7 +262,6 @@ class ComposerController extends BaseController if (PlatformInfo.isWeb) { responsiveContainerKey = GlobalKey(); richTextWebController = getBinding(tag: composerId); - responsiveContainerKey = GlobalKey(); menuMoreOptionController = CustomPopupMenuController(); } else { richTextMobileTabletController = getBinding(tag: composerId); @@ -310,7 +309,6 @@ class ComposerController extends BaseController richTextMobileTabletController = null; } _identityContentOnOpenPolicy = SignatureStatus.editedAvailable; - responsiveContainerKey = null; super.onClose(); } diff --git a/lib/features/composer/presentation/composer_overlay_view.dart b/lib/features/composer/presentation/composer_overlay_view.dart index 8de5fe7a6..ea070bf17 100644 --- a/lib/features/composer/presentation/composer_overlay_view.dart +++ b/lib/features/composer/presentation/composer_overlay_view.dart @@ -1,8 +1,14 @@ +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:core/presentation/resources/image_paths.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; import 'package:tmail_ui_user/features/composer/presentation/manager/composer_manager.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; +import 'package:tmail_ui_user/features/composer/presentation/utils/composer_utils.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/expand_composer_button.dart'; class ComposerOverlayView extends StatefulWidget { const ComposerOverlayView({super.key}); @@ -14,13 +20,94 @@ class ComposerOverlayView extends StatefulWidget { class _ComposerOverlayViewState extends State { final ComposerManager _composerManager = Get.find(); + final ImagePaths _imagePaths = Get.find(); @override Widget build(BuildContext context) { return Obx(() { - if (!_composerManager.hasComposer) return const SizedBox.shrink(); + final composers = _composerManager.composers; + if (composers.isEmpty) return const SizedBox.shrink(); - return ComposerView(composerId: _composerManager.singleComposerId); + if (composers.length == 1) { + return _composerManager.getComposerView(_composerManager.singleComposerId); + } + + return Align( + alignment: AlignmentDirectional.bottomEnd, + child: Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 24, + vertical: 16, + ), + child: LayoutBuilder( + builder: (_, constraints) { + final availableWidth = constraints.maxWidth - ComposerUtils.composerExpandMoreButtonMaxWidth; + + final composerIds = _composerManager.composerIdsQueue.toList(); + final composerViews = composerIds + .map((id) => _composerManager.getComposerView(id)) + .toList(); + + final countNormalComposer = composerViews.where( + (view) => view.controller.screenDisplayMode.value == ScreenDisplayMode.normal, + ).length; + + final maxNormalComposer = ComposerUtils.calculateNormalWidgets(availableWidth); + final countDisplayNormalComposer = min( + countNormalComposer, + maxNormalComposer, + ); + + final countDisplayComposer = countDisplayNormalComposer > 0 + ? ComposerUtils.calculateNormalAndMinimizeWidgets(availableWidth, countDisplayNormalComposer) + : ComposerUtils.calculateMinimizeWidgets(availableWidth); + + if (countDisplayComposer < 1) return const SizedBox.shrink(); + + Iterable composerIdsDisplayed = []; + if (composerIds.length > countDisplayComposer) { + composerIdsDisplayed = composerIds + .reversed + .take(countDisplayComposer) + .toList() + .reversed; + } else { + composerIdsDisplayed = composerIds; + } + + final countMinimizeComposer = + composerIdsDisplayed.length - countDisplayNormalComposer; + + int countComposerHidden = composerIds.length - countDisplayComposer; + + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if (countComposerHidden > 0) + ExpandComposerButton( + imagePaths: _imagePaths, + countComposerHidden: countComposerHidden, + onToggleDisplayComposerAction: () {}, + ), + ...composerIdsDisplayed.mapIndexed((index, id) { + final composerView = _composerManager.getComposerView(id); + final isMinimized = countDisplayNormalComposer < 1 || + (countMinimizeComposer > 0 && index < countMinimizeComposer); + + composerView.controller.screenDisplayMode.value = isMinimized + ? ScreenDisplayMode.minimize + : ScreenDisplayMode.normal; + + return composerView; + }).toList(), + ], + ); + } + ), + ), + ); }); } } diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index b492fc962..e016b3173 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -639,6 +639,7 @@ class ComposerView extends GetWidget { ]), ); }, + responsiveUtils: controller.responsiveUtils, displayMode: controller.screenDisplayMode.value, emailSubject: controller.subjectEmail.value ?? '', onCloseViewAction: () => controller.handleClickCloseComposer(context), diff --git a/lib/features/composer/presentation/manager/composer_manager.dart b/lib/features/composer/presentation/manager/composer_manager.dart index bb169f368..f0841692c 100644 --- a/lib/features/composer/presentation/manager/composer_manager.dart +++ b/lib/features/composer/presentation/manager/composer_manager.dart @@ -1,11 +1,16 @@ +import 'dart:collection'; + import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_bindings.dart'; -import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; +import 'package:tmail_ui_user/features/composer/presentation/utils/composer_utils.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; class ComposerManager extends GetxController { - final RxMap composers = {}.obs; + final RxMap composers = {}.obs; + final Queue composerIdsQueue = Queue(); void addComposer(ComposerArguments composerArguments) { final String id = DateTime.now().millisecondsSinceEpoch.toString(); @@ -14,22 +19,36 @@ class ComposerManager extends GetxController { composerId: id, composerArguments: composerArguments, ).dependencies(); - composers[id] = Get.find(tag: id); + composerIdsQueue.add(id); + composers[id] = ComposerView( + key: Key(id), + composerId: id, + ); log('ComposerManager::addComposer:Success'); } void removeComposer(String id) { log('ComposerManager::removeComposer:ID = $id'); if (composers.containsKey(id)) { - ComposerBindings(composerId: id).dispose(); + composerIdsQueue.remove(id); composers.remove(id); + ComposerBindings(composerId: id).dispose(); log('ComposerManager::removeComposer:Success'); } } - bool get hasComposer => composers.isNotEmpty; + ({ + String? previousTargetId, + String? targetId, + String? nextTargetId, + }) findSurroundingComposerIds(String targetId) => + ComposerUtils.findSurroundingElements(composerIdsQueue, targetId); - List get composerIds => composers.keys.toList(); + bool get hasComposer => composerIdsQueue.isNotEmpty; - String get singleComposerId => composerIds.first; + List get composerIds => composerIdsQueue.toList(); + + String get singleComposerId => composerIdsQueue.first; + + ComposerView getComposerView(String id) => composers[id]!; } \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/minimize_composer_widget_style.dart b/lib/features/composer/presentation/styles/minimize_composer_widget_style.dart index a41d7e5d5..dd6a94c8c 100644 --- a/lib/features/composer/presentation/styles/minimize_composer_widget_style.dart +++ b/lib/features/composer/presentation/styles/minimize_composer_widget_style.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/composer/presentation/utils/composer_utils.dart'; class MinimizeComposerWidgetStyle { static const double radius = 24; static const double elevation = 16; - static const double width = 500; - static const double height = 50; + static const double width = ComposerUtils.minimizeWidth; + static const double height = ComposerUtils.minimizeHeight; static const double space = 8; static const double iconSize = 20; diff --git a/lib/features/composer/presentation/styles/web/desktop_responsive_container_view_style.dart b/lib/features/composer/presentation/styles/web/desktop_responsive_container_view_style.dart index c0bec80ee..6b3f87140 100644 --- a/lib/features/composer/presentation/styles/web/desktop_responsive_container_view_style.dart +++ b/lib/features/composer/presentation/styles/web/desktop_responsive_container_view_style.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/composer/presentation/utils/composer_utils.dart'; class DesktopResponsiveContainerViewStyle { static const double radius = 28; static const double margin = 20; static const double elevation = 16; + static const double normalScreenMaxWidth = ComposerUtils.normalWidth; + static const double normalScreenMaxHeight = ComposerUtils.normalHeight; static const Color backgroundColor = Colors.white; static const Color outSideBackgroundColor = Colors.black38; diff --git a/lib/features/composer/presentation/utils/composer_utils.dart b/lib/features/composer/presentation/utils/composer_utils.dart new file mode 100644 index 000000000..a5c26bb50 --- /dev/null +++ b/lib/features/composer/presentation/utils/composer_utils.dart @@ -0,0 +1,38 @@ + +import 'dart:collection'; + +class ComposerUtils { + static const double normalWidth = 725; + static const double normalHeight = 525; + static const double minimizeWidth = 400; + static const double minimizeHeight = 52; + static const double composerExpandMoreButtonMaxWidth = 130; + + static int calculateNormalAndMinimizeWidgets(double width, int countNormalWidgets) { + double remainingWidth = width - (countNormalWidgets * normalWidth); + if (remainingWidth < 0) return countNormalWidgets; + int countWidgets = countNormalWidgets + (remainingWidth / minimizeWidth).floor(); + return countWidgets; + } + + static int calculateNormalWidgets(double width) => (width / normalWidth).floor(); + + static int calculateMinimizeWidgets(double width) => (width / minimizeWidth).floor(); + + static ({ + String? previousTargetId, + String? targetId, + String? nextTargetId, + }) findSurroundingElements(Queue queue, String targetId) { + var ids = queue.toList(), + index = ids.indexOf(targetId); + + return index == -1 + ? (previousTargetId: null, targetId: null, nextTargetId: null) + : ( + previousTargetId: index > 0 ? ids[index - 1] : null, + targetId: ids[index], + nextTargetId: index < ids.length - 1 ? ids[index + 1] : null + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/view/web/desktop_responsive_container_view.dart b/lib/features/composer/presentation/view/web/desktop_responsive_container_view.dart index 4b32e2d5f..c3ecf9806 100644 --- a/lib/features/composer/presentation/view/web/desktop_responsive_container_view.dart +++ b/lib/features/composer/presentation/view/web/desktop_responsive_container_view.dart @@ -2,23 +2,26 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/manager/composer_manager.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/web/desktop_responsive_container_view_style.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/minimize_composer_widget.dart'; class DesktopResponsiveContainerView extends StatelessWidget { + final ResponsiveUtils responsiveUtils; final ScreenDisplayMode displayMode; final String emailSubject; final VoidCallback onCloseViewAction; final OnChangeDisplayModeAction onChangeDisplayModeAction; final Widget Function(BuildContext context, BoxConstraints constraints) childBuilder; - final _responsiveUtils = Get.find(); + final ComposerManager _composerManager = Get.find(); DesktopResponsiveContainerView({ super.key, required this.childBuilder, + required this.responsiveUtils, required this.displayMode, required this.emailSubject, required this.onCloseViewAction, @@ -28,45 +31,74 @@ class DesktopResponsiveContainerView extends StatelessWidget { @override Widget build(BuildContext context) { if (displayMode == ScreenDisplayMode.minimize) { - return PositionedDirectional( - end: DesktopResponsiveContainerViewStyle.margin, - bottom: DesktopResponsiveContainerViewStyle.margin, - child: PointerInterceptor( + if (_composerManager.composerIds.length == 1) { + return PositionedDirectional( + end: DesktopResponsiveContainerViewStyle.margin, + bottom: DesktopResponsiveContainerViewStyle.margin, + child: PointerInterceptor( + child: MinimizeComposerWidget( + emailSubject: emailSubject, + onCloseViewAction: onCloseViewAction, + onChangeDisplayModeAction: onChangeDisplayModeAction, + ), + ) + ); + } else { + return PointerInterceptor( child: MinimizeComposerWidget( emailSubject: emailSubject, onCloseViewAction: onCloseViewAction, onChangeDisplayModeAction: onChangeDisplayModeAction, ), - ) - ); - } else if (displayMode == ScreenDisplayMode.normal) { - final maxWidth = _responsiveUtils.getSizeScreenWidth(context) * 0.5; - final maxHeight = _responsiveUtils.getSizeScreenHeight(context) * 0.75; + ); + } - return PositionedDirectional( - end: DesktopResponsiveContainerViewStyle.margin, - bottom: DesktopResponsiveContainerViewStyle.margin, - child: Card( + } else if (displayMode == ScreenDisplayMode.normal) { + if (_composerManager.composerIds.length == 1) { + return PositionedDirectional( + end: DesktopResponsiveContainerViewStyle.margin, + bottom: DesktopResponsiveContainerViewStyle.margin, + child: Card( + elevation: DesktopResponsiveContainerViewStyle.elevation, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(DesktopResponsiveContainerViewStyle.radius)) + ), + clipBehavior: Clip.antiAlias, + child: Container( + color: DesktopResponsiveContainerViewStyle.backgroundColor, + width: DesktopResponsiveContainerViewStyle.normalScreenMaxWidth, + height: DesktopResponsiveContainerViewStyle.normalScreenMaxHeight, + child: LayoutBuilder(builder: (context, constraints) => + PointerInterceptor( + child: childBuilder.call(context, constraints) + ) + ) + ), + ) + ); + } else { + return Card( elevation: DesktopResponsiveContainerViewStyle.elevation, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(DesktopResponsiveContainerViewStyle.radius)) + borderRadius: BorderRadius.all(Radius.circular(DesktopResponsiveContainerViewStyle.radius)) ), clipBehavior: Clip.antiAlias, child: Container( - color: DesktopResponsiveContainerViewStyle.backgroundColor, - width: maxWidth, - height: maxHeight, - child: LayoutBuilder(builder: (context, constraints) => - PointerInterceptor( - child: childBuilder.call(context, constraints) + color: DesktopResponsiveContainerViewStyle.backgroundColor, + width: DesktopResponsiveContainerViewStyle.normalScreenMaxWidth, + height: DesktopResponsiveContainerViewStyle.normalScreenMaxHeight, + child: LayoutBuilder(builder: (context, constraints) => + PointerInterceptor( + child: childBuilder.call(context, constraints) + ) ) - ) ), - ) - ); + ); + } + } else if (displayMode == ScreenDisplayMode.fullScreen) { - final maxWidth = _responsiveUtils.getSizeScreenWidth(context) * 0.85; - final maxHeight = _responsiveUtils.getSizeScreenHeight(context) * 0.9; + final maxWidth = responsiveUtils.getSizeScreenWidth(context) * 0.85; + final maxHeight = responsiveUtils.getSizeScreenHeight(context) * 0.9; return Scaffold( backgroundColor: DesktopResponsiveContainerViewStyle.outSideBackgroundColor, diff --git a/lib/features/composer/presentation/widgets/expand_composer_button.dart b/lib/features/composer/presentation/widgets/expand_composer_button.dart new file mode 100644 index 000000000..68975f408 --- /dev/null +++ b/lib/features/composer/presentation/widgets/expand_composer_button.dart @@ -0,0 +1,58 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tmail_ui_user/features/composer/presentation/utils/composer_utils.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class ExpandComposerButton extends StatelessWidget { + + final ImagePaths imagePaths; + final int countComposerHidden; + final VoidCallback onToggleDisplayComposerAction; + + const ExpandComposerButton({ + super.key, + required this.imagePaths, + required this.countComposerHidden, + required this.onToggleDisplayComposerAction, + }); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 16, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)) + ), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () => {}, + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Container( + color: Colors.white, + width: ComposerUtils.composerExpandMoreButtonMaxWidth, + height: ComposerUtils.minimizeHeight, + padding: const EdgeInsetsDirectional.symmetric(horizontal: 8), + child: Row( + children: [ + SvgPicture.asset(imagePaths.icDoubleArrowUp), + const SizedBox(width: 8), + Expanded( + child: Text( + '+$countComposerHidden ${AppLocalizations.of(context).more.toLowerCase()}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ] + ) + ), + ), + ); + } +} \ No newline at end of file