checkpoint

This commit is contained in:
2025-11-25 17:05:08 -05:00
parent bbcd6d9bf7
commit d46ac9dc14
17 changed files with 705 additions and 193 deletions
+39 -38
View File
@@ -1,14 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/delivery_route.dart';
import '../theme/spacing_system.dart';
import '../theme/size_system.dart';
import '../theme/animation_system.dart';
import '../theme/color_system.dart';
import '../utils/breakpoints.dart';
import '../providers/providers.dart';
import 'route_list_item.dart';
class CollapsibleRoutesSidebar extends StatefulWidget {
class CollapsibleRoutesSidebar extends ConsumerStatefulWidget {
final List<DeliveryRoute> routes;
final DeliveryRoute? selectedRoute;
final ValueChanged<DeliveryRoute> onRouteSelected;
@@ -21,14 +23,13 @@ class CollapsibleRoutesSidebar extends StatefulWidget {
});
@override
State<CollapsibleRoutesSidebar> createState() =>
ConsumerState<CollapsibleRoutesSidebar> createState() =>
_CollapsibleRoutesSidebarState();
}
class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
class _CollapsibleRoutesSidebarState extends ConsumerState<CollapsibleRoutesSidebar>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
bool _isExpanded = true;
@override
void initState() {
@@ -37,9 +38,15 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
duration: const Duration(milliseconds: 300),
vsync: this,
);
if (_isExpanded) {
_animationController.forward();
}
// Set initial animation state based on provider value
WidgetsBinding.instance.addPostFrameCallback((_) {
final isExpanded = ref.read(collapseStateProvider);
if (isExpanded) {
_animationController.forward();
} else {
_animationController.value = 0;
}
});
}
@override
@@ -49,10 +56,11 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
}
void _toggleSidebar() {
setState(() {
_isExpanded = !_isExpanded;
});
if (_isExpanded) {
// Use shared provider state
ref.read(collapseStateProvider.notifier).toggle();
final isExpanded = ref.read(collapseStateProvider);
if (isExpanded) {
_animationController.forward();
} else {
_animationController.reverse();
@@ -63,6 +71,7 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
Widget build(BuildContext context) {
final isMobile = context.isMobile;
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final isExpanded = ref.watch(collapseStateProvider);
// On mobile, always show as collapsible
if (isMobile) {
@@ -84,19 +93,15 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (_isExpanded)
if (isExpanded)
Text(
'Routes',
style: Theme.of(context).textTheme.titleMedium,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
IconButton(
icon: AnimatedRotation(
turns: _isExpanded ? 0 : -0.25,
duration: Duration(
milliseconds: AppAnimations.durationFast.inMilliseconds,
),
child: const Icon(Icons.chevron_right),
),
icon: Icon(isExpanded ? Icons.menu_open : Icons.menu),
onPressed: _toggleSidebar,
iconSize: AppSizes.iconMd,
),
@@ -104,18 +109,18 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
),
),
// Collapsible content
if (_isExpanded)
if (isExpanded)
Expanded(
child: _buildRoutesList(context),
child: _buildRoutesList(context, isExpanded),
),
],
),
);
}
// On tablet/desktop, show full sidebar with toggle (expanded: 420px, collapsed: 80px for badge)
// On tablet/desktop, show full sidebar with toggle (expanded: 300px, collapsed: 80px for badge)
return Container(
width: _isExpanded ? 420 : 80,
width: isExpanded ? 300 : 80,
color: isDarkMode ? SvrntyColors.almostBlack : Colors.white,
child: Column(
children: [
@@ -132,13 +137,15 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
),
),
child: Row(
mainAxisAlignment: _isExpanded ? MainAxisAlignment.spaceBetween : MainAxisAlignment.center,
mainAxisAlignment: isExpanded ? MainAxisAlignment.spaceBetween : MainAxisAlignment.center,
children: [
if (_isExpanded)
if (isExpanded)
Expanded(
child: Text(
'Routes',
style: Theme.of(context).textTheme.titleMedium,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
overflow: TextOverflow.ellipsis,
),
),
@@ -146,13 +153,7 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
width: AppSizes.buttonHeightMd,
height: AppSizes.buttonHeightMd,
child: IconButton(
icon: AnimatedRotation(
turns: _isExpanded ? 0 : -0.5,
duration: Duration(
milliseconds: AppAnimations.durationFast.inMilliseconds,
),
child: const Icon(Icons.chevron_right),
),
icon: Icon(isExpanded ? Icons.menu_open : Icons.menu),
onPressed: _toggleSidebar,
iconSize: AppSizes.iconMd,
),
@@ -161,15 +162,15 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
),
),
// Routes list
Expanded(
child: _buildRoutesList(context),
Flexible(
child: _buildRoutesList(context, isExpanded),
),
],
),
);
}
Widget _buildRoutesList(BuildContext context) {
Widget _buildRoutesList(BuildContext context, bool isExpanded) {
return ListView.builder(
padding: const EdgeInsets.only(top: 4, bottom: 8),
physics: const AlwaysScrollableScrollPhysics(),
@@ -183,7 +184,7 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
isSelected: isSelected,
onTap: () => widget.onRouteSelected(route),
animationIndex: index,
isCollapsed: !_isExpanded,
isCollapsed: !isExpanded,
);
},
);
+60 -37
View File
@@ -34,6 +34,7 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
String _loadingMessage = 'Initializing...';
Brightness? _lastBrightness;
bool _isMapViewReady = false;
bool _isDisposed = false;
@override
void initState() {
@@ -41,6 +42,13 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
_initializeNavigation();
}
@override
void dispose() {
_isDisposed = true;
_navigationController = null;
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
@@ -49,7 +57,8 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
final currentBrightness = Theme.of(context).brightness;
if (_lastBrightness != null &&
_lastBrightness != currentBrightness &&
_navigationController != null) {
_navigationController != null &&
!_isDisposed) {
_applyDarkModeStyle();
}
_lastBrightness = currentBrightness;
@@ -135,39 +144,13 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
Future<void> _applyDarkModeStyle() async {
// Check if widget is still mounted and controller exists
if (!mounted || _navigationController == null) return;
if (!mounted || _navigationController == null || _isDisposed || !_isMapViewReady) return;
try {
if (!mounted) return;
if (!mounted || _isDisposed) 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);
}
// Always use default (light) map style
await _navigationController!.setMapStyle(null);
} catch (e) {
if (mounted) {
debugPrint('Error applying map style: $e');
@@ -358,14 +341,20 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
// This is critical for iOS to display turn-by-turn directions, ETA, distance
initialNavigationUIEnabledPreference: NavigationUIEnabledPreference.automatic,
onViewCreated: (controller) async {
// Early exit if widget is already disposed
if (_isDisposed || !mounted) return;
_navigationController = controller;
// Wait longer for the map to be fully initialized on Android
// This helps prevent crashes when the view is disposed during initialization
await Future.delayed(const Duration(milliseconds: 1000));
await Future.delayed(const Duration(milliseconds: 1500));
// Safety check: ensure widget is still mounted before proceeding
if (!mounted) return;
if (!mounted || _isDisposed) {
_navigationController = null;
return;
}
// Mark map as ready only after the delay
_isMapViewReady = true;
@@ -373,34 +362,68 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
// Enable navigation UI elements (header with turn directions, footer with ETA/distance)
// This is required for iOS to show trip info, duration, and ETA
try {
if (!mounted || _isDisposed) return;
await controller.setNavigationUIEnabled(true);
if (!mounted || _isDisposed) return;
await controller.setNavigationHeaderEnabled(true);
if (!mounted || _isDisposed) return;
await controller.setNavigationFooterEnabled(true);
if (!mounted || _isDisposed) return;
await controller.setNavigationTripProgressBarEnabled(true);
if (!mounted || _isDisposed) return;
// Disable report incident button
await controller.setReportIncidentButtonEnabled(false);
debugPrint('Navigation UI elements enabled');
// Configure map settings to reduce GPU load for devices with limited graphics capabilities
if (!mounted || _isDisposed) return;
await controller.settings.setTrafficEnabled(true);
if (!mounted || _isDisposed) return;
await controller.settings.setRotateGesturesEnabled(true);
if (!mounted || _isDisposed) return;
await controller.settings.setTiltGesturesEnabled(false);
if (!mounted || _isDisposed) return;
debugPrint('Map settings configured for performance');
} catch (e) {
debugPrint('Error enabling navigation UI: $e');
debugPrint('Error configuring map: $e');
if (_isDisposed || !mounted) return;
}
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
try {
if (mounted && _navigationController != null && _isMapViewReady) {
if (mounted && _navigationController != null && _isMapViewReady && !_isDisposed) {
await controller.animateCamera(
CameraUpdate.newLatLngZoom(initialPosition, 12),
);
// 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');
}
}
} catch (e) {
debugPrint('Camera animation error (view may not be ready): $e');
if (_isDisposed || !mounted) return;
// Retry once after a longer delay
await Future.delayed(const Duration(milliseconds: 1000));
if (mounted && _navigationController != null && _isMapViewReady) {
await Future.delayed(const Duration(milliseconds: 1500));
if (mounted && _navigationController != null && _isMapViewReady && !_isDisposed) {
try {
await controller.animateCamera(
CameraUpdate.newLatLngZoom(initialPosition, 12),
);
// 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)');
}
} catch (e2) {
debugPrint('Camera animation retry failed: $e2');
}
+22 -24
View File
@@ -54,7 +54,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
}
});
_slideAnimation = Tween<double>(begin: 20, end: 0).animate(
_slideAnimation = Tween<double>(begin: 0, end: 0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
@@ -154,16 +154,14 @@ class _DeliveryListItemState extends State<DeliveryListItem>
child: AnimatedContainer(
duration: AppAnimations.durationFast,
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
horizontal: 2,
vertical: 4,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(8),
color: widget.delivery.delivered
? Colors.green.withValues(alpha: 0.15)
: (_isHovered || widget.isSelected
? Theme.of(context).colorScheme.surfaceContainer
: Colors.transparent),
: Theme.of(context).colorScheme.surfaceContainer,
boxShadow: (_isHovered || widget.isSelected) && !widget.delivery.delivered
? [
BoxShadow(
@@ -176,7 +174,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
]
: [],
),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 24),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: Column(
children: [
// Main delivery info row
@@ -185,63 +183,63 @@ class _DeliveryListItemState extends State<DeliveryListItem>
children: [
// Order number badge (left of status bar)
Container(
width: 60,
height: 60,
width: 40,
height: 40,
decoration: BoxDecoration(
color: statusColor,
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'${widget.delivery.deliveryIndex + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 26,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: 12),
const SizedBox(width: 8),
// Left accent bar (vertical status bar)
Container(
width: 6,
height: 80,
width: 4,
height: 50,
decoration: BoxDecoration(
color: statusColor,
borderRadius: BorderRadius.circular(3),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 16),
const SizedBox(width: 10),
// Delivery info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Customer Name (20% larger - 24px)
// Customer Name
Text(
widget.delivery.name,
style: Theme.of(context)
.textTheme
.titleLarge
.titleMedium
?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 24,
fontSize: 16,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 10),
// Address (20% larger - 18px)
const SizedBox(height: 4),
// Address
Text(
widget.delivery.deliveryAddress
?.formattedAddress ??
'No address',
style: Theme.of(context)
.textTheme
.bodyLarge
.bodyMedium
?.copyWith(
fontSize: 18,
fontSize: 13,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
+20 -20
View File
@@ -149,11 +149,11 @@ class _RouteListItemState extends State<RouteListItem>
child: AnimatedContainer(
duration: AppAnimations.durationFast,
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
horizontal: 2,
vertical: 6,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(8),
color: widget.route.completed
? Colors.green.withValues(alpha: 0.15)
: (_isHovered || widget.isSelected
@@ -171,7 +171,7 @@ class _RouteListItemState extends State<RouteListItem>
]
: [],
),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 24),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 12),
child: Column(
children: [
// Main route info row
@@ -180,61 +180,61 @@ class _RouteListItemState extends State<RouteListItem>
children: [
// Route number badge (left of status bar)
Container(
width: 60,
height: 60,
width: 45,
height: 45,
decoration: BoxDecoration(
color: statusColor,
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'${(widget.animationIndex ?? 0) + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 26,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: 12),
const SizedBox(width: 8),
// Left accent bar (vertical status bar)
Container(
width: 6,
height: 80,
width: 4,
height: 50,
decoration: BoxDecoration(
color: statusColor,
borderRadius: BorderRadius.circular(3),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 16),
const SizedBox(width: 10),
// Route info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Route Name (24px)
// Route Name
Text(
widget.route.name,
style: Theme.of(context)
.textTheme
.titleLarge
.titleMedium
?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 24,
fontSize: 16,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 10),
// Route details (18px)
const SizedBox(height: 4),
// Route details
Text(
'${widget.route.deliveredCount}/${widget.route.deliveriesCount} deliveries',
style: Theme.of(context)
.textTheme
.bodyLarge
.bodyMedium
?.copyWith(
fontSize: 18,
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,