Refactor theme system and remove unused platforms
- Overhaul theme system with Svrnty design and WCAG AAA compliance - Remove android, macos, and web platform files (iOS-only focus) - Update components with improved dark mode map and UI refinements - Enhance settings page with additional configuration options - Add theme system documentation in lib/theme/README.md - Update CLAUDE.md with comprehensive theme guidelines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:google_navigation_flutter/google_navigation_flutter.dart';
|
||||
import '../models/delivery.dart';
|
||||
import '../theme/color_system.dart';
|
||||
import '../utils/breakpoints.dart';
|
||||
import '../utils/toast_helper.dart';
|
||||
|
||||
/// Enhanced dark-mode aware map component with custom styling
|
||||
@@ -36,11 +37,21 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
Brightness? _lastBrightness;
|
||||
bool _isMapViewReady = false;
|
||||
bool _isDisposed = false;
|
||||
bool _isAudioGuidanceEnabled = false; // Audio guidance off by default
|
||||
bool _pendingNavigationStart = false; // User requested navigation before map was ready
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Set destination if delivery is already selected when widget is created
|
||||
_updateDestination();
|
||||
_initializeNavigation();
|
||||
// Ensure state is synced after a brief delay to allow map to fully load
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (mounted && !_isDisposed) {
|
||||
_syncNavigationState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -55,6 +66,8 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.selectedDelivery != widget.selectedDelivery) {
|
||||
_updateDestination();
|
||||
// Sync navigation state with actual SDK state
|
||||
_syncNavigationState();
|
||||
|
||||
// If navigation was active, restart navigation to new delivery
|
||||
if (_isNavigating &&
|
||||
@@ -95,13 +108,48 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync local navigation state with actual SDK navigation state
|
||||
Future<void> _syncNavigationState() async {
|
||||
try {
|
||||
final bool isActuallyNavigating = await GoogleMapsNavigator.isGuidanceRunning();
|
||||
if (mounted && !_isDisposed) {
|
||||
if (_isNavigating != isActuallyNavigating) {
|
||||
debugPrint('State mismatch detected! Local: $_isNavigating, SDK: $isActuallyNavigating');
|
||||
setState(() {
|
||||
_isNavigating = isActuallyNavigating;
|
||||
});
|
||||
debugPrint('Navigation state synced to: $_isNavigating');
|
||||
} else {
|
||||
debugPrint('Navigation state already in sync: $_isNavigating');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Failed to sync navigation state: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initializeNavigation() async {
|
||||
if (_isInitializing || _isSessionInitialized) return;
|
||||
if (_isInitializing || _isSessionInitialized) {
|
||||
debugPrint('Skipping initialization: initializing=$_isInitializing, initialized=$_isSessionInitialized');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('Initializing navigation session...');
|
||||
|
||||
setState(() {
|
||||
_isInitializing = true;
|
||||
});
|
||||
|
||||
// Safety timeout to reset flag if initialization takes too long
|
||||
Future.delayed(const Duration(seconds: 15), () {
|
||||
if (mounted && _isInitializing && !_isSessionInitialized) {
|
||||
debugPrint('Initialization timeout - resetting flag');
|
||||
setState(() {
|
||||
_isInitializing = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
final termsAccepted = await GoogleMapsNavigator.areTermsAccepted();
|
||||
if (!termsAccepted) {
|
||||
@@ -148,15 +196,25 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
if (widget.selectedDelivery != null) {
|
||||
final address = widget.selectedDelivery!.deliveryAddress;
|
||||
if (address?.latitude != null && address?.longitude != null) {
|
||||
final lat = address!.latitude!;
|
||||
final lon = address.longitude!;
|
||||
setState(() {
|
||||
_destinationLocation = LatLng(
|
||||
latitude: address!.latitude!,
|
||||
longitude: address.longitude!,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
);
|
||||
});
|
||||
debugPrint('Destination set to: $lat, $lon for delivery: ${widget.selectedDelivery!.name}');
|
||||
// Just store the destination, don't move camera
|
||||
// The navigation will handle camera positioning
|
||||
} else {
|
||||
debugPrint('Delivery ${widget.selectedDelivery!.name} has no valid address');
|
||||
}
|
||||
} else {
|
||||
debugPrint('No delivery selected, clearing destination');
|
||||
setState(() {
|
||||
_destinationLocation = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +225,15 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
try {
|
||||
if (!mounted || _isDisposed) return;
|
||||
|
||||
// Get current theme brightness
|
||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
// Force night mode based on theme
|
||||
await _navigationController!.setForceNightMode(
|
||||
isDarkMode ? NavigationForceNightMode.forceNight : NavigationForceNightMode.forceDay,
|
||||
);
|
||||
debugPrint('Night mode set to: ${isDarkMode ? 'night' : 'day'}');
|
||||
|
||||
// Force dark mode map style using Google's standard dark theme
|
||||
const String darkMapStyle = '''
|
||||
[
|
||||
@@ -269,8 +336,51 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleAudioGuidance() async {
|
||||
if (_isDisposed) return;
|
||||
|
||||
try {
|
||||
final newState = !_isAudioGuidanceEnabled;
|
||||
await GoogleMapsNavigator.setAudioGuidance(
|
||||
NavigationAudioGuidanceSettings(
|
||||
guidanceType: newState
|
||||
? NavigationAudioGuidanceType.alertsAndGuidance
|
||||
: NavigationAudioGuidanceType.silent,
|
||||
),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isAudioGuidanceEnabled = newState;
|
||||
});
|
||||
debugPrint('Audio guidance ${newState ? 'enabled' : 'disabled'}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error toggling audio guidance: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startNavigation() async {
|
||||
if (_destinationLocation == null) return;
|
||||
if (_destinationLocation == null) {
|
||||
debugPrint('Cannot start navigation: no destination set');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('Starting navigation to: $_destinationLocation');
|
||||
|
||||
// Check if map is ready before attempting to start
|
||||
if (!_isMapViewReady) {
|
||||
debugPrint('Map not ready yet - navigation will start automatically when ready');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_pendingNavigationStart = true;
|
||||
_isStartingNavigation = true;
|
||||
_loadingMessage = 'Waiting for map to load...';
|
||||
});
|
||||
ToastHelper.showInfo(context, 'Map is loading. Navigation will start automatically...');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading indicator
|
||||
if (mounted) {
|
||||
@@ -280,6 +390,19 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
});
|
||||
}
|
||||
|
||||
// Safety timeout to reset flag if navigation start takes too long
|
||||
Future.delayed(const Duration(seconds: 10), () {
|
||||
if (mounted && _isStartingNavigation) {
|
||||
debugPrint('Navigation start timeout - resetting flag');
|
||||
setState(() {
|
||||
_isStartingNavigation = false;
|
||||
});
|
||||
ToastHelper.showError(context, 'Navigation start timed out. Please try again.');
|
||||
}
|
||||
});
|
||||
|
||||
bool navigationStarted = false;
|
||||
|
||||
try {
|
||||
// Ensure session is initialized before starting navigation
|
||||
if (!_isSessionInitialized && !_isInitializing) {
|
||||
@@ -296,9 +419,6 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
|
||||
if (!_isSessionInitialized) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isStartingNavigation = false;
|
||||
});
|
||||
ToastHelper.showError(context, 'Navigation initialization timeout');
|
||||
}
|
||||
return;
|
||||
@@ -364,6 +484,7 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
setState(() {
|
||||
_isNavigating = true;
|
||||
_isStartingNavigation = false;
|
||||
_pendingNavigationStart = false; // Clear pending flag on success
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -374,6 +495,7 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isStartingNavigation = false;
|
||||
_pendingNavigationStart = false; // Clear pending flag on error
|
||||
});
|
||||
|
||||
ToastHelper.showError(context, 'Navigation error: $errorMessage', duration: const Duration(seconds: 4));
|
||||
@@ -396,14 +518,28 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
|
||||
await GoogleMapsNavigator.stopGuidance();
|
||||
await GoogleMapsNavigator.clearDestinations();
|
||||
|
||||
// Wait a moment for the SDK to fully stop
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
// Verify navigation actually stopped by checking SDK state
|
||||
final bool isStillRunning = await GoogleMapsNavigator.isGuidanceRunning();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
_isNavigating = isStillRunning;
|
||||
_pendingNavigationStart = false; // Clear pending flag when stopping
|
||||
});
|
||||
debugPrint('Navigation stopped, state synced: $_isNavigating');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
debugPrint('Navigation stop error: $e');
|
||||
setState(() {
|
||||
_pendingNavigationStart = false; // Clear pending flag on error
|
||||
});
|
||||
// Even on error, try to sync state
|
||||
await _syncNavigationState();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,12 +565,17 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
|
||||
@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);
|
||||
// Driver's current location (defaults to Trois-Rivières if not available)
|
||||
final initialPosition = const LatLng(latitude: 46.33857534324389, longitude: -72.60787418369715);
|
||||
|
||||
// Get safe area insets to avoid rounded corners and notches
|
||||
final bottomSafeArea = MediaQuery.of(context).padding.bottom;
|
||||
|
||||
// Calculate dynamic padding for bottom button bar
|
||||
// Must account for full button container height to prevent map showing underneath
|
||||
// Button container height = top padding (8) + button height (~44) + bottom padding (bottomSafeArea + 8)
|
||||
final topPadding = 0.0;
|
||||
final bottomPadding = 60.0;
|
||||
final bottomPadding = 100.0; // Full height of button container to properly cut off map
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
@@ -477,11 +618,34 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
if (!mounted || _isDisposed) return;
|
||||
await controller.setNavigationFooterEnabled(true);
|
||||
if (!mounted || _isDisposed) return;
|
||||
await controller.setNavigationTripProgressBarEnabled(true);
|
||||
// Disable navigation trip progress bar
|
||||
await controller.setNavigationTripProgressBarEnabled(false);
|
||||
if (!mounted || _isDisposed) return;
|
||||
// Enable recenter button
|
||||
await controller.setRecenterButtonEnabled(true);
|
||||
if (!mounted || _isDisposed) return;
|
||||
// Enable speed limit icon
|
||||
await controller.setSpeedLimitIconEnabled(true);
|
||||
if (!mounted || _isDisposed) return;
|
||||
// Enable traffic incident cards
|
||||
await controller.setTrafficIncidentCardsEnabled(true);
|
||||
if (!mounted || _isDisposed) return;
|
||||
// Disable traffic prompts
|
||||
await controller.setTrafficPromptsEnabled(false);
|
||||
if (!mounted || _isDisposed) return;
|
||||
// Disable report incident button
|
||||
await controller.setReportIncidentButtonEnabled(false);
|
||||
debugPrint('Navigation UI elements enabled');
|
||||
if (!mounted || _isDisposed) return;
|
||||
// Enable speedometer
|
||||
await controller.setSpeedometerEnabled(true);
|
||||
if (!mounted || _isDisposed) return;
|
||||
// Set audio guidance to silent by default
|
||||
await GoogleMapsNavigator.setAudioGuidance(
|
||||
NavigationAudioGuidanceSettings(
|
||||
guidanceType: NavigationAudioGuidanceType.silent,
|
||||
),
|
||||
);
|
||||
debugPrint('Navigation UI elements configured');
|
||||
|
||||
// Configure map settings to reduce GPU load for devices with limited graphics capabilities
|
||||
if (!mounted || _isDisposed) return;
|
||||
@@ -500,40 +664,42 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
if (!mounted || _isDisposed) return;
|
||||
await _applyDarkModeStyle();
|
||||
|
||||
// Wrap camera animation in try-catch to handle "No valid view found" errors
|
||||
// This can happen on Android when the view isn't fully ready
|
||||
// Immediately follow user location on map initialization
|
||||
try {
|
||||
if (mounted && _navigationController != null && _isMapViewReady && !_isDisposed) {
|
||||
await controller.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(initialPosition, 12),
|
||||
);
|
||||
// Start following user location immediately
|
||||
await _recenterMap();
|
||||
debugPrint('Map initialized following user location');
|
||||
// Sync navigation state to ensure button reflects actual navigation state
|
||||
await _syncNavigationState();
|
||||
|
||||
// Auto-recenter to current location after initial setup
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
if (mounted && _navigationController != null && !_isDisposed) {
|
||||
await _recenterMap();
|
||||
debugPrint('Auto-recentered map to current location on initialization');
|
||||
// Auto-start navigation if user requested it before map was ready
|
||||
if (_pendingNavigationStart && !_isNavigating && !_isDisposed && mounted) {
|
||||
debugPrint('Auto-starting navigation as requested by user');
|
||||
_pendingNavigationStart = false;
|
||||
await _startNavigation();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Camera animation error (view may not be ready): $e');
|
||||
debugPrint('Follow location error (view may not be ready): $e');
|
||||
if (_isDisposed || !mounted) return;
|
||||
// Retry once after a longer delay
|
||||
await Future.delayed(const Duration(milliseconds: 1500));
|
||||
if (mounted && _navigationController != null && _isMapViewReady && !_isDisposed) {
|
||||
try {
|
||||
await controller.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(initialPosition, 12),
|
||||
);
|
||||
await _recenterMap();
|
||||
debugPrint('Map initialized following user location (retry)');
|
||||
// Sync navigation state to ensure button reflects actual navigation state
|
||||
await _syncNavigationState();
|
||||
|
||||
// Auto-recenter to current location after retry
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
if (mounted && _navigationController != null && !_isDisposed) {
|
||||
await _recenterMap();
|
||||
debugPrint('Auto-recentered map to current location on initialization (retry)');
|
||||
// Auto-start navigation if user requested it before map was ready
|
||||
if (_pendingNavigationStart && !_isNavigating && !_isDisposed && mounted) {
|
||||
debugPrint('Auto-starting navigation as requested by user (after retry)');
|
||||
_pendingNavigationStart = false;
|
||||
await _startNavigation();
|
||||
}
|
||||
} catch (e2) {
|
||||
debugPrint('Camera animation retry failed: $e2');
|
||||
debugPrint('Follow location retry failed: $e2');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -560,63 +726,108 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
padding: EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 8,
|
||||
// Add safe area padding plus extra margin to avoid rounded corners
|
||||
bottom: bottomSafeArea > 0 ? bottomSafeArea + 8 : 16,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Start button
|
||||
Expanded(
|
||||
child: _buildBottomActionButton(
|
||||
label: _isNavigating ? 'Stop' : 'Start',
|
||||
icon: _isNavigating ? Icons.stop : Icons.navigation,
|
||||
onPressed: _isStartingNavigation || _isInitializing || (widget.selectedDelivery == null && !_isNavigating)
|
||||
? null
|
||||
: (_isNavigating ? _stopNavigation : _startNavigation),
|
||||
isDanger: _isNavigating,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Photo button (disabled when no delivery selected or warehouse delivery)
|
||||
Expanded(
|
||||
child: _buildBottomActionButton(
|
||||
label: 'Photo',
|
||||
icon: Icons.camera_alt,
|
||||
onPressed: widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
|
||||
? () => widget.onAction?.call('photo')
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Note button (only enabled if delivery has notes and not warehouse)
|
||||
Expanded(
|
||||
child: _buildBottomActionButton(
|
||||
label: 'Note',
|
||||
icon: Icons.note_add,
|
||||
onPressed: _hasNotes() && widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
|
||||
? () => widget.onAction?.call('note')
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Completed button (disabled for warehouse delivery)
|
||||
Expanded(
|
||||
child: _buildBottomActionButton(
|
||||
label: widget.selectedDelivery?.delivered == true ? 'Undo' : 'Completed',
|
||||
icon: widget.selectedDelivery?.delivered == true ? Icons.undo : Icons.check_circle,
|
||||
onPressed: widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
|
||||
? () => widget.onAction?.call(
|
||||
widget.selectedDelivery!.delivered ? 'uncomplete' : 'complete',
|
||||
)
|
||||
: null,
|
||||
isPrimary: widget.selectedDelivery != null && !widget.selectedDelivery!.delivered && !widget.selectedDelivery!.isWarehouseDelivery,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final isMobile = context.isMobile;
|
||||
final showButtonLabels = !isMobile;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// Start button
|
||||
Expanded(
|
||||
child: _buildBottomActionButton(
|
||||
label: _isNavigating ? 'Stop' : 'Start',
|
||||
icon: _isNavigating ? Icons.stop : Icons.navigation,
|
||||
onPressed: _isStartingNavigation || _isInitializing || (!_isMapViewReady && !_isNavigating) || (widget.selectedDelivery == null && !_isNavigating)
|
||||
? null
|
||||
: (_isNavigating ? _stopNavigation : _startNavigation),
|
||||
isDanger: _isNavigating,
|
||||
showLabel: showButtonLabels,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Photo button (disabled when no delivery selected or warehouse delivery)
|
||||
Expanded(
|
||||
child: _buildBottomActionButton(
|
||||
label: 'Photo',
|
||||
icon: Icons.camera_alt,
|
||||
onPressed: widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
|
||||
? () => widget.onAction?.call('photo')
|
||||
: null,
|
||||
showLabel: showButtonLabels,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Note button (only enabled if delivery has notes and not warehouse)
|
||||
Expanded(
|
||||
child: _buildBottomActionButton(
|
||||
label: 'Note',
|
||||
icon: Icons.note_add,
|
||||
onPressed: _hasNotes() && widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
|
||||
? () => widget.onAction?.call('note')
|
||||
: null,
|
||||
showLabel: showButtonLabels,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Completed button (disabled for warehouse delivery)
|
||||
Expanded(
|
||||
child: _buildBottomActionButton(
|
||||
label: widget.selectedDelivery?.delivered == true ? 'Undo' : 'Completed',
|
||||
icon: widget.selectedDelivery?.delivered == true ? Icons.undo : Icons.check_circle,
|
||||
onPressed: widget.selectedDelivery != null && !widget.selectedDelivery!.isWarehouseDelivery
|
||||
? () => widget.onAction?.call(
|
||||
widget.selectedDelivery!.delivered ? 'uncomplete' : 'complete',
|
||||
)
|
||||
: null,
|
||||
isPrimary: widget.selectedDelivery != null && !widget.selectedDelivery!.delivered && !widget.selectedDelivery!.isWarehouseDelivery,
|
||||
showLabel: showButtonLabels,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
// Audio guidance toggle button - positioned below green navigation card
|
||||
Positioned(
|
||||
top: topPadding + 48,
|
||||
right: 16,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
_isAudioGuidanceEnabled ? Icons.volume_up : Icons.volume_off,
|
||||
color: _isAudioGuidanceEnabled
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onPressed: _toggleAudioGuidance,
|
||||
tooltip: _isAudioGuidanceEnabled ? 'Mute navigation' : 'Unmute navigation',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Loading overlay during navigation initialization and start
|
||||
if (_isStartingNavigation || _isInitializing)
|
||||
Positioned.fill(
|
||||
@@ -684,6 +895,7 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
required VoidCallback? onPressed,
|
||||
bool isPrimary = false,
|
||||
bool isDanger = false,
|
||||
bool showLabel = true,
|
||||
}) {
|
||||
Color backgroundColor;
|
||||
Color textColor = Colors.white;
|
||||
@@ -709,9 +921,9 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
onTap: onPressed,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 10,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: showLabel ? 8 : 12,
|
||||
vertical: showLabel ? 10 : 12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -720,17 +932,19 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
||||
Icon(
|
||||
icon,
|
||||
color: textColor,
|
||||
size: 18,
|
||||
size: showLabel ? 18 : 20,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
if (showLabel) ...[
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -125,7 +125,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: widget.isSelected
|
||||
? Border.all(
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
width: 3,
|
||||
)
|
||||
: null,
|
||||
@@ -134,7 +134,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
||||
BoxShadow(
|
||||
color: widget.isSelected
|
||||
? statusColor.withValues(alpha: 0.5)
|
||||
: Colors.black.withValues(
|
||||
: Theme.of(context).colorScheme.scrim.withValues(
|
||||
alpha: isDark ? 0.3 : 0.15,
|
||||
),
|
||||
blurRadius: widget.isSelected ? 12 : 8,
|
||||
@@ -146,15 +146,15 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
||||
),
|
||||
child: Center(
|
||||
child: widget.delivery.isWarehouseDelivery
|
||||
? const Icon(
|
||||
? Icon(
|
||||
Icons.warehouse,
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
size: 32,
|
||||
)
|
||||
: Text(
|
||||
'${widget.delivery.deliveryIndex + 1}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
@@ -178,10 +178,10 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
||||
),
|
||||
child: Transform.rotate(
|
||||
angle: 4.71239, // 270 degrees in radians (3*pi/2)
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.note,
|
||||
size: 12,
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -232,7 +232,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
||||
boxShadow: (_isHovered || widget.isSelected) && !widget.delivery.delivered
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(
|
||||
color: Theme.of(context).colorScheme.scrim.withValues(
|
||||
alpha: isDark ? 0.3 : 0.08,
|
||||
),
|
||||
blurRadius: 8,
|
||||
@@ -261,15 +261,15 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
||||
),
|
||||
child: Center(
|
||||
child: widget.delivery.isWarehouseDelivery
|
||||
? const Icon(
|
||||
? Icon(
|
||||
Icons.warehouse,
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
size: 24,
|
||||
)
|
||||
: Text(
|
||||
'${widget.delivery.deliveryIndex + 1}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
@@ -343,7 +343,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
color: Theme.of(context).colorScheme.scrim.withValues(alpha: 0.2),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -351,10 +351,10 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
||||
),
|
||||
child: Transform.rotate(
|
||||
angle: 4.71239, // 270 degrees in radians (3*pi/2)
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.note,
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -50,21 +50,21 @@ class _GlassmorphicRouteCardState extends State<GlassmorphicRouteCard>
|
||||
// Red to orange (0-30%)
|
||||
return Color.lerp(
|
||||
SvrntyColors.crimsonRed,
|
||||
const Color(0xFFFF9800),
|
||||
SvrntyColors.progressLow,
|
||||
(progress / 0.3),
|
||||
)!;
|
||||
} else if (progress < 0.7) {
|
||||
// Orange to yellow (30-70%)
|
||||
return Color.lerp(
|
||||
const Color(0xFFFF9800),
|
||||
const Color(0xFFFFC107),
|
||||
SvrntyColors.progressLow,
|
||||
SvrntyColors.progressMedium,
|
||||
((progress - 0.3) / 0.4),
|
||||
)!;
|
||||
} else {
|
||||
// Yellow to green (70-100%)
|
||||
return Color.lerp(
|
||||
const Color(0xFFFFC107),
|
||||
const Color(0xFF4CAF50),
|
||||
SvrntyColors.progressMedium,
|
||||
SvrntyColors.progressHigh,
|
||||
((progress - 0.7) / 0.3),
|
||||
)!;
|
||||
}
|
||||
|
||||
@@ -193,8 +193,8 @@ class _MobileMapWithOverlayState extends ConsumerState<MobileMapWithOverlay>
|
||||
scrollController: _listScrollController,
|
||||
onDeliverySelected: (delivery) {
|
||||
widget.onDeliverySelected(delivery);
|
||||
// Optionally close the overlay after selection
|
||||
// _toggleList();
|
||||
// Auto-close the overlay after selection for better mobile UX
|
||||
_toggleList();
|
||||
},
|
||||
onItemAction: (delivery, action) {
|
||||
widget.onDeliveryAction(delivery, action);
|
||||
@@ -213,14 +213,21 @@ class _MobileMapWithOverlayState extends ConsumerState<MobileMapWithOverlay>
|
||||
// Floating toggle button (FAB) - only show when list is closed
|
||||
if (!isListOpen)
|
||||
Positioned(
|
||||
bottom: 80, // Above bottom action buttons
|
||||
bottom: 110, // Slightly lowered for better positioning
|
||||
right: 16,
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: 'mobile_deliveries_toggle_fab',
|
||||
onPressed: _toggleList,
|
||||
icon: const Icon(Icons.list),
|
||||
label: Text('$_completedCount/$_totalCount'),
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
label: Text(
|
||||
'$_completedCount/$_totalCount',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 4,
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user