feat(scribe-mobile):Fix sometimes the top part is cut off

This commit is contained in:
dab246
2026-02-13 17:24:35 +07:00
committed by Dat H. Pham
parent 5937055789
commit b64aa4e4c7
@@ -35,6 +35,8 @@ class AiScribeSuggestionWidget extends StatefulWidget {
class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget> class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
with AiScribeSuggestionStateMixin { with AiScribeSuggestionStateMixin {
static const double _defaultPadding = 32.0;
@override @override
AIAction get aiAction => widget.aiAction; AIAction get aiAction => widget.aiAction;
@@ -48,11 +50,23 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
OnSelectAiScribeSuggestionAction get onSelectAction => OnSelectAiScribeSuggestionAction get onSelectAction =>
widget.onSelectAiScribeSuggestionAction; widget.onSelectAiScribeSuggestionAction;
bool get _hasAnchor =>
widget.buttonPosition != null && widget.buttonSize != null;
bool get _isMobileView =>
PlatformInfo.isMobile || PlatformInfo.isWebTouchDevice;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; // Cache MediaQuery data to avoid multiple lookups
final keyboardHeightWithSpacing = keyboardHeight > 0 ? keyboardHeight + AIScribeSizes.keyboardSpacing : 0; final mediaQuery = MediaQuery.of(context);
final screenSize = MediaQuery.of(context).size; final screenSize = mediaQuery.size;
final viewInsetsBottom = mediaQuery.viewInsets.bottom;
final keyboardHeightWithSpacing = viewInsetsBottom > 0
? viewInsetsBottom + AIScribeSizes.keyboardSpacing
: 0.0;
final availableHeight = screenSize.height - keyboardHeightWithSpacing; final availableHeight = screenSize.height - keyboardHeightWithSpacing;
final modalWidth = min( final modalWidth = min(
@@ -65,33 +79,93 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
AIScribeSizes.suggestionModalMaxHeight, AIScribeSizes.suggestionModalMaxHeight,
); );
final hasContent = widget.content?.trim().isNotEmpty == true; final dialogContent = _buildDialogContent(context);
final dialogContent = _buildDialogContent(context, hasContent);
// No Anchor - Center the modal
if (!_hasAnchor) { if (!_hasAnchor) {
return Center( return _buildCenteredLayout(
child: _buildModalContainer( modalWidth: modalWidth,
width: modalWidth, modalMaxHeight: modalMaxHeight,
maxHeight: modalMaxHeight, child: dialogContent,
child: dialogContent,
),
); );
} }
// Anchored Modal
return _buildAnchoredLayout(
screenSize: screenSize,
availableHeight: availableHeight,
keyboardHeightWithSpacing: keyboardHeightWithSpacing,
modalWidth: modalWidth,
modalMaxHeight: modalMaxHeight,
child: dialogContent,
);
}
Widget _buildCenteredLayout({
required double modalWidth,
required double modalMaxHeight,
required Widget child,
}) {
return Center(
child: _buildModalContainer(
width: modalWidth,
maxHeight: modalMaxHeight,
child: child,
),
);
}
Widget _buildAnchoredLayout({
required Size screenSize,
required double availableHeight,
required double keyboardHeightWithSpacing,
required double modalWidth,
required double modalMaxHeight,
required Widget child,
}) {
// Safe unwrap because we checked _hasAnchor before calling this
final anchorPos = widget.buttonPosition!;
final anchorSize = widget.buttonSize!;
// Calculate layout using the helper
final layout = AnchoredModalLayoutCalculator.calculateAnchoredSuggestLayout( final layout = AnchoredModalLayoutCalculator.calculateAnchoredSuggestLayout(
screenSize: Size(screenSize.width, availableHeight), screenSize: Size(screenSize.width, availableHeight),
anchorPosition: widget.buttonPosition!, anchorPosition: anchorPos,
anchorSize: widget.buttonSize!, anchorSize: anchorSize,
menuSize: Size(modalWidth, modalMaxHeight), menuSize: Size(modalWidth, modalMaxHeight),
preferredPlacement: widget.preferredPlacement, preferredPlacement: widget.preferredPlacement,
padding: AIScribeSizes.screenEdgePadding, padding: AIScribeSizes.screenEdgePadding,
); );
final modalStartOffset = layout.left; // Calculate specific dimensions based on platform logic
final modalBottomOffset = final double? top;
_isMobileView ? null : layout.bottom + keyboardHeightWithSpacing; final double? bottom;
final modalTopOffset = _isMobileView ? widget.buttonPosition!.dy : null; final double height;
final double width;
if (_isMobileView) {
// Mobile logic: Anchor usually relates to top position
top = anchorPos.dy;
bottom = null;
height = min(
layout.availableHeight,
screenSize.height - anchorPos.dy - anchorSize.height - _defaultPadding,
);
width = min(
modalWidth,
screenSize.width - anchorPos.dx - anchorSize.width - _defaultPadding,
);
} else {
// Desktop/Web logic: Uses calculated bottom offset
top = null;
// Layout bottom doesn't account for keyboard in calculation usually, so we add it back
bottom = layout.bottom + keyboardHeightWithSpacing;
height = layout.availableHeight;
width = modalWidth;
}
return PointerInterceptor( return PointerInterceptor(
child: Stack( child: Stack(
@@ -102,17 +176,15 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
onTap: _handleClickOutside, onTap: _handleClickOutside,
), ),
), ),
// The Modal
PositionedDirectional( PositionedDirectional(
start: modalStartOffset, start: layout.left,
// layout.bottom is calculated by taking keyboard into account top: top,
// but positioned without taking keyboard into account bottom: bottom,
// that's why we need to add keyboard height here
top: modalTopOffset,
bottom: modalBottomOffset,
child: _buildModalContainer( child: _buildModalContainer(
width: modalWidth, width: width,
maxHeight: layout.availableHeight, maxHeight: height,
child: dialogContent, child: child,
), ),
), ),
], ],
@@ -120,7 +192,7 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
); );
} }
Widget _buildDialogContent(BuildContext context, bool hasContent) { Widget _buildDialogContent(BuildContext context) {
final localizations = ScribeLocalizations.of(context); final localizations = ScribeLocalizations.of(context);
return Column( return Column(
@@ -154,8 +226,8 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
padding: AIScribeSizes.suggestionContentPadding, padding: AIScribeSizes.suggestionContentPadding,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AIScribeColors.background, color: AIScribeColors.background,
borderRadius: BorderRadius.circular( borderRadius: const BorderRadius.all(
AIScribeSizes.menuRadius, Radius.circular(AIScribeSizes.menuRadius),
), ),
boxShadow: AIScribeShadows.modal, boxShadow: AIScribeShadows.modal,
), ),
@@ -164,17 +236,13 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
); );
} }
bool get _hasAnchor =>
widget.buttonPosition != null && widget.buttonSize != null;
bool get _isMobileView =>
PlatformInfo.isMobile || PlatformInfo.isWebTouchDevice;
void _handleClickOutside() { void _handleClickOutside() {
final shouldDismiss = suggestionState.value.fold( final state = suggestionState.value;
final shouldDismiss = state.fold(
(failure) => failure is GenerateAITextFailure, (failure) => failure is GenerateAITextFailure,
(success) => success is GenerateAITextSuccess, (success) => success is GenerateAITextSuccess,
); );
if (shouldDismiss) { if (shouldDismiss) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }