Files
social-app/apps/lib/features/home/presentation/widgets/home_composer_stack.dart
T

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),
),
);
}),
);
}
}