bug fixes

This commit is contained in:
2026-08-29 00:00:09 +03:00
parent 6513a004d9
commit a556dc5736
15 changed files with 361 additions and 271 deletions

View File

@@ -498,6 +498,35 @@ class ApiService {
} }
} }
Future<ZonesResponse?> getAllZones({
int page = 1,
int pageSize = 500,
}) async {
const path = "$baseUrl/zone/available2";
final queryParams = {
'readAll': 'true',
'page': page,
'pageSize': pageSize,
};
try {
final response = await _dio.get(
path,
queryParameters: queryParams,
options: await _getAuthOptions(),
);
if (response.statusCode == 200) {
return ZonesResponse.fromJson(response.data);
}
return null;
} catch (e) {
print("ERROR: getZones failed: $e");
return null;
}
}
Future<TariffsResponse?> getAvailableTariffs({required int scooterId}) async { Future<TariffsResponse?> getAvailableTariffs({required int scooterId}) async {
final url = "$baseUrl/scooterplan/$scooterId/available"; final url = "$baseUrl/scooterplan/$scooterId/available";

View File

@@ -10,60 +10,60 @@ class ZoneRepositoryImpl extends ZoneRepository {
ZoneRepositoryImpl(this._apiService); ZoneRepositoryImpl(this._apiService);
@override @override
Future<List<Zone>> getZones(List<double> area, Future<List<Zone>> getZones(List<double> area, int page, int pageSize) async {
int page,
int pageSize,) async {
final List<Point> list = []; final List<Point> list = [];
final List<Point> list2 = []; final List<Point> list2 = [];
/*list.add(Point(55.182372,30.203347));
list.add(Point(55.173746,30.200257));
list.add(Point(55.172153,30.212531));
list.add(Point(55.179946,30.215964));
list2.add(Point(55.178304,30.227380));
list2.add(Point(55.178059,30.229654));
list2.add(Point(55.191339,30.234718));
list2.add(Point(55.194352,30.234890));
list2.add(Point(55.194377,30.231972));
list2.add(Point(55.192123,30.232744));
final zone = Zone(id: 123,
title: "Zone 01",
description: "description",
type: "Finish",
isActive: true,
shapeType: "polygon",
points: list,
speedLimit: "speedLimit");
final zone2 = Zone(id: 124,
title: "Zone 02",
description: "description",
type: "NotDrive",
isActive: true,
shapeType: "polygon",
points: list2,
speedLimit: "speedLimit");
*/
final responce = await _apiService.getZones( final responce = await _apiService.getZones(
area: area, area: area,
page: page, page: page,
pageSize: pageSize, pageSize: pageSize,
); );
final List<Zone>? zones = responce?.zones; if (responce == null) {
// zones?.add(zone); throw Exception(
// zones?.add(zone2); 'Failed to load zones for area: empty response from server',
);
print("Scooters: ${zones?.first.title}");
if (responce != null) {
return zones ?? List.empty();
} }
return List.empty(); final zones = responce.zones;
print("Zones: ${zones.length}");
return zones;
}
@override
Future<List<Zone>> getAllZones(int page, int pageSize) async {
final List<Point> list = [];
final List<Point> list2 = [];
final responce = await _apiService.getAllZones(
page: page,
pageSize: pageSize,
);
// apiService.getAllZones ловит свои ошибки (сеть/401/5xx) и в этом
// случае возвращает null. Раньше это тихо превращалось в "зон нет" —
// из-за чего сбой запроса выглядел как легитимный пустой ответ и
// (в связке с кешем на уровне MapBloc) навсегда "прятал" зоны на
// время сессии. Бросаем исключение, чтобы вызывающий код (usecase/bloc)
// мог отличить реальный сбой от честного "зон действительно нет"
// и, при необходимости, повторить запрос.
if (responce == null) {
throw Exception('Failed to load zones: empty response from server');
}
final zones = responce.zones;
print(zones.map((z) => '${z.type}:${z.id}'));
for (final z in zones) {
print("Zone ${z.type}:${z.id} geometry.length=${z.geometry.length}");
}
print("Zones: ${zones.length}");
return zones;
} }
} }

View File

@@ -18,6 +18,7 @@ import 'package:be_happy/domain/repositories/zone_repository.dart';
import 'package:be_happy/domain/service/security_service.dart'; import 'package:be_happy/domain/service/security_service.dart';
import 'package:be_happy/domain/usecase/add_payment_card_usecase.dart'; // ← новый import 'package:be_happy/domain/usecase/add_payment_card_usecase.dart'; // ← новый
import 'package:be_happy/domain/usecase/delete_profile_usecase.dart'; import 'package:be_happy/domain/usecase/delete_profile_usecase.dart';
import 'package:be_happy/domain/usecase/get_all_zones_usecase.dart';
import 'package:be_happy/domain/usecase/get_certificates_usecase.dart'; import 'package:be_happy/domain/usecase/get_certificates_usecase.dart';
import 'package:be_happy/domain/usecase/get_scooter_order_route_history_usecase.dart'; import 'package:be_happy/domain/usecase/get_scooter_order_route_history_usecase.dart';
import 'package:be_happy/domain/usecase/is_pin_set_usecase.dart'; import 'package:be_happy/domain/usecase/is_pin_set_usecase.dart';
@@ -239,6 +240,9 @@ Future<void> setupDependencies() async {
getIt.registerSingleton<GetAvailableZonesUsecase>( getIt.registerSingleton<GetAvailableZonesUsecase>(
GetAvailableZonesUsecase(getIt()), GetAvailableZonesUsecase(getIt()),
); );
getIt.registerSingleton<GetAllZonesUsecase>(
GetAllZonesUsecase(getIt()),
);
getIt.registerSingleton<GetMapSettingsUsecase>( getIt.registerSingleton<GetMapSettingsUsecase>(
GetMapSettingsUsecase(getIt()), GetMapSettingsUsecase(getIt()),
); );

View File

@@ -2,7 +2,6 @@ class Tariff {
final int id; final int id;
final String title; final String title;
final String description; final String description;
final String descriptionJson;
final bool isActive; final bool isActive;
final String currency; final String currency;
final double holdPrice; // Старт / бронь final double holdPrice; // Старт / бронь
@@ -16,7 +15,6 @@ class Tariff {
required this.id, required this.id,
required this.title, required this.title,
required this.description, required this.description,
required this.descriptionJson,
required this.isActive, required this.isActive,
required this.currency, required this.currency,
required this.holdPrice, required this.holdPrice,
@@ -35,7 +33,6 @@ class Tariff {
id: json['id'] ?? 0, id: json['id'] ?? 0,
title: json['title'] ?? 'Unknown', title: json['title'] ?? 'Unknown',
description: json['description'] ?? '', description: json['description'] ?? '',
descriptionJson: json['descriptionJson'] ?? '',
isActive: json['isActive'] ?? false, isActive: json['isActive'] ?? false,
currency: currency['currency'] ?? 'BYN', currency: currency['currency'] ?? 'BYN',
holdPrice: (planPrice['hold'] as num?)?.toDouble() ?? 0.0, holdPrice: (planPrice['hold'] as num?)?.toDouble() ?? 0.0,

View File

@@ -1,4 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:be_happy/domain/entities/point.dart'; import 'package:be_happy/domain/entities/point.dart';
class Zone { class Zone {
@@ -8,9 +10,6 @@ class Zone {
final String type; final String type;
final bool isActive; final bool isActive;
final String shapeType; final String shapeType;
// Трехмерная структура для поддержки MultiPolygon и дыр (holes):
// geometry[индекс_полигона][индекс_кольца][индекс_точки]
// индекс_кольца == 0 — внешняя граница, > 0 — внутренние вырезы (дыры)
final List<List<List<Point>>> geometry; final List<List<List<Point>>> geometry;
final String speedLimit; final String speedLimit;
@@ -25,7 +24,6 @@ class Zone {
required this.speedLimit, required this.speedLimit,
}); });
// Геттер для обратной совместимости (возвращает внешнее кольцо первого полигона)
List<Point> get points { List<Point> get points {
if (geometry.isNotEmpty && geometry[0].isNotEmpty) { if (geometry.isNotEmpty && geometry[0].isNotEmpty) {
return geometry[0][0]; return geometry[0][0];
@@ -45,77 +43,64 @@ class Zone {
if (decoded is List && decoded.isNotEmpty) { if (decoded is List && decoded.isNotEmpty) {
if (shapeType == 'Polygon') { if (shapeType == 'Polygon') {
// Структура: [[[lon, lat], [lon, lat], ...], [hole1], ...] final polygonRings = _parseRings(decoded);
List<List<Point>> polygonRings = [];
for (var ring in decoded) {
if (ring is List) {
List<Point> ringPoints = [];
for (var coord in ring) {
if (coord is List && coord.length >= 2) {
// В GeoJSON сначала идет Longitude, затем Latitude
ringPoints.add(Point(
(coord[1] as num).toDouble(),
(coord[0] as num).toDouble(),
));
}
}
if (ringPoints.isNotEmpty) polygonRings.add(ringPoints);
}
}
if (polygonRings.isNotEmpty) parsedGeometry.add(polygonRings); if (polygonRings.isNotEmpty) parsedGeometry.add(polygonRings);
} else if (shapeType == 'MultiPolygon') { } else if (shapeType == 'MultiPolygon') {
// Структура: [[[[lon, lat], ...]], [[[lon, lat], ...]]]
for (var poly in decoded) { for (var poly in decoded) {
if (poly is List) { if (poly is List) {
List<List<Point>> polygonRings = []; final polygonRings = _parseRings(poly);
for (var ring in poly) {
if (ring is List) {
List<Point> ringPoints = [];
for (var coord in ring) {
if (coord is List && coord.length >= 2) {
ringPoints.add(Point(
(coord[1] as num).toDouble(),
(coord[0] as num).toDouble(),
));
}
}
if (ringPoints.isNotEmpty) polygonRings.add(ringPoints);
}
}
if (polygonRings.isNotEmpty) parsedGeometry.add(polygonRings); if (polygonRings.isNotEmpty) parsedGeometry.add(polygonRings);
} }
} }
} else { } else {
// Fallback для LineString или плоских списков final singleRing = _parseCoordinateList(decoded);
List<Point> ringPoints = []; if (singleRing.isNotEmpty) parsedGeometry.add([singleRing]);
for (var coord in decoded) {
if (coord is List && coord.length >= 2) {
ringPoints.add(Point(
(coord[1] as num).toDouble(),
(coord[0] as num).toDouble(),
));
}
}
if (ringPoints.isNotEmpty) parsedGeometry.add([ringPoints]);
} }
} }
} catch (e) { } catch (e, stackTrace) {
print("PARSE ERROR for Zone ID ${json['id']}: $e"); // Replace with your project logger (e.g., debugPrint or Logger)
assert(() {
print("PARSE ERROR for Zone ID ${json['id']}: $e\n$stackTrace");
return true;
}());
} }
return Zone( return Zone(
id: json['id'] ?? 0, id: json['id'] as int? ?? 0,
title: json['title'] ?? 'Unknown', title: json['title'] as String? ?? 'Unknown',
description: json['description'] ?? '', description: json['description'] as String? ?? '',
type: json['type'] ?? '', type: json['type'] as String? ?? '',
isActive: json['isActive'] ?? false, isActive: json['isActive'] as bool? ?? false,
speedLimit: json['speedLimit'] ?? '', speedLimit: json['speedLimit'] as String? ?? '',
shapeType: shapeType, shapeType: shapeType,
geometry: parsedGeometry, geometry: parsedGeometry,
); );
} }
static List<List<Point>> _parseRings(List rawRings) {
List<List<Point>> polygonRings = [];
for (var ring in rawRings) {
if (ring is List) {
final ringPoints = _parseCoordinateList(ring);
if (ringPoints.isNotEmpty) polygonRings.add(ringPoints);
}
}
return polygonRings;
}
static List<Point> _parseCoordinateList(List rawCoords) {
List<Point> ringPoints = [];
for (var coord in rawCoords) {
if (coord is List && coord.length >= 2) {
ringPoints.add(Point(
(coord[1] as num).toDouble(),
(coord[0] as num).toDouble(),
));
}
}
return ringPoints;
}
@override @override
String toString() { String toString() {
return 'Zone{id: $id, title: $title, type: $type, polygonsCount: ${geometry.length}}'; return 'Zone{id: $id, title: $title, type: $type, polygonsCount: ${geometry.length}}';

View File

@@ -2,5 +2,6 @@ import '../entities/zone.dart';
abstract class ZoneRepository { abstract class ZoneRepository {
Future<List<Zone>> getZones(List<double> area, int page, int pageSize); Future<List<Zone>> getZones(List<double> area, int page, int pageSize);
Future<List<Zone>> getAllZones(int page, int pageSize);
} }

View File

@@ -0,0 +1,30 @@
import '../entities/zone.dart';
import '../repositories/zone_repository.dart';
class GetAllZonesUsecase {
final ZoneRepository repository;
GetAllZonesUsecase(this.repository);
static const int _pageSize = 100;
Future<List<Zone>> call() async {
final List<Zone> allZones = [];
int page = 0;
while (true) {
final zones = await repository.getAllZones(page, _pageSize);
if (zones == null || zones.isEmpty) break;
allZones.addAll(zones);
// если пришло меньше, чем размер страницы — это последняя страница
if (zones.length < _pageSize) break;
page++;
}
return allZones;
}
}

View File

@@ -240,7 +240,7 @@ class _ScooterCard extends StatelessWidget {
showArrows: true, showArrows: true,
height: 32, height: 32,
width: double.infinity, width: double.infinity,
fontSize: 12, fontSize: 11,
onTap: () { onTap: () {
context.go('/home/scooter/${scooter.id}', extra: [address, scooter.distance]); context.go('/home/scooter/${scooter.id}', extra: [address, scooter.distance]);
}, },

View File

@@ -123,8 +123,7 @@ class _TariffInfoSheetState extends State<TariffInfoSheet> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: ContentParser.parseContent( children: ContentParser.parseContent(
textJson: widget.tariff.description, text: widget.tariff.description,
text: widget.tariff.descriptionJson,
), ),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),

View File

@@ -265,7 +265,7 @@ class _TariffSheetState extends State<TariffSheet> {
const SizedBox(height: 20), const SizedBox(height: 20),
SizedBox( SizedBox(
height: 150, height: 160,
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.only(left: 20), padding: const EdgeInsets.only(left: 20),

View File

@@ -19,7 +19,7 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
}, },
{ {
'image': 'assets/onboard_2.jpg', 'image': 'assets/onboard_2.jpg',
'title': 'Отсканируйте QR-код или введите номер который указан под козлом', 'title': 'Отсканируйте QR-код или введите номер который указан под кодом',
}, },
{ {
'image': 'assets/onboard_3.png', 'image': 'assets/onboard_3.png',

View File

@@ -37,7 +37,6 @@ class _ProfileScreenState extends State<ProfileScreen> {
context.read<ProfileBloc>().add( context.read<ProfileBloc>().add(
ProfilePhotoUpdated(File(pickedImage.path)), ProfilePhotoUpdated(File(pickedImage.path)),
); );
} catch (e) { } catch (e) {
print("Error picking or uploading image: $e"); print("Error picking or uploading image: $e");
} }
@@ -63,9 +62,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Container( body: Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
gradient: AppColors.phoneScreenBg,
),
child: SafeArea( child: SafeArea(
child: BlocBuilder<ProfileBloc, ProfileState>( child: BlocBuilder<ProfileBloc, ProfileState>(
builder: (context, state) { builder: (context, state) {
@@ -88,82 +85,91 @@ class _ProfileScreenState extends State<ProfileScreen> {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return SingleChildScrollView( return Column(
child: ConstrainedBox( children: [
constraints: BoxConstraints( Expanded(
minHeight: constraints.maxHeight, child: SingleChildScrollView(
), child: ConstrainedBox(
child: Padding( constraints: BoxConstraints(
padding: const EdgeInsets.symmetric(horizontal: 20), minHeight: constraints.maxHeight,
child: Column( ),
children: [ child: Padding(
const SizedBox(height: 16), padding: const EdgeInsets.symmetric(
CustomAppBar(title: 'Профиль'), horizontal: 20,
const SizedBox(height: 32), ),
child: Column(
children: [
const SizedBox(height: 16),
CustomAppBar(title: 'Профиль'),
const SizedBox(height: 32),
Stack( Stack(
alignment: Alignment.topRight, alignment: Alignment.topRight,
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 60, radius: 60,
backgroundColor: AppColors.checkboxFill, backgroundColor: AppColors.checkboxFill,
backgroundImage: backgroundImage:
(profile.avatarUrl != null && (profile.avatarUrl != null &&
profile.avatarUrl!.isNotEmpty) profile.avatarUrl!.isNotEmpty)
? NetworkImage( ? NetworkImage(
"${profile.avatarUrl!}?v=${DateTime.now().minute}", "${profile.avatarUrl!}?v=${DateTime.now().minute}",
) )
: null, : null,
child: child:
(profile.avatarUrl == null || (profile.avatarUrl == null ||
profile.avatarUrl!.isEmpty) profile.avatarUrl!.isEmpty)
? Text( ? Text(
profile.name.isNotEmpty profile.name.isNotEmpty
? profile.name[0].toUpperCase() ? profile.name[0]
: '', .toUpperCase()
style: const TextStyle( : '',
fontSize: 50, style: const TextStyle(
color: AppColors.darkBlue, fontSize: 50,
), color: AppColors.darkBlue,
) ),
: null, )
), : null,
),
GestureDetector( GestureDetector(
onTap: _pickImage, onTap: _pickImage,
child: Container( child: Container(
child: Image.asset( child: Image.asset(
'assets/icons/edit.png', 'assets/icons/edit.png',
width: 24, width: 24,
height: 24, height: 24,
), ),
),
),
],
), ),
),
],
),
const SizedBox(height: 32), const SizedBox(height: 32),
_ProfileInfoBlock( _ProfileInfoBlock(
profile: profile, profile: profile,
onEditTap: () => context.go("/home/profile/edit"), onEditTap: () =>
), context.go("/home/profile/edit"),
),
Padding( ],
padding: const EdgeInsets.all(20),
child: GradientButton(
text: 'Удалить учётную запись',
onTap: () => _confirmDeleteAccount(context),
showArrows: true,
height: 56,
width: double.infinity,
fontSize: 16,
), ),
), ),
], ),
), ),
), ),
), Padding(
padding: const EdgeInsets.all(20),
child: GradientButton(
text: 'Удалить учётную запись',
onTap: () => _confirmDeleteAccount(context),
showArrows: true,
height: 56,
width: double.infinity,
fontSize: 16,
),
),
],
); );
}, },
); );
@@ -388,4 +394,4 @@ class _SettingsBlock extends StatelessWidget {
), ),
); );
} }
} }

View File

@@ -116,13 +116,19 @@ class _SendPhotoViewState extends State<SendPhotoView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isUploading = context.select(
(SendPhotoBloc bloc) => bloc.state.status == SendPhotoStatus.loading,
);
return Scaffold( return Scaffold(
body: Container( body: Stack(
width: double.infinity, children: [
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg), Container(
child: SafeArea( width: double.infinity,
child: Column( decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
children: [ child: SafeArea(
child: Column(
children: [
const SizedBox(height: 16), const SizedBox(height: 16),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
@@ -190,7 +196,8 @@ class _SendPhotoViewState extends State<SendPhotoView> {
}, },
builder: (context, state) { builder: (context, state) {
final photoCount = state.selectedImages.length; final photoCount = state.selectedImages.length;
final isEnabled = photoCount >= 2; final isEnabled = photoCount >= 2 &&
state.status != SendPhotoStatus.loading;
return GradientButton( return GradientButton(
text: 'Отправить', text: 'Отправить',
@@ -210,9 +217,40 @@ class _SendPhotoViewState extends State<SendPhotoView> {
), ),
), ),
), ),
], ],
),
),
), ),
), // Оверлей загрузки: показывается, пока фото отправляются на сервер
if (isUploading)
Positioned.fill(
child: AbsorbPointer(
absorbing: true,
child: Container(
color: Colors.black.withOpacity(0.45),
child: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
color: Color(0xFF8BFFAA),
),
SizedBox(height: 16),
Text(
'Загружаем фото...',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
),
],
), ),
); );
} }

View File

@@ -4,7 +4,7 @@ import 'package:be_happy/core/result.dart';
import 'package:be_happy/domain/entities/client_notification.dart'; import 'package:be_happy/domain/entities/client_notification.dart';
import 'package:be_happy/domain/entities/map_settings.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/check_user_usecase.dart';
import 'package:be_happy/domain/usecase/get_available_zones_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_client_orders_usecase.dart';
import 'package:be_happy/domain/usecase/get_map_settings_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/get_notifications_stream_usecase.dart';
@@ -26,7 +26,7 @@ import 'package:maps_toolkit/maps_toolkit.dart' as mt;
class MapBloc extends Bloc<ScooterEvent, ScooterState> { class MapBloc extends Bloc<ScooterEvent, ScooterState> {
final GetAvailableScootersUsecase getScootersUsecase; final GetAvailableScootersUsecase getScootersUsecase;
final GetClientOrdersUsecase getClientOrdersUsecase; final GetClientOrdersUsecase getClientOrdersUsecase;
final GetAvailableZonesUsecase getAvailableZonesUsecase; final GetAllZonesUsecase getAllZonesUsecase;
final GetMapSettingsUsecase getMapSettingsUsecase; final GetMapSettingsUsecase getMapSettingsUsecase;
final GetNotificationsStreamUseCase getNotificationsStreamUseCase; final GetNotificationsStreamUseCase getNotificationsStreamUseCase;
final GetProfileUseCase getProfileUseCase; final GetProfileUseCase getProfileUseCase;
@@ -35,17 +35,19 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
final SplashBloc splashBloc; final SplashBloc splashBloc;
StreamSubscription<ClientNotification>? _notificationSubscription; StreamSubscription<ClientNotification>? _notificationSubscription;
Future<List<Zone>>? _zonesFuture;
MapBloc( MapBloc(
this.getAvailableZonesUsecase, this.getAllZonesUsecase,
this.getScootersUsecase, this.getScootersUsecase,
this.getClientOrdersUsecase, this.getClientOrdersUsecase,
this.getMapSettingsUsecase, this.getMapSettingsUsecase,
this.getNotificationsStreamUseCase, this.getNotificationsStreamUseCase,
this.getProfileUseCase, this.getProfileUseCase,
this.checkUserUseCase, this.checkUserUseCase,
this.logoutUseCase, this.logoutUseCase,
this.splashBloc, this.splashBloc,
) : super(ScooterState(isGeomarksShowed: true)) { ) : super(ScooterState(isGeomarksShowed: true)) {
on<FetchScooters>(_onFetchScooters); on<FetchScooters>(_onFetchScooters);
on<UpdateMap>(_onUpdateMap); on<UpdateMap>(_onUpdateMap);
on<UpdateUserLocation>(_onUpdateUserLocation); on<UpdateUserLocation>(_onUpdateUserLocation);
@@ -58,11 +60,43 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
on<ClearMapFocus>(_onClearMapFocus); 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() { void startNotificationStream() {
_notificationSubscription?.cancel(); _notificationSubscription?.cancel();
_notificationSubscription = getNotificationsStreamUseCase().listen( _notificationSubscription = getNotificationsStreamUseCase().listen(
(notification) { (notification) {
add(NotificationReceived(notification)); add(NotificationReceived(notification));
}, },
onError: (error) { onError: (error) {
@@ -94,15 +128,15 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
} }
Future<void> _onFetchScooters( Future<void> _onFetchScooters(
FetchScooters event, FetchScooters event,
Emitter<ScooterState> emit, Emitter<ScooterState> emit,
) async { ) async {
emit(state.copyWith(status: ScooterStatus.loading)); emit(state.copyWith(status: ScooterStatus.loading));
try { try {
final results = await Future.wait([ final results = await Future.wait([
getScootersUsecase(event.areaScooters, 0, 100), getScootersUsecase(event.areaScooters, 0, 100),
getAvailableZonesUsecase(event.area, 0, 100), _getZones(),
getMapSettingsUsecase(), getMapSettingsUsecase(),
getClientOrdersUsecase(), getClientOrdersUsecase(),
]); ]);
@@ -112,23 +146,7 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
final settings = results[2] as MapSettings; final settings = results[2] as MapSettings;
final orders = results[3]; final orders = results[3];
final filteredZones = _filterZones(zones, settings);
zones.forEach(print);
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"));
}
}
print( print(
" FETCH: filteredZones.length = ${filteredZones.length}, all_zones=${settings.all_zones}", " FETCH: filteredZones.length = ${filteredZones.length}, all_zones=${settings.all_zones}",
@@ -162,7 +180,7 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
try { try {
final results = await Future.wait([ final results = await Future.wait([
getScootersUsecase(state.areaScooters, 0, 100), getScootersUsecase(state.areaScooters, 0, 100),
getAvailableZonesUsecase(state.area, 0, 100), _getZones(),
getMapSettingsUsecase(), getMapSettingsUsecase(),
]); ]);
@@ -170,33 +188,17 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
final zones = results[1] as List<Zone>; final zones = results[1] as List<Zone>;
final settings = results[2] as MapSettings; final settings = results[2] as MapSettings;
List<Zone> filteredZones = []; final filteredZones = _filterZones(zones, settings);
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"));
}
}
print( print(
" UPDATE MAP: filteredZones.length = ${filteredZones.length}, all_zones=${settings.all_zones}", " UPDATE MAP: filteredZones.length = ${filteredZones.length}, all_zones=${settings.all_zones}",
); );
final zonesToEmit = filteredZones.isNotEmpty
? filteredZones
: state.zones;
emit( emit(
state.copyWith( state.copyWith(
status: ScooterStatus.success, status: ScooterStatus.success,
scooters: scooters, scooters: scooters,
zones: zonesToEmit, zones: filteredZones,
isGeomarksShowed: settings.all_placemarks, isGeomarksShowed: settings.all_placemarks,
), ),
); );
@@ -212,9 +214,9 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
} }
FutureOr<void> _onUpdateUserLocation( FutureOr<void> _onUpdateUserLocation(
UpdateUserLocation event, UpdateUserLocation event,
Emitter<ScooterState> emit, Emitter<ScooterState> emit,
) { ) {
print("USER LOCATION UPDATED EVENT start"); print("USER LOCATION UPDATED EVENT start");
state.zones.forEach((z) { state.zones.forEach((z) {
if (checkUserInZone(Point(event.latitude, event.longitude), z.points)) { if (checkUserInZone(Point(event.latitude, event.longitude), z.points)) {
@@ -224,9 +226,9 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
} }
FutureOr<void> _onNotificationReceived( FutureOr<void> _onNotificationReceived(
NotificationReceived event, NotificationReceived event,
Emitter<ScooterState> emit, Emitter<ScooterState> emit,
) { ) {
print("NOTIFICATION RECEIVED: ${event.notification.content}"); print("NOTIFICATION RECEIVED: ${event.notification.content}");
emit(state.copyWith(lastNotification: event.notification)); emit(state.copyWith(lastNotification: event.notification));
} }
@@ -242,9 +244,9 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
} }
FutureOr<void> _onFetchProfileData( FutureOr<void> _onFetchProfileData(
FetchProfileData event, FetchProfileData event,
Emitter<ScooterState> emit, Emitter<ScooterState> emit,
) async { ) async {
try { try {
final profile = await getProfileUseCase(); final profile = await getProfileUseCase();
@@ -262,18 +264,19 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
} }
FutureOr<void> _onLogoutPressed( FutureOr<void> _onLogoutPressed(
LogoutPressed event, LogoutPressed event,
Emitter<ScooterState> emit, Emitter<ScooterState> emit,
) async { ) async {
await logoutUseCase(); await logoutUseCase();
stopNotificationStream(); stopNotificationStream();
_zonesFuture = null;
splashBloc.add(AuthLogoutRequested()); splashBloc.add(AuthLogoutRequested());
} }
FutureOr<void> _onCheckUser( FutureOr<void> _onCheckUser(
CheckUser event, CheckUser event,
Emitter<ScooterState> emit, Emitter<ScooterState> emit,
) async { ) async {
try { try {
final flags = await checkUserUseCase(); final flags = await checkUserUseCase();
@@ -341,6 +344,4 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
selectedScooterForFocus: null, selectedScooterForFocus: null,
)); ));
} }
} }

View File

@@ -77,10 +77,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.0"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@@ -633,26 +633,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.19" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.0" version: "0.11.1"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.17.0"
mime: mime:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -910,10 +910,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.11" version: "0.7.7"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description: