Files
Xpress-rider/lib/views/Dashboard/deliveries/nav.dart

1005 lines
35 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
part of 'deliveries.dart';
// -------------------------------------------------------------------------
// SCREEN 2: RIDER NAVIGATION (Live tracking + auto-opens Google Maps once)
// -------------------------------------------------------------------------
class _RiderNavigationScreen extends StatefulWidget {
final Map<String, dynamic> delivery;
final _MyDeliveriesState? parentState;
const _RiderNavigationScreen({
// ignore: unused_element_parameter
super.key,
required this.delivery,
this.parentState,
});
@override
State<_RiderNavigationScreen> createState() => _RiderNavigationScreenState();
}
class _RiderNavigationScreenState extends State<_RiderNavigationScreen>
with WidgetsBindingObserver {
GoogleMapController? mapController;
bool _mapReady = false;
bool _isDisposed = false;
Position? _currentPosition;
StreamSubscription<Position>? _positionStream;
final Set<Marker> _markers = {};
late final LatLng dropLocation;
bool _isInitializing = true;
bool _hasOpenedGoogleMaps = false;
Timer? _autoOpenTimer;
Timer? _pipLoaderTimer;
bool _showPipExpandLoader = false;
bool _wasInPipBeforePause = false;
// ETA timer for full navigation screen (top-right corner)
final CountDownController _navEtaController = CountDownController();
int _navEtaRemainingSeconds = 0;
final DeliveriesController deliveriesController = Get.put(
DeliveriesController(),
permanent: true,
);
final Floating floating = Floating();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_resolveDropFromDelivery();
_initializeMap();
_initEtaForNav();
WidgetsBinding.instance.addPostFrameCallback((_) {
_attemptAutoOpenNavigation();
});
_autoOpenTimer = Timer(
const Duration(seconds: 3),
() => _attemptAutoOpenNavigation(fromTimer: true),
);
}
// Initialize remaining ETA for navigation screen using same persisted end time
Future<void> _initEtaForNav() async {
try {
final prefs = await SharedPreferences.getInstance();
final orderId = widget.delivery['orderid']?.toString() ??
widget.delivery['OrderId']?.toString() ??
'';
if (orderId.isEmpty) return;
final endKey = 'eta_endtime_$orderId';
final endSeconds = prefs.getInt(endKey);
final nowSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000;
int remaining = 0;
// ✅ FIX: Checking (endSeconds > nowSeconds) caused the timer to RESET if user was late.
// We must trust existing endSeconds even if it's in the past.
if (endSeconds != null && endSeconds > 0) {
remaining = endSeconds - nowSeconds;
debugPrint('[NAV] Loaded saved ETA end time: $endSeconds (Remaining: $remaining)');
} else {
// Only start a NEW timer if one doesn't exist
final rawEta = widget.delivery['eta'];
int etaMinutes = 0;
if (rawEta != null) {
etaMinutes = int.tryParse(rawEta.toString()) ?? 0;
}
if (etaMinutes > 0) {
remaining = etaMinutes * 60;
final newEndSeconds = nowSeconds + remaining;
await prefs.setInt(endKey, newEndSeconds);
debugPrint('[NAV] Saved NEW ETA end time: $newEndSeconds (Remaining: $remaining)');
}
}
if (!mounted) return;
setState(() {
_navEtaRemainingSeconds = remaining.clamp(0, 24 * 60 * 60);
});
} catch (_) {
// Ignore errors timer simply won't show
}
}
@override
void dispose() {
_isDisposed = true;
WidgetsBinding.instance.removeObserver(this);
_autoOpenTimer?.cancel();
_pipLoaderTimer?.cancel();
_positionStream?.cancel();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState lifecycleState) {
if (!mounted) return;
if (lifecycleState == AppLifecycleState.paused ||
lifecycleState == AppLifecycleState.inactive) {
final wasInPip = deliveriesController.isPipEnabled.value;
_wasInPipBeforePause = wasInPip;
if (wasInPip) {
_pipLoaderTimer?.cancel();
if (mounted) {
setState(() {
_showPipExpandLoader = true;
});
}
}
} else if (lifecycleState == AppLifecycleState.resumed) {
// When app comes back to foreground, consider PiP session ended.
deliveriesController.isPipEnabled.value = false;
// ✅ FIX: Refresh the timer when coming back from PiP/Background
_initEtaForNav();
if (_wasInPipBeforePause) {
_wasInPipBeforePause = false;
_pipLoaderTimer?.cancel();
_pipLoaderTimer = Timer(const Duration(milliseconds: 900), () {
if (!mounted) return;
setState(() {
_showPipExpandLoader = false;
});
});
}
}
}
Future<void> enablePip(
BuildContext context, {
bool autoEnable = false,
}) async {
if (!mounted) return;
debugPrint('[NAV] Enabling PiP for navigation screen');
const rational = Rational.landscape();
final screenSize =
MediaQuery.of(context).size * MediaQuery.of(context).devicePixelRatio;
final height = (screenSize.height * 0.5).toInt();
final width = (screenSize.width * 0.9).toInt();
final arguments = autoEnable
? OnLeavePiP(
aspectRatio: rational,
sourceRectHint: math.Rectangle<int>(
0,
(screenSize.height ~/ 1) - (height ~/ 1),
screenSize.width.toInt(),
height,
),
)
: ImmediatePiP(
aspectRatio: rational,
sourceRectHint: math.Rectangle<int>(
((screenSize.width - width) ~/ 2).toInt(),
((screenSize.height - height) ~/ 2).toInt(),
width,
height,
),
);
final status = await floating.enable(arguments);
deliveriesController.isPipEnabled.value = true;
debugPrint('PiP enabled? $status');
}
double _parseD(dynamic v) {
if (v == null) return 0.0;
if (v is num) return v.toDouble();
return double.tryParse(v.toString()) ?? 0.0;
}
void _resolveDropFromDelivery() {
final d = widget.delivery;
final double dropLat = _parseD(
d['droplat'] ?? d['DropLat'] ?? d['deliverylat'],
);
final double dropLon = _parseD(
d['droplon'] ?? d['DropLon'] ?? d['deliverylong'],
);
dropLocation = (dropLat != 0 && dropLon != 0)
? LatLng(dropLat, dropLon)
: const LatLng(11.018356, 77.012596);
}
Future<void> _initializeMap() async {
try {
if (mounted) {
setState(() {
Geolocator.getLastKnownPosition().then((lastPos) {
if (lastPos != null && mounted) {
setState(() {
_currentPosition = lastPos;
});
_addMarkers(LatLng(lastPos.latitude, lastPos.longitude));
_startLiveTracking();
}
});
if (_currentPosition == null) {
_currentPosition = Position(
latitude: dropLocation.latitude,
longitude: dropLocation.longitude,
timestamp: DateTime.now(),
accuracy: 0,
altitude: 0,
heading: 0,
speed: 0,
speedAccuracy: 0,
altitudeAccuracy: 0,
headingAccuracy: 0,
);
_addMarkers(dropLocation);
}
_isInitializing = false;
});
}
_getCurrentLocation().then((_) {
_startLiveTracking();
});
} catch (e) {
debugPrint('[NAVIGATION] Error initializing map: $e');
if (mounted) {
setState(() {
if (_currentPosition == null) {
_currentPosition = Position(
latitude: dropLocation.latitude,
longitude: dropLocation.longitude,
timestamp: DateTime.now(),
accuracy: 0,
altitude: 0,
heading: 0,
speed: 0,
speedAccuracy: 0,
altitudeAccuracy: 0,
headingAccuracy: 0,
);
_addMarkers(dropLocation);
}
_isInitializing = false;
});
}
}
}
Future<void> _getCurrentLocation() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
final lastPos = await Geolocator.getLastKnownPosition();
if (lastPos != null && mounted) {
setState(() {
_currentPosition = lastPos;
_isInitializing = false;
});
}
Geolocator.openLocationSettings();
return;
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.deniedForever ||
permission == LocationPermission.denied) {
final lastPos = await Geolocator.getLastKnownPosition();
if (lastPos != null && mounted) {
setState(() {
_currentPosition = lastPos;
_isInitializing = false;
});
_addMarkers(LatLng(lastPos.latitude, lastPos.longitude));
} else if (mounted) {
setState(() => _isInitializing = false);
}
return;
}
Position? pos;
try {
pos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.medium,
timeLimit: const Duration(seconds: 5),
).timeout(const Duration(seconds: 5));
} catch (e) {
debugPrint('[NAVIGATION] Timeout getting position, using last known: $e');
pos = await Geolocator.getLastKnownPosition();
}
if (pos != null && mounted) {
setState(() {
_currentPosition = pos;
_isInitializing = false;
});
_addMarkers(LatLng(pos.latitude, pos.longitude));
} else if (mounted) {
setState(() => _isInitializing = false);
}
}
void _startLiveTracking() {
_positionStream =
Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.bestForNavigation,
distanceFilter: 5,
),
).listen((Position position) {
if (mounted) {
setState(() {
_currentPosition = position;
});
_addMarkers(LatLng(position.latitude, position.longitude));
final distance = Geolocator.distanceBetween(
position.latitude,
position.longitude,
dropLocation.latitude,
dropLocation.longitude,
);
deliveriesController.deliverableDistance.value = distance;
_safeAnimateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(position.latitude, position.longitude),
zoom: 16.5,
tilt: 45,
),
),
);
}
});
}
void _addMarkers(LatLng riderPos) {
_markers
..clear()
..add(
Marker(
markerId: const MarkerId('rider'),
position: riderPos,
icon: BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueAzure,
),
infoWindow: const InfoWindow(title: 'You (Rider)'),
),
)
..add(
Marker(
markerId: const MarkerId('drop'),
position: dropLocation,
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
infoWindow: const InfoWindow(title: 'Drop Location'),
),
);
}
Future<void> _attemptAutoOpenNavigation({bool fromTimer = false}) async {
if (!mounted || _hasOpenedGoogleMaps) return;
final bool opened = await _openGoogleMapsNavigation();
if (!opened && fromTimer) {
debugPrint('[NAVIGATION] Auto open failed, waiting for manual trigger');
}
}
Future<bool> _openGoogleMapsNavigation() async {
if (!mounted) return false;
if (_hasOpenedGoogleMaps) return true;
try {
if (Platform.isAndroid) {
try {
// ✅ CRITICAL: ENFORCE PiP when starting navigation (compulsory for active deliveries)
final dc = Get.put(DeliveriesController(), permanent: true);
// Enable PiP using floating package (primary)
if (!dc.isPipEnabled.value) {
await enablePip(context);
// Wait a moment for PiP to activate
await Future.delayed(const Duration(milliseconds: 300));
// Also tell native side (if available) best-effort
const channel = MethodChannel('nearle/pip');
try {
await channel.invokeMethod<bool>('enterPip');
} catch (_) {}
dc.isPipEnabled.value = true;
}
} on PlatformException catch (e) {
debugPrint('[NAV] Method channel PiP failed: ${e.message}');
} catch (e) {
debugPrint('[NAV] Error enabling PiP: $e');
}
}
Position? currentPos;
try {
currentPos = await Geolocator.getLastKnownPosition();
} catch (_) {}
if (currentPos == null) {
try {
currentPos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.medium,
timeLimit: const Duration(seconds: 2),
);
} catch (_) {}
}
final originLat = currentPos?.latitude ?? dropLocation.latitude;
final originLng = currentPos?.longitude ?? dropLocation.longitude;
final destLat = dropLocation.latitude;
final destLng = dropLocation.longitude;
final Uri nativeUri = Uri.parse(
'google.navigation:q=$destLat,$destLng&mode=d',
);
final Uri webUri = Uri.parse(
'https://www.google.com/maps/dir/?api=1'
'&origin=$originLat,$originLng'
'&destination=$destLat,$destLng'
'&travelmode=driving'
'&dir_action=navigate',
);
// Try native intent first without relying on canLaunch (can be flaky)
bool launched = false;
try {
launched = await launchUrl(
nativeUri,
mode: LaunchMode.externalApplication,
);
} catch (e) {
debugPrint('[NAVIGATION] Native intent failed: $e');
}
if (!launched) {
try {
launched = await launchUrl(
webUri,
mode: LaunchMode.externalApplication,
);
} catch (e) {
debugPrint('[NAVIGATION] Web intent failed: $e');
}
}
if (launched && mounted) {
setState(() {
_hasOpenedGoogleMaps = true;
});
debugPrint('[NAVIGATION] Opened Google Maps');
return true;
}
debugPrint('[NAVIGATION] Could not open Google Maps');
} catch (e) {
debugPrint('[NAVIGATION] Error opening Google Maps: $e');
}
return false;
}
@override
Widget build(BuildContext context) {
return Obx(() {
if (deliveriesController.isPipEnabled.value) {
final rawEta = widget.delivery['eta'];
int etaMinutes = 0;
if (rawEta != null) {
etaMinutes = int.tryParse(rawEta.toString()) ?? 0;
}
return PipInfoCard(
orderId: widget.delivery['orderid']?.toString() ??
widget.delivery['OrderId']?.toString() ??
'N/A',
etaMinutes: etaMinutes,
);
}
return Scaffold(
body: Stack(
children: [
GoogleMap(
mapType: MapType.normal,
onMapCreated: (controller) {
mapController = controller;
_mapReady = true;
},
initialCameraPosition: CameraPosition(
target: LatLng(
_currentPosition?.latitude ?? dropLocation.latitude,
_currentPosition?.longitude ?? dropLocation.longitude,
),
zoom: 15,
tilt: 45,
),
markers: _markers,
myLocationEnabled: true,
myLocationButtonEnabled: false,
trafficEnabled: true,
compassEnabled: true,
buildingsEnabled: true,
tiltGesturesEnabled: true,
),
if (!_hasOpenedGoogleMaps)
Positioned(
top: 50,
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
decoration: BoxDecoration(
color: ColorConstants.primaryColor,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
),
const SizedBox(width: 12),
Text(
'Opening Google Maps...',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
),
),
Positioned(
top: _hasOpenedGoogleMaps ? 50 : 120,
left: 16,
child: CircleAvatar(
backgroundColor: Colors.white,
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () {
_autoOpenTimer?.cancel();
// Navigate back to BottomPage on Deliveries tab
Get.offAll(() => const BottomPage(initialIndex: 1));
},
),
),
),
if (_hasOpenedGoogleMaps)
Positioned(
top: 50,
right: 16,
child: CircleAvatar(
backgroundColor: Colors.white,
child: IconButton(
icon: const Icon(Icons.navigation, color: Colors.blue),
onPressed: () async {
await _openGoogleMapsNavigation();
},
tooltip: 'Open Google Maps',
),
),
),
// Small real-time ETA timer on top-right in full navigation (not in PiP)
if (!deliveriesController.isPipEnabled.value &&
_navEtaRemainingSeconds > 0)
Positioned(
top: 110,
right: 16,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 32,
height: 32,
child: CircularCountDownTimer(
key: ValueKey('nav_timer_$_navEtaRemainingSeconds'), // Force rebuild if duration changes significantly
duration: _navEtaRemainingSeconds,
initialDuration: 0,
controller: _navEtaController,
width: 32,
height: 32,
ringColor: ColorConstants.primaryColor
.withValues(alpha: 0.15),
fillColor: ColorConstants.primaryColor,
backgroundColor: Colors.white,
strokeWidth: 3,
strokeCap: StrokeCap.round,
textStyle: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: ColorConstants.primaryColor,
fontFamily: FontConstants.fontFamily,
),
isReverse: true,
isReverseAnimation: true,
isTimerTextShown: true,
autoStart: true,
timeFormatterFunction:
(defaultFormatter, duration) {
return duration.inSeconds <= 0
? 'Out'
: defaultFormatter(duration);
},
),
),
const SizedBox(width: 6),
Text(
'ETA',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
color: Colors.black87,
),
),
],
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, -2),
),
],
),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Customer Details',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
InkWell(
onTap: () async {
// Enable PiP mode first before launching dialer
if (!deliveriesController.isPipEnabled.value) {
await enablePip(context);
// Wait a moment for PiP to activate
await Future.delayed(
const Duration(milliseconds: 300),
);
}
final phone =
(widget.delivery['deliverycontactno'] ?? '')
.toString();
final bool success = await launchPhoneDialer(
phone.isNotEmpty ? phone : '9876543210',
);
if (!success && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Could not launch dialer'),
),
);
}
},
child: Image.asset(
'assets/images/phone-call .png',
height: 27,
width: 27,
errorBuilder: (c, e, s) => const Icon(
Icons.phone,
size: 27,
color: Colors.green,
),
),
),
],
),
const SizedBox(height: 10),
const Divider(thickness: 1.5),
const SizedBox(height: 10),
_buildCustomerInfo(),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () async {
// Optimize: Run PiP enable and Prefs saving in parallel
final d = widget.delivery;
final double? lat = double.tryParse("${d['droplat']}");
final double? lng = double.tryParse("${d['droplon']}");
if (lat == null || lng == null) {
debugPrint("[RIDE] Invalid drop location");
return;
}
final prefsFuture = SharedPreferences.getInstance().then((prefs) async {
await prefs.setString("drop_lat", lat.toString());
await prefs.setString("drop_lng", lng.toString());
});
// PiP Logic
if (!deliveriesController.isPipEnabled.value) {
try {
debugPrint('[NAV] Enforcing PiP before Ride button');
// Don't await extremely long, just enough to start transition
await enablePip(context);
await Future.delayed(const Duration(milliseconds: 150));
} catch (e) {
debugPrint('[NAV] Error enabling PiP: $e');
}
}
// Construct URL (Map uses current location by default if origin not specified)
final String url = "https://www.google.com/maps/dir/?api=1&destination=$lat,$lng&travelmode=driving";
final Uri uri = Uri.parse(url);
// Fire and forget prefs
prefsFuture.ignore();
try {
// Direct launch is faster and avoids package visibility queries
await launchUrl(uri, mode: LaunchMode.externalApplication);
debugPrint("[RIDE] Opening Google Maps...");
} catch (e) {
debugPrint("[RIDE] Could not launch Maps: $e");
}
},
child: Text(
'Ride',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
SizedBox(width: 12), // space between buttons
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
minimumSize: const Size(
0,
48,
), // FIX: no infinity width
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () async {
final result = await showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
builder: (context) => _DeliveryBottomSheet(
delivery: widget.delivery,
parentState: widget.parentState,
),
);
if (result == true) {
// ✅ If delivery updated/skipped, close navigation screen immediately
if (mounted && Navigator.canPop(context)) {
Navigator.pop(context);
}
}
},
child: Text(
'Update',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
],
),
],
),
),
),
),
if (_showPipExpandLoader)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.45),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
width: 56,
height: 56,
child: CircularProgressIndicator(
strokeWidth: 4,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
),
const SizedBox(height: 16),
Text(
'Restoring full view...',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
),
],
),
);
});
}
Widget _buildCustomerInfo() {
final rawCollection = widget.delivery['collectionamt'];
double collectionAmt = 0.0;
if (rawCollection != null) {
if (rawCollection is num) {
collectionAmt = rawCollection.toDouble();
} else {
collectionAmt = double.tryParse(rawCollection.toString()) ?? 0.0;
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(
'Name:',
(widget.delivery['deliverycustomer'] ?? 'Customer').toString(),
),
const SizedBox(height: 10),
_buildInfoRow(
'Address:',
(widget.delivery['deliveryaddress'] ?? 'Address not available')
.toString(),
isExpanded: true,
),
const SizedBox(height: 10),
_buildInfoRow(
'Order ID:',
'#${(widget.delivery['orderid'] ?? '').toString()}',
),
if (collectionAmt > 0) ...[
const SizedBox(height: 10),
_buildInfoRow(
'To Collect:',
'${collectionAmt.toStringAsFixed(0)}',
),
],
],
);
}
Widget _buildInfoRow(String label, String value, {bool isExpanded = false}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
),
),
const SizedBox(width: 15),
isExpanded
? Expanded(
child: Text(
value,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18,
fontFamily: FontConstants.fontFamily,
color: Colors.black87,
),
),
)
: Text(
value,
style: TextStyle(
fontSize: 18,
fontFamily: FontConstants.fontFamily,
color: Colors.black87,
),
),
],
);
}
void _safeAnimateCamera(CameraUpdate update) {
if (!_mapReady || _isDisposed || mapController == null) return;
try {
mapController?.animateCamera(update);
} catch (e) {
debugPrint('[NAVIGATION] animateCamera skipped: $e');
}
}
}