feat(apps): 重构 UI 架构为 presentation 层并新增 l10n 国际化支持
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user