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>
678 lines
20 KiB
Dart
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|