Files
flutter/lib/state/place_controller.dart
Ruslan Bakiev 238521b11b
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 3m26s
Load map places from backend
2026-05-08 15:54:15 +07:00

148 lines
4.0 KiB
Dart

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, PlaceState>(PlaceController.new);
class PlaceState {
const PlaceState({
required this.intent,
required this.places,
required this.selectedPlaceId,
required this.reviewDraft,
});
final UserIntent intent;
final List<PlaceRecommendation> places;
final String? selectedPlaceId;
final VoiceReviewDraft reviewDraft;
List<PlaceRecommendation> get recommendations {
final wanted = intent.traits;
final ranked = [...places]
..sort((a, b) {
final aScore = a.traits.intersection(wanted).length;
final bScore = b.traits.intersection(wanted).length;
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({
UserIntent? intent,
List<PlaceRecommendation>? places,
String? selectedPlaceId,
VoiceReviewDraft? reviewDraft,
}) {
return PlaceState(
intent: intent ?? this.intent,
places: places ?? this.places,
selectedPlaceId: selectedPlaceId ?? this.selectedPlaceId,
reviewDraft: reviewDraft ?? this.reviewDraft,
);
}
}
class PlaceController extends AsyncNotifier<PlaceState> {
final _api = MapflowApi();
@override
Future<PlaceState> build() async {
final places = await _api.fetchPlaces();
return PlaceState(
intent: UserIntent.exhale,
places: places,
selectedPlaceId: places.isEmpty ? null : places.first.id,
reviewDraft: const VoiceReviewDraft(
placeName: '',
duration: Duration.zero,
extractedTraits: {},
suggestedIntents: {},
evidence: [],
),
);
}
void selectIntent(UserIntent intent) {
final value = state.requireValue;
PlaceRecommendation? next;
for (final place in value.places) {
if (place.traits.intersection(intent.traits).isNotEmpty) {
next = place;
break;
}
}
state = AsyncData(
value.copyWith(intent: intent, 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<void> publishReview({LatLng? coordinate}) async {
final value = state.requireValue;
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: {},
suggestedIntents: {},
evidence: [],
),
),
);
}
}