Files
flutter/lib/state/place_controller.dart
Ruslan Bakiev 697f029ad2
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 4m2s
Separate browsing filters from review radius
2026-05-14 22:34:59 +07:00

215 lines
5.9 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:latlong2/latlong.dart';
import '../api/mapflow_api.dart';
import '../location/current_location.dart';
import '../models/place_models.dart';
final placeControllerProvider =
AsyncNotifierProvider<PlaceController, PlaceState>(PlaceController.new);
const _unset = Object();
class PlaceState {
const PlaceState({
required this.selectedTrait,
required this.places,
required this.selectedPlaceId,
required this.currentUser,
required this.hasTelegramAuth,
required this.userCoordinate,
required this.reviewDraft,
});
static final _distance = Distance();
final PlaceTrait? selectedTrait;
final List<PlaceRecommendation> places;
final String? selectedPlaceId;
final AppUser? currentUser;
final bool hasTelegramAuth;
final LatLng? userCoordinate;
final VoiceReviewDraft reviewDraft;
List<PlaceRecommendation> get recommendations {
final selected = selectedTrait;
final matchingPlaces = selected == null
? places
: places.where((place) => place.traits.contains(selected));
final coordinate = userCoordinate;
if (coordinate == null) {
return matchingPlaces.toList();
}
final ranked = [...matchingPlaces]
..sort((a, b) {
return _distance(
coordinate,
a.coordinate,
).compareTo(_distance(coordinate, b.coordinate));
});
return ranked;
}
PlaceRecommendation? get selectedPlace {
final visiblePlaces = recommendations;
for (final place in visiblePlaces) {
if (place.id == selectedPlaceId) {
return place;
}
}
return visiblePlaces.isEmpty ? null : visiblePlaces.first;
}
PlaceState copyWith({
Object? selectedTrait = _unset,
List<PlaceRecommendation>? places,
Object? selectedPlaceId = _unset,
AppUser? currentUser,
bool? hasTelegramAuth,
LatLng? userCoordinate,
VoiceReviewDraft? reviewDraft,
}) {
return PlaceState(
selectedTrait: identical(selectedTrait, _unset)
? this.selectedTrait
: selectedTrait as PlaceTrait?,
places: places ?? this.places,
selectedPlaceId: identical(selectedPlaceId, _unset)
? this.selectedPlaceId
: selectedPlaceId as String?,
currentUser: currentUser ?? this.currentUser,
hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth,
userCoordinate: userCoordinate ?? this.userCoordinate,
reviewDraft: reviewDraft ?? this.reviewDraft,
);
}
}
class PlaceController extends AsyncNotifier<PlaceState> {
final _api = MapflowApi();
final _location = CurrentLocation();
@override
Future<PlaceState> build() async {
if (!_api.hasTelegramAuth) {
return const PlaceState(
selectedTrait: null,
places: [],
selectedPlaceId: null,
currentUser: null,
hasTelegramAuth: false,
userCoordinate: null,
reviewDraft: VoiceReviewDraft(
placeName: '',
duration: Duration.zero,
extractedTraits: {},
evidence: [],
),
);
}
final currentUser = await _api.authenticateTelegram();
final userCoordinate = await _location.resolve();
final places = await _api.fetchPlaces();
return PlaceState(
selectedTrait: null,
places: places,
selectedPlaceId: places.isEmpty ? null : places.first.id,
currentUser: currentUser,
hasTelegramAuth: _api.hasTelegramAuth,
userCoordinate: userCoordinate,
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 clearTrait() {
final value = state.requireValue;
final selectedPlaceId = value.places.isEmpty ? null : value.places.first.id;
state = AsyncData(
value.copyWith(selectedTrait: null, selectedPlaceId: selectedPlaceId),
);
}
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({
required PlaceRecommendation place,
required String audioObjectKey,
required String audioContentBase64,
required String audioMimeType,
}) async {
final value = state.requireValue;
if (!value.hasTelegramAuth) {
throw StateError('Открой через Telegram, чтобы оставить голос.');
}
final draft = value.reviewDraft;
await _api.createVoiceExperience(
googlePlaceId: place.googlePlaceId,
googleName: place.name,
coordinate: place.coordinate,
durationSeconds: draft.duration.inSeconds,
audioObjectKey: audioObjectKey,
audioContentBase64: audioContentBase64,
audioMimeType: audioMimeType,
);
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: [],
),
),
);
}
}