TF-3479 Allow open multiple composer on screen

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-02-13 16:59:02 +07:00
committed by Dat H. Pham
parent 78d40f7be5
commit c603b5e40b
12 changed files with 295 additions and 40 deletions
@@ -262,7 +262,6 @@ class ComposerController extends BaseController
if (PlatformInfo.isWeb) {
responsiveContainerKey = GlobalKey();
richTextWebController = getBinding<RichTextWebController>(tag: composerId);
responsiveContainerKey = GlobalKey();
menuMoreOptionController = CustomPopupMenuController();
} else {
richTextMobileTabletController = getBinding<RichTextMobileTabletController>(tag: composerId);
@@ -310,7 +309,6 @@ class ComposerController extends BaseController
richTextMobileTabletController = null;
}
_identityContentOnOpenPolicy = SignatureStatus.editedAvailable;
responsiveContainerKey = null;
super.onClose();
}
@@ -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<ComposerOverlayView> {
final ComposerManager _composerManager = Get.find<ComposerManager>();
final ImagePaths _imagePaths = Get.find<ImagePaths>();
@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<String> 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(),
],
);
}
),
),
);
});
}
}
@@ -639,6 +639,7 @@ class ComposerView extends GetWidget<ComposerController> {
]),
);
},
responsiveUtils: controller.responsiveUtils,
displayMode: controller.screenDisplayMode.value,
emailSubject: controller.subjectEmail.value ?? '',
onCloseViewAction: () => controller.handleClickCloseComposer(context),
@@ -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<String, ComposerController> composers = <String, ComposerController>{}.obs;
final RxMap<String, ComposerView> composers = <String, ComposerView>{}.obs;
final Queue<String> composerIdsQueue = Queue<String>();
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<ComposerController>(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<String> get composerIds => composers.keys.toList();
bool get hasComposer => composerIdsQueue.isNotEmpty;
String get singleComposerId => composerIds.first;
List<String> get composerIds => composerIdsQueue.toList();
String get singleComposerId => composerIdsQueue.first;
ComposerView getComposerView(String id) => composers[id]!;
}
@@ -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;
@@ -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;
@@ -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<String> 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
);
}
}
@@ -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<ResponsiveUtils>();
final ComposerManager _composerManager = Get.find<ComposerManager>();
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,
@@ -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,
),
),
]
)
),
),
);
}
}