import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:latlong2/latlong.dart'; import '../api/mapflow_api.dart'; import '../models/place_models.dart'; final placeControllerProvider = AsyncNotifierProvider(PlaceController.new); class PlaceState { const PlaceState({ required this.selectedTrait, required this.places, required this.selectedPlaceId, required this.currentUser, required this.hasTelegramAuth, required this.reviewDraft, }); final PlaceTrait selectedTrait; final List places; final String? selectedPlaceId; final AppUser? currentUser; final bool hasTelegramAuth; final VoiceReviewDraft reviewDraft; List get recommendations { final ranked = [...places] ..sort((a, b) { final aScore = a.traits.contains(selectedTrait) ? 1 : 0; final bScore = b.traits.contains(selectedTrait) ? 1 : 0; return bScore.compareTo(aScore); }); return ranked.take(4).toList(); } PlaceRecommendation? get selectedPlace { for (final place in places) { if (place.id == selectedPlaceId) { return place; } } return recommendations.isEmpty ? null : recommendations.first; } PlaceState copyWith({ PlaceTrait? selectedTrait, List? places, String? selectedPlaceId, AppUser? currentUser, bool? hasTelegramAuth, VoiceReviewDraft? reviewDraft, }) { return PlaceState( selectedTrait: selectedTrait ?? this.selectedTrait, places: places ?? this.places, selectedPlaceId: selectedPlaceId ?? this.selectedPlaceId, currentUser: currentUser ?? this.currentUser, hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth, reviewDraft: reviewDraft ?? this.reviewDraft, ); } } class PlaceController extends AsyncNotifier { final _api = MapflowApi(); @override Future build() async { final currentUser = await _api.authenticateTelegram(); final places = await _api.fetchPlaces(); return PlaceState( selectedTrait: PlaceTrait.calm, places: places, selectedPlaceId: places.isEmpty ? null : places.first.id, currentUser: currentUser, hasTelegramAuth: _api.hasTelegramAuth, reviewDraft: const VoiceReviewDraft( placeName: '', duration: Duration.zero, extractedTraits: {}, evidence: [], ), ); } void selectTrait(PlaceTrait trait) { final value = state.requireValue; PlaceRecommendation? next; for (final place in value.places) { if (place.traits.contains(trait)) { next = place; break; } } state = AsyncData( value.copyWith(selectedTrait: trait, selectedPlaceId: next?.id), ); } void selectPlace(String placeId) { final value = state.requireValue; state = AsyncData(value.copyWith(selectedPlaceId: placeId)); } void setReviewPlace(String placeName) { final value = state.requireValue; state = AsyncData( value.copyWith( reviewDraft: value.reviewDraft.copyWith(placeName: placeName), ), ); } void setReviewDuration(Duration duration) { final value = state.requireValue; state = AsyncData( value.copyWith( reviewDraft: value.reviewDraft.copyWith(duration: duration), ), ); } Future publishReview({LatLng? coordinate}) async { final value = state.requireValue; if (!value.hasTelegramAuth) { throw StateError('Открой через Telegram, чтобы оставить голос.'); } final draft = value.reviewDraft; final placeName = draft.placeName.trim().isEmpty ? 'Место на карте' : draft.placeName.trim(); final point = coordinate ?? const LatLng(10.7729, 106.7004); await _api.createVoiceExperience( googlePlaceId: 'manual-${point.latitude}-${point.longitude}-$placeName', googleName: placeName, coordinate: point, durationSeconds: draft.duration.inSeconds, audioObjectKey: 'web-recording-${DateTime.now().microsecondsSinceEpoch}', ); final places = await _api.fetchPlaces(); final selectedPlace = places.isEmpty ? null : places.first.id; state = AsyncData( value.copyWith( places: places, selectedPlaceId: selectedPlace, reviewDraft: const VoiceReviewDraft( placeName: '', duration: Duration.zero, extractedTraits: {}, evidence: [], ), ), ); } }