ionic-planb-logistic-app-fl.../lib/components/dark_mode_map.dart
Jean-Philippe Brule c58da58f1f Remove floating navigation buttons from map
Remove the floating "Navigate" and "Stop" buttons positioned on the right side
of the map. These actions are already available in the bottom button bar, so
the floating buttons were redundant.

Updated dark_mode_map.dart:
- Removed Positioned floating button column (right: 16, bottom: 120)
- Removed floating "Navigate" button (shown when delivery selected)
- Removed floating "Stop" button (shown when navigating)

The bottom action bar now contains all necessary navigation controls.

Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 08:57:05 -05:00

595 lines
18 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;
bool _isSessionInitialized = false;
bool _isInitializing = false;
bool _isStartingNavigation = false;
String _loadingMessage = 'Initializing...';
Brightness? _lastBrightness;
@override
void initState() {
super.initState();
_initializeNavigation();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Detect theme changes and reapply map style
final currentBrightness = Theme.of(context).brightness;
if (_lastBrightness != null &&
_lastBrightness != currentBrightness &&
_navigationController != null) {
_applyDarkModeStyle();
}
_lastBrightness = currentBrightness;
}
Future<void> _initializeNavigation() async {
if (_isInitializing || _isSessionInitialized) return;
setState(() {
_isInitializing = true;
});
try {
final termsAccepted = await GoogleMapsNavigator.areTermsAccepted();
if (!termsAccepted) {
await GoogleMapsNavigator.showTermsAndConditionsDialog(
'Plan B Logistics',
'com.goutezplanb.planbLogistic',
);
}
await GoogleMapsNavigator.initializeNavigationSession();
if (mounted) {
setState(() {
_isSessionInitialized = true;
_isInitializing = false;
});
}
} catch (e) {
final errorMessage = _formatErrorMessage(e);
debugPrint('Map initialization error: $errorMessage');
if (mounted) {
setState(() {
_isInitializing = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Navigation initialization failed: $errorMessage'),
duration: const Duration(seconds: 5),
),
);
}
}
}
String _formatErrorMessage(Object error) {
final errorString = error.toString();
if (errorString.contains('SessionNotInitializedException')) {
return 'Google Maps navigation session could not be initialized';
} else if (errorString.contains('permission')) {
return 'Location permission is required for navigation';
} else if (errorString.contains('network')) {
return 'Network connection error';
}
return errorString;
}
@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!,
);
});
// Just store the destination, don't move camera
// The navigation will handle camera positioning
}
}
}
Future<void> _applyDarkModeStyle() async {
// Check if widget is still mounted and controller exists
if (!mounted || _navigationController == null) return;
try {
if (!mounted) return;
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
if (isDarkMode) {
// Dark mode style - Note: Currently only supported on Android
const simpleDarkStyle = '''[
{
"elementType": "geometry",
"stylers": [{"color": "#242424"}]
},
{
"elementType": "labels.text.fill",
"stylers": [{"color": "#746855"}]
},
{
"elementType": "labels.text.stroke",
"stylers": [{"color": "#242424"}]
},
{
"featureType": "water",
"elementType": "geometry",
"stylers": [{"color": "#17263c"}]
}
]''';
await _navigationController!.setMapStyle(simpleDarkStyle);
} else {
// Reset to default light style
await _navigationController!.setMapStyle(null);
}
} catch (e) {
if (mounted) {
debugPrint('Error applying map style: $e');
}
}
}
Future<void> _startNavigation() async {
if (_destinationLocation == null) return;
// Show loading indicator
if (mounted) {
setState(() {
_isStartingNavigation = true;
_loadingMessage = 'Starting navigation...';
});
}
try {
// Ensure session is initialized before starting navigation
if (!_isSessionInitialized && !_isInitializing) {
debugPrint('Initializing navigation session...');
await _initializeNavigation();
}
// Wait for initialization to complete if it's in progress
int retries = 0;
while (!_isSessionInitialized && retries < 30) {
await Future.delayed(const Duration(milliseconds: 100));
retries++;
}
if (!_isSessionInitialized) {
if (mounted) {
setState(() {
_isStartingNavigation = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Navigation initialization timeout'),
duration: Duration(seconds: 3),
),
);
}
return;
}
if (mounted) {
setState(() {
_loadingMessage = 'Setting destination...';
});
}
final waypoint = NavigationWaypoint.withLatLngTarget(
title: widget.selectedDelivery?.name ?? 'Destination',
target: _destinationLocation!,
);
final destinations = Destinations(
waypoints: [waypoint],
displayOptions: NavigationDisplayOptions(showDestinationMarkers: true),
);
if (mounted) {
setState(() {
_loadingMessage = 'Starting guidance...';
});
}
debugPrint('Setting destinations: ${_destinationLocation!.latitude}, ${_destinationLocation!.longitude}');
await GoogleMapsNavigator.setDestinations(destinations);
debugPrint('Starting guidance...');
await GoogleMapsNavigator.startGuidance();
debugPrint('Navigation started successfully');
// Reapply dark mode style after navigation starts
if (mounted) {
await _applyDarkModeStyle();
}
// Auto-recenter on driver location when navigation starts
await _recenterMap();
debugPrint('Camera recentered on driver location');
if (mounted) {
setState(() {
_isNavigating = true;
_isStartingNavigation = false;
});
}
} catch (e) {
final errorMessage = _formatErrorMessage(e);
debugPrint('Navigation start error: $errorMessage');
debugPrint('Full error: $e');
if (mounted) {
setState(() {
_isStartingNavigation = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Navigation error: $errorMessage'),
duration: const Duration(seconds: 4),
),
);
}
}
}
Future<void> _stopNavigation() async {
try {
await GoogleMapsNavigator.stopGuidance();
await GoogleMapsNavigator.clearDestinations();
if (mounted) {
setState(() {
_isNavigating = false;
});
}
} catch (e) {
if (mounted) {
debugPrint('Navigation stop error: $e');
}
}
}
Future<void> _recenterMap() async {
if (_navigationController == null) return;
try {
// Use the navigation controller's follow location feature
// This tells the navigation to follow the driver's current location
await _navigationController!.followMyLocation(CameraPerspective.tilted);
debugPrint('Navigation set to follow driver location');
} catch (e) {
debugPrint('Recenter map error: $e');
}
}
@override
Widget build(BuildContext context) {
// Driver's current location (defaults to Montreal if not available)
final initialPosition = const LatLng(latitude: 45.5017, longitude: -73.5673);
// Calculate dynamic padding for bottom button bar
final topPadding = 0.0;
final bottomPadding = widget.selectedDelivery != null ? 110.0 : 0.0;
return Stack(
children: [
// Map with padding to accommodate overlaid elements
Padding(
padding: EdgeInsets.only(
top: topPadding,
bottom: bottomPadding,
),
child: GoogleMapsNavigationView(
onViewCreated: (controller) async {
_navigationController = controller;
// Apply dark mode style with a small delay to ensure map is ready
await Future.delayed(const Duration(milliseconds: 500));
await _applyDarkModeStyle();
controller.animateCamera(
CameraUpdate.newLatLngZoom(initialPosition, 12),
);
},
initialCameraPosition: CameraPosition(
target: initialPosition,
zoom: 12,
),
),
),
// 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.withValues(alpha: 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: _isInitializing ? 'Initializing...' : 'Start Navigation',
icon: Icons.directions,
onPressed: _isInitializing ? null : _startNavigation,
isPrimary: true,
),
),
if (!_isNavigating && !widget.selectedDelivery!.delivered)
const SizedBox(width: 12),
if (!_isNavigating && !widget.selectedDelivery!.delivered)
Expanded(
child: _buildBottomActionButton(
label: _isStartingNavigation || _isInitializing ? 'Loading...' : 'Navigate',
icon: Icons.directions,
onPressed: _isStartingNavigation || _isInitializing ? null : _startNavigation,
),
),
if (_isNavigating)
const SizedBox(width: 12),
if (_isNavigating)
Expanded(
child: _buildBottomActionButton(
label: 'Stop',
icon: Icons.stop,
onPressed: _stopNavigation,
isDanger: true,
),
),
],
),
),
),
// Loading overlay during navigation initialization and start
if (_isStartingNavigation || _isInitializing)
Positioned.fill(
child: Container(
color: Colors.black.withValues(alpha: 0.4),
child: Center(
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Circular progress indicator
SizedBox(
width: 60,
height: 60,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
strokeWidth: 3,
),
),
const SizedBox(height: 16),
// Loading message
Text(
_loadingMessage,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
// Secondary message
Text(
'Please wait...',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
],
),
),
),
),
),
],
);
}
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;
}
// Reduce opacity when disabled
if (onPressed == null) {
backgroundColor = backgroundColor.withValues(alpha: 0.5);
}
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,
}) {
final isDisabled = onPressed == null;
final buttonColor = isDisabled ? color.withValues(alpha: 0.5) : color;
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: buttonColor,
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,
),
),
],
),
),
),
),
);
}
}