Require place before voice review
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m8s

This commit is contained in:
Ruslan Bakiev
2026-05-08 20:31:36 +07:00
parent 929d3a46d3
commit 56703c887f

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
@@ -719,7 +720,6 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
static const _minimumVoiceSeconds = 30;
Timer? _timer;
late final TextEditingController _placeNameController;
var _step = 0;
var _seconds = 0;
var _recording = false;
@@ -729,13 +729,11 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
@override
void initState() {
super.initState();
_placeNameController = TextEditingController();
}
@override
void dispose() {
_timer?.cancel();
_placeNameController.dispose();
super.dispose();
}
@@ -743,6 +741,9 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
setState(() => _recording = !_recording);
if (_recording) {
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
if (!mounted) {
return;
}
setState(() => _seconds += 1);
ref
.read(placeControllerProvider.notifier)
@@ -753,47 +754,45 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
}
}
String get _time =>
'${(_seconds ~/ 60).toString().padLeft(2, '0')}:'
'${(_seconds % 60).toString().padLeft(2, '0')}';
@override
Widget build(BuildContext context) {
final controller = ref.read(placeControllerProvider.notifier);
final places = ref.watch(placeControllerProvider).value?.places ?? [];
final time =
'${(_seconds ~/ 60).toString().padLeft(2, '0')}:'
'${(_seconds % 60).toString().padLeft(2, '0')}';
final content = switch (_step) {
0 => _IntroStep(onNext: () => setState(() => _step = 1)),
1 => _PlaceStep(
places: places,
controller: _placeNameController,
onSelect: (place) {
setState(() {
_selectedPlace = place;
_placeNameController.text = place.name;
_step = 2;
});
controller.setReviewPlace(place.name);
},
onManualNext: () {
controller.setReviewPlace(_placeNameController.text);
setState(() => _step = 2);
},
),
_ => _VoiceStep(
placeName: _placeNameController.text,
placeName: _selectedPlace?.name ?? '',
hasTelegramAuth: widget.hasTelegramAuth,
seconds: _seconds,
minimumSeconds: _minimumVoiceSeconds,
time: time,
time: _time,
isRecording: _recording,
isSubmitting: _submitting,
canContinue: widget.hasTelegramAuth && _seconds >= _minimumVoiceSeconds,
onToggleRecording: _toggleRecording,
onNext: () async {
final selectedPlace = _selectedPlace;
if (selectedPlace == null) {
return;
}
setState(() => _submitting = true);
final coordinate = _selectedPlace?.coordinate ?? widget.coordinate;
controller.setReviewPlace(_placeNameController.text);
await controller.publishReview(coordinate: coordinate);
controller.setReviewPlace(selectedPlace.name);
await controller.publishReview(coordinate: selectedPlace.coordinate);
if (!context.mounted) {
return;
}
@@ -873,66 +872,56 @@ class _IntroStep extends StatelessWidget {
}
class _PlaceStep extends StatelessWidget {
const _PlaceStep({
required this.places,
required this.controller,
required this.onSelect,
required this.onManualNext,
});
const _PlaceStep({required this.places, required this.onSelect});
final List<PlaceRecommendation> places;
final TextEditingController controller;
final ValueChanged<PlaceRecommendation> onSelect;
final VoidCallback onManualNext;
@override
Widget build(BuildContext context) {
return _StepLayout(
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Место',
'Выбери место рядом',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w900,
letterSpacing: 0,
),
),
const SizedBox(height: 16),
TextField(
controller: controller,
decoration: const InputDecoration(hintText: 'Название места'),
textInputAction: TextInputAction.done,
),
const SizedBox(height: 12),
Expanded(
child: ListView.separated(
itemCount: places.length,
separatorBuilder: (_, _) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final place = places[index];
return ListTile(
onTap: () => onSelect(place),
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 8,
child: places.isEmpty
? const Center(
child: Icon(Icons.location_off_outlined, size: 42),
)
: ListView.separated(
itemCount: places.length,
separatorBuilder: (_, _) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final place = places[index];
return ListTile(
onTap: () => onSelect(place),
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 8,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
tileColor: const Color(0xFFFFFBF5),
leading: const Icon(Icons.place_outlined),
title: Text(
place.name,
style: const TextStyle(fontWeight: FontWeight.w800),
),
);
},
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
tileColor: const Color(0xFFFFFBF5),
leading: const Icon(Icons.place_outlined),
title: Text(
place.name,
style: const TextStyle(fontWeight: FontWeight.w800),
),
);
},
),
),
],
),
action: FilledButton(onPressed: onManualNext, child: const Text('Далее')),
);
}
}
@@ -983,6 +972,12 @@ class _VoiceStep extends StatelessWidget {
textAlign: TextAlign.center,
),
const SizedBox(height: 26),
if (isRecording || seconds > 0) ...[
_VoiceWave(seconds: seconds, active: isRecording),
const SizedBox(height: 20),
] else ...[
const SizedBox(height: 70),
],
SizedBox(
width: 132,
height: 132,
@@ -1016,6 +1011,49 @@ class _VoiceStep extends StatelessWidget {
}
}
class _VoiceWave extends StatelessWidget {
const _VoiceWave({required this.seconds, required this.active});
final int seconds;
final bool active;
@override
Widget build(BuildContext context) {
final color = Theme.of(context).colorScheme.primary;
return SizedBox(
height: 50,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
for (var index = 0; index < 23; index++)
AnimatedContainer(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
width: 4,
height: _barHeight(index),
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
color: color.withValues(alpha: active ? 0.88 : 0.42),
borderRadius: BorderRadius.circular(8),
),
),
],
),
);
}
double _barHeight(int index) {
final phase = seconds * 0.72 + index * 0.58;
final height = 16.0 + (math.sin(phase).abs() * 28.0);
if (active) {
return height;
}
return height * 0.55;
}
}
class _StepLayout extends StatelessWidget {
const _StepLayout({required this.body, this.action});