diff --git a/lib/screens/mapflow_shell.dart b/lib/screens/mapflow_shell.dart index 2826572..1f7e303 100644 --- a/lib/screens/mapflow_shell.dart +++ b/lib/screens/mapflow_shell.dart @@ -1034,74 +1034,147 @@ class _VoiceStep extends StatelessWidget { @override Widget build(BuildContext context) { - return _StepLayout( - body: Column( - children: [ - const Spacer(), - Text( - placeName.trim().isEmpty ? 'Место' : placeName.trim(), - textAlign: TextAlign.center, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w900), - ), - const SizedBox(height: 8), - Text(hasTelegramAuth ? 'Минимум $minimumSeconds секунд' : ''), - const SizedBox(height: 26), - _VoiceWave(samples: samples, active: isRecording), - const SizedBox(height: 24), - SizedBox( - width: 132, - height: 132, - child: FilledButton( - onPressed: isSubmitting || !hasTelegramAuth - ? null - : () => onToggleRecording(), - style: FilledButton.styleFrom(shape: const CircleBorder()), - child: Icon(isRecording ? Icons.stop : Icons.mic, size: 54), + final progress = (seconds / minimumSeconds).clamp(0.0, 1.0); + final showNext = canContinue && !isRecording; + + return Column( + children: [ + Text( + placeName.trim().isEmpty ? 'Место' : placeName.trim(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w900), + ), + const SizedBox(height: 10), + Expanded( + child: Center( + child: _VoiceWave( + samples: samples, + active: isRecording, + progress: progress, ), ), - const SizedBox(height: 22), - Text( - time, - style: Theme.of( - context, - ).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w900), + ), + if (!micAllowed) + const Padding( + padding: EdgeInsets.only(bottom: 10), + child: Icon(Icons.mic_off_outlined, size: 22), ), - const SizedBox(height: 12), - LinearProgressIndicator( - value: (seconds / minimumSeconds).clamp(0.0, 1.0), + AnimatedSwitcher( + duration: const Duration(milliseconds: 180), + child: showNext + ? Padding( + key: const ValueKey('next'), + padding: const EdgeInsets.only(bottom: 14), + child: FilledButton( + onPressed: isSubmitting ? null : onNext, + child: Text(isSubmitting ? 'Отправляем' : 'Далее'), + ), + ) + : const SizedBox.shrink(key: ValueKey('empty-next')), + ), + _VoiceRecordButton( + time: time, + progress: progress, + isRecording: isRecording, + enabled: hasTelegramAuth && !isSubmitting, + onPressed: onToggleRecording, + ), + ], + ); + } +} + +class _VoiceRecordButton extends StatelessWidget { + const _VoiceRecordButton({ + required this.time, + required this.progress, + required this.isRecording, + required this.enabled, + required this.onPressed, + }); + + final String time; + final double progress; + final bool isRecording; + final bool enabled; + final Future Function() onPressed; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return SizedBox( + width: 148, + height: 148, + child: Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 148, + height: 148, + child: CircularProgressIndicator( + value: progress, + strokeWidth: 8, + strokeCap: StrokeCap.round, + backgroundColor: colorScheme.primary.withValues(alpha: 0.14), + ), + ), + SizedBox( + width: 118, + height: 118, + child: FilledButton( + onPressed: enabled ? () => onPressed() : null, + style: FilledButton.styleFrom( + shape: const CircleBorder(), + padding: EdgeInsets.zero, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(isRecording ? Icons.stop : Icons.mic, size: 42), + const SizedBox(height: 6), + Text( + time, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: colorScheme.onPrimary, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + ), ), - if (!micAllowed) ...[ - const SizedBox(height: 12), - const Icon(Icons.mic_off_outlined, size: 22), - ], - const Spacer(), ], ), - action: FilledButton( - onPressed: canContinue && !isSubmitting ? onNext : null, - child: Text(isSubmitting ? 'Отправляем' : 'Далее'), - ), ); } } class _VoiceWave extends StatelessWidget { - const _VoiceWave({required this.samples, required this.active}); + const _VoiceWave({ + required this.samples, + required this.active, + required this.progress, + }); final List samples; final bool active; + final double progress; @override Widget build(BuildContext context) { return SizedBox( - height: 112, + height: double.infinity, width: double.infinity, child: CustomPaint( painter: _VoiceWavePainter( samples: samples, active: active, + progress: progress, color: Theme.of(context).colorScheme.primary, ), ), @@ -1113,16 +1186,19 @@ class _VoiceWavePainter extends CustomPainter { const _VoiceWavePainter({ required this.samples, required this.active, + required this.progress, required this.color, }); final List samples; final bool active; + final double progress; final Color color; @override void paint(Canvas canvas, Size size) { final centerY = size.height / 2; + final quietScale = active ? 1.0 : 0.42 + progress * 0.32; final top = []; final bottom = []; @@ -1131,7 +1207,8 @@ class _VoiceWavePainter extends CustomPainter { ? 0.0 : index / (samples.length - 1) * size.width; final envelope = math.sin(index / (samples.length - 1) * math.pi); - final amplitude = samples[index] * envelope * size.height * 0.46; + final amplitude = + samples[index] * quietScale * envelope * size.height * 0.46; final yTop = centerY - amplitude; final yBottom = centerY + amplitude;