214 lines
7.0 KiB
Dart
214 lines
7.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
|
|
import '../../../../core/l10n/l10n.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.onSubmit,
|
|
required this.keyboardInset,
|
|
});
|
|
|
|
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 onSubmit;
|
|
final double keyboardInset;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final process = isRecording
|
|
? MessageComposerProcess.recording
|
|
: isTranscribing
|
|
? MessageComposerProcess.transcribing
|
|
: MessageComposerProcess.idle;
|
|
|
|
return Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: Padding(
|
|
padding: EdgeInsets.fromLTRB(
|
|
AppSpacing.lg,
|
|
AppSpacing.lg,
|
|
AppSpacing.lg,
|
|
AppSpacing.lg + keyboardInset,
|
|
),
|
|
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(context),
|
|
recordingAnimation: const SizedBox.shrink(),
|
|
recordingText: isCancelGestureActive
|
|
? context.l10n.homeRecordingReleaseCancel
|
|
: context.l10n.homeRecordingReleaseSend,
|
|
recordingHintText: isCancelGestureActive
|
|
? context.l10n.homeRecordingHintReleaseCancel
|
|
: context.l10n.homeRecordingHintReleaseSend,
|
|
showRecordingInlineFeedback: false,
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTextInputContent(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
if (isTranscribing) {
|
|
return _buildTranscribingIndicator(context);
|
|
}
|
|
return SizedBox.expand(
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: TextField(
|
|
controller: messageController,
|
|
focusNode: messageFocusNode,
|
|
minLines: 1,
|
|
maxLines: 1,
|
|
style: TextStyle(
|
|
fontSize: AppSpacing.lg,
|
|
height: 1,
|
|
color: colorScheme.onSurface,
|
|
),
|
|
textAlignVertical: TextAlignVertical.center,
|
|
decoration: InputDecoration(
|
|
hintText: context.l10n.homeInputHint,
|
|
hintStyle: TextStyle(
|
|
fontSize: AppSpacing.lg,
|
|
height: 1,
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
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,
|
|
),
|
|
onSubmitted: (_) => onSubmit(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTranscribingIndicator(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
SizedBox(
|
|
width: 18,
|
|
height: 18,
|
|
child: AppLoadingIndicator(
|
|
variant: AppLoadingVariant.inline,
|
|
size: 18,
|
|
strokeWidth: 2,
|
|
color: colorScheme.primary,
|
|
trackColor: colorScheme.primaryContainer,
|
|
),
|
|
),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
_buildWaveDots(context),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
Expanded(
|
|
child: Text(
|
|
context.l10n.homeTranscribing,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: colorScheme.primary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildWaveDots(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
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: colorScheme.primary,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
}
|