diff --git a/lib/widgets/voice_mic_button.dart b/lib/widgets/voice_mic_button.dart new file mode 100644 index 0000000..1476993 --- /dev/null +++ b/lib/widgets/voice_mic_button.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; + +import '../providers/voice_provider.dart'; + +/// Animated mic button that reflects the current [VoiceMode]. +/// +/// - idle: muted background, mic_none icon +/// - recording: red with pulsing shadow ring +/// - transcribing: indigo with spinner +/// - playing: indigo with volume_up icon +class VoiceMicButton extends StatefulWidget { + final VoiceMode mode; + final bool voiceModeActive; + final VoidCallback? onTap; + + const VoiceMicButton({ + super.key, + required this.mode, + required this.voiceModeActive, + this.onTap, + }); + + @override + State createState() => _VoiceMicButtonState(); +} + +class _VoiceMicButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _pulseController; + late Animation _pulseAnimation; + + @override + void initState() { + super.initState(); + _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 900), + )..repeat(reverse: true); + _pulseAnimation = Tween(begin: 1.0, end: 1.25).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _pulseController.dispose(); + super.dispose(); + } + + Color _bgColor(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return switch (widget.mode) { + VoiceMode.recording => const Color(0xFFEF4444), + VoiceMode.transcribing || VoiceMode.playing => cs.primary, + VoiceMode.idle => cs.surfaceContainerHighest, + }; + } + + Widget _icon(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final iconColor = widget.mode == VoiceMode.idle + ? cs.onSurfaceVariant + : Colors.white; + + return switch (widget.mode) { + VoiceMode.transcribing => SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: iconColor, + ), + ), + VoiceMode.playing => Icon(Icons.volume_up, color: iconColor, size: 20), + _ => Icon(Icons.mic, color: iconColor, size: 20), + }; + } + + @override + Widget build(BuildContext context) { + final isRecording = widget.mode == VoiceMode.recording; + + final button = Material( + color: _bgColor(context), + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + onTap: widget.onTap, + child: SizedBox( + width: 40, + height: 40, + child: Center(child: _icon(context)), + ), + ), + ); + + if (!isRecording) return button; + + return AnimatedBuilder( + animation: _pulseAnimation, + builder: (_, child) => Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: const Color(0xFFEF4444).withValues(alpha: 0.35), + blurRadius: 8 * _pulseAnimation.value, + spreadRadius: 2 * _pulseAnimation.value, + ), + ], + ), + child: child, + ), + child: button, + ); + } +}