347 lines
10 KiB
Dart
347 lines
10 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:be_happy/core/result.dart';
|
||
import 'package:be_happy/domain/entities/client_notification.dart';
|
||
import 'package:be_happy/domain/entities/map_settings.dart';
|
||
import 'package:be_happy/domain/usecase/check_user_usecase.dart';
|
||
import 'package:be_happy/domain/usecase/get_all_zones_usecase.dart';
|
||
import 'package:be_happy/domain/usecase/get_client_orders_usecase.dart';
|
||
import 'package:be_happy/domain/usecase/get_map_settings_usecase.dart';
|
||
import 'package:be_happy/domain/usecase/get_notifications_stream_usecase.dart';
|
||
import 'package:be_happy/domain/usecase/logout_usecase.dart';
|
||
import 'package:be_happy/presentation/viewmodel/splash_bloc.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
|
||
import '../../domain/entities/point.dart';
|
||
import '../../domain/entities/scooter.dart';
|
||
import '../../domain/entities/scooter_order.dart';
|
||
import '../../domain/entities/zone.dart';
|
||
import '../../domain/usecase/get_available_scooters_usecase.dart';
|
||
import '../../domain/usecase/get_profile_usecase.dart';
|
||
import '../event/map_event.dart';
|
||
import '../event/spalsh_event.dart';
|
||
import '../state/map_state.dart';
|
||
import 'package:maps_toolkit/maps_toolkit.dart' as mt;
|
||
|
||
class MapBloc extends Bloc<ScooterEvent, ScooterState> {
|
||
final GetAvailableScootersUsecase getScootersUsecase;
|
||
final GetClientOrdersUsecase getClientOrdersUsecase;
|
||
final GetAllZonesUsecase getAllZonesUsecase;
|
||
final GetMapSettingsUsecase getMapSettingsUsecase;
|
||
final GetNotificationsStreamUseCase getNotificationsStreamUseCase;
|
||
final GetProfileUseCase getProfileUseCase;
|
||
final CheckUserUseCase checkUserUseCase;
|
||
final LogoutUseCase logoutUseCase;
|
||
final SplashBloc splashBloc;
|
||
StreamSubscription<ClientNotification>? _notificationSubscription;
|
||
|
||
Future<List<Zone>>? _zonesFuture;
|
||
|
||
MapBloc(
|
||
this.getAllZonesUsecase,
|
||
this.getScootersUsecase,
|
||
this.getClientOrdersUsecase,
|
||
this.getMapSettingsUsecase,
|
||
this.getNotificationsStreamUseCase,
|
||
this.getProfileUseCase,
|
||
this.checkUserUseCase,
|
||
this.logoutUseCase,
|
||
this.splashBloc,
|
||
) : super(ScooterState(isGeomarksShowed: true)) {
|
||
on<FetchScooters>(_onFetchScooters);
|
||
on<UpdateMap>(_onUpdateMap);
|
||
on<UpdateUserLocation>(_onUpdateUserLocation);
|
||
on<NotificationReceived>(_onNotificationReceived);
|
||
on<FetchProfileData>(_onFetchProfileData);
|
||
on<CheckUser>(_onCheckUser);
|
||
on<LogoutPressed>(_onLogoutPressed);
|
||
on<FocusOnScooter>(_onFocusOnScooter);
|
||
on<ClearMapPlacemarks>(_onClearMapPlacemarks);
|
||
on<ClearMapFocus>(_onClearMapFocus);
|
||
}
|
||
|
||
Future<List<Zone>> _getZones() async {
|
||
_zonesFuture ??= getAllZonesUsecase();
|
||
|
||
try {
|
||
return await _zonesFuture!;
|
||
} catch (e) {
|
||
// Не кешируем неудачную попытку — иначе сбой первого запроса
|
||
// (сеть/авторизация) навсегда "выключает" зоны до перезапуска
|
||
// приложения. Следующий FetchScooters/UpdateMap попробует заново.
|
||
_zonesFuture = null;
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
List<Zone> _filterZones(List<Zone> zones, MapSettings settings) {
|
||
List<Zone> filteredZones = [];
|
||
|
||
if (settings.all_zones) {
|
||
if (settings.parking_zones) {
|
||
filteredZones.addAll(zones.where((el) => el.type == "Finish"));
|
||
}
|
||
if (settings.restricted_parking_zones) {
|
||
filteredZones.addAll(zones.where((el) => el.type == "Drive"));
|
||
}
|
||
if (settings.restricted_driving_zones) {
|
||
filteredZones.addAll(zones.where((el) => el.type == "NotDrive"));
|
||
}
|
||
}
|
||
|
||
return filteredZones;
|
||
}
|
||
|
||
void startNotificationStream() {
|
||
_notificationSubscription?.cancel();
|
||
|
||
_notificationSubscription = getNotificationsStreamUseCase().listen(
|
||
(notification) {
|
||
add(NotificationReceived(notification));
|
||
},
|
||
onError: (error) {
|
||
print(" SSE BLOC ERROR: $error");
|
||
_handleReconnect();
|
||
},
|
||
onDone: () {
|
||
print(" SSE Stream closed by server (onDone)");
|
||
_handleReconnect();
|
||
},
|
||
cancelOnError: true,
|
||
);
|
||
}
|
||
|
||
void _handleReconnect() {
|
||
if (isClosed) return;
|
||
|
||
Future.delayed(const Duration(seconds: 5), () {
|
||
if (!isClosed) {
|
||
print(" Attempting to reconnect to SSE...");
|
||
startNotificationStream();
|
||
}
|
||
});
|
||
}
|
||
|
||
void stopNotificationStream() {
|
||
_notificationSubscription?.cancel();
|
||
_notificationSubscription = null;
|
||
}
|
||
|
||
Future<void> _onFetchScooters(
|
||
FetchScooters event,
|
||
Emitter<ScooterState> emit,
|
||
) async {
|
||
emit(state.copyWith(status: ScooterStatus.loading));
|
||
|
||
try {
|
||
final results = await Future.wait([
|
||
getScootersUsecase(event.areaScooters, 0, 100),
|
||
_getZones(),
|
||
getMapSettingsUsecase(),
|
||
getClientOrdersUsecase(),
|
||
]);
|
||
|
||
final scooters = results[0] as List<Scooter>;
|
||
final zones = results[1] as List<Zone>;
|
||
final settings = results[2] as MapSettings;
|
||
final orders = results[3];
|
||
|
||
final filteredZones = _filterZones(zones, settings);
|
||
|
||
print(
|
||
" FETCH: filteredZones.length = ${filteredZones.length}, all_zones=${settings.all_zones}",
|
||
);
|
||
|
||
emit(
|
||
state.copyWith(
|
||
status: ScooterStatus.success,
|
||
scooters: scooters,
|
||
// reservedScooters: reservedScooters,
|
||
zones: filteredZones,
|
||
area: event.area,
|
||
areaScooters: event.areaScooters,
|
||
isGeomarksShowed: settings.all_placemarks,
|
||
),
|
||
);
|
||
} catch (e) {
|
||
print("FETCH ERROR: $e");
|
||
emit(
|
||
state.copyWith(
|
||
status: ScooterStatus.failure,
|
||
errorMessage: e.toString(),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> _onUpdateMap(UpdateMap event, Emitter<ScooterState> emit) async {
|
||
emit(state.copyWith(status: ScooterStatus.loading));
|
||
|
||
try {
|
||
final results = await Future.wait([
|
||
getScootersUsecase(state.areaScooters, 0, 100),
|
||
_getZones(),
|
||
getMapSettingsUsecase(),
|
||
]);
|
||
|
||
final scooters = results[0] as List<Scooter>;
|
||
final zones = results[1] as List<Zone>;
|
||
final settings = results[2] as MapSettings;
|
||
|
||
final filteredZones = _filterZones(zones, settings);
|
||
|
||
print(
|
||
" UPDATE MAP: filteredZones.length = ${filteredZones.length}, all_zones=${settings.all_zones}",
|
||
);
|
||
|
||
emit(
|
||
state.copyWith(
|
||
status: ScooterStatus.success,
|
||
scooters: scooters,
|
||
zones: filteredZones,
|
||
isGeomarksShowed: settings.all_placemarks,
|
||
),
|
||
);
|
||
} catch (e) {
|
||
print("UPDATE ERROR: $e");
|
||
emit(
|
||
state.copyWith(
|
||
status: ScooterStatus.failure,
|
||
errorMessage: e.toString(),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
FutureOr<void> _onUpdateUserLocation(
|
||
UpdateUserLocation event,
|
||
Emitter<ScooterState> emit,
|
||
) {
|
||
print("USER LOCATION UPDATED EVENT start");
|
||
state.zones.forEach((z) {
|
||
if (checkUserInZone(Point(event.latitude, event.longitude), z.points)) {
|
||
print("USER IN ZONE - ${z.type}");
|
||
}
|
||
});
|
||
}
|
||
|
||
FutureOr<void> _onNotificationReceived(
|
||
NotificationReceived event,
|
||
Emitter<ScooterState> emit,
|
||
) {
|
||
print("NOTIFICATION RECEIVED: ${event.notification.content}");
|
||
emit(state.copyWith(lastNotification: event.notification));
|
||
}
|
||
|
||
bool checkUserInZone(Point userPos, List<Point> zonePoints) {
|
||
List<mt.LatLng> polygon = zonePoints
|
||
.map((p) => mt.LatLng(p.latitude, p.longitude))
|
||
.toList();
|
||
|
||
mt.LatLng userLatLng = mt.LatLng(userPos.latitude, userPos.longitude);
|
||
|
||
return mt.PolygonUtil.containsLocation(userLatLng, polygon, true);
|
||
}
|
||
|
||
FutureOr<void> _onFetchProfileData(
|
||
FetchProfileData event,
|
||
Emitter<ScooterState> emit,
|
||
) async {
|
||
try {
|
||
final profile = await getProfileUseCase();
|
||
|
||
emit(
|
||
state.copyWith(phoneNumber: profile.phone, balance: profile.balance),
|
||
);
|
||
} catch (e) {
|
||
emit(
|
||
state.copyWith(
|
||
status: ScooterStatus.failure,
|
||
errorMessage: "FetchProfileData for SideMenu: ${e.toString()}",
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
FutureOr<void> _onLogoutPressed(
|
||
LogoutPressed event,
|
||
Emitter<ScooterState> emit,
|
||
) async {
|
||
await logoutUseCase();
|
||
stopNotificationStream();
|
||
_zonesFuture = null;
|
||
splashBloc.add(AuthLogoutRequested());
|
||
}
|
||
|
||
FutureOr<void> _onCheckUser(
|
||
CheckUser event,
|
||
Emitter<ScooterState> emit,
|
||
) async {
|
||
try {
|
||
final flags = await checkUserUseCase();
|
||
|
||
print("flags: $flags");
|
||
|
||
if (flags == null) {
|
||
return;
|
||
}
|
||
|
||
print("check user success");
|
||
|
||
emit(state.copyWith(flags: flags));
|
||
} catch (e) {
|
||
emit(
|
||
state.copyWith(
|
||
status: ScooterStatus.failure,
|
||
errorMessage: "CheckUser: ${e.toString()}",
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
FutureOr<void> _onFocusOnScooter(FocusOnScooter event, Emitter<ScooterState> emit) {
|
||
final updatedReserved = List<Scooter>.from(state.reservedScooters ?? []);
|
||
if (!updatedReserved.any((s) => s.id == event.scooter.id)) {
|
||
updatedReserved.add(event.scooter);
|
||
}
|
||
|
||
emit(state.copyWith(
|
||
selectedScooterForFocus: event.scooter,
|
||
reservedScooters: updatedReserved,
|
||
));
|
||
}
|
||
|
||
|
||
|
||
FutureOr<void> _onClearMapPlacemarks(ClearMapPlacemarks event, Emitter<ScooterState> emit) async {
|
||
try{
|
||
final orders = await getClientOrdersUsecase();
|
||
|
||
List<Scooter> updatedReservedScooters = [];
|
||
|
||
if (orders is Success<List<ScooterOrder>>) {
|
||
print("FETCH: orders.data.length = ${orders.data?.length}");
|
||
|
||
updatedReservedScooters = orders.data?.map((order) {
|
||
return order.scooter;
|
||
}).toList() ?? [];
|
||
}
|
||
|
||
emit(state.copyWith(
|
||
reservedScooters: updatedReservedScooters,
|
||
));
|
||
} catch (e) {
|
||
print("Error in _onClearMapPlacemarks: $e");
|
||
emit(state.copyWith(
|
||
status: ScooterStatus.failure,
|
||
errorMessage: e.toString(),
|
||
));
|
||
}
|
||
}
|
||
|
||
FutureOr<void> _onClearMapFocus(ClearMapFocus event, Emitter<ScooterState> emit) {
|
||
emit(state.copyWith(
|
||
selectedScooterForFocus: null,
|
||
));
|
||
}
|
||
} |