feat: conditional show of mic/send

This commit is contained in:
cogwheel0
2025-09-20 19:54:00 +05:30
parent 9662e22cce
commit b1b3e813a4

View File

@@ -52,89 +52,7 @@ class ModernChatInput extends ConsumerStatefulWidget {
ConsumerState<ModernChatInput> createState() => _ModernChatInputState(); ConsumerState<ModernChatInput> createState() => _ModernChatInputState();
} }
class _MicButton extends StatelessWidget { // (Removed legacy _MicButton; inline mic logic now lives in primary button)
final bool isRecording;
final int intensity; // 0..10
final VoidCallback? onTap;
final String tooltip;
const _MicButton({
required this.isRecording,
required this.intensity,
required this.onTap,
required this.tooltip,
});
@override
Widget build(BuildContext context) {
final Color iconColor = isRecording
? context.conduitTheme.buttonPrimaryText
: context.conduitTheme.textPrimary.withValues(alpha: Alpha.strong);
final double normalized = (intensity.clamp(0, 10)) / 10.0;
return Tooltip(
message: tooltip,
child: Material(
color: Colors.transparent,
shape: const CircleBorder(),
child: InkWell(
customBorder: const CircleBorder(),
onTap: onTap == null
? null
: () {
HapticFeedback.selectionClick();
onTap!();
},
child: SizedBox(
width: TouchTarget.minimum,
height: TouchTarget.minimum,
child: Stack(
alignment: Alignment.center,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.easeOutCubic,
width: TouchTarget.minimum * 0.74,
height: TouchTarget.minimum * 0.74,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isRecording
? context.conduitTheme.buttonPrimary.withValues(
alpha: 0.12 + (0.14 * normalized),
)
: Colors.transparent,
boxShadow: isRecording
? [
BoxShadow(
color: context.conduitTheme.buttonPrimary
.withValues(
alpha: 0.22 + (0.18 * normalized),
),
blurRadius: 14 + (8 * normalized),
spreadRadius: 2 + (normalized * 2),
),
]
: const [],
),
),
AnimatedScale(
scale: isRecording ? 1.05 + (normalized * 0.05) : 1.0,
duration: const Duration(milliseconds: 180),
curve: Curves.easeOutCubic,
child: Icon(
Platform.isIOS ? CupertinoIcons.mic_fill : Icons.mic,
size: IconSize.medium,
color: iconColor,
),
),
],
),
),
),
),
);
}
}
class _ModernChatInputState extends ConsumerState<ModernChatInput> class _ModernChatInputState extends ConsumerState<ModernChatInput>
with TickerProviderStateMixin { with TickerProviderStateMixin {
@@ -147,9 +65,9 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
bool _hasText = false; // track locally without rebuilding on each keystroke bool _hasText = false; // track locally without rebuilding on each keystroke
StreamSubscription<String>? _voiceStreamSubscription; StreamSubscription<String>? _voiceStreamSubscription;
late VoiceInputService _voiceService; late VoiceInputService _voiceService;
StreamSubscription<int>? _intensitySub; StreamSubscription<int>?
_intensitySub; // removed usage; will be cleaned fully
StreamSubscription<String>? _textSub; StreamSubscription<String>? _textSub;
int _intensity = 0; // 0..10 from service
String _baseTextAtStart = ''; String _baseTextAtStart = '';
bool _isDeactivated = false; bool _isDeactivated = false;
int _lastHandledFocusTick = 0; int _lastHandledFocusTick = 0;
@@ -650,10 +568,10 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
), ),
Padding( Padding(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
Spacing.inputPadding, Spacing.inputPadding - Spacing.xs,
0,
Spacing.lg + Spacing.xs,
0, 0,
Spacing.inputPadding,
Spacing.sm,
), ),
child: Row( child: Row(
children: [ children: [
@@ -685,14 +603,11 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (voiceAvailable) ...[
_buildVoiceButton(voiceAvailable),
const SizedBox(width: Spacing.xs),
],
_buildPrimaryButton( _buildPrimaryButton(
_hasText, _hasText,
isGenerating, isGenerating,
stopGeneration, stopGeneration,
voiceAvailable,
), ),
], ],
), ),
@@ -725,54 +640,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
); );
} }
Widget _buildVoiceButton(bool voiceAvailable) { // (Removed legacy _buildVoiceButton; mic functionality moved to primary button)
if (!voiceAvailable) {
return const SizedBox.shrink();
}
return Builder(
builder: (context) {
const double buttonSize = TouchTarget.minimum;
final double t = _isRecording ? (_intensity.clamp(0, 10) / 10.0) : 0.0;
final double ringMaxExtra = 16.0;
final double ringSize = buttonSize + (ringMaxExtra * t);
final double ringOpacity = _isRecording ? 0.15 + (0.35 * t) : 0.0;
return SizedBox(
width: buttonSize,
height: buttonSize,
child: Stack(
alignment: Alignment.center,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 120),
width: ringSize,
height: ringSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: context.conduitTheme.buttonPrimary.withValues(
alpha: ringOpacity,
),
),
),
Transform.scale(
scale: _isRecording
? 1.0 + (_intensity.clamp(0, 10) / 200)
: 1.0,
child: _MicButton(
isRecording: _isRecording,
intensity: _intensity,
onTap: (widget.enabled && voiceAvailable)
? _toggleVoice
: null,
tooltip: AppLocalizations.of(context)!.voiceInput,
),
),
],
),
);
},
);
}
List<Widget> _withHorizontalSpacing(List<Widget> children, double gap) { List<Widget> _withHorizontalSpacing(List<Widget> children, double gap) {
if (children.length <= 1) { if (children.length <= 1) {
@@ -850,6 +718,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
bool hasText, bool hasText,
bool isGenerating, bool isGenerating,
void Function() stopGeneration, void Function() stopGeneration,
bool voiceAvailable,
) { ) {
// Compact 44px touch target, circular radius, md icon size // Compact 44px touch target, circular radius, md icon size
const double buttonSize = TouchTarget.minimum; // 44.0 const double buttonSize = TouchTarget.minimum; // 44.0
@@ -912,52 +781,117 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
); );
} }
// Default SEND variant // If there's text, render SEND variant; otherwise render MIC variant
if (hasText) {
return Tooltip(
message: enabled
? AppLocalizations.of(context)!.sendMessage
: AppLocalizations.of(context)!.send,
child: Opacity(
opacity: enabled ? Alpha.primary : Alpha.disabled,
child: IgnorePointer(
ignoring: !enabled,
child: Material(
color: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radius),
side: BorderSide(
color: enabled
? context.conduitTheme.buttonPrimary
: context.conduitTheme.cardBorder.withValues(
alpha: Alpha.medium,
),
width: BorderWidth.regular,
),
),
child: InkWell(
borderRadius: BorderRadius.circular(radius),
onTap: enabled
? () {
PlatformUtils.lightHaptic();
_sendMessage();
}
: null,
child: Container(
width: buttonSize,
height: buttonSize,
decoration: BoxDecoration(
color: enabled
? context.conduitTheme.buttonPrimary
: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(radius),
boxShadow: ConduitShadows.button,
),
child: Icon(
Platform.isIOS
? CupertinoIcons.arrow_up
: Icons.arrow_upward,
size: IconSize.medium,
color: enabled
? context.conduitTheme.buttonPrimaryText
: context.conduitTheme.textPrimary.withValues(
alpha: Alpha.disabled,
),
),
),
),
),
),
),
);
}
// MIC variant when no text is present
final bool enabledMic = widget.enabled && voiceAvailable;
return Tooltip( return Tooltip(
message: enabled message: AppLocalizations.of(context)!.voiceInput,
? AppLocalizations.of(context)!.sendMessage
: AppLocalizations.of(context)!.send,
child: Opacity( child: Opacity(
opacity: enabled ? Alpha.primary : Alpha.disabled, opacity: enabledMic ? Alpha.primary : Alpha.disabled,
child: IgnorePointer( child: IgnorePointer(
ignoring: !enabled, ignoring: !enabledMic,
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radius), borderRadius: BorderRadius.circular(radius),
side: BorderSide( side: BorderSide(
color: enabled color: _isRecording
? context.conduitTheme.cardBorder ? context.conduitTheme.buttonPrimary
: context.conduitTheme.cardBorder.withValues( : context.conduitTheme.cardBorder.withValues(
alpha: Alpha.medium, alpha: enabledMic ? Alpha.strong : Alpha.medium,
), ),
width: BorderWidth.regular, width: BorderWidth.regular,
), ),
), ),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(radius), borderRadius: BorderRadius.circular(radius),
onTap: enabled onTap: enabledMic
? () { ? () {
PlatformUtils.lightHaptic(); HapticFeedback.selectionClick();
_sendMessage(); _toggleVoice();
} }
: null, : null,
child: Container( child: Container(
width: buttonSize, width: buttonSize,
height: buttonSize, height: buttonSize,
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.conduitTheme.cardBackground, color: _isRecording
? context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.buttonPressed,
)
: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(radius), borderRadius: BorderRadius.circular(radius),
boxShadow: ConduitShadows.button, boxShadow: ConduitShadows.button,
), ),
child: Icon( child: Icon(
Platform.isIOS ? CupertinoIcons.arrow_up : Icons.arrow_upward, Platform.isIOS ? CupertinoIcons.mic_fill : Icons.mic,
size: IconSize.medium, size: IconSize.medium,
color: enabled color: _isRecording
? context.conduitTheme.textPrimary ? context.conduitTheme.buttonPrimary
: context.conduitTheme.textPrimary.withValues( : (enabledMic
alpha: Alpha.disabled, ? context.conduitTheme.textPrimary
), : context.conduitTheme.textPrimary.withValues(
alpha: Alpha.disabled,
)),
), ),
), ),
), ),
@@ -1781,10 +1715,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
_baseTextAtStart = _controller.text; _baseTextAtStart = _controller.text;
}); });
_intensitySub?.cancel(); _intensitySub?.cancel();
_intensitySub = _voiceService.intensityStream.listen((value) { // intensity stream no longer used for UI; stop listening
if (!mounted) return;
setState(() => _intensity = value);
});
_textSub?.cancel(); _textSub?.cancel();
_textSub = stream.listen( _textSub = stream.listen(
(text) async { (text) async {