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