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>
with AiScribeSuggestionStateMixin {
static const double _defaultPadding = 32.0;
@override
AIAction get aiAction => widget.aiAction;
@@ -48,11 +50,23 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
OnSelectAiScribeSuggestionAction get onSelectAction =>
widget.onSelectAiScribeSuggestionAction;
bool get _hasAnchor =>
widget.buttonPosition != null && widget.buttonSize != null;
bool get _isMobileView =>
PlatformInfo.isMobile || PlatformInfo.isWebTouchDevice;
@override
Widget build(BuildContext context) {
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
final keyboardHeightWithSpacing = keyboardHeight > 0 ? keyboardHeight + AIScribeSizes.keyboardSpacing : 0;
final screenSize = MediaQuery.of(context).size;
// Cache MediaQuery data to avoid multiple lookups
final mediaQuery = MediaQuery.of(context);
final screenSize = mediaQuery.size;
final viewInsetsBottom = mediaQuery.viewInsets.bottom;
final keyboardHeightWithSpacing = viewInsetsBottom > 0
? viewInsetsBottom + AIScribeSizes.keyboardSpacing
: 0.0;
final availableHeight = screenSize.height - keyboardHeightWithSpacing;
final modalWidth = min(
@@ -65,33 +79,93 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
AIScribeSizes.suggestionModalMaxHeight,
);
final hasContent = widget.content?.trim().isNotEmpty == true;
final dialogContent = _buildDialogContent(context, hasContent);
final dialogContent = _buildDialogContent(context);
// No Anchor - Center the modal
if (!_hasAnchor) {
return Center(
child: _buildModalContainer(
width: modalWidth,
maxHeight: modalMaxHeight,
child: dialogContent,
),
return _buildCenteredLayout(
modalWidth: modalWidth,
modalMaxHeight: modalMaxHeight,
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(
screenSize: Size(screenSize.width, availableHeight),
anchorPosition: widget.buttonPosition!,
anchorSize: widget.buttonSize!,
anchorPosition: anchorPos,
anchorSize: anchorSize,
menuSize: Size(modalWidth, modalMaxHeight),
preferredPlacement: widget.preferredPlacement,
padding: AIScribeSizes.screenEdgePadding,
);
final modalStartOffset = layout.left;
final modalBottomOffset =
_isMobileView ? null : layout.bottom + keyboardHeightWithSpacing;
final modalTopOffset = _isMobileView ? widget.buttonPosition!.dy : null;
// Calculate specific dimensions based on platform logic
final double? top;
final double? bottom;
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(
child: Stack(
@@ -102,17 +176,15 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
onTap: _handleClickOutside,
),
),
// The Modal
PositionedDirectional(
start: modalStartOffset,
// layout.bottom is calculated by taking keyboard into account
// but positioned without taking keyboard into account
// that's why we need to add keyboard height here
top: modalTopOffset,
bottom: modalBottomOffset,
start: layout.left,
top: top,
bottom: bottom,
child: _buildModalContainer(
width: modalWidth,
maxHeight: layout.availableHeight,
child: dialogContent,
width: width,
maxHeight: height,
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);
return Column(
@@ -154,8 +226,8 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
padding: AIScribeSizes.suggestionContentPadding,
decoration: BoxDecoration(
color: AIScribeColors.background,
borderRadius: BorderRadius.circular(
AIScribeSizes.menuRadius,
borderRadius: const BorderRadius.all(
Radius.circular(AIScribeSizes.menuRadius),
),
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() {
final shouldDismiss = suggestionState.value.fold(
final state = suggestionState.value;
final shouldDismiss = state.fold(
(failure) => failure is GenerateAITextFailure,
(success) => success is GenerateAITextSuccess,
);
if (shouldDismiss) {
Navigator.of(context).pop();
}