feat(apps/home): 新增 HomeScreen 录音交互与导航组件
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../../shared/widgets/message_composer.dart';
|
||||
import 'home_attachment_strip.dart';
|
||||
|
||||
class HomeComposerStack extends StatelessWidget {
|
||||
const HomeComposerStack({
|
||||
super.key,
|
||||
required this.selectedImages,
|
||||
required this.onRemoveImage,
|
||||
required this.isHoldToSpeakMode,
|
||||
required this.isRecording,
|
||||
required this.isCancelGestureActive,
|
||||
required this.isTranscribing,
|
||||
required this.isWaitingAgent,
|
||||
required this.messageController,
|
||||
required this.messageFocusNode,
|
||||
required this.onTapPlus,
|
||||
required this.onTapRightAction,
|
||||
required this.onHoldToSpeakStart,
|
||||
required this.onHoldToSpeakEnd,
|
||||
required this.onHoldToSpeakMoveUpdate,
|
||||
required this.onHoldToSpeakCancel,
|
||||
required this.onTextFieldTap,
|
||||
required this.onSubmit,
|
||||
});
|
||||
|
||||
final List<XFile> selectedImages;
|
||||
final ValueChanged<int> onRemoveImage;
|
||||
final bool isHoldToSpeakMode;
|
||||
final bool isRecording;
|
||||
final bool isCancelGestureActive;
|
||||
final bool isTranscribing;
|
||||
final bool isWaitingAgent;
|
||||
final TextEditingController messageController;
|
||||
final FocusNode messageFocusNode;
|
||||
final VoidCallback onTapPlus;
|
||||
final VoidCallback onTapRightAction;
|
||||
final VoidCallback onHoldToSpeakStart;
|
||||
final VoidCallback onHoldToSpeakEnd;
|
||||
final ValueChanged<LongPressMoveUpdateDetails> onHoldToSpeakMoveUpdate;
|
||||
final VoidCallback onHoldToSpeakCancel;
|
||||
final VoidCallback onTextFieldTap;
|
||||
final VoidCallback onSubmit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final process = isRecording
|
||||
? MessageComposerProcess.recording
|
||||
: isTranscribing
|
||||
? MessageComposerProcess.transcribing
|
||||
: MessageComposerProcess.idle;
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: KeyedSubtree(
|
||||
key: const ValueKey('home_bottom_input_stack'),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
HomeAttachmentStrip(
|
||||
images: selectedImages,
|
||||
onRemove: onRemoveImage,
|
||||
),
|
||||
if (selectedImages.isNotEmpty)
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: messageController,
|
||||
builder: (context, value, child) {
|
||||
final hasMessage = value.text.trim().isNotEmpty;
|
||||
return MessageComposer(
|
||||
mode: isHoldToSpeakMode
|
||||
? MessageComposerMode.holdToSpeak
|
||||
: MessageComposerMode.text,
|
||||
process: process,
|
||||
hasMessage: hasMessage,
|
||||
isWaitingAgent: isWaitingAgent,
|
||||
iconSize: 24,
|
||||
composerMinHeight: AppSpacing.xxl + AppSpacing.lg,
|
||||
onTapPlus: onTapPlus,
|
||||
onTapRightAction: onTapRightAction,
|
||||
onHoldToSpeakStart: onHoldToSpeakStart,
|
||||
onHoldToSpeakEnd: onHoldToSpeakEnd,
|
||||
onHoldToSpeakMoveUpdate: onHoldToSpeakMoveUpdate,
|
||||
onHoldToSpeakCancel: onHoldToSpeakCancel,
|
||||
textInputChild: _buildTextInputContent(),
|
||||
recordingAnimation: const SizedBox.shrink(),
|
||||
recordingText: isCancelGestureActive ? '松手取消' : '松手发送',
|
||||
recordingHintText: isCancelGestureActive
|
||||
? '松开取消'
|
||||
: '松开发送,上滑取消',
|
||||
showRecordingInlineFeedback: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextInputContent() {
|
||||
if (isTranscribing) {
|
||||
return _buildTranscribingIndicator();
|
||||
}
|
||||
return SizedBox.expand(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextField(
|
||||
controller: messageController,
|
||||
focusNode: messageFocusNode,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontSize: AppSpacing.lg,
|
||||
height: 1,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '输入消息...',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: AppSpacing.lg,
|
||||
height: 1,
|
||||
color: AppColors.slate400,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
filled: false,
|
||||
),
|
||||
onTap: onTextFieldTap,
|
||||
onSubmitted: (_) => onSubmit(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTranscribingIndicator() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: const AppLoadingIndicator(
|
||||
variant: AppLoadingVariant.inline,
|
||||
size: 18,
|
||||
strokeWidth: 2,
|
||||
color: AppColors.blue600,
|
||||
trackColor: AppColors.blue100,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
_buildWaveDots(),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'语音识别中...',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.blue600,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWaveDots() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: List.generate(3, (index) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 3),
|
||||
width: 3,
|
||||
height: 6 + index * 2,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.blue500,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user