import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'home_composer_stack.dart'; class HomeInputHost extends StatefulWidget { const HomeInputHost({ super.key, required this.selectedImages, required this.onRemoveImage, required this.isRecording, required this.isCancelGestureActive, required this.isTranscribing, required this.isWaitingAgent, required this.messageController, required this.onTapPlus, required this.onStopGenerating, required this.onHoldToSpeakStart, required this.onHoldToSpeakEnd, required this.onHoldToSpeakMoveUpdate, required this.onHoldToSpeakCancel, required this.onSubmitText, required this.keyboardInset, }); final List selectedImages; final ValueChanged onRemoveImage; final bool isRecording; final bool isCancelGestureActive; final bool isTranscribing; final bool isWaitingAgent; final TextEditingController messageController; final VoidCallback onTapPlus; final VoidCallback onStopGenerating; final VoidCallback onHoldToSpeakStart; final VoidCallback onHoldToSpeakEnd; final ValueChanged onHoldToSpeakMoveUpdate; final VoidCallback onHoldToSpeakCancel; final Future Function(String text) onSubmitText; final double keyboardInset; @override State createState() => HomeInputHostState(); } class HomeInputHostState extends State { final FocusNode _messageFocusNode = FocusNode(); Timer? _keyboardShowFallbackTimer; bool _isHoldToSpeakMode = true; void unfocusInput() { _messageFocusNode.unfocus(); } @override void initState() { super.initState(); _messageFocusNode.addListener(_handleMessageFocusChanged); } @override void dispose() { _keyboardShowFallbackTimer?.cancel(); _messageFocusNode.removeListener(_handleMessageFocusChanged); _messageFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return HomeComposerStack( selectedImages: widget.selectedImages, onRemoveImage: widget.onRemoveImage, isHoldToSpeakMode: _isHoldToSpeakMode, isRecording: widget.isRecording, isCancelGestureActive: widget.isCancelGestureActive, isTranscribing: widget.isTranscribing, isWaitingAgent: widget.isWaitingAgent, messageController: widget.messageController, messageFocusNode: _messageFocusNode, onTapPlus: widget.onTapPlus, onTapRightAction: _onRightActionTap, onHoldToSpeakStart: widget.onHoldToSpeakStart, onHoldToSpeakEnd: widget.onHoldToSpeakEnd, onHoldToSpeakMoveUpdate: widget.onHoldToSpeakMoveUpdate, onHoldToSpeakCancel: widget.onHoldToSpeakCancel, onSubmit: _onSubmit, keyboardInset: widget.keyboardInset, ); } void _onRightActionTap() { if (widget.isTranscribing || widget.isRecording) { return; } if (widget.isWaitingAgent) { widget.onStopGenerating(); return; } final draft = widget.messageController.text.trim(); if (draft.isNotEmpty) { _onSubmit(); return; } _toggleInputMode(); } void _toggleInputMode() { if (widget.isRecording || widget.isTranscribing) { return; } final switchToText = _isHoldToSpeakMode; setState(() { _isHoldToSpeakMode = !_isHoldToSpeakMode; }); if (!switchToText) { _messageFocusNode.unfocus(); _keyboardShowFallbackTimer?.cancel(); return; } WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted || _isHoldToSpeakMode) { return; } _messageFocusNode.requestFocus(); }); } void _handleMessageFocusChanged() { if (!_messageFocusNode.hasFocus || _isHoldToSpeakMode) { _keyboardShowFallbackTimer?.cancel(); return; } _scheduleKeyboardShowFallback(); } void _scheduleKeyboardShowFallback() { if (!_supportsProgrammaticKeyboardShow() || _isKeyboardVisible()) { return; } _keyboardShowFallbackTimer?.cancel(); _keyboardShowFallbackTimer = Timer(const Duration(milliseconds: 120), () { if (!mounted || !_messageFocusNode.hasFocus || _isHoldToSpeakMode) { return; } if (_isKeyboardVisible()) { return; } SystemChannels.textInput.invokeMethod('TextInput.show'); }); } bool _supportsProgrammaticKeyboardShow() { if (kIsWeb) { return false; } return defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS; } bool _isKeyboardVisible() { final mediaQuery = MediaQuery.maybeOf(context); if (mediaQuery == null) { return false; } return mediaQuery.viewInsets.bottom > 0; } void _onSubmit() { final draft = widget.messageController.text.trim(); if (draft.isEmpty) { return; } widget.onSubmitText(draft); } }