181 lines
5.0 KiB
Dart
181 lines
5.0 KiB
Dart
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<XFile> selectedImages;
|
|
final ValueChanged<int> 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<LongPressMoveUpdateDetails> onHoldToSpeakMoveUpdate;
|
|
final VoidCallback onHoldToSpeakCancel;
|
|
final Future<void> Function(String text) onSubmitText;
|
|
final double keyboardInset;
|
|
|
|
@override
|
|
State<HomeInputHost> createState() => HomeInputHostState();
|
|
}
|
|
|
|
class HomeInputHostState extends State<HomeInputHost> {
|
|
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<void>('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);
|
|
}
|
|
}
|