ionic-planb-logistic-app-fl.../lib/components/dark_mode_map.dart
Jean-Philippe Brule 9cb5b51f6d Fix Google Navigation initialization timing issues
Restructures navigation session initialization to occur after the view is
created, eliminating race conditions. Session initialization now happens in
onViewCreated callback with proper delay before setting destination.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-15 20:49:20 -05:00

678 lines
20 KiB
Dart

import 'package:flutter/material.dart';
import 'package:google_navigation_flutter/google_navigation_flutter.dart';
import '../models/delivery.dart';
import '../theme/color_system.dart';
/// Enhanced dark-mode aware map component with custom styling
class DarkModeMapComponent extends StatefulWidget {
final List<Delivery> deliveries;
final Delivery? selectedDelivery;
final ValueChanged<Delivery?>? onDeliverySelected;
final Function(String)? onAction;
const DarkModeMapComponent({
super.key,
required this.deliveries,
this.selectedDelivery,
this.onDeliverySelected,
this.onAction,
});
@override
State<DarkModeMapComponent> createState() => _DarkModeMapComponentState();
}
class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
GoogleNavigationViewController? _navigationController;
bool _isNavigating = false;
LatLng? _destinationLocation;
@override
void initState() {
super.initState();
_initializeNavigation();
}
Future<void> _initializeNavigation() async {
try {
final termsAccepted = await GoogleMapsNavigator.areTermsAccepted();
if (!termsAccepted) {
await GoogleMapsNavigator.showTermsAndConditionsDialog(
'Plan B Logistics',
'com.goutezplanb.planbLogistic',
);
}
await GoogleMapsNavigator.initializeNavigationSession();
} catch (e) {
debugPrint('Map initialization error: $e');
}
}
@override
void didUpdateWidget(DarkModeMapComponent oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.selectedDelivery != widget.selectedDelivery) {
_updateDestination();
}
}
void _updateDestination() {
if (widget.selectedDelivery != null) {
final address = widget.selectedDelivery!.deliveryAddress;
if (address?.latitude != null && address?.longitude != null) {
setState(() {
_destinationLocation = LatLng(
latitude: address!.latitude!,
longitude: address.longitude!,
);
});
_navigateToLocation(_destinationLocation!);
}
}
}
Future<void> _navigateToLocation(LatLng location) async {
if (_navigationController == null) return;
try {
await _navigationController!.animateCamera(
CameraUpdate.newLatLngZoom(location, 15),
);
} catch (e) {
debugPrint('Camera navigation error: $e');
}
}
Future<void> _applyDarkModeStyle() async {
if (_navigationController == null) return;
try {
// Apply dark mode style configuration for Google Maps
// This reduces eye strain in low-light environments
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
if (isDarkMode) {
// Dark map style with warm accent colors
await _navigationController!.setMapStyle(_getDarkMapStyle());
}
} catch (e) {
debugPrint('Error applying map style: $e');
}
}
String _getDarkMapStyle() {
// Google Maps style JSON for dark mode with warm accents
return '''[
{
"elementType": "geometry",
"stylers": [{"color": "#212121"}]
},
{
"elementType": "labels.icon",
"stylers": [{"visibility": "off"}]
},
{
"elementType": "labels.text.fill",
"stylers": [{"color": "#757575"}]
},
{
"elementType": "labels.text.stroke",
"stylers": [{"color": "#212121"}]
},
{
"featureType": "administrative",
"elementType": "geometry",
"stylers": [{"color": "#757575"}]
},
{
"featureType": "administrative.country",
"elementType": "labels.text.fill",
"stylers": [{"color": "#9e9e9e"}]
},
{
"featureType": "administrative.land_parcel",
"stylers": [{"visibility": "off"}]
},
{
"featureType": "administrative.locality",
"elementType": "labels.text.fill",
"stylers": [{"color": "#bdbdbd"}]
},
{
"featureType": "administrative.neighborhood",
"stylers": [{"visibility": "off"}]
},
{
"featureType": "administrative.province",
"elementType": "labels.text.fill",
"stylers": [{"color": "#9e9e9e"}]
},
{
"featureType": "landscape",
"elementType": "geometry",
"stylers": [{"color": "#000000"}]
},
{
"featureType": "poi",
"elementType": "geometry",
"stylers": [{"color": "#383838"}]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [{"color": "#9e9e9e"}]
},
{
"featureType": "poi.park",
"elementType": "geometry",
"stylers": [{"color": "#181818"}]
},
{
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [{"color": "#616161"}]
},
{
"featureType": "road",
"elementType": "geometry.fill",
"stylers": [{"color": "#2c2c2c"}]
},
{
"featureType": "road",
"elementType": "labels.text.fill",
"stylers": [{"color": "#8a8a8a"}]
},
{
"featureType": "road.arterial",
"elementType": "geometry",
"stylers": [{"color": "#373737"}]
},
{
"featureType": "road.highway",
"elementType": "geometry",
"stylers": [{"color": "#3c3c3c"}]
},
{
"featureType": "road.highway.controlled_access",
"elementType": "geometry",
"stylers": [{"color": "#4e4e4e"}]
},
{
"featureType": "road.local",
"elementType": "labels.text.fill",
"stylers": [{"color": "#616161"}]
},
{
"featureType": "transit",
"elementType": "labels.text.fill",
"stylers": [{"color": "#757575"}]
},
{
"featureType": "water",
"elementType": "geometry",
"stylers": [{"color": "#0c1221"}]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [{"color": "#3d3d3d"}]
}
]''';
}
Future<void> _startNavigation() async {
if (_destinationLocation == null) return;
try {
final waypoint = NavigationWaypoint.withLatLngTarget(
title: widget.selectedDelivery?.name ?? 'Destination',
target: _destinationLocation!,
);
final destinations = Destinations(
waypoints: [waypoint],
displayOptions: NavigationDisplayOptions(showDestinationMarkers: true),
);
await GoogleMapsNavigator.setDestinations(destinations);
await GoogleMapsNavigator.startGuidance();
setState(() {
_isNavigating = true;
});
} catch (e) {
debugPrint('Navigation start error: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Navigation error: $e')),
);
}
}
}
Future<void> _stopNavigation() async {
try {
await GoogleMapsNavigator.stopGuidance();
await GoogleMapsNavigator.clearDestinations();
setState(() {
_isNavigating = false;
});
} catch (e) {
debugPrint('Navigation stop error: $e');
}
}
Future<void> _zoomIn() async {
if (_navigationController == null) return;
try {
final currentCamera = await _navigationController!.getCameraPosition();
await _navigationController!.animateCamera(
CameraUpdate.newLatLngZoom(
currentCamera.target,
currentCamera.zoom + 1,
),
);
} catch (e) {
debugPrint('Zoom in error: $e');
}
}
Future<void> _zoomOut() async {
if (_navigationController == null) return;
try {
final currentCamera = await _navigationController!.getCameraPosition();
await _navigationController!.animateCamera(
CameraUpdate.newLatLngZoom(
currentCamera.target,
currentCamera.zoom - 1,
),
);
} catch (e) {
debugPrint('Zoom out error: $e');
}
}
Future<void> _recenterMap() async {
if (_navigationController == null || _destinationLocation == null) return;
try {
await _navigationController!.animateCamera(
CameraUpdate.newLatLngZoom(_destinationLocation!, 15),
);
} catch (e) {
debugPrint('Recenter map error: $e');
}
}
@override
Widget build(BuildContext context) {
final initialPosition = widget.selectedDelivery?.deliveryAddress != null &&
widget.selectedDelivery!.deliveryAddress!.latitude != null &&
widget.selectedDelivery!.deliveryAddress!.longitude != null
? LatLng(
latitude: widget.selectedDelivery!.deliveryAddress!.latitude!,
longitude: widget.selectedDelivery!.deliveryAddress!.longitude!,
)
: const LatLng(latitude: 45.5017, longitude: -73.5673);
return Stack(
children: [
GoogleMapsNavigationView(
onViewCreated: (controller) {
_navigationController = controller;
_applyDarkModeStyle();
controller.animateCamera(
CameraUpdate.newLatLngZoom(initialPosition, 12),
);
},
initialCameraPosition: CameraPosition(
target: initialPosition,
zoom: 12,
),
),
// Custom dark-themed controls overlay
Positioned(
top: 16,
right: 16,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Zoom in button
_buildIconButton(
icon: Icons.add,
onPressed: _zoomIn,
),
const SizedBox(height: 8),
// Zoom out button
_buildIconButton(
icon: Icons.remove,
onPressed: _zoomOut,
),
],
),
),
// Navigation and action buttons
Positioned(
bottom: 16,
right: 16,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Start navigation button
if (widget.selectedDelivery != null && !_isNavigating)
_buildActionButton(
label: 'Navigate',
icon: Icons.directions,
onPressed: _startNavigation,
color: SvrntyColors.crimsonRed,
),
if (_isNavigating)
_buildActionButton(
label: 'Stop',
icon: Icons.stop,
onPressed: _stopNavigation,
color: Theme.of(context).colorScheme.onSurface,
),
],
),
),
// Selected delivery info panel (dark themed)
if (widget.selectedDelivery != null)
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.selectedDelivery!.name,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w600),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
widget.selectedDelivery!.deliveryAddress
?.formattedAddress ??
'No address',
style: Theme.of(context)
.textTheme
.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 8),
// Status indicator
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: widget.selectedDelivery!.delivered
? SvrntyColors.statusCompleted
: SvrntyColors.statusPending,
borderRadius: BorderRadius.circular(4),
),
child: Text(
widget.selectedDelivery!.delivered
? 'Delivered'
: 'Pending',
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
// Bottom action button bar
if (widget.selectedDelivery != null)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
children: [
// Recenter button
Expanded(
child: _buildBottomActionButton(
label: 'Recenter',
icon: Icons.location_on,
onPressed: _recenterMap,
),
),
const SizedBox(width: 12),
// Mark Complete button (if not already delivered)
if (!widget.selectedDelivery!.delivered)
Expanded(
child: _buildBottomActionButton(
label: 'Mark Complete',
icon: Icons.check_circle,
onPressed: () => widget.onAction?.call('complete'),
isPrimary: true,
),
),
if (widget.selectedDelivery!.delivered)
Expanded(
child: _buildBottomActionButton(
label: 'Start Navigation',
icon: Icons.directions,
onPressed: _startNavigation,
isPrimary: true,
),
),
if (!_isNavigating && !widget.selectedDelivery!.delivered)
const SizedBox(width: 12),
if (!_isNavigating && !widget.selectedDelivery!.delivered)
Expanded(
child: _buildBottomActionButton(
label: 'Navigate',
icon: Icons.directions,
onPressed: _startNavigation,
),
),
if (_isNavigating)
const SizedBox(width: 12),
if (_isNavigating)
Expanded(
child: _buildBottomActionButton(
label: 'Stop',
icon: Icons.stop,
onPressed: _stopNavigation,
isDanger: true,
),
),
],
),
),
),
],
);
}
Widget _buildIconButton({
required IconData icon,
required VoidCallback onPressed,
}) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Theme.of(context).colorScheme.surface,
shape: const CircleBorder(),
child: InkWell(
onTap: onPressed,
customBorder: const CircleBorder(),
child: Padding(
padding: const EdgeInsets.all(12),
child: Icon(
icon,
color: Theme.of(context).colorScheme.onSurface,
size: 24,
),
),
),
),
);
}
Widget _buildBottomActionButton({
required String label,
required IconData icon,
required VoidCallback onPressed,
bool isPrimary = false,
bool isDanger = false,
}) {
Color backgroundColor;
Color textColor = Colors.white;
if (isDanger) {
backgroundColor = Colors.red.shade600;
} else if (isPrimary) {
backgroundColor = SvrntyColors.crimsonRed;
} else {
backgroundColor = Theme.of(context).colorScheme.surfaceContainerHighest;
textColor = Theme.of(context).colorScheme.onSurface;
}
return Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(8),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: textColor,
size: 18,
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
],
),
),
),
);
}
Widget _buildActionButton({
required String label,
required IconData icon,
required VoidCallback onPressed,
required Color color,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: color,
borderRadius: BorderRadius.circular(8),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: Colors.white,
size: 18,
),
const SizedBox(width: 6),
Text(
label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
],
),
),
),
),
);
}
}