diff --git a/lib/screens/mapflow_shell.dart b/lib/screens/mapflow_shell.dart index ab55660..39022cb 100644 --- a/lib/screens/mapflow_shell.dart +++ b/lib/screens/mapflow_shell.dart @@ -1090,7 +1090,7 @@ class _VoiceStep extends StatelessWidget { @override Widget build(BuildContext context) { - final showNext = canContinue && !isRecording; + final canFinish = canContinue; return Column( children: [ @@ -1106,8 +1106,11 @@ class _VoiceStep extends StatelessWidget { ), const SizedBox(height: 10), ], + _VoiceProgressGrid(progress: informationProgress), + const SizedBox(height: 18), Expanded( - child: Center( + child: Align( + alignment: Alignment.bottomCenter, child: _LibraryWaveSurface( controller: waveController, active: isRecording, @@ -1125,40 +1128,76 @@ class _VoiceStep extends StatelessWidget { size: 22, ), ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 180), - child: showNext - ? Padding( - key: const ValueKey('next'), - padding: const EdgeInsets.only(bottom: 14), - child: FilledButton( - style: FilledButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: const Color(0xFF090613), - ), - onPressed: isSubmitting ? null : onNext, - child: Text(isSubmitting ? 'Отправляем' : 'Далее'), - ), - ) - : const SizedBox.shrink(key: ValueKey('empty-next')), - ), _VoiceRecordButton( progress: informationProgress, liveLevel: liveLevel, isRecording: isRecording, + canFinish: canFinish, enabled: hasTelegramAuth && !isSubmitting, - onPressed: onToggleRecording, + onPressed: canFinish ? onNext : onToggleRecording, ), ], ); } } +class _VoiceProgressGrid extends StatelessWidget { + const _VoiceProgressGrid({required this.progress}); + + final double progress; + + @override + Widget build(BuildContext context) { + const columns = 14; + const rows = 3; + const total = columns * rows; + final filled = (progress.clamp(0.0, 1.0) * total).round(); + + return SizedBox( + width: 230, + child: Wrap( + spacing: 5, + runSpacing: 5, + alignment: WrapAlignment.center, + children: [ + for (var index = 0; index < total; index++) + AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOutCubic, + width: 10, + height: 10, + decoration: BoxDecoration( + color: _gridOrder(index, total) < filled + ? const Color(0xFFFF2D75) + : Colors.white.withValues(alpha: 0.11), + borderRadius: BorderRadius.circular(3), + boxShadow: _gridOrder(index, total) < filled + ? [ + BoxShadow( + color: const Color( + 0xFFFF2D75, + ).withValues(alpha: 0.34), + blurRadius: 12, + spreadRadius: 1, + ), + ] + : null, + ), + ), + ], + ), + ); + } + + static int _gridOrder(int index, int total) => ((index + 7) * 29) % total; +} + class _VoiceRecordButton extends StatefulWidget { const _VoiceRecordButton({ required this.progress, required this.liveLevel, required this.isRecording, + required this.canFinish, required this.enabled, required this.onPressed, }); @@ -1166,8 +1205,9 @@ class _VoiceRecordButton extends StatefulWidget { final double progress; final double liveLevel; final bool isRecording; + final bool canFinish; final bool enabled; - final Future Function() onPressed; + final VoidCallback onPressed; @override State<_VoiceRecordButton> createState() => _VoiceRecordButtonState(); @@ -1264,7 +1304,7 @@ class _VoiceRecordButtonState extends State<_VoiceRecordButton> width: 120, height: 120, child: FilledButton( - onPressed: widget.enabled ? () => widget.onPressed() : null, + onPressed: widget.enabled ? widget.onPressed : null, style: FilledButton.styleFrom( backgroundColor: Colors.white, foregroundColor: const Color(0xFF090613), @@ -1275,7 +1315,11 @@ class _VoiceRecordButtonState extends State<_VoiceRecordButton> elevation: 0, ), child: Icon( - widget.isRecording ? Icons.pause_rounded : Icons.mic_rounded, + widget.canFinish + ? Icons.check_rounded + : widget.isRecording + ? Icons.pause_rounded + : Icons.mic_rounded, size: 48, ), ),