TF-3479 Handle Show all & Hide all popup menu composer

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-02-14 10:10:30 +07:00
committed by Dat H. Pham
parent 1bad81dbf2
commit 16cb8c8a37
12 changed files with 341 additions and 125 deletions
@@ -1,6 +1,7 @@
import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
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';
@@ -73,23 +74,25 @@ class ComposerManager extends GetxController {
bool isExceedsScreenSize({required double screenWidth}) {
final availableWidth = screenWidth
- ComposerUtils.composerExpandMoreButtonMaxWidth
- ComposerUtils.horizontalPadding * 2;
- ComposerUtils.padding * 2;
double totalWidth = composerIdsQueue.fold<double>(
0,
(sum, id) => sum + getComposerView(id).controller.composerWidth,
);
return totalWidth > availableWidth;
bool isExceedsScreenSize = totalWidth > availableWidth;
log('ComposerManager::isExceedsScreenSize:isExceedsScreenSize = $isExceedsScreenSize');
return isExceedsScreenSize;
}
void syncComposerQueue({required double screenWidth}) {
if (composerIdsQueue.isEmpty) return;
log('ComposerManager::syncComposerQueue:screenWidth = $screenWidth');
final availableWidth = screenWidth
- ComposerUtils.composerExpandMoreButtonMaxWidth
- ComposerUtils.horizontalPadding * 2;
- ComposerUtils.padding * 2;
log('ComposerManager::syncComposerQueue:availableWidth = $availableWidth');
double totalWidth = 0;
final composerControllers = <String, ComposerController>{};
@@ -99,7 +102,7 @@ class ComposerManager extends GetxController {
composerControllers[id] = controller;
totalWidth += composerWidth;
}
log('ComposerManager::syncComposerQueue:totalWidth = $totalWidth');
for (var id in composerIdsQueue) {
final controller = composerControllers[id]!;
@@ -113,6 +116,7 @@ class ComposerManager extends GetxController {
if (totalWidth <= availableWidth) break;
}
log('ComposerManager::syncComposerQueue:totalWidth_after = $totalWidth');
for (var id in composerIdsQueue) {
final controller = composerControllers[id]!;
@@ -126,8 +130,7 @@ class ComposerManager extends GetxController {
totalWidth += (ComposerUtils.minimizeWidth - composerWidth);
}
}
composers.refresh();
log('ComposerManager::syncComposerQueue:totalWidth_finally = $totalWidth');
}
bool get hasComposer => composerIdsQueue.isNotEmpty;
@@ -164,6 +167,20 @@ class ComposerManager extends GetxController {
: null;
}
List<String> get hiddenComposerIds {
return composerIdsQueue
.where((id) => getComposerView(id).controller.isHiddenScreen)
.toList();
}
void showComposerIfHidden(String composerId) {
if (!composers.containsKey(composerId)) return;
composerIdsQueue.remove(composerId);
composerIdsQueue.add(composerId);
final composerView = getComposerView(composerId);
composerView.controller.setScreenDisplayMode(ScreenDisplayMode.normal);
}
ComposerView getComposerView(String id) => composers[id]!;
@override
@@ -0,0 +1,16 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:flutter/material.dart';
class HiddenComposerItemStyle {
static const double radius = 12;
static const double elevation = 16;
static const double width = 320;
static const double height = 52;
static const double space = 8;
static const double iconSize = 24;
static const Color backgroundColor = Colors.white;
static const Color iconColor = AppColor.steelGray200;
static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 8);
}
@@ -5,5 +5,6 @@ class ComposerUtils {
static const double minimizeWidth = 400;
static const double minimizeHeight = 52;
static const double composerExpandMoreButtonMaxWidth = 130;
static const double horizontalPadding = 24;
static const double composerExpandMoreButtonMaxHeight = 52;
static const double padding = 16;
}
@@ -1,11 +1,11 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/update_screen_display_mode_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/manager/composer_manager.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/expand_composer_button.dart';
import 'package:tmail_ui_user/features/composer/presentation/utils/composer_utils.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/web/expand_composer_button.dart';
class ComposerOverlayView extends StatefulWidget {
const ComposerOverlayView({super.key});
@@ -16,14 +16,12 @@ class ComposerOverlayView extends StatefulWidget {
class _ComposerOverlayViewState extends State<ComposerOverlayView> {
late final ComposerManager _composerManager;
late final ImagePaths _imagePaths;
late final ResponsiveUtils _responsiveUtils;
@override
void initState() {
super.initState();
_composerManager = Get.find<ComposerManager>();
_imagePaths = Get.find<ImagePaths>();
_responsiveUtils = Get.find<ResponsiveUtils>();
}
@@ -63,16 +61,17 @@ class _ComposerOverlayViewState extends State<ComposerOverlayView> {
return Align(
alignment: AlignmentDirectional.bottomEnd,
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 24, vertical: 16),
padding: const EdgeInsetsDirectional.all(ComposerUtils.padding),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
if (countComposerHidden > 0)
ExpandComposerButton(
imagePaths: _imagePaths,
countComposerHidden: countComposerHidden,
onToggleDisplayComposerAction: () {},
onRemoveHiddenComposerItem: (controller) =>
controller.handleClickCloseComposer(context),
onShowComposerAction: _composerManager.showComposerIfHidden,
),
...visibleComposers,
],
@@ -1,58 +0,0 @@
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,
),
),
]
)
),
),
);
}
}
@@ -0,0 +1,122 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_svg/flutter_svg.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/utils/composer_utils.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/web/hidden_composer_list_view_overlay.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class ExpandComposerButton extends StatefulWidget {
final int countComposerHidden;
final OnRemoveHiddenComposerItem onRemoveHiddenComposerItem;
final OnShowComposerAction onShowComposerAction;
const ExpandComposerButton({
super.key,
required this.countComposerHidden,
required this.onRemoveHiddenComposerItem,
required this.onShowComposerAction,
});
@override
State<ExpandComposerButton> createState() => _ExpandComposerButtonState();
}
class _ExpandComposerButtonState extends State<ExpandComposerButton> {
late final ComposerManager _composerManager;
late final ImagePaths _imagePaths;
late final ResponsiveUtils _responsiveUtils;
bool _visible = false;
@override
void initState() {
super.initState();
_composerManager = Get.find<ComposerManager>();
_imagePaths = Get.find<ImagePaths>();
_responsiveUtils = Get.find<ResponsiveUtils>();
}
@override
Widget build(BuildContext context) {
final expandButton = PointerInterceptor(
child: Card(
elevation: 16,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12))
),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => setState(() => _visible = !_visible),
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Container(
color: Colors.white,
width: ComposerUtils.composerExpandMoreButtonMaxWidth,
height: ComposerUtils.composerExpandMoreButtonMaxHeight,
padding: const EdgeInsetsDirectional.symmetric(horizontal: 8),
child: Row(
children: [
SvgPicture.asset(
_visible
? _imagePaths.icDoubleArrowDown
: _imagePaths.icDoubleArrowUp,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_visible
? AppLocalizations.of(context).hideAll
: '+${widget.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,
),
),
]
)
),
),
),
);
return PortalTarget(
visible: _visible,
portalFollower: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => setState(() => _visible = false),
),
child: PortalTarget(
anchor: const Aligned(
follower: Alignment.bottomLeft,
target: Alignment.topLeft,
offset: Offset(0.0, 0.0),
),
portalFollower: HiddenComposerListViewOverlay(
composerManager: _composerManager,
imagePaths: _imagePaths,
responsiveUtils: _responsiveUtils,
onRemoveHiddenComposerItem: (controller) {
widget.onRemoveHiddenComposerItem(controller);
setState(() => _visible = false);
},
onShowComposerAction: (composerId) {
widget.onShowComposerAction(composerId);
setState(() => _visible = false);
}
),
visible: _visible,
child: expandButton,
)
);
}
}
@@ -0,0 +1,60 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/views/button/tmail_button_widget.dart';
import 'package:flutter/material.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:tmail_ui_user/features/composer/presentation/styles/web/hidden_composer_item_style.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/title_composer_widget.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class HiddenComposerItem extends StatelessWidget {
final ImagePaths imagePaths;
final String composerSubject;
final VoidCallback onCloseViewAction;
final VoidCallback onShowComposer;
const HiddenComposerItem({
super.key,
required this.imagePaths,
required this.composerSubject,
required this.onCloseViewAction,
required this.onShowComposer,
});
@override
Widget build(BuildContext context) {
return PointerInterceptor(
child: Card(
elevation: HiddenComposerItemStyle.elevation,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(HiddenComposerItemStyle.radius))
),
clipBehavior: Clip.antiAlias,
child: Container(
color: HiddenComposerItemStyle.backgroundColor,
width: HiddenComposerItemStyle.width,
height: HiddenComposerItemStyle.height,
padding: HiddenComposerItemStyle.padding,
child: InkWell(
onTap: onShowComposer,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
TMailButtonWidget.fromIcon(
icon: imagePaths.icCancel,
backgroundColor: Colors.transparent,
tooltipMessage: AppLocalizations.of(context).saveAndClose,
iconSize: HiddenComposerItemStyle.iconSize,
iconColor: HiddenComposerItemStyle.iconColor,
onTapActionCallback: onCloseViewAction
),
const SizedBox(width: HiddenComposerItemStyle.space),
Expanded(child: TitleComposerWidget(emailSubject: composerSubject)),
]
),
)
),
),
);
}
}
@@ -0,0 +1,58 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/manager/composer_manager.dart';
import 'package:tmail_ui_user/features/composer/presentation/styles/web/hidden_composer_item_style.dart';
import 'package:tmail_ui_user/features/composer/presentation/utils/composer_utils.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/web/hidden_composer_item.dart';
typedef OnRemoveHiddenComposerItem = Function(ComposerController controller);
typedef OnShowComposerAction = Function(String composerId);
class HiddenComposerListViewOverlay extends StatelessWidget {
final ComposerManager composerManager;
final ImagePaths imagePaths;
final ResponsiveUtils responsiveUtils;
final OnRemoveHiddenComposerItem onRemoveHiddenComposerItem;
final OnShowComposerAction onShowComposerAction;
const HiddenComposerListViewOverlay({
super.key,
required this.composerManager,
required this.imagePaths,
required this.responsiveUtils,
required this.onRemoveHiddenComposerItem,
required this.onShowComposerAction,
});
@override
Widget build(BuildContext context) {
final hiddenComposerIds = composerManager.hiddenComposerIds;
final maxHeight = responsiveUtils.getSizeScreenHeight(context)
- ComposerUtils.composerExpandMoreButtonMaxHeight
- ComposerUtils.padding;
return Container(
constraints: BoxConstraints(maxHeight: maxHeight),
width: HiddenComposerItemStyle.width,
child: ListView.builder(
itemCount: hiddenComposerIds.length,
shrinkWrap: true,
itemBuilder: (context, index) {
final composerId = hiddenComposerIds[index];
final controller = composerManager.getComposerView(composerId).controller;
final subjectEmail = controller.subjectEmail.value ?? '';
return HiddenComposerItem(
imagePaths: imagePaths,
composerSubject: subjectEmail,
onCloseViewAction: () => onRemoveHiddenComposerItem(controller),
onShowComposer: () => onShowComposerAction(composerId),
);
}),
);
}
}
@@ -37,52 +37,40 @@ class MinimizeComposerWidget extends StatelessWidget {
width: MinimizeComposerWidgetStyle.width,
height: MinimizeComposerWidgetStyle.height,
padding: MinimizeComposerWidgetStyle.padding,
child: Stack(
child: Row(
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: MinimizeComposerWidgetStyle.width / 2),
child: TitleComposerWidget(emailSubject: emailSubject),
),
TMailButtonWidget.fromIcon(
icon: _imagePaths.icCancel,
backgroundColor: Colors.transparent,
tooltipMessage: AppLocalizations.of(context).saveAndClose,
iconSize: MinimizeComposerWidgetStyle.iconSize,
iconColor: MinimizeComposerWidgetStyle.iconColor,
padding: MinimizeComposerWidgetStyle.iconPadding,
onTapActionCallback: onCloseViewAction
),
Align(
alignment: AlignmentDirectional.centerStart,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
TMailButtonWidget.fromIcon(
icon: _imagePaths.icCancel,
backgroundColor: Colors.transparent,
tooltipMessage: AppLocalizations.of(context).saveAndClose,
iconSize: MinimizeComposerWidgetStyle.iconSize,
iconColor: MinimizeComposerWidgetStyle.iconColor,
padding: MinimizeComposerWidgetStyle.iconPadding,
onTapActionCallback: onCloseViewAction
),
const SizedBox(width: MinimizeComposerWidgetStyle.space),
TMailButtonWidget.fromIcon(
icon: _imagePaths.icFullScreen,
backgroundColor: Colors.transparent,
tooltipMessage: AppLocalizations.of(context).fullscreen,
iconSize: MinimizeComposerWidgetStyle.iconSize,
iconColor: MinimizeComposerWidgetStyle.iconColor,
padding: MinimizeComposerWidgetStyle.iconPadding,
onTapActionCallback: () => onChangeDisplayModeAction(ScreenDisplayMode.fullScreen)
),
const SizedBox(width: MinimizeComposerWidgetStyle.space),
TMailButtonWidget.fromIcon(
icon: _imagePaths.icChevronUp,
backgroundColor: Colors.transparent,
tooltipMessage: AppLocalizations.of(context).show,
iconSize: MinimizeComposerWidgetStyle.iconSize,
iconColor: MinimizeComposerWidgetStyle.iconColor,
padding: MinimizeComposerWidgetStyle.iconPadding,
onTapActionCallback: () => onChangeDisplayModeAction(ScreenDisplayMode.normal)
),
]
),
)
],
const SizedBox(width: MinimizeComposerWidgetStyle.space),
TMailButtonWidget.fromIcon(
icon: _imagePaths.icFullScreen,
backgroundColor: Colors.transparent,
tooltipMessage: AppLocalizations.of(context).fullscreen,
iconSize: MinimizeComposerWidgetStyle.iconSize,
iconColor: MinimizeComposerWidgetStyle.iconColor,
padding: MinimizeComposerWidgetStyle.iconPadding,
onTapActionCallback: () => onChangeDisplayModeAction(ScreenDisplayMode.fullScreen)
),
const SizedBox(width: MinimizeComposerWidgetStyle.space),
TMailButtonWidget.fromIcon(
icon: _imagePaths.icChevronUp,
backgroundColor: Colors.transparent,
tooltipMessage: AppLocalizations.of(context).show,
iconSize: MinimizeComposerWidgetStyle.iconSize,
iconColor: MinimizeComposerWidgetStyle.iconColor,
padding: MinimizeComposerWidgetStyle.iconPadding,
onTapActionCallback: () => onChangeDisplayModeAction(ScreenDisplayMode.normal)
),
const SizedBox(width: MinimizeComposerWidgetStyle.space),
Expanded(child: TitleComposerWidget(emailSubject: emailSubject)),
]
)
),
);