TF-2948 Apply new identity view for mobile

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-07-30 19:22:03 +07:00
committed by Dat H. Pham
parent 23ecdfbf11
commit 5e3f4458c5
2 changed files with 143 additions and 94 deletions
@@ -5,11 +5,12 @@ import 'package:core/presentation/constants/constants_ui.dart';
import 'package:core/presentation/views/loading/cupertino_loading_widget.dart'; import 'package:core/presentation/views/loading/cupertino_loading_widget.dart';
import 'package:core/utils/app_logger.dart'; import 'package:core/utils/app_logger.dart';
import 'package:core/utils/html/html_interaction.dart'; import 'package:core/utils/html/html_interaction.dart';
import 'package:core/utils/html/html_template.dart';
import 'package:core/utils/html/html_utils.dart'; import 'package:core/utils/html/html_utils.dart';
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart' as launcher; import 'package:url_launcher/url_launcher.dart' as launcher;
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@@ -30,10 +31,12 @@ class HtmlContentViewer extends StatefulWidget {
final double? contentPadding; final double? contentPadding;
final bool useDefaultFont; final bool useDefaultFont;
final double? maxHtmlContentHeight; final double? maxHtmlContentHeight;
final double minHtmlContentHeight; final double htmlContentMinHeight;
final double offsetHtmlContentHeight; final double offsetHtmlContentHeight;
final bool keepAlive; final bool keepAlive;
final bool enableQuoteToggle; final bool enableQuoteToggle;
final bool disableScrolling;
final double? maxViewHeight;
final OnLoadWidthHtmlViewerAction? onLoadWidthHtmlViewer; final OnLoadWidthHtmlViewerAction? onLoadWidthHtmlViewer;
final OnMailtoDelegateAction? onMailtoDelegateAction; final OnMailtoDelegateAction? onMailtoDelegateAction;
@@ -47,13 +50,15 @@ class HtmlContentViewer extends StatefulWidget {
required this.contentHtml, required this.contentHtml,
this.initialWidth, this.initialWidth,
this.direction, this.direction,
this.minHtmlContentHeight = ConstantsUI.htmlContentMinHeight, this.htmlContentMinHeight = ConstantsUI.htmlContentMinHeight,
this.offsetHtmlContentHeight = ConstantsUI.htmlContentOffsetHeight, this.offsetHtmlContentHeight = ConstantsUI.htmlContentOffsetHeight,
this.keepAlive = false, this.keepAlive = false,
this.enableQuoteToggle = false, this.enableQuoteToggle = false,
this.keepWidthWhileLoading = false, this.keepWidthWhileLoading = false,
this.contentPadding, this.contentPadding,
this.useDefaultFont = false, this.useDefaultFont = false,
this.disableScrolling = false,
this.maxViewHeight,
this.maxHtmlContentHeight, this.maxHtmlContentHeight,
this.onLoadWidthHtmlViewer, this.onLoadWidthHtmlViewer,
this.onMailtoDelegateAction, this.onMailtoDelegateAction,
@@ -72,45 +77,39 @@ class HtmlContentViewState extends State<HtmlContentViewer> with AutomaticKeepAl
late InAppWebViewController _webViewController; late InAppWebViewController _webViewController;
late double _actualHeight; late double _actualHeight;
late Set<Factory<OneSequenceGestureRecognizer>> _gestureRecognizers; late Set<Factory<OneSequenceGestureRecognizer>> _gestureRecognizers;
late StringBuffer _customScriptsBuilder; late InAppWebViewSettings _webViewSetting;
final _loadingBarNotifier = ValueNotifier(true); final _loadingBarNotifier = ValueNotifier(true);
String? _htmlData; String? _htmlData;
final _webViewSetting = InAppWebViewSettings(
transparentBackground: true,
verticalScrollBarEnabled: false,
supportZoom: false,
);
@visibleForTesting @visibleForTesting
InAppWebViewController get webViewController => _webViewController; InAppWebViewController get webViewController => _webViewController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (PlatformInfo.isAndroid) { _webViewSetting = InAppWebViewSettings(
_gestureRecognizers = { transparentBackground: true,
Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer()), verticalScrollBarEnabled: false,
supportZoom: false,
disableHorizontalScroll: widget.disableScrolling,
disableVerticalScroll: widget.disableScrolling,
horizontalScrollBarEnabled: !widget.disableScrolling,
);
_gestureRecognizers = {
Factory<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(
duration: PlatformInfo.isAndroid
? null
: _longPressGestureDurationIOS,
),
),
if (PlatformInfo.isAndroid && !widget.disableScrolling)
Factory<ScaleGestureRecognizer>(() => ScaleGestureRecognizer()), Factory<ScaleGestureRecognizer>(() => ScaleGestureRecognizer()),
}; };
} else {
_gestureRecognizers = {
Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer(duration: _longPressGestureDurationIOS)),
};
}
_customScriptsBuilder = StringBuffer();
_customScriptsBuilder.write(HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage);
if (widget.enableQuoteToggle) {
_customScriptsBuilder.write(HtmlUtils.quoteToggleScript);
}
if (widget.initialWidth != null) {
_customScriptsBuilder.write(HtmlInteraction.generateNormalizeImageScript(widget.initialWidth!));
}
if (PlatformInfo.isAndroid) {
_customScriptsBuilder.write(HtmlInteraction.scriptsHandleContentSizeChanged);
}
_initialData(); _initialData();
} }
@@ -125,16 +124,33 @@ class HtmlContentViewState extends State<HtmlContentViewer> with AutomaticKeepAl
} }
void _initialData() { void _initialData() {
_actualHeight = widget.minHtmlContentHeight; _actualHeight = widget.htmlContentMinHeight;
_htmlData = HtmlUtils.generateHtmlDocument(
content: widget.enableQuoteToggle final processedContent = widget.enableQuoteToggle
? HtmlUtils.addQuoteToggle(widget.contentHtml) ? HtmlUtils.addQuoteToggle(widget.contentHtml)
: widget.contentHtml, : widget.contentHtml;
final combinedCss = [
if (widget.enableQuoteToggle) HtmlUtils.quoteToggleStyle,
if (widget.disableScrolling) HtmlTemplate.disableScrollingStyleCSS,
].join();
final combinedScripts = [
HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage,
if (widget.enableQuoteToggle) HtmlUtils.quoteToggleScript,
if (widget.initialWidth != null)
HtmlInteraction.generateNormalizeImageScript(widget.initialWidth!),
if (PlatformInfo.isAndroid)
HtmlInteraction.scriptsHandleContentSizeChanged,
].join();
_htmlData = HtmlUtils.generateHtmlDocument(
content: processedContent,
direction: widget.direction, direction: widget.direction,
javaScripts: _customScriptsBuilder.toString(), javaScripts: combinedScripts,
styleCSS: combinedCss,
contentPadding: widget.contentPadding, contentPadding: widget.contentPadding,
useDefaultFont: widget.useDefaultFont, useDefaultFont: widget.useDefaultFont,
styleCSS: widget.enableQuoteToggle ? HtmlUtils.quoteToggleStyle : null
); );
} }
@@ -142,23 +158,20 @@ class HtmlContentViewState extends State<HtmlContentViewer> with AutomaticKeepAl
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
final child = Stack(children: [ final child = Stack(children: [
if (_htmlData == null) if (_htmlData?.trim().isNotEmpty == true)
const SizedBox.shrink() if (widget.maxViewHeight != null)
else Container(
SizedBox( height: _actualHeight,
height: _actualHeight, width: widget.initialWidth,
width: widget.initialWidth, constraints: BoxConstraints(maxHeight: widget.maxViewHeight!),
child: InAppWebView( child: _buildWebView(),
key: ValueKey(_htmlData),
initialSettings: _webViewSetting,
onWebViewCreated: _onWebViewCreated,
onLoadStop: _onLoadStop,
onContentSizeChanged: _onContentSizeChanged,
shouldOverrideUrlLoading: _shouldOverrideUrlLoading,
gestureRecognizers: _gestureRecognizers,
onScrollChanged: (controller, x, y) => controller.scrollTo(x: 0, y: 0)
) )
), else
SizedBox(
height: _actualHeight,
width: widget.initialWidth,
child: _buildWebView(),
),
ValueListenableBuilder( ValueListenableBuilder(
valueListenable: _loadingBarNotifier, valueListenable: _loadingBarNotifier,
builder: (context, loading, child) { builder: (context, loading, child) {
@@ -171,23 +184,42 @@ class HtmlContentViewState extends State<HtmlContentViewer> with AutomaticKeepAl
), ),
]); ]);
if (!widget.keepWidthWhileLoading) return child; if (!widget.keepWidthWhileLoading) {
return SizedBox( return child;
width: widget.initialWidth, } else {
child: child, return SizedBox(width: widget.initialWidth, child: child);
}
}
Widget _buildWebView() {
return InAppWebView(
key: ValueKey(_htmlData),
initialSettings: _webViewSetting,
onWebViewCreated: _onWebViewCreated,
onLoadStop: _onLoadStop,
onContentSizeChanged: _onContentSizeChanged,
shouldOverrideUrlLoading: _shouldOverrideUrlLoading,
gestureRecognizers: _gestureRecognizers,
onScrollChanged: widget.disableScrolling ? null : _onScrollChanged,
); );
} }
void _onScrollChanged(InAppWebViewController controller, int x, int y) {
controller.scrollTo(x: 0, y: 0);
}
void _onWebViewCreated(InAppWebViewController controller) async { void _onWebViewCreated(InAppWebViewController controller) async {
log('_HtmlContentViewState::_onWebViewCreated:'); log('_HtmlContentViewState::_onWebViewCreated:');
_webViewController = controller; _webViewController = controller;
await controller.loadData(data: _htmlData ?? ''); await controller.loadData(data: _htmlData ?? '');
controller.addJavaScriptHandler( if (!widget.disableScrolling) {
handlerName: HtmlInteraction.scrollEventJSChannelName, controller.addJavaScriptHandler(
callback: _onHandleScrollEvent handlerName: HtmlInteraction.scrollEventJSChannelName,
); callback: _onHandleScrollEvent,
);
}
if (PlatformInfo.isAndroid) { if (PlatformInfo.isAndroid) {
controller.addJavaScriptHandler( controller.addJavaScriptHandler(
@@ -230,12 +262,8 @@ class HtmlContentViewState extends State<HtmlContentViewer> with AutomaticKeepAl
double currentHeight = maxContentHeight + widget.offsetHtmlContentHeight; double currentHeight = maxContentHeight + widget.offsetHtmlContentHeight;
if (PlatformInfo.isIOS && widget.maxHtmlContentHeight != null) { if (PlatformInfo.isIOS && widget.maxHtmlContentHeight != null) {
final bool isClipped = currentHeight > widget.maxHtmlContentHeight!; currentHeight = _reStandardizeHeight(
if (isClipped) { currentHeight,
widget.onHtmlContentClippedAction?.call(true);
}
currentHeight = currentHeight.clamp(
widget.minHtmlContentHeight,
widget.maxHtmlContentHeight!, widget.maxHtmlContentHeight!,
); );
} }
@@ -259,58 +287,74 @@ class HtmlContentViewState extends State<HtmlContentViewer> with AutomaticKeepAl
log('_HtmlContentViewState::_getActualSizeHtmlViewer(): listSize: $listSize'); log('_HtmlContentViewState::_getActualSizeHtmlViewer(): listSize: $listSize');
bool isScrollActivated = false;
Set<Factory<OneSequenceGestureRecognizer>>? newGestureRecognizers;
final double? scrollWidth = listSize[0] is num ? (listSize[0] as num).toDouble() : null; final double? scrollWidth = listSize[0] is num ? (listSize[0] as num).toDouble() : null;
final double? offsetWidth = listSize[1] is num ? (listSize[1] as num).toDouble() : null; final double? offsetWidth = listSize[1] is num ? (listSize[1] as num).toDouble() : null;
final double? scrollHeight = listSize[2] is num ? (listSize[2] as num).toDouble() : null; final double? scrollHeight = listSize[2] is num ? (listSize[2] as num).toDouble() : null;
if (scrollWidth != null && offsetWidth != null) { bool isContentFullyVisible = scrollWidth != null &&
isScrollActivated = scrollWidth.round() == offsetWidth.round(); offsetWidth != null &&
if (!isScrollActivated && PlatformInfo.isIOS) { scrollWidth.round() == offsetWidth.round();
newGestureRecognizers = {
Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer(duration: _longPressGestureDurationIOS)), final isIOSScrollingEnabled = !isContentFullyVisible && PlatformInfo.isIOS;
Factory<HorizontalDragGestureRecognizer>(() => HorizontalDragGestureRecognizer()), log('_HtmlContentViewState::_getActualSizeHtmlViewer: isIOSScrollingEnabled = $isIOSScrollingEnabled | isContentFullyVisible = $isContentFullyVisible');
};
}
}
if (scrollHeight != null && scrollHeight > 0) { if (scrollHeight != null && scrollHeight > 0) {
double currentHeight = scrollHeight + widget.offsetHtmlContentHeight; double currentHeight = scrollHeight + widget.offsetHtmlContentHeight;
if (PlatformInfo.isIOS && widget.maxHtmlContentHeight != null) { if (PlatformInfo.isIOS && widget.maxHtmlContentHeight != null) {
final bool isClipped = currentHeight > widget.maxHtmlContentHeight!; currentHeight = _reStandardizeHeight(
if (isClipped) { currentHeight,
widget.onHtmlContentClippedAction?.call(true);
}
currentHeight = currentHeight.clamp(
widget.minHtmlContentHeight,
widget.maxHtmlContentHeight!, widget.maxHtmlContentHeight!,
); );
} }
log('_HtmlContentViewState::_getActualSizeHtmlViewer: currentHeight = $currentHeight');
if (_actualHeight != currentHeight || newGestureRecognizers != null) { if (_actualHeight != currentHeight || isIOSScrollingEnabled) {
log('_HtmlContentViewState::_getActualSizeHtmlViewer: currentHeight = $currentHeight');
setState(() { setState(() {
_actualHeight = currentHeight; _actualHeight = currentHeight;
if (newGestureRecognizers != null) { if (isIOSScrollingEnabled && !widget.disableScrolling) {
_gestureRecognizers = newGestureRecognizers; _gestureRecognizers = _iOSGestureRecognizersWithScrolling;
} }
}); });
} }
} else if (newGestureRecognizers != null) { } else if (isIOSScrollingEnabled && !widget.disableScrolling) {
setState(() { setState(() {
_gestureRecognizers = newGestureRecognizers!; _gestureRecognizers = _iOSGestureRecognizersWithScrolling;
}); });
} }
if (!isScrollActivated) { if (!isContentFullyVisible && !widget.disableScrolling) {
await _webViewController.evaluateJavascript(source: HtmlInteraction.runScriptsHandleScrollEvent); await _webViewController.evaluateJavascript(
source: HtmlInteraction.runScriptsHandleScrollEvent,
);
widget.onLoadWidthHtmlViewer?.call(isContentFullyVisible);
} }
widget.onLoadWidthHtmlViewer?.call(isScrollActivated);
} }
double _reStandardizeHeight(double currentHeight, double maxHtmlContentHeight) {
final bool isClipped = currentHeight > maxHtmlContentHeight;
if (isClipped) {
widget.onHtmlContentClippedAction?.call(true);
}
return currentHeight.clamp(
widget.htmlContentMinHeight,
maxHtmlContentHeight,
);
}
Set<Factory<OneSequenceGestureRecognizer>> get _iOSGestureRecognizersWithScrolling => {
Factory<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(
duration: _longPressGestureDurationIOS,
),
),
Factory<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(),
),
};
Future<NavigationActionPolicy?> _shouldOverrideUrlLoading( Future<NavigationActionPolicy?> _shouldOverrideUrlLoading(
InAppWebViewController controller, InAppWebViewController controller,
NavigationAction navigationAction NavigationAction navigationAction
@@ -1,6 +1,7 @@
import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/views/checkbox/custom_icon_labeled_checkbox.dart'; import 'package:core/presentation/views/checkbox/custom_icon_labeled_checkbox.dart';
import 'package:core/presentation/views/dialog/confirm_dialog_button.dart'; import 'package:core/presentation/views/dialog/confirm_dialog_button.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart';
@@ -82,7 +83,11 @@ class IdentityCreatorFormBottomView extends StatelessWidget {
if (isMobile) { if (isMobile) {
return Padding( return Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 17), padding: EdgeInsetsDirectional.only(
start: 17,
end: 17,
bottom: PlatformInfo.isMobile ? 24 : 0,
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,