918 lines
29 KiB
Dart
918 lines
29 KiB
Dart
import 'dart:async';
|
||
import 'dart:typed_data';
|
||
import 'dart:ui' as ui;
|
||
|
||
import 'package:bot_toast/bot_toast.dart';
|
||
import 'package:be_happy/domain/entities/client_notification.dart';
|
||
import 'package:be_happy/domain/usecase/book_scooter_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_payment_cards_usecase.dart';
|
||
import 'package:be_happy/domain/usecase/get_pedestrian_routes_usecase.dart';
|
||
import 'package:be_happy/domain/usecase/get_scooter_usecase.dart';
|
||
import 'package:be_happy/domain/usecase/save_map_settings_usecase.dart';
|
||
import 'package:be_happy/presentation/components/fine_notification_card.dart';
|
||
import 'package:be_happy/presentation/components/map_icon_painter/clusterized_icon_painter.dart';
|
||
import 'package:be_happy/presentation/components/payment_notification_card.dart';
|
||
import 'package:be_happy/presentation/components/sheet/current_rides_sheet.dart';
|
||
import 'package:be_happy/presentation/components/sheet/map_settings_sheet.dart';
|
||
import 'package:be_happy/presentation/components/sheet/tariff_sheet.dart';
|
||
import 'package:be_happy/presentation/event/current_rides_event.dart';
|
||
import 'package:be_happy/presentation/event/map_settings_modal_event.dart';
|
||
import 'package:be_happy/presentation/viewmodel/current_rides_bloc.dart';
|
||
import 'package:be_happy/presentation/viewmodel/map_settings_modal_bloc.dart';
|
||
import 'package:be_happy/presentation/viewmodel/tariff_sheet_bloc.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:geolocator/geolocator.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import 'package:yandex_mapkit/yandex_mapkit.dart';
|
||
|
||
import '../../core/app_colors.dart';
|
||
import '../../di/service_locator.dart';
|
||
import '../../domain/entities/scooter.dart';
|
||
import '../../domain/entities/zone.dart';
|
||
import '../../domain/usecase/get_address_by_point_usecase.dart';
|
||
import '../../domain/usecase/get_available_tariffs_usecase.dart';
|
||
import '../../domain/usecase/get_notifications_stream_usecase.dart';
|
||
import '../components/notification_toast.dart';
|
||
import '../components/sheet/scooter_bottom_sheet.dart';
|
||
import '../components/side_menu.dart';
|
||
import '../components/unpaid_order_notification_card.dart';
|
||
import '../event/map_event.dart';
|
||
import '../event/scooter_detail_modal_event.dart';
|
||
import '../state/map_state.dart';
|
||
import '../viewmodel/map_bloc.dart';
|
||
import '../viewmodel/scooter_detail_modal_bloc.dart';
|
||
|
||
class MapScreen extends StatefulWidget {
|
||
const MapScreen({super.key});
|
||
|
||
@override
|
||
State<MapScreen> createState() => _MapScreenState();
|
||
}
|
||
|
||
class _MapScreenState extends State<MapScreen> {
|
||
YandexMapController? mapController;
|
||
Position? _currentPosition;
|
||
StreamSubscription<Position>? _positionStreamSubscription;
|
||
StreamSubscription<ClientNotification>? _notificationStreamSubscription;
|
||
bool _isFirstLocationUpdate = true;
|
||
Timer? _debounceTimer;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_checkLocationPermission();
|
||
_initScooterIcon();
|
||
_startNotificationStream();
|
||
context.read<MapBloc>().add(FetchProfileData());
|
||
context.read<MapBloc>().add(CheckUser());
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_debounceTimer?.cancel();
|
||
_positionStreamSubscription?.cancel();
|
||
_notificationStreamSubscription?.cancel();
|
||
context.read<MapBloc>().stopNotificationStream();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: Colors.transparent,
|
||
drawer: const SideMenu(),
|
||
body: Container(
|
||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||
child: SafeArea(
|
||
child: Stack(
|
||
children: [
|
||
BlocConsumer<MapBloc, ScooterState>(
|
||
listenWhen: (previous, current) {
|
||
return current.lastNotification !=
|
||
previous.lastNotification ||
|
||
current.flags != previous.flags ||
|
||
previous.selectedScooterForFocus?.id
|
||
!= current.selectedScooterForFocus?.id;
|
||
},
|
||
|
||
listener: (context, state) {
|
||
if (state.lastNotification != null) {
|
||
_showNotificationToast(state.lastNotification!);
|
||
}
|
||
|
||
if (state.flags != null) {
|
||
if (!state.flags.hasCard) {
|
||
BotToast.showCustomNotification(
|
||
duration: null,
|
||
toastBuilder: (_) {
|
||
return Container(
|
||
margin: const EdgeInsets.only(top: 120),
|
||
child: Material(
|
||
color: Colors.transparent,
|
||
child: PaymentNotificationCard(
|
||
onBindCard: () {
|
||
BotToast.cleanAll();
|
||
context.push("/home/payment-methods");
|
||
},
|
||
onClose: () => BotToast.cleanAll(),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
if (state.flags.hasFine) {
|
||
BotToast.showCustomNotification(
|
||
duration: null,
|
||
toastBuilder: (_) {
|
||
return Container(
|
||
margin: const EdgeInsets.only(top: 120),
|
||
child: Material(
|
||
color: Colors.transparent,
|
||
child: FineNotificationCard(
|
||
/*onBindCard: () {
|
||
BotToast.cleanAll();
|
||
context.push("/home/payment-methods");
|
||
},*/
|
||
onClose: () => BotToast.cleanAll(),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
if (state.flags.hasUnpaidOrder) {
|
||
BotToast.showCustomNotification(
|
||
duration: null,
|
||
toastBuilder: (_) {
|
||
return Container(
|
||
margin: const EdgeInsets.only(top: 120),
|
||
child: Material(
|
||
color: Colors.transparent,
|
||
child: UnpaidOrderNotificationCard(
|
||
/*onBindCard: () {
|
||
BotToast.cleanAll();
|
||
context.push("/home/payment-methods");
|
||
},*/
|
||
onClose: () => BotToast.cleanAll(),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
if (state.selectedScooterForFocus != null) {
|
||
final targetScooter = state.selectedScooterForFocus!;
|
||
|
||
print("RESERVED SCOOTER: $targetScooter");
|
||
|
||
_moveCameraToPoint(
|
||
targetScooter.longitude,
|
||
targetScooter.latitude,
|
||
zoom: 17,
|
||
);
|
||
|
||
context.read<MapBloc>().add(ClearMapFocus());
|
||
}
|
||
},
|
||
buildWhen: (previous, current) {
|
||
return previous.scooters != current.scooters ||
|
||
previous.reservedScooters != current.reservedScooters ||
|
||
previous.zones != current.zones ||
|
||
previous.status != current.status;
|
||
},
|
||
|
||
builder: (context, state) {
|
||
final freeScooters = _buildScooterPlacemarks(
|
||
scooters: state.scooters,
|
||
iconAsset: 'assets/icons/scooter_placemark_fill.png',
|
||
isClickable: true,
|
||
);
|
||
|
||
final reservedScooters = _buildScooterPlacemarks(
|
||
scooters: state.reservedScooters ?? [],
|
||
iconAsset: 'assets/icons/scooter_reserved_placemark_fill.png',
|
||
isClickable: false,
|
||
);
|
||
|
||
final zonePolygons = _buildZonePolygons(state.zones);
|
||
|
||
return RepaintBoundary(
|
||
child: YandexMap(
|
||
onMapCreated: (controller) {
|
||
controller.toggleUserLayer(visible: true);
|
||
mapController = controller;
|
||
if (_currentPosition != null) {
|
||
_fetchScooters();
|
||
}
|
||
},
|
||
onCameraPositionChanged:
|
||
(cameraPosition, reason, finished) {
|
||
if (finished) {
|
||
_fetchScooters();
|
||
}
|
||
},
|
||
mapObjects: [
|
||
...zonePolygons,
|
||
...reservedScooters,
|
||
ClusterizedPlacemarkCollection(
|
||
mapId: const MapObjectId('scooters_cluster'),
|
||
placemarks: freeScooters,
|
||
radius: 30,
|
||
minZoom: 15,
|
||
consumeTapEvents: true,
|
||
onClusterTap: (collection, cluster) {
|
||
final clusteredPlacemarks = cluster.placemarks;
|
||
final filtered = state.scooters.where((scooter) {
|
||
return clusteredPlacemarks.any(
|
||
(pm) => pm.mapId.value == scooter.id.toString(),
|
||
);
|
||
}).toList();
|
||
_onMarkerTap(filtered);
|
||
},
|
||
onClusterAdded: (self, cluster) async {
|
||
return cluster.copyWith(
|
||
appearance: cluster.appearance.copyWith(
|
||
opacity: 1.0,
|
||
icon: PlacemarkIcon.single(
|
||
PlacemarkIconStyle(
|
||
image: BitmapDescriptor.fromBytes(
|
||
await ClusterIconPainter(
|
||
cluster.size,
|
||
).getClusterIconBytes(),
|
||
),
|
||
scale: 0.8,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
),
|
||
|
||
BlocBuilder<MapBloc, ScooterState>(
|
||
buildWhen: (previous, current) =>
|
||
previous.status != current.status,
|
||
builder: (context, state) {
|
||
if (state.status == ScooterStatus.loading) {
|
||
return const Positioned(
|
||
top: 80,
|
||
left: 0,
|
||
right: 0,
|
||
child: Center(child: CircularProgressIndicator()),
|
||
);
|
||
}
|
||
return const SizedBox.shrink();
|
||
},
|
||
),
|
||
|
||
// Кнопки управления (Меню, Уведомления)
|
||
_buildTopButtons(),
|
||
|
||
// Кнопки навигации
|
||
if (_currentPosition != null) _buildSideControls(),
|
||
|
||
_buildCentralQrButton(),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _startNotificationStream() {
|
||
final notificationsStreamUseCase = getIt<GetNotificationsStreamUseCase>();
|
||
|
||
_notificationStreamSubscription = notificationsStreamUseCase().listen(
|
||
(notification) {
|
||
if (mounted) {
|
||
context.read<MapBloc>().add(NotificationReceived(notification));
|
||
}
|
||
},
|
||
onError: (error) {
|
||
print("SSE NOTIFICATION ERROR: $error");
|
||
},
|
||
);
|
||
}
|
||
|
||
void _checkLocationPermission() async {
|
||
final permission = await Geolocator.checkPermission();
|
||
if (permission == LocationPermission.denied ||
|
||
permission == LocationPermission.deniedForever) {
|
||
await Geolocator.requestPermission();
|
||
}
|
||
_getCurrentLocation();
|
||
// _startTrackingLocation();
|
||
}
|
||
|
||
void _startTrackingLocation() {
|
||
const LocationSettings locationSettings = LocationSettings(
|
||
accuracy: LocationAccuracy.high,
|
||
distanceFilter: 10,
|
||
);
|
||
|
||
_positionStreamSubscription =
|
||
Geolocator.getPositionStream(
|
||
locationSettings: locationSettings,
|
||
).listen((Position position) {
|
||
if (!mounted) return;
|
||
|
||
print(
|
||
"----------------------------------------------------- tracking... --------------------------------------------------------",
|
||
);
|
||
|
||
setState(() => _currentPosition = position);
|
||
|
||
context.read<MapBloc>().add(
|
||
UpdateUserLocation(position.latitude, position.longitude),
|
||
);
|
||
|
||
if (_isFirstLocationUpdate) {
|
||
_moveCameraToPoint(position.latitude, position.longitude, zoom: 15);
|
||
_isFirstLocationUpdate = false;
|
||
}
|
||
_fetchScooters();
|
||
});
|
||
}
|
||
|
||
void _getCurrentLocation() async {
|
||
try {
|
||
final position = await Geolocator.getCurrentPosition(
|
||
desiredAccuracy: LocationAccuracy.high,
|
||
);
|
||
setState(() => _currentPosition = position);
|
||
|
||
if (mapController != null && mounted) {
|
||
await _moveCameraToPoint(position.latitude, position.longitude);
|
||
_fetchScooters();
|
||
}
|
||
} catch (e) {
|
||
debugPrint('Ошибка геолокации: $e');
|
||
}
|
||
}
|
||
|
||
void _fetchScooters() async {
|
||
final controller = mapController;
|
||
if (controller == null) return;
|
||
|
||
if (_debounceTimer?.isActive ?? false) _debounceTimer!.cancel();
|
||
|
||
_debounceTimer = Timer(const Duration(milliseconds: 500), () async {
|
||
final visibleRegion = await controller.getVisibleRegion();
|
||
|
||
final areaScooters = [
|
||
visibleRegion.bottomRight.longitude,
|
||
visibleRegion.bottomRight.latitude,
|
||
visibleRegion.topLeft.latitude,
|
||
visibleRegion.topLeft.longitude,
|
||
];
|
||
|
||
final areaZones = [
|
||
visibleRegion.bottomRight.longitude,
|
||
visibleRegion.bottomRight.latitude,
|
||
visibleRegion.topLeft.longitude,
|
||
visibleRegion.topLeft.latitude,
|
||
];
|
||
|
||
if (mounted) {
|
||
context.read<MapBloc>().add(FetchScooters(areaZones, areaScooters));
|
||
}
|
||
});
|
||
}
|
||
Future<void> _moveCameraToPoint(
|
||
double lat,
|
||
double lon, {
|
||
double zoom = 15,
|
||
}) async {
|
||
await mapController?.moveCamera(
|
||
CameraUpdate.newCameraPosition(
|
||
CameraPosition(
|
||
target: Point(latitude: lat, longitude: lon),
|
||
zoom: zoom,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _onMarkerTap(List<Scooter> scooters) async {
|
||
context.read<MapBloc>().add(CheckUser());
|
||
final flags = context.read<MapBloc>().state.flags;
|
||
|
||
if (!flags.hasCard) {
|
||
_showExistingNotification(
|
||
child: PaymentNotificationCard(
|
||
onBindCard: () {
|
||
BotToast.cleanAll();
|
||
context.push("/home/payment-methods");
|
||
},
|
||
onClose: () => BotToast.cleanAll(),
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
if (flags.hasUnpaidOrder) {
|
||
_showExistingNotification(
|
||
child: UnpaidOrderNotificationCard(
|
||
onClose: () => BotToast.cleanAll(),
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
if (flags.hasFine) {
|
||
_showExistingNotification(
|
||
child: FineNotificationCard(
|
||
onClose: () => BotToast.cleanAll(),
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
|
||
context.push(
|
||
"/home/scooter-sheet",
|
||
extra: {'scooters': scooters, 'currentLocation': _currentPosition},
|
||
);
|
||
}
|
||
|
||
void _onMapSettingsTap() {
|
||
context.push("/home/map-settings-sheet");
|
||
/*showModalBottomSheet(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: Colors.transparent,
|
||
isDismissible: true,
|
||
builder: (context) {
|
||
return BlocProvider(
|
||
create: (context) => MapSettingsModalBloc(
|
||
getIt<GetMapSettingsUsecase>(),
|
||
getIt<SaveMapSettingsUsecase>(),
|
||
)..add(MapSettingsModalStarted()),
|
||
child: MapSettingsSheet(),
|
||
);
|
||
},
|
||
);*/
|
||
}
|
||
|
||
void _onNotificationTap() {
|
||
context.push("/home/current-rides-sheet");
|
||
|
||
/*showModalBottomSheet(
|
||
context: context,
|
||
builder: (context) => BlocProvider(
|
||
create: (context) =>
|
||
CurrentRidesBloc(getIt<GetClientOrdersUsecase>())
|
||
..add(LoadClientOrders()),
|
||
child: CurrentRidesSheet(),
|
||
),
|
||
);*/
|
||
|
||
// BotToast.showCustomNotification(
|
||
// duration: const Duration(seconds: 4),
|
||
//
|
||
// toastBuilder: (_) {
|
||
// return NotificationToast(
|
||
// title: "",
|
||
// onClose: () {
|
||
// BotToast.cleanAll();
|
||
// },
|
||
// );
|
||
// },
|
||
// );
|
||
}
|
||
|
||
void _showNotificationToast(ClientNotification notification) {
|
||
String title = _getNotificationTitle(notification.type);
|
||
Color backgroundColor = _getNotificationColor(notification.type);
|
||
|
||
BotToast.showCustomNotification(
|
||
duration: null,
|
||
toastBuilder: (_) {
|
||
return Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 50),
|
||
child: Material(
|
||
elevation: 8,
|
||
shadowColor: Colors.black26,
|
||
color: backgroundColor,
|
||
borderRadius: BorderRadius.circular(12),
|
||
clipBehavior: Clip.antiAlias,
|
||
child: IntrinsicHeight(
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Container(
|
||
width: 60,
|
||
child: Image.asset(
|
||
'assets/icons/clichnik.png',
|
||
fit: BoxFit.contain,
|
||
),
|
||
),
|
||
Expanded(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(12.0),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
title,
|
||
style: const TextStyle(
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: 16,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
notification.content,
|
||
style: TextStyle(fontSize: 14),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
Align(
|
||
alignment: Alignment.topRight,
|
||
child: IconButton(
|
||
icon: const Icon(Icons.close, size: 20),
|
||
onPressed: () {
|
||
BotToast.cleanAll();
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
String _getNotificationTitle(NotificationType type) {
|
||
switch (type) {
|
||
case NotificationType.info:
|
||
return 'Информация';
|
||
case NotificationType.attention:
|
||
return 'Внимание';
|
||
case NotificationType.warning:
|
||
return 'Предупреждение';
|
||
}
|
||
}
|
||
|
||
Color _getNotificationColor(NotificationType type) {
|
||
switch (type) {
|
||
case NotificationType.info:
|
||
return const Color(0xFF2196F3);
|
||
case NotificationType.attention:
|
||
return const Color(0xFFFF9800);
|
||
case NotificationType.warning:
|
||
return Colors.red;
|
||
}
|
||
}
|
||
|
||
void _initScooterIcon() async {
|
||
await ClusterIconPainter.initImage('assets/icons/scooter_placemark.png');
|
||
}
|
||
|
||
List<PlacemarkMapObject> _buildScooterPlacemarks({
|
||
required List<Scooter> scooters,
|
||
required String iconAsset,
|
||
required bool isClickable,
|
||
}) {
|
||
return scooters.map((scooter) {
|
||
return PlacemarkMapObject(
|
||
mapId: MapObjectId('${isClickable ? "" : "reserved_"}${scooter.id}'), // уникальный ID для карты
|
||
point: Point(latitude: scooter.longitude, longitude: scooter.latitude),
|
||
icon: PlacemarkIcon.single(
|
||
PlacemarkIconStyle(
|
||
image: BitmapDescriptor.fromAssetImage(iconAsset),
|
||
scale: 0.2,
|
||
),
|
||
),
|
||
opacity: 1.0,
|
||
consumeTapEvents: isClickable,
|
||
onTap: isClickable
|
||
? (object, point) async => _onMarkerTap([scooter])
|
||
: null,
|
||
);
|
||
}).toList();
|
||
}
|
||
|
||
List<MapObject> _buildZonePolygons(List<Zone>? zones) {
|
||
if (zones == null || zones.isEmpty) return [];
|
||
|
||
List<MapObject> objects = [];
|
||
|
||
List<LinearRing> allZoneHoles = [];
|
||
|
||
for (var zone in zones) {
|
||
var points = zone.points.map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList();
|
||
|
||
var cleanPoints = <Point>[];
|
||
for (var p in points) {
|
||
if (cleanPoints.isEmpty || cleanPoints.last != p) {
|
||
cleanPoints.add(p);
|
||
}
|
||
}
|
||
|
||
if (cleanPoints.length > 2) {
|
||
if (cleanPoints.first != cleanPoints.last) {
|
||
cleanPoints.add(cleanPoints.first);
|
||
}
|
||
|
||
allZoneHoles.add(LinearRing(points: cleanPoints));
|
||
}
|
||
}
|
||
|
||
objects.add(
|
||
PolygonMapObject(
|
||
mapId: const MapObjectId('global_inverse_mask'),
|
||
polygon: Polygon(
|
||
outerRing: const LinearRing(points: [
|
||
Point(latitude: 85, longitude: -179.9),
|
||
Point(latitude: 85, longitude: 179.9),
|
||
Point(latitude: -85, longitude: 179.9),
|
||
Point(latitude: -85, longitude: -179.9),
|
||
]),
|
||
innerRings: allZoneHoles,
|
||
),
|
||
strokeWidth: 0,
|
||
fillColor: Colors.red.withOpacity(0.15),
|
||
zIndex: 0,
|
||
),
|
||
);
|
||
|
||
for (var zone in zones) {
|
||
Color borderColor;
|
||
if (zone.type == "Drive") {
|
||
borderColor = const Color(0xFF5ECD4C);
|
||
} else if (zone.type == "NotDrive") {
|
||
borderColor = const Color(0xFFEF4444);
|
||
} else {
|
||
borderColor = const Color(0xFFA78BFA);
|
||
}
|
||
|
||
objects.add(
|
||
PolylineMapObject(
|
||
mapId: MapObjectId('zone_contour_${zone.id}'),
|
||
polyline: Polyline(
|
||
points: zone.points.map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList(),
|
||
),
|
||
strokeColor: borderColor,
|
||
strokeWidth: 2.0,
|
||
zIndex: 1,
|
||
),
|
||
);
|
||
}
|
||
|
||
return objects;
|
||
}
|
||
|
||
Widget _buildTopButtons() {
|
||
return Stack(
|
||
children: [
|
||
Positioned(
|
||
top: 16,
|
||
left: 16,
|
||
child: Builder(
|
||
builder: (innerContext) => _RoundIconButton(
|
||
icon: Icons.menu,
|
||
onPressed: () => {Scaffold.of(innerContext).openDrawer()},
|
||
),
|
||
),
|
||
),
|
||
Positioned(
|
||
top: 16,
|
||
right: 16,
|
||
child: Column(
|
||
children: [
|
||
_RoundIconButton(
|
||
icon: Icons.notifications_sharp,
|
||
onPressed: () => context.push("/home/notifications"),
|
||
),
|
||
const SizedBox(height: 12),
|
||
_RoundButton(
|
||
imagePath: 'assets/icons/scooter_placemark.png',
|
||
onPressed: () => context.push("/home/current-rides-sheet"),
|
||
)
|
||
],
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildCentralQrButton() {
|
||
return Positioned(
|
||
bottom: 24,
|
||
left: 0,
|
||
right: 0,
|
||
child: Center(
|
||
child: GestureDetector(
|
||
onTap: () {
|
||
final flags = context.read<MapBloc>().state.flags;
|
||
|
||
// 🔹 Проверка флагов — показываем те же карточки, что и в listener
|
||
if (!flags.hasCard) {
|
||
_showExistingNotification(
|
||
child: PaymentNotificationCard(
|
||
onBindCard: () {
|
||
BotToast.cleanAll();
|
||
context.push("/home/payment-methods");
|
||
},
|
||
onClose: () => BotToast.cleanAll(),
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
if (flags.hasUnpaidOrder) {
|
||
_showExistingNotification(
|
||
child: UnpaidOrderNotificationCard(
|
||
onClose: () => BotToast.cleanAll(),
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
if (flags.hasFine) {
|
||
_showExistingNotification(
|
||
child: FineNotificationCard(
|
||
onClose: () => BotToast.cleanAll(),
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
|
||
// ✅ Все проверки пройдены — переход на сканирование
|
||
context.push("/home/qr-info");
|
||
},
|
||
child: Container(
|
||
width: 64,
|
||
height: 64,
|
||
decoration: BoxDecoration(
|
||
color: AppColors.darkBlue,
|
||
shape: BoxShape.circle,
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.3),
|
||
blurRadius: 10,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
],
|
||
),
|
||
child: const Icon(
|
||
Icons.qr_code_scanner,
|
||
color: Colors.white,
|
||
size: 32,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSideControls() {
|
||
return Positioned(
|
||
right: 16,
|
||
top: 0,
|
||
bottom: 0,
|
||
child: Center(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
_CircleIconButton(icon: Icons.map, onPressed: _onMapSettingsTap),
|
||
const SizedBox(height: 16),
|
||
_CircleIconButton(
|
||
icon: Icons.my_location,
|
||
onPressed: () {
|
||
context.read<MapBloc>().add(
|
||
UpdateUserLocation(
|
||
_currentPosition!.latitude,
|
||
_currentPosition!.longitude,
|
||
),
|
||
);
|
||
_moveCameraToPoint(
|
||
_currentPosition!.latitude,
|
||
_currentPosition!.longitude,
|
||
zoom: 17,
|
||
);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<Uint8List> painterToBytes(CustomPainter painter, Size size) async {
|
||
final recorder = ui.PictureRecorder();
|
||
final canvas = Canvas(recorder);
|
||
painter.paint(canvas, size);
|
||
final picture = recorder.endRecording();
|
||
final image = await picture.toImage(size.width.toInt(), size.height.toInt());
|
||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||
return byteData!.buffer.asUint8List();
|
||
}
|
||
|
||
class _RoundIconButton extends StatelessWidget {
|
||
final IconData icon;
|
||
final VoidCallback onPressed;
|
||
|
||
const _RoundIconButton({required this.icon, required this.onPressed});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
width: 40,
|
||
height: 40,
|
||
decoration: BoxDecoration(
|
||
color: AppColors.darkBlue,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: IconButton(
|
||
icon: Icon(icon, color: Colors.white, size: 24),
|
||
onPressed: onPressed,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _RoundButton extends StatelessWidget {
|
||
final String imagePath;
|
||
final VoidCallback onPressed;
|
||
|
||
const _RoundButton({
|
||
required this.imagePath,
|
||
required this.onPressed,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
width: 40,
|
||
height: 40,
|
||
decoration: BoxDecoration(
|
||
color: AppColors.darkBlue,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: GestureDetector(
|
||
onTap: onPressed,
|
||
child: Center(
|
||
child: Image.asset(
|
||
imagePath,
|
||
width: 20,
|
||
height: 20,
|
||
fit: BoxFit.contain,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _CircleIconButton extends StatelessWidget {
|
||
final IconData icon;
|
||
final VoidCallback onPressed;
|
||
|
||
const _CircleIconButton({required this.icon, required this.onPressed});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
width: 40,
|
||
height: 40,
|
||
decoration: const BoxDecoration(
|
||
color: AppColors.darkBlue,
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: IconButton(
|
||
icon: Icon(icon, color: Colors.white, size: 24),
|
||
onPressed: onPressed,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
void _showExistingNotification({required Widget child}) {
|
||
BotToast.showCustomNotification(
|
||
duration: null, // не исчезает, пока пользователь не закроет
|
||
toastBuilder: (_) {
|
||
return Container(
|
||
margin: const EdgeInsets.only(top: 120), // тот же отступ, что в listener
|
||
child: Material(
|
||
color: Colors.transparent,
|
||
child: child, // PaymentNotificationCard / UnpaidOrderNotificationCard / FineNotificationCard
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|