diff --git a/lib/views/home/navigation/navigation_controller.dart b/lib/views/home/navigation/navigation_controller.dart index 31bef4d..a6927c7 100644 --- a/lib/views/home/navigation/navigation_controller.dart +++ b/lib/views/home/navigation/navigation_controller.dart @@ -70,12 +70,16 @@ class NavigationController extends GetxController Set circles = {}; Set polygons = {}; - Timer? _locationUpdateTimer; + StreamSubscription? _locationStreamSubscription; LatLng? _lastProcessedLocation; List placesDestination = []; Timer? _debounce; + // Alternative route handling + bool _hasAlternativeRoutes = false; + DateTime? _lastAutoRerouteTime; + LatLng? _finalDestination; LatLng? _intermediateStop; List> routeSteps = []; @@ -301,7 +305,7 @@ class NavigationController extends GetxController void onInit() { super.onInit(); _animController = AnimationController( - vsync: this, duration: const Duration(milliseconds: 3800)); + vsync: this, duration: const Duration(milliseconds: 1000)); _animController!.addListener(() { if (_oldLoc != null && _targetLoc != null && _mapReady) { final t = _animController!.value; @@ -360,7 +364,7 @@ class NavigationController extends GetxController @override void onClose() { - _locationUpdateTimer?.cancel(); + _locationStreamSubscription?.cancel(); _recordTimer?.cancel(); _uploadBatchTimer?.cancel(); _debounce?.cancel(); @@ -468,29 +472,43 @@ class NavigationController extends GetxController _smoothedHeading = position.heading; update(); if (isStyleLoaded) animateCameraToPosition(myLocation!); - _startLocationTimer(); + // Start the Location Stream for real-time updates + _startLocationStream(); _startBatchTimers(); } catch (e) { Log.print("DEBUG: Error getting initial location: $e"); } } - void _startLocationTimer() { - _locationUpdateTimer?.cancel(); - _locationUpdateTimer = - Timer.periodic(const Duration(seconds: 4), (_) => _tick()); + void _startLocationStream() { + _locationStreamSubscription?.cancel(); + // Listen to location updates with minimum distance filter of 2 meters + // This provides real-time updates without the 3-4 second delay + _locationStreamSubscription = Geolocator.getPositionStream( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 2, // Update every 2 meters + ), + ).listen( + (Position position) { + _handleLocationUpdate(position); + }, + onError: (error) { + Log.print("DEBUG: Location stream error: $error"); + }, + ); } - bool _isTicking = false; - Future _tick() async { - if (_isTicking) return; - _isTicking = true; - try { - final position = await Geolocator.getCurrentPosition( - desiredAccuracy: LocationAccuracy.high); - final newLoc = LatLng(position.latitude, position.longitude); - currentSpeed = position.speed * 3.6; + bool _isProcessing = false; + Future _handleLocationUpdate(Position position) async { + if (_isProcessing) return; + _isProcessing = true; + try { + final newLoc = LatLng(position.latitude, position.longitude); + currentSpeed = position.speed * 3.6; // Convert m/s to km/h + + // Skip if movement is too small if (_lastProcessedLocation != null) { final d = Geolocator.distanceBetween( newLoc.latitude, @@ -498,11 +516,16 @@ class NavigationController extends GetxController _lastProcessedLocation!.latitude, _lastProcessedLocation!.longitude, ); - if (d < _minMoveToProcess) return; + if (d < _minMoveToProcess) { + _isProcessing = false; + return; + } } - Log.print( - "DEBUG: Location tick - Speed: ${currentSpeed.toStringAsFixed(1)} km/h, Loc: $newLoc"); + Log.print( + "DEBUG: Location update - Speed: ${currentSpeed.toStringAsFixed(1)} km/h, Loc: $newLoc"); + + // Update total distance if (_lastDistanceLocation != null) { final d = Geolocator.distanceBetween( _lastDistanceLocation!.latitude, @@ -542,9 +565,9 @@ class NavigationController extends GetxController } update(); } catch (e) { - Log.print("DEBUG: Error in _tick: $e"); + Log.print("DEBUG: Error in _handleLocationUpdate: $e"); } finally { - _isTicking = false; + _isProcessing = false; } } @@ -581,7 +604,8 @@ class NavigationController extends GetxController if (elapsed >= _offRouteTriggerSeconds) { _offRouteStartTime = null; _autoRecalcInProgress = true; - recalculateRoute().then((_) => _autoRecalcInProgress = false); + // Smart reroute: check if we have alternative routes available + _smartRecalculateRoute(pos); } } } else { @@ -589,6 +613,48 @@ class NavigationController extends GetxController } } + /// الحل الذكي: إذا كان هناك مسارات بديلة متاحة، اختر الأقرب. + /// وإلا فاطلب مسار جديد من الموقع الحالي إلى الوجهة. + Future _smartRecalculateRoute(LatLng currentPos) async { + try { + // Check if we have alternative routes + if (routes.isNotEmpty && selectedRouteIndex < routes.length - 1) { + // Try using the next alternative route + final nextIndex = selectedRouteIndex + 1; + final nextRoute = routes[nextIndex]; + + // Calculate distance from current position to this alternative route's start + double minDist = double.infinity; + for (var coord in nextRoute.coordinates) { + final d = Geolocator.distanceBetween( + currentPos.latitude, + currentPos.longitude, + coord.latitude, + coord.longitude, + ); + if (d < minDist) minDist = d; + } + + // If this alternative is reasonable, switch to it + if (minDist < 100) { + selectRoute(nextIndex); + Log.print("DEBUG: Switched to alternative route due to deviation"); + _autoRecalcInProgress = false; + return; + } + } + + // No good alternative, recalculate from current position to destination + if (_finalDestination != null) { + await recalculateRoute(); + } + _autoRecalcInProgress = false; + } catch (e) { + Log.print("DEBUG: Error in smart recalculate: $e"); + _autoRecalcInProgress = false; + } + } + void _startBatchTimers() { _recordTimer?.cancel(); _uploadBatchTimer?.cancel(); @@ -851,6 +917,7 @@ class NavigationController extends GetxController 'toLat': destination.latitude.toString(), 'toLng': destination.longitude.toString(), 'steps': 'true', + 'alternatives': 'true', 'locale': langCode, }; @@ -890,7 +957,9 @@ class NavigationController extends GetxController } // ── Parse alternative routes (in data['alternatives']) ── + // إذا كان هناك routes بديلة متاحة من API if (data['alternatives'] != null && data['alternatives'] is List) { + _hasAlternativeRoutes = data['alternatives'].isNotEmpty; for (var alt in data['alternatives']) { final altPts = alt['points']?.toString() ?? ""; if (altPts.isEmpty) continue; @@ -904,6 +973,11 @@ class NavigationController extends GetxController points: altPts, )); } + if (_hasAlternativeRoutes) { + Log.print("DEBUG: ${routes.length - 1} alternative routes available"); + } + } else { + _hasAlternativeRoutes = false; } if (routes.isEmpty) { @@ -1053,6 +1127,48 @@ class NavigationController extends GetxController update(); } + Future startActiveNavigation() async { + if (routes.isEmpty) { + mySnackbarWarning(box.read(BoxName.lang) == 'ar' + ? 'لا يوجد مسار لبدء الملاحة.' + : 'No route to start navigation.'); + return; + } + if (isNavigating) return; + + isNavigating = true; + _cameraLockedToUser = true; + + // Ensure ETA and distances are up-to-date + _lastTraveledIndexInFullRoute = _lastTraveledIndexInFullRoute; + _recomputeETA(); + + // Initialize current instruction if available + if (routeSteps.isNotEmpty && currentStepIndex < routeSteps.length) { + currentInstruction = routeSteps[currentStepIndex]['text'] ?? ""; + currentManeuverModifier = routeSteps[currentStepIndex]['sign'] ?? 0; + nextInstruction = (currentStepIndex + 1) < routeSteps.length + ? (box.read(BoxName.lang) == 'ar' + ? "ثم ${routeSteps[currentStepIndex + 1]['text']}" + : "Then ${routeSteps[currentStepIndex + 1]['text']}") + : (box.read(BoxName.lang) == 'ar' ? 'الوجهة' : 'Destination'); + + if (!isMuted) { + try { + Get.find().speakText(currentInstruction); + } catch (_) {} + } + } + + // Center camera on user for navigation mode + if (myLocation != null) { + animateCameraToPosition(myLocation!, + bearing: _smoothedHeading, zoom: _targetZoom, tilt: _targetTilt); + } + + update(); + } + Future clearEverything() async { placeDestinationController.clear(); placesDestination = []; @@ -1077,14 +1193,14 @@ class NavigationController extends GetxController _lastTraveledIndexInFullRoute = 0; currentInstruction = ""; nextInstruction = ""; - currentManeuverModifier = "straight"; + currentManeuverModifier = "intaleq"; distanceToNextStep = ""; totalDistanceRemaining = ""; estimatedTimeRemaining = ""; arrivalTime = "--:--"; _routeTotalDistanceM = 0; _routeTotalDurationS = 0; - + if (!isNewRoute) { await _updateCarMarker(); } @@ -1169,38 +1285,23 @@ class NavigationController extends GetxController update(); return; } - if (myLocation == null) return; - - final lat = myLocation!.latitude; - final lng = myLocation!.longitude; - const radiusKm = 200.0; - final payload = { - 'query': q, - 'lat_min': (lat - _kmToLatDelta(radiusKm)).toString(), - 'lat_max': (lat + _kmToLatDelta(radiusKm)).toString(), - 'lng_min': (lng - _kmToLngDelta(radiusKm, lat)).toString(), - 'lng_max': (lng + _kmToLngDelta(radiusKm, lat)).toString(), - }; + if (mapController == null) return; try { - final response = - await CRUD().post(link: AppLink.getPlacesSyria, payload: payload); - List list; - if (response is Map && response['status'] == 'success') { - list = List.from(response['message'] as List); - } else if (response is List) - list = List.from(response); - else - return; - - for (final p in list) { - final plat = double.tryParse(p['latitude']?.toString() ?? '0') ?? 0.0; - final plng = double.tryParse(p['longitude']?.toString() ?? '0') ?? 0.0; - p['distanceKm'] = _haversineKm(lat, lng, plat, plng); + // ✅ Use searchPlaces from intaleq_maps SDK + final results = await mapController!.searchPlaces(q); + + if (myLocation != null) { + for (final p in results) { + final plat = double.tryParse(p['latitude']?.toString() ?? '0') ?? 0.0; + final plng = double.tryParse(p['longitude']?.toString() ?? '0') ?? 0.0; + p['distanceKm'] = _haversineKm(myLocation!.latitude, myLocation!.longitude, plat, plng); + } + results.sort((a, b) => + (a['distanceKm'] as double).compareTo(b['distanceKm'] as double)); } - list.sort((a, b) => - (a['distanceKm'] as double).compareTo(b['distanceKm'] as double)); - placesDestination = list; + + placesDestination = results; update(); } catch (e) { Log.print('getPlaces error: $e'); @@ -1218,7 +1319,7 @@ class NavigationController extends GetxController void onSearchChanged(String query) { if (_debounce?.isActive ?? false) _debounce!.cancel(); - _debounce = Timer(const Duration(milliseconds: 700), () => getPlaces()); + _debounce = Timer(const Duration(milliseconds: 500), () => getPlaces()); } double _haversineKm(double lat1, double lon1, double lat2, double lon2) { diff --git a/lib/views/home/navigation/navigation_view.dart b/lib/views/home/navigation/navigation_view.dart index fbe7f68..c654484 100644 --- a/lib/views/home/navigation/navigation_view.dart +++ b/lib/views/home/navigation/navigation_view.dart @@ -1,6 +1,5 @@ import 'dart:math'; import 'dart:ui'; -import 'package:Intaleq/constant/colors.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; @@ -8,26 +7,19 @@ import 'package:intaleq_maps/intaleq_maps.dart'; import 'package:Intaleq/env/env.dart'; import '../../../constant/box_name.dart'; +import '../../../constant/colors.dart'; import '../../../main.dart'; import '../../widgets/error_snakbar.dart'; import 'navigation_controller.dart'; -// ─── Color Palette Matching the HTML Specs ────────────────────────────────── +// ─── Color Palette ────────────────────────────────────────────────────────── Color get _kSurface => - Get.isDarkMode ? const Color(0xFF131b2e) : const Color(0xFFfaf8ff); -Color get _kSurfaceContainerLowest => - Get.isDarkMode ? const Color(0xFF1e293b) : const Color(0xFFffffff); -Color get _kSurfaceContainerHigh => - Get.isDarkMode ? const Color(0xFF334155) : const Color(0xFFe2e7ff); -Color get _kPrimary => const Color(0xFF000000); -Color get _kPrimaryContainer => const Color(0xFF131b2e); -Color get _kOnPrimaryContainer => const Color(0xFF7c839b); + Get.isDarkMode ? const Color(0xFF0F172A) : const Color(0xFFF8FAFC); +Color get _kCardColor => + Get.isDarkMode ? const Color(0xFF1E293B) : const Color(0xFFFFFFFF); Color get _kOnSurface => - Get.isDarkMode ? const Color(0xFFffffff) : const Color(0xFF131b2e); -Color get _kOnSurfaceVariant => const Color(0xFF45464d); -Color get _kErrorContainer => const Color(0xFFffdad6); -Color get _kError => const Color(0xFFba1a1a); -Color get _kOutlineVariant => const Color(0xFFc6c6cd); + Get.isDarkMode ? const Color(0xFFF1F5F9) : const Color(0xFF0F172A); +Color get _kPrimary => const Color(0xFF2563EB); class NavigationView extends StatelessWidget { const NavigationView({super.key}); @@ -49,9 +41,7 @@ class NavigationView extends StatelessWidget { IntaleqMap( apiKey: Env.mapSaasKey, onMapCreated: c.onMapCreated, - - onLongPress: (pos) => c.onMapLongPressed( - Point(0, 0), pos), // Adapted for IntaleqMap API + onLongPress: (pos) => c.onMapLongPressed(Point(0, 0), pos), onTap: (pos) => c.onMapTapped(Point(0, 0), pos), markers: c.markers, polylines: c.polylines, @@ -63,60 +53,122 @@ class NavigationView extends StatelessWidget { initialCameraPosition: CameraPosition( target: c.myLocation ?? const LatLng(33.5138, 36.2765), zoom: 16.0), - myLocationEnabled: true, + myLocationEnabled: false, ), // ── 2. Top UI (Explore Mode) ────────────────────────────────── if (!c.isNavigating) _ExploreTopUI(controller: c), - // ── 3. Top UI (Active Navigation Banner) ────────────────────── + // ── 3. Top UI (Active Navigation Banner) ── if (c.isNavigating && c.currentInstruction.isNotEmpty) _ActiveTopInstruction(controller: c), - // ── 4. Explore Action Row (Capsules) ────────────────────────── + // ── 4. Explore Action Row ───────────────────────────────────── if (!c.isNavigating) _ExploreActionRow(controller: c), - // ── 5. Bottom Panel (Explore Mode / Route Setup) ────────────── + // ── 5. Bottom Panel (Explore Mode) ──────────────────────────── if (!c.isNavigating) _ExploreBottomPanel(controller: c), // ── 6. Bottom HUD (Active Navigation) ───────────────────────── if (c.isNavigating) _ActiveBottomHUD(controller: c), - // ── 7. Speedometer Badge ────────────────────────────────────── - if (c.isNavigating) _SpeedBadge(speed: c.currentSpeed), - - // ── 8. Search Results Dropdown ──────────────────────────────── + // ── 7. Search Results Dropdown ──────────────────────────────── if (c.placesDestination.isNotEmpty && !c.isNavigating) _SearchResults(controller: c), - // ── 9. Loading Overlay ──────────────────────────────────────── + // ── 8. Loading Overlay ──────────────────────────────────────── if (c.isLoading) const _LoadingOverlay(), - // ── 10. Location Picker Overlay (Place Creation) ────────────── + // ── 9. Location Picker Overlay (Place Creation) ────────────── _LocationPickerOverlay(controller: c), - // ── 11. Recenter / Follow Me Button ─────────────────────────── - if (!c.isCameraLocked) + // ── 10. Recenter Button ─────────────────────────────────────── + if (!c.isCameraLocked && !c.isNavigating) Positioned( right: 16, - bottom: c.isNavigating ? 140 : 250, - child: FloatingActionButton.small( - onPressed: () => c.relockCameraToUser(), - backgroundColor: Colors.white, - child: const Icon(Icons.my_location_rounded, - color: Color(0xFF0D47A1)), + bottom: 250, + child: _buildMapFAB( + icon: Icons.my_location_rounded, + onTap: () => c.relockCameraToUser(), ), ), + + // ── 11. Navigation Icon (Fixed Center) ──────────────────────── + if (c.isNavigating) const _NavigationCenterIcon(), ], ), ), ), ); } + + Widget _buildMapFAB({required IconData icon, required VoidCallback onTap}) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: _kCardColor, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 12, + offset: const Offset(0, 4), + ) + ], + ), + child: Icon(icon, color: _kPrimary, size: 22), + ), + ); + } +} + +// ============================================================================= +// NAVIGATION CENTER ICON (FIXED) +// ============================================================================= + +class _NavigationCenterIcon extends StatelessWidget { + const _NavigationCenterIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: _kPrimary, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ) + ], + border: Border.all(color: Colors.white, width: 3), + ), + padding: const EdgeInsets.all(12), + child: const Icon( + Icons.navigation_rounded, + color: Colors.white, + size: 32, + ), + ), + const SizedBox(height: 40), // Adjust to align with map focus + ], + ), + ), + ); + } } // ============================================================================= -// EXPLORE MODE COMPONENTS +// UI COMPONENTS // ============================================================================= class _ExploreTopUI extends StatelessWidget { @@ -131,95 +183,68 @@ class _ExploreTopUI extends StatelessWidget { right: 0, child: SafeArea( bottom: false, - child: Column( - children: [ - // Search Pill - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - child: ClipRRect( - borderRadius: BorderRadius.circular(28), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: _kSurfaceContainerLowest.withOpacity(0.85), - borderRadius: BorderRadius.circular(28), - border: Border.all( - color: Colors.white.withOpacity(0.3), width: 1.5), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.12), - blurRadius: 32, - offset: const Offset(0, 12), - ) - ], - ), - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - child: Row( - children: [ - IconButton( - icon: Icon(Icons.menu_rounded, - color: _kOnSurface, size: 26), - onPressed: () {}, // Drawer or Menu logic here - ), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: controller.placeDestinationController, - onChanged: controller.onSearchChanged, - textInputAction: TextInputAction.search, - style: TextStyle( - fontSize: 18, - color: _kOnSurface, - fontWeight: FontWeight.w700), - decoration: InputDecoration( - hintText: box.read(BoxName.lang) == 'ar' - ? 'إلى أين؟' - : 'Where to?', - hintStyle: TextStyle( - color: _kOnSurfaceVariant.withOpacity(0.7), - fontSize: 18, - fontWeight: FontWeight.w600), - border: InputBorder.none, - isDense: true, - contentPadding: - const EdgeInsets.symmetric(vertical: 12), - ), - ), - ), - if (controller - .placeDestinationController.text.isNotEmpty || - controller.routes.isNotEmpty) - Container( - decoration: const BoxDecoration( - color: Color(0xFFFFEBEE), - shape: BoxShape.circle, - ), - child: IconButton( - icon: const Icon(Icons.close_rounded), - color: Colors.red, - onPressed: () => controller.clearEverything(), - ), - ) - else - // Avatar - const Padding( - padding: EdgeInsets.only(right: 4), - child: CircleAvatar( - radius: 20, - backgroundColor: Color(0xFF0D47A1), - child: Icon(Icons.person_rounded, - color: Colors.white, size: 22), - ), - ), - ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Container( + decoration: BoxDecoration( + color: _kCardColor, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 24, + offset: const Offset(0, 8), + ) + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Row( + children: [ + IconButton( + icon: Icon(Icons.menu_rounded, color: _kOnSurface, size: 24), + onPressed: () {}, + ), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: controller.placeDestinationController, + onChanged: controller.onSearchChanged, + textInputAction: TextInputAction.search, + style: TextStyle( + fontSize: 16, + color: _kOnSurface, + fontWeight: FontWeight.w600), + decoration: InputDecoration( + hintText: box.read(BoxName.lang) == 'ar' + ? 'إلى أين؟' + : 'Where to?', + hintStyle: TextStyle( + color: _kOnSurface.withOpacity(0.4), fontSize: 16), + border: InputBorder.none, + isDense: true, ), ), ), - ), + if (controller.placeDestinationController.text.isNotEmpty || + controller.routes.isNotEmpty) + IconButton( + icon: Icon(Icons.close_rounded, + color: Colors.red.shade400, size: 22), + onPressed: () => controller.clearEverything(), + ) + else + Padding( + padding: const EdgeInsets.only(right: 8), + child: CircleAvatar( + radius: 18, + backgroundColor: _kPrimary, + child: const Icon(Icons.person_rounded, + color: Colors.white, size: 20), + ), + ), + ], ), - ], + ), ), ), ); @@ -230,270 +255,136 @@ class _ExploreBottomPanel extends StatelessWidget { final NavigationController controller; const _ExploreBottomPanel({required this.controller}); - String _formatDuration(double seconds) { - final mins = (seconds / 60).toInt(); - if (mins >= 60) { - final h = mins ~/ 60; - final m = mins % 60; - return box.read(BoxName.lang) == 'ar' - ? '$h ساعة ${m > 0 ? '$m د' : ''}' - : '${h}h ${m > 0 ? '${m}m' : ''}'; - } - return box.read(BoxName.lang) == 'ar' ? '$mins دقيقة' : '$mins min'; - } - - String _formatDistance(double meters) { - if (meters >= 1000) { - return box.read(BoxName.lang) == 'ar' - ? '${(meters / 1000).toStringAsFixed(1)} كم' - : '${(meters / 1000).toStringAsFixed(1)} km'; - } - return box.read(BoxName.lang) == 'ar' - ? '${meters.toInt()} م' - : '${meters.toInt()} m'; - } - @override Widget build(BuildContext context) { final bool hasRoutes = controller.routes.isNotEmpty; - final bool isArabic = box.read(BoxName.lang) == 'ar'; + final isArabic = box.read(BoxName.lang) == 'ar'; final bottomPad = MediaQuery.of(context).padding.bottom; - if (!hasRoutes && controller.recentLocations.isEmpty) { - return const SizedBox.shrink(); - } + if (!hasRoutes) return const SizedBox.shrink(); return Positioned( bottom: 0, left: 0, right: 0, - child: ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), - child: Container( - padding: EdgeInsets.only(bottom: bottomPad + 8), - decoration: BoxDecoration( - color: _kSurfaceContainerLowest.withOpacity(0.92), - boxShadow: const [ - BoxShadow( - color: Color(0x14000000), - blurRadius: 48, - offset: Offset(0, -12)) - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Minimal Drag Handle - Container( - width: 32, - height: 3, - margin: const EdgeInsets.only(top: 8, bottom: 8), - decoration: BoxDecoration( - color: _kOutlineVariant.withOpacity(0.3), - borderRadius: BorderRadius.circular(10))), - - // ── Route Selection Cards ── - if (hasRoutes) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - children: - List.generate(controller.routes.length, (index) { - final r = controller.routes[index]; - final isSelected = - controller.selectedRouteIndex == index; - return _RouteOptionCard( - index: index, - distance: _formatDistance(r.distanceM), - duration: _formatDuration(r.durationS), - isSelected: isSelected, - isArabic: isArabic, - onTap: () => controller.selectRoute(index), - ); - }), - ), - ), - const SizedBox(height: 12), - // Start Button - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - HapticFeedback.mediumImpact(); - controller.isNavigating = true; - controller.relockCameraToUser(); - controller.update(); - }, - borderRadius: BorderRadius.circular(16), - child: Ink( - padding: const EdgeInsets.symmetric(vertical: 16), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFF0D47A1), Color(0xFF1565C0)], - ), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: - const Color(0xFF0D47A1).withOpacity(0.35), - blurRadius: 16, - offset: const Offset(0, 6)) - ], - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.navigation_rounded, - color: Colors.white, size: 20), - const SizedBox(width: 8), - Text( - isArabic - ? 'ابدأ الملاحة' - : 'Start Navigation', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w800)), - ], - ), - ), - ), - ), - ), - ] else if (!hasRoutes) ...[ - const SizedBox.shrink(), - ], - ], - ), - ), - ), - ), - ); - } -} - -class _RouteOptionCard extends StatelessWidget { - final int index; - final String distance; - final String duration; - final bool isSelected; - final bool isArabic; - final VoidCallback onTap; - - const _RouteOptionCard({ - required this.index, - required this.distance, - required this.duration, - required this.isSelected, - required this.isArabic, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - HapticFeedback.selectionClick(); - onTap(); - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Container( + padding: EdgeInsets.only(bottom: bottomPad + 16, top: 16), decoration: BoxDecoration( - color: isSelected - ? const Color(0xFF0D47A1) - : _kSurfaceContainerHigh.withOpacity(0.4), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected - ? const Color(0xFF42A5F5).withOpacity(0.4) - : _kOutlineVariant.withOpacity(0.15), - width: isSelected ? 1.5 : 1, - ), + color: _kCardColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 32, + offset: const Offset(0, -4), + ) + ], ), - child: Row( + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - // Route icon - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: isSelected - ? Colors.white.withOpacity(0.15) - : _kSurfaceContainerHigh.withOpacity(0.5), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - index == 0 ? Icons.route_rounded : Icons.alt_route_rounded, - color: isSelected ? Colors.white : _kOnSurfaceVariant, - size: 20, - ), - ), - const SizedBox(width: 14), - - // Route Label - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( children: [ Text( - index == 0 - ? (isArabic ? 'أفضل مسار' : 'Best Route') - : (isArabic - ? 'مسار بديل $index' - : 'Alternative $index'), + isArabic ? 'المسارات المتاحة' : 'Available Routes', style: TextStyle( - color: isSelected ? Colors.white : _kOnSurface, + color: _kOnSurface, + fontWeight: FontWeight.bold, fontSize: 14, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 2), - Text( - distance, - style: TextStyle( - color: isSelected - ? Colors.white.withOpacity(0.7) - : _kOnSurfaceVariant, - fontSize: 12, ), ), ], ), ), - - // Duration (prominent) - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: isSelected - ? Colors.white.withOpacity(0.15) - : const Color(0xFFE3F2FD), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - duration, - style: TextStyle( - color: isSelected ? Colors.white : const Color(0xFF0D47A1), - fontSize: 14, - fontWeight: FontWeight.w800, - ), + const SizedBox(height: 12), + SizedBox( + height: 90, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 12), + scrollDirection: Axis.horizontal, + itemCount: controller.routes.length, + itemBuilder: (context, index) { + final r = controller.routes[index]; + final isSelected = controller.selectedRouteIndex == index; + return GestureDetector( + onTap: () => controller.selectRoute(index), + child: Container( + width: 140, + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isSelected ? _kPrimary : _kSurface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected ? _kPrimary : Colors.grey.shade300, + width: 1.5, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.route_rounded, + color: isSelected ? Colors.white : _kOnSurface, + ), + const SizedBox(height: 6), + Text( + "${(r.distanceM / 1000).toStringAsFixed(1)} km", + style: TextStyle( + color: isSelected ? Colors.white : _kOnSurface, + fontWeight: FontWeight.bold, + ), + ), + Text( + "${(r.durationS / 60).toInt()} min", + style: TextStyle( + color: isSelected + ? Colors.white70 + : _kOnSurface.withOpacity(0.6), + fontSize: 12, + ), + ), + ], + ), + ), + ); + }, ), ), - - // Selection indicator - const SizedBox(width: 8), - Icon( - isSelected - ? Icons.check_circle_rounded - : Icons.radio_button_unchecked_rounded, - color: isSelected ? Colors.white : _kOutlineVariant, - size: 22, + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: _kPrimary, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + ), + onPressed: () { + HapticFeedback.mediumImpact(); + controller.startActiveNavigation(); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.navigation_rounded, + color: Colors.white, size: 20), + const SizedBox(width: 8), + Text(isArabic ? 'ابدأ الملاحة' : 'Start Navigation', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w700)), + ], + ), + ), + ), ), ], ), @@ -502,46 +393,6 @@ class _RouteOptionCard extends StatelessWidget { } } -class _CompactRecentPlace extends StatelessWidget { - final String title; - final String subtitle; - final VoidCallback onTap; - - const _CompactRecentPlace({ - required this.title, - required this.subtitle, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return ListTile( - onTap: onTap, - dense: true, - contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 0), - leading: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: _kSurfaceContainerHigh.withOpacity(0.5), - shape: BoxShape.circle, - ), - child: Icon(Icons.history_rounded, size: 18, color: _kOnSurface), - ), - title: Text(title, - style: TextStyle( - fontWeight: FontWeight.w700, color: _kOnSurface, fontSize: 14)), - subtitle: Text(subtitle, - style: TextStyle(color: _kOnSurfaceVariant, fontSize: 12), - maxLines: 1, - overflow: TextOverflow.ellipsis), - ); - } -} - -// ============================================================================= -// ACTIVE NAVIGATION MODE COMPONENTS -// ============================================================================= - class _ActiveTopInstruction extends StatelessWidget { final NavigationController controller; const _ActiveTopInstruction({required this.controller}); @@ -552,71 +403,56 @@ class _ActiveTopInstruction extends StatelessWidget { top: MediaQuery.of(context).padding.top + 12, left: 16, right: 16, - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFF1B5E20), Color(0xFF2E7D32)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFF1B5E20), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.25), + blurRadius: 16, + offset: const Offset(0, 8)) + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + shape: BoxShape.circle, ), - borderRadius: BorderRadius.circular(24), - border: - Border.all(color: Colors.white.withOpacity(0.2), width: 1), - boxShadow: const [ - BoxShadow( - color: Color(0x33000000), - blurRadius: 24, - offset: Offset(0, 12)) - ], + child: Icon(controller.currentManeuverIcon, + color: Colors.white, size: 28), ), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: Row( - children: [ - // Direction indicator - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - shape: BoxShape.circle, + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + controller.currentInstruction, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w700), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - child: Icon(controller.currentManeuverIcon, - color: Colors.white, size: 36), - ), - const SizedBox(width: 16), - // Full instruction text - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - controller.currentInstruction, - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w800, - letterSpacing: -0.5), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 2), - Text( - controller.distanceToNextStep, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 16, - fontWeight: FontWeight.w700), - ), - ], + const SizedBox(height: 2), + Text( + controller.distanceToNextStep, + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 14, + fontWeight: FontWeight.w600), ), - ), - ], + ], + ), ), - ), + ], ), ), ); @@ -636,149 +472,142 @@ class _ActiveBottomHUD extends StatelessWidget { bottom: bottomPad + 16, left: 16, right: 16, - child: ClipRRect( - borderRadius: BorderRadius.circular(32), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + width: 70, + height: 70, decoration: BoxDecoration( - color: _kSurfaceContainerLowest.withOpacity(0.85), - borderRadius: BorderRadius.circular(32), - border: - Border.all(color: Colors.white.withOpacity(0.4), width: 1.5), - boxShadow: const [ - BoxShadow( - color: Color(0x14000000), - blurRadius: 24, - offset: Offset(0, 12)) + color: _kCardColor, + shape: BoxShape.circle, + border: Border.all(color: Colors.red, width: 3), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10) ], ), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Column( - mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ - // Stats Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Text( - controller.arrivalTime, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.w900, - color: Color(0xFF0D47A1), - ), - ), - Container( - width: 1.5, - height: 24, - color: _kOutlineVariant.withOpacity(0.3)), - Text( - isArabic - ? '${controller.estimatedTimeRemaining} دقيقة' - : '${controller.estimatedTimeRemaining} min', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - color: _kOnSurface, - ), - ), - Container( - width: 1.5, - height: 24, - color: _kOutlineVariant.withOpacity(0.3)), - Text( - controller.distanceWithUnit, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - color: _kOnSurfaceVariant, - ), - ), - ], + Text( + controller.currentSpeed.toStringAsFixed(0), + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + color: Colors.black, + ), ), - const SizedBox(height: 16), - // Action Buttons Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _IconBtn( - icon: Icons.report_problem_rounded, - bgColor: const Color(0xFFFFF3E0), - iconColor: Colors.orange.shade800, - onTap: () {}, - ), - _IconBtn( - icon: controller.isMuted - ? Icons.volume_off_rounded - : Icons.volume_up_rounded, - bgColor: const Color(0xFFE3F2FD), - iconColor: const Color(0xFF1976D2), - onTap: () => controller.toggleMute(), - ), - _IconBtn( - icon: Icons.add_rounded, - bgColor: const Color(0xFFE8F5E9), - iconColor: const Color(0xFF388E3C), - onTap: () => controller.togglePlaceSelectionMode(), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: GestureDetector( - onTap: () => controller.clearRoute(), - child: Container( - height: 48, - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFFE53935), Color(0xFFC62828)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(24), - boxShadow: [ - BoxShadow( - color: Colors.red.withOpacity(0.3), - blurRadius: 12, - offset: const Offset(0, 6), - ) - ], - ), - child: Center( - child: Text( - isArabic ? 'إنهاء الملاحة' : 'End Route', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), + const Text( + 'km/h', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: Colors.grey, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + Expanded( + child: Container( + decoration: BoxDecoration( + color: _kCardColor, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 24, + offset: const Offset(0, 8)) + ], + ), + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + controller.arrivalTime, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + color: Color(0xFF2563EB), + ), + ), + Text( + isArabic + ? '${controller.estimatedTimeRemaining} د' + : '${controller.estimatedTimeRemaining} min', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: _kOnSurface, + ), + ), + Text( + controller.distanceWithUnit, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: _kOnSurface.withOpacity(0.7), + ), + ), + ], + ), + const SizedBox(height: 14), + Row( + children: [ + _CircleIconBtn( + icon: controller.isMuted + ? Icons.volume_off_rounded + : Icons.volume_up_rounded, + color: const Color(0xFFE3F2FD), + iconColor: _kPrimary, + onTap: () => controller.toggleMute()), + const Spacer(), + GestureDetector( + onTap: () => controller.clearRoute(), + child: Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 32), + decoration: BoxDecoration( + color: Colors.red.shade600, + borderRadius: BorderRadius.circular(22), + ), + child: Center( + child: Text( + isArabic ? 'إنهاء' : 'End', + style: const TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.bold, ), ), ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), - ), + ], ), ); } } -class _IconBtn extends StatelessWidget { +class _CircleIconBtn extends StatelessWidget { final IconData icon; - final Color bgColor; + final Color color; final Color iconColor; final VoidCallback onTap; - - const _IconBtn( + const _CircleIconBtn( {required this.icon, - required this.bgColor, + required this.color, required this.iconColor, required this.onTap}); @@ -790,19 +619,318 @@ class _IconBtn extends StatelessWidget { onTap(); }, child: Container( - width: 48, - height: 48, - margin: const EdgeInsets.only(right: 12), - decoration: BoxDecoration( - color: bgColor, - shape: BoxShape.circle, - ), - child: Icon(icon, color: iconColor, size: 24), + width: 44, + height: 44, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + child: Icon(icon, color: iconColor, size: 22), ), ); } } +class _SearchResults extends StatelessWidget { + final NavigationController controller; + const _SearchResults({required this.controller}); + + @override + Widget build(BuildContext context) { + return Positioned( + top: MediaQuery.of(context).padding.top + 80, + left: 16, + right: 16, + child: Material( + color: _kCardColor, + borderRadius: BorderRadius.circular(16), + elevation: 4, + child: Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.4, + ), + child: ListView.separated( + shrinkWrap: true, + itemCount: controller.placesDestination.length, + separatorBuilder: (_, __) => + Divider(height: 1, color: _kOnSurface.withOpacity(0.1)), + itemBuilder: (context, index) { + final place = controller.placesDestination[index]; + return ListTile( + onTap: () => controller.selectDestination(place), + leading: Icon(Icons.location_on_rounded, color: _kPrimary), + title: Text( + place['name'] ?? 'Unknown', + style: TextStyle( + color: _kOnSurface, fontWeight: FontWeight.w600), + ), + subtitle: Text( + place['address'] ?? place['formatted_address'] ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: _kOnSurface.withOpacity(0.5), fontSize: 12), + ), + ); + }, + ), + ), + ), + ); + } +} + +class _LoadingOverlay extends StatelessWidget { + const _LoadingOverlay(); + + @override + Widget build(BuildContext context) { + return Positioned.fill( + child: Container( + color: Colors.black38, + child: Center( + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: _kCardColor, + borderRadius: BorderRadius.circular(20), + ), + child: + const CircularProgressIndicator(color: AppColor.primaryColor), + ), + ), + ), + ); + } +} + +class _ExploreActionRow extends StatelessWidget { + final NavigationController controller; + const _ExploreActionRow({required this.controller}); + + @override + Widget build(BuildContext context) { + final bool hasRoutes = controller.routes.isNotEmpty; + final isAr = box.read(BoxName.lang) == 'ar'; + final double safeBottom = MediaQuery.of(context).padding.bottom; + + if (hasRoutes) return const SizedBox.shrink(); + + return Positioned( + bottom: safeBottom + 20, + left: 0, + right: 0, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + physics: const BouncingScrollPhysics(), + child: Row( + children: [ + _ActionCapsule( + icon: Icons.add_rounded, + label: isAr ? 'إضافة' : 'Add', + onTap: () => controller.togglePlaceSelectionMode(), + isPrimary: true, + ), + _ActionCapsule( + icon: Icons.home_rounded, + label: isAr ? 'المنزل' : 'Home', + onTap: () => controller.goToFavorite('home'), + ), + _ActionCapsule( + icon: Icons.work_rounded, + label: isAr ? 'العمل' : 'Work', + onTap: () => controller.goToFavorite('work'), + ), + _ActionCapsule( + icon: Icons.bookmark_rounded, + label: isAr ? 'المحفوظات' : 'Saved', + onTap: () {}, + ), + ], + ), + ), + ); + } +} + +class _ActionCapsule extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + final bool isPrimary; + + const _ActionCapsule({ + required this.icon, + required this.label, + required this.onTap, + this.isPrimary = false, + }); + + @override + Widget build(BuildContext context) { + Color bgColor; + Color textColor; + + if (isPrimary) { + bgColor = const Color(0xFF0D47A1).withOpacity(0.9); + textColor = Colors.white; + } else { + bgColor = _kCardColor.withOpacity(0.85); + textColor = _kOnSurface; + } + + return Padding( + padding: const EdgeInsets.only(right: 8), + child: GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + onTap(); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(50), + border: Border.all( + color: Colors.white.withOpacity(0.2), width: 1.2), + boxShadow: const [ + BoxShadow( + color: Color(0x14000000), + blurRadius: 8, + offset: Offset(0, 4)) + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 18, color: textColor), + const SizedBox(width: 8), + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: textColor, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _LocationPickerOverlay extends StatelessWidget { + final NavigationController controller; + const _LocationPickerOverlay({required this.controller}); + + @override + Widget build(BuildContext context) { + if (!controller.isSelectingPlaceLocation) return const SizedBox.shrink(); + + final isAr = box.read(BoxName.lang) == 'ar'; + + return Stack( + children: [ + IgnorePointer( + child: Container(color: Colors.black.withOpacity(0.1)), + ), + IgnorePointer( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4)) + ], + ), + child: const Icon(Icons.add_location_alt_rounded, + color: Color(0xFF0D47A1), size: 40), + ), + const SizedBox(height: 40), + ], + ), + ), + ), + Positioned( + bottom: 110, + left: 32, + right: 32, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1B5E20), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + elevation: 8, + shadowColor: const Color(0xFF1B5E20).withOpacity(0.5), + ), + onPressed: () => _showAddPlaceFormDialog(context, controller), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.check_circle_rounded, color: Colors.white), + const SizedBox(width: 12), + Text( + isAr ? 'تأكيد الموقع' : 'Confirm Location', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w800), + ), + ], + ), + ), + ), + Positioned( + top: 140, + left: 40, + right: 40, + child: IgnorePointer( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(30), + ), + child: Text( + isAr + ? "حرك الخريطة لتحديد موقع المكان" + : "Move map to pick place location", + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white, fontSize: 13), + ), + ), + ), + ), + Positioned( + top: 60, + right: 20, + child: FloatingActionButton.small( + backgroundColor: Colors.white, + elevation: 4, + child: const Icon(Icons.close_rounded, color: Colors.black87), + onPressed: () => controller.togglePlaceSelectionMode(), + ), + ), + ], + ); + } +} + void _showAddPlaceFormDialog( BuildContext context, NavigationController controller) { final nameController = TextEditingController(); @@ -812,7 +940,7 @@ void _showAddPlaceFormDialog( Get.dialog( AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), - backgroundColor: _kSurfaceContainerLowest, + backgroundColor: _kCardColor, title: Row( children: [ const Icon(Icons.add_business_rounded, @@ -832,7 +960,8 @@ void _showAddPlaceFormDialog( isAr ? "ساهم في تحسين الخريطة بإضافة الأماكن الناقصة." : "Help improve the map by adding missing places.", - style: TextStyle(color: _kOnSurfaceVariant, fontSize: 13), + style: + TextStyle(color: _kOnSurface.withOpacity(0.6), fontSize: 13), ), const SizedBox(height: 20), TextField( @@ -840,18 +969,16 @@ void _showAddPlaceFormDialog( style: TextStyle(color: _kOnSurface), decoration: InputDecoration( labelText: isAr ? 'اسم المكان' : 'Place Name', - labelStyle: TextStyle(color: _kOnSurfaceVariant), - prefixIcon: - Icon(Icons.label_rounded, color: _kOnSurfaceVariant), + labelStyle: TextStyle(color: _kOnSurface.withOpacity(0.6)), + prefixIcon: Icon(Icons.label_rounded, + color: _kOnSurface.withOpacity(0.6)), filled: true, - fillColor: _kSurfaceContainerHigh.withOpacity(0.3), + fillColor: _kSurface.withOpacity(0.3), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), const SizedBox(height: 16), - - // Category Picker Trigger ValueListenableBuilder?>( valueListenable: categoryNotifier, builder: (context, selected, _) { @@ -864,15 +991,14 @@ void _showAddPlaceFormDialog( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 16), decoration: BoxDecoration( - color: _kSurfaceContainerHigh.withOpacity(0.3), + color: _kSurface.withOpacity(0.3), borderRadius: BorderRadius.circular(12), - border: Border.all( - color: _kOutlineVariant.withOpacity(0.5)), + border: Border.all(color: Colors.grey.withOpacity(0.3)), ), child: Row( children: [ Icon(Icons.category_rounded, - color: _kOnSurfaceVariant), + color: _kOnSurface.withOpacity(0.6)), const SizedBox(width: 12), Expanded( child: Text( @@ -882,19 +1008,18 @@ void _showAddPlaceFormDialog( style: TextStyle( color: selected != null ? _kOnSurface - : _kOnSurfaceVariant, + : _kOnSurface.withOpacity(0.4), fontSize: 16, ), ), ), Icon(Icons.keyboard_arrow_down_rounded, - color: _kOnSurfaceVariant), + color: _kOnSurface.withOpacity(0.6)), ], ), ), ); }), - const SizedBox(height: 24), ElevatedButton( style: ElevatedButton.styleFrom( @@ -921,7 +1046,7 @@ void _showAddPlaceFormDialog( TextButton( onPressed: () => Get.back(), child: Text(isAr ? 'إلغاء' : 'Cancel', - style: TextStyle(color: _kOnSurfaceVariant)), + style: TextStyle(color: _kOnSurface.withOpacity(0.6))), ), ], ), @@ -937,7 +1062,7 @@ void _showCategoryPicker( Get.bottomSheet( Container( decoration: BoxDecoration( - color: _kSurfaceContainerLowest, + color: _kCardColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), padding: const EdgeInsets.only(top: 12, bottom: 24), @@ -948,7 +1073,7 @@ void _showCategoryPicker( width: 40, height: 4, decoration: BoxDecoration( - color: _kOutlineVariant.withOpacity(0.3), + color: Colors.grey.withOpacity(0.3), borderRadius: BorderRadius.circular(2), ), ), @@ -1031,581 +1156,3 @@ IconData _getIconData(String name) { return Icons.place_rounded; } } - -class _LocationPickerOverlay extends StatelessWidget { - final NavigationController controller; - const _LocationPickerOverlay({required this.controller}); - - @override - Widget build(BuildContext context) { - if (!controller.isSelectingPlaceLocation) return const SizedBox.shrink(); - - final isAr = box.read(BoxName.lang) == 'ar'; - - return Stack( - children: [ - // Dim the background slightly - Non-blocking - IgnorePointer( - child: Container(color: Colors.black.withOpacity(0.1)), - ), - - // Center Crosshair/Pointer - Non-blocking - IgnorePointer( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.9), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 10, - offset: const Offset(0, 4)) - ], - ), - child: const Icon(Icons.add_location_alt_rounded, - color: Color(0xFF0D47A1), size: 40), - ), - const SizedBox(height: 40), - ], - ), - ), - ), - - // Confirm Button - Positioned( - bottom: 110, - left: 32, - right: 32, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF1B5E20), - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16)), - elevation: 8, - shadowColor: const Color(0xFF1B5E20).withOpacity(0.5), - ), - onPressed: () => _showAddPlaceFormDialog(context, controller), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.check_circle_rounded, color: Colors.white), - const SizedBox(width: 12), - Text( - isAr ? 'تأكيد الموقع' : 'Confirm Location', - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w800), - ), - ], - ), - ), - ), - - // Help Tooltip - Positioned( - top: 140, - left: 40, - right: 40, - child: IgnorePointer( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), - borderRadius: BorderRadius.circular(30), - ), - child: Text( - isAr - ? "حرك الخريطة لتحديد موقع المكان" - : "Move map to pick place location", - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white, fontSize: 13), - ), - ), - ), - ), - - // Cancel Button - Positioned( - top: 60, - right: 20, - child: FloatingActionButton.small( - backgroundColor: Colors.white, - elevation: 4, - child: const Icon(Icons.close_rounded, color: Colors.black87), - onPressed: () => controller.togglePlaceSelectionMode(), - ), - ), - ], - ); - } -} - -class _StatItem extends StatelessWidget { - final String label; - final String value; - final Color color; - const _StatItem( - {required this.label, required this.value, required this.color}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Text(label, - style: TextStyle( - fontSize: 10, - color: _kOnSurfaceVariant, - fontWeight: FontWeight.bold)), - Text(value, - style: TextStyle( - fontSize: 16, color: color, fontWeight: FontWeight.w900)), - ], - ); - } -} - -class _ActionButton extends StatelessWidget { - final IconData icon; - final String label; - final Color color; - final Color? iconColor; - final VoidCallback onPressed; - const _ActionButton( - {required this.icon, - required this.label, - required this.color, - this.iconColor, - required this.onPressed}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - IconButton( - onPressed: onPressed, - icon: Icon(icon, color: iconColor ?? _kOnSurface), - style: IconButton.styleFrom( - backgroundColor: color, fixedSize: const Size(50, 50)), - ), - const SizedBox(height: 4), - Text(label, style: const TextStyle(fontSize: 11)), - ], - ); - } -} - -class _SpeedBadge extends StatelessWidget { - final double speed; - const _SpeedBadge({required this.speed}); - - @override - Widget build(BuildContext context) { - final int kmh = speed.toInt(); - - // Exact positioning from HTML (bottom-64 left-6, which is approx 256px from bottom) - return Positioned( - bottom: 256, - left: 24, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: _kSurfaceContainerLowest, - shape: BoxShape.circle, - border: Border.all(color: _kPrimary, width: 4), - boxShadow: const [ - BoxShadow( - color: Color(0x0F000000), blurRadius: 32, offset: Offset(0, 8)) - ], - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('$kmh', - style: TextStyle( - color: _kOnSurface, - fontSize: 28, - fontWeight: FontWeight.w900, - height: 1.0)), - Text('km/h', - style: TextStyle( - color: _kOnSurfaceVariant, - fontSize: 10, - fontWeight: FontWeight.w800, - letterSpacing: -0.5, - )), - ], - ), - ), - ); - } -} - -// ============================================================================= -// SHARED UTILITIES -// ============================================================================= - -class _MapFab extends StatelessWidget { - final IconData icon; - final Color iconColor; - final VoidCallback onTap; - const _MapFab( - {required this.icon, required this.iconColor, required this.onTap}); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: Container( - width: 50, // Slightly smaller - height: 50, - decoration: BoxDecoration( - color: _kSurfaceContainerLowest.withOpacity(0.95), - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow( - color: Color(0x1A000000), - blurRadius: 16, - offset: Offset(0, 8)) - ]), - child: Icon(icon, color: iconColor, size: 24), - ), - ), - ), - ); - } -} - -class _ExploreActionRow extends StatelessWidget { - final NavigationController controller; - const _ExploreActionRow({required this.controller}); - - @override - Widget build(BuildContext context) { - final bool hasRoutes = controller.routes.isNotEmpty; - final bool hasRecents = controller.recentLocations.isNotEmpty; - final isAr = box.read(BoxName.lang) == 'ar'; - - final double safeBottom = MediaQuery.of(context).padding.bottom; - final double bottomOffset = safeBottom + 20; - - return AnimatedPositioned( - duration: const Duration(milliseconds: 300), - curve: Curves.easeOutCubic, - bottom: bottomOffset, - left: 0, - right: 0, - child: Container( - color: Colors.transparent, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // Row 1: Favorites - SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - physics: const BouncingScrollPhysics(), - child: Row( - children: [ - _ActionCapsule( - icon: Icons.add_rounded, - label: isAr ? 'إضافة' : 'Add', - onTap: () => controller.togglePlaceSelectionMode(), - isPrimary: true, - ), - _ActionCapsule( - icon: Icons.home_rounded, - label: isAr ? 'المنزل' : 'Home', - onTap: () => controller.goToFavorite('home'), - ), - _ActionCapsule( - icon: Icons.work_rounded, - label: isAr ? 'العمل' : 'Work', - onTap: () => controller.goToFavorite('work'), - ), - _ActionCapsule( - icon: Icons.bookmark_rounded, - label: isAr ? 'المحفوظات' : 'Saved', - onTap: () {}, - ), - _ActionCapsule( - icon: Icons.flight_rounded, - label: isAr ? 'المطار' : 'Airport', - onTap: () => controller.goToFavorite('airport'), - ), - const SizedBox(width: 8), - _MapFab( - icon: Icons.my_location_rounded, - iconColor: controller.isCameraLocked - ? const Color(0xFF0D47A1) - : Colors.grey[400]!, - onTap: () { - HapticFeedback.lightImpact(); - controller.relockCameraToUser(); - }, - ), - ], - ), - ), - // Row 2: Recent History (if any) - if (hasRecents && !hasRoutes) ...[ - const SizedBox(height: 12), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - physics: const BouncingScrollPhysics(), - child: Row( - children: controller.recentLocations.take(5).map((place) { - return _ActionCapsule( - icon: Icons.history_rounded, - label: place['name'] ?? '', - onTap: () => controller.selectDestination(place), - isRecent: true, - ); - }).toList(), - ), - ), - ], - - const SizedBox(height: 12), - ], - ), - ), - ); - } -} - -class _ActionCapsule extends StatelessWidget { - final IconData icon; - final String label; - final VoidCallback onTap; - final bool isPrimary; - final bool isRecent; - - const _ActionCapsule({ - required this.icon, - required this.label, - required this.onTap, - this.isPrimary = false, - this.isRecent = false, - }); - - @override - Widget build(BuildContext context) { - Color bgColor; - Color textColor; - - if (isPrimary) { - bgColor = const Color(0xFF0D47A1).withOpacity(0.9); - textColor = Colors.white; - } else if (isRecent) { - bgColor = _kSurfaceContainerHigh.withOpacity(0.8); - textColor = _kOnSurfaceVariant; - } else { - bgColor = _kSurfaceContainerLowest.withOpacity(0.85); - textColor = _kOnSurface; - } - - return Padding( - padding: const EdgeInsets.only(right: 8), - child: GestureDetector( - onTap: () { - HapticFeedback.lightImpact(); - onTap(); - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(50), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(50), - border: Border.all( - color: isRecent - ? Colors.transparent - : Colors.white.withOpacity(0.2), - width: 1.2), - boxShadow: const [ - BoxShadow( - color: Color(0x14000000), - blurRadius: 8, - offset: Offset(0, 4)) - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 18, color: textColor), - const SizedBox(width: 8), - Text( - label, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: textColor, - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} - -class _SearchResults extends StatelessWidget { - final NavigationController controller; - const _SearchResults({required this.controller}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: MediaQuery.of(context).padding.top + 90, - left: 16, - right: 16, - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), - child: Container( - decoration: BoxDecoration( - color: _kSurfaceContainerLowest.withOpacity(0.95), - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Colors.white.withOpacity(0.2)), - boxShadow: const [ - BoxShadow( - color: Color(0x1A000000), - blurRadius: 32, - offset: Offset(0, 16)) - ]), - padding: const EdgeInsets.symmetric(vertical: 8), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 300), - child: ListView.separated( - shrinkWrap: true, - padding: EdgeInsets.zero, - itemCount: controller.placesDestination.length, - separatorBuilder: (_, __) => Divider( - height: 1, - color: _kOutlineVariant.withOpacity(0.2), - indent: 72), - itemBuilder: (_, i) { - final place = controller.placesDestination[i]; - final dist = place['distanceKm'] as double?; - return InkWell( - onTap: () => controller.selectDestination(place), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 16), - child: Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: _kSurfaceContainerHigh, - borderRadius: BorderRadius.circular(12)), - child: Icon(Icons.place_rounded, - color: _kOnSurfaceVariant, size: 20)), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(place['name'] ?? '', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: _kOnSurface), - maxLines: 1, - overflow: TextOverflow.ellipsis), - if ((place['address'] ?? '').isNotEmpty) - Text(place['address'], - style: TextStyle( - fontSize: 14, - color: _kOnSurfaceVariant), - maxLines: 1, - overflow: TextOverflow.ellipsis), - ], - ), - ), - if (dist != null) ...[ - const SizedBox(width: 12), - Text('${dist.toStringAsFixed(1)} km', - style: TextStyle( - color: _kOnSurfaceVariant, - fontSize: 14, - fontWeight: FontWeight.bold)), - ], - ], - ), - ), - ); - }, - ), - ), - ), - ), - ), - ); - } -} - -class _LoadingOverlay extends StatelessWidget { - const _LoadingOverlay(); - - @override - Widget build(BuildContext context) { - return Positioned.fill( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), - child: Container( - color: _kPrimaryContainer.withOpacity(0.4), - child: Center( - child: Container( - padding: const EdgeInsets.all(32), - decoration: BoxDecoration( - color: _kSurfaceContainerLowest, - borderRadius: BorderRadius.circular(24), - boxShadow: const [ - BoxShadow( - color: Color(0x33000000), - blurRadius: 48, - offset: Offset(0, 16)) - ]), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.black), - strokeWidth: 4), - const SizedBox(height: 24), - Text('Routing...'.tr, - style: TextStyle( - color: _kOnSurface, - fontSize: 16, - fontWeight: FontWeight.w800)), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/views/home/navigation/navigation_view_old.dart b/lib/views/home/navigation/navigation_view_old.dart new file mode 100644 index 0000000..add5b39 --- /dev/null +++ b/lib/views/home/navigation/navigation_view_old.dart @@ -0,0 +1,1613 @@ +import 'dart:math'; +import 'dart:ui'; +import 'package:Intaleq/constant/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import 'package:Intaleq/env/env.dart'; + +import '../../../constant/box_name.dart'; +import '../../../main.dart'; +import '../../widgets/error_snakbar.dart'; +import 'navigation_controller.dart'; + +// ─── Color Palette Matching the HTML Specs ────────────────────────────────── +Color get _kSurface => + Get.isDarkMode ? const Color(0xFF131b2e) : const Color(0xFFfaf8ff); +Color get _kSurfaceContainerLowest => + Get.isDarkMode ? const Color(0xFF1e293b) : const Color(0xFFffffff); +Color get _kSurfaceContainerHigh => + Get.isDarkMode ? const Color(0xFF334155) : const Color(0xFFe2e7ff); +Color get _kPrimary => const Color(0xFF000000); +Color get _kPrimaryContainer => const Color(0xFF131b2e); +Color get _kOnPrimaryContainer => const Color(0xFF7c839b); +Color get _kOnSurface => + Get.isDarkMode ? const Color(0xFFffffff) : const Color(0xFF131b2e); +Color get _kOnSurfaceVariant => const Color(0xFF45464d); +Color get _kErrorContainer => const Color(0xFFffdad6); +Color get _kError => const Color(0xFFba1a1a); +Color get _kOutlineVariant => const Color(0xFFc6c6cd); + +class NavigationView extends StatelessWidget { + const NavigationView({super.key}); + + @override + Widget build(BuildContext context) { + final NavigationController c = Get.put(NavigationController()); + + return AnnotatedRegion( + value: Get.isDarkMode + ? SystemUiOverlayStyle.light + : SystemUiOverlayStyle.dark, + child: Scaffold( + backgroundColor: _kSurface, + body: GetBuilder( + builder: (_) => Stack( + children: [ + // ── 1. Map Layer ────────────────────────────────────────────── + IntaleqMap( + apiKey: Env.mapSaasKey, + onMapCreated: c.onMapCreated, + + onLongPress: (pos) => c.onMapLongPressed( + Point(0, 0), pos), // Adapted for IntaleqMap API + onTap: (pos) => c.onMapTapped(Point(0, 0), pos), + markers: c.markers, + polylines: c.polylines, + circles: c.circles, + polygons: c.polygons, + mapType: Get.isDarkMode + ? IntaleqMapType.normal + : IntaleqMapType.light, + initialCameraPosition: CameraPosition( + target: c.myLocation ?? const LatLng(33.5138, 36.2765), + zoom: 16.0), + myLocationEnabled: true, + ), + + // ── 2. Top UI (Explore Mode) ────────────────────────────────── + if (!c.isNavigating) _ExploreTopUI(controller: c), + + // ── 3. Top UI (Active Navigation Banner) ────────────────────── + if (c.isNavigating && c.currentInstruction.isNotEmpty) + _ActiveTopInstruction(controller: c), + + // ── 4. Explore Action Row (Capsules) ────────────────────────── + if (!c.isNavigating) _ExploreActionRow(controller: c), + + // ── 5. Bottom Panel (Explore Mode / Route Setup) ────────────── + if (!c.isNavigating) _ExploreBottomPanel(controller: c), + + // ── 6. Bottom HUD (Active Navigation) ───────────────────────── + if (c.isNavigating) _ActiveBottomHUD(controller: c), + + // ── 7. Speedometer Badge (Bottom) ────────────────────────────── + if (c.isNavigating) _SpeedBadge(speed: c.currentSpeed), + + // ── 8. Search Results Dropdown ──────────────────────────────── + if (c.placesDestination.isNotEmpty && !c.isNavigating) + _SearchResults(controller: c), + + // ── 9. Loading Overlay ──────────────────────────────────────── + if (c.isLoading) const _LoadingOverlay(), + + // ── 10. Location Picker Overlay (Place Creation) ────────────── + _LocationPickerOverlay(controller: c), + + // ── 11. Recenter / Follow Me Button ─────────────────────────── + if (!c.isCameraLocked) + Positioned( + right: 16, + bottom: c.isNavigating ? 140 : 250, + child: FloatingActionButton.small( + onPressed: () => c.relockCameraToUser(), + backgroundColor: Colors.white, + child: const Icon(Icons.my_location_rounded, + color: Color(0xFF0D47A1)), + ), + ), + ], + ), + ), + ), + ); + } +} + +// ============================================================================= +// EXPLORE MODE COMPONENTS +// ============================================================================= + +class _ExploreTopUI extends StatelessWidget { + final NavigationController controller; + const _ExploreTopUI({required this.controller}); + + @override + Widget build(BuildContext context) { + return Positioned( + top: 0, + left: 0, + right: 0, + child: SafeArea( + bottom: false, + child: Column( + children: [ + // Search Pill + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular(28), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), + child: Container( + decoration: BoxDecoration( + color: _kSurfaceContainerLowest.withOpacity(0.85), + borderRadius: BorderRadius.circular(28), + border: Border.all( + color: Colors.white.withOpacity(0.3), width: 1.5), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 32, + offset: const Offset(0, 12), + ) + ], + ), + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Row( + children: [ + IconButton( + icon: Icon(Icons.menu_rounded, + color: _kOnSurface, size: 26), + onPressed: () {}, // Drawer or Menu logic here + ), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: controller.placeDestinationController, + onChanged: controller.onSearchChanged, + textInputAction: TextInputAction.search, + style: TextStyle( + fontSize: 18, + color: _kOnSurface, + fontWeight: FontWeight.w700), + decoration: InputDecoration( + hintText: box.read(BoxName.lang) == 'ar' + ? 'إلى أين؟' + : 'Where to?', + hintStyle: TextStyle( + color: _kOnSurfaceVariant.withOpacity(0.7), + fontSize: 18, + fontWeight: FontWeight.w600), + border: InputBorder.none, + isDense: true, + contentPadding: + const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + if (controller + .placeDestinationController.text.isNotEmpty || + controller.routes.isNotEmpty) + Container( + decoration: const BoxDecoration( + color: Color(0xFFFFEBEE), + shape: BoxShape.circle, + ), + child: IconButton( + icon: const Icon(Icons.close_rounded), + color: Colors.red, + onPressed: () => controller.clearEverything(), + ), + ) + else + // Avatar + const Padding( + padding: EdgeInsets.only(right: 4), + child: CircleAvatar( + radius: 20, + backgroundColor: Color(0xFF0D47A1), + child: Icon(Icons.person_rounded, + color: Colors.white, size: 22), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +class _ExploreBottomPanel extends StatelessWidget { + final NavigationController controller; + const _ExploreBottomPanel({required this.controller}); + + String _formatDuration(double seconds) { + final mins = (seconds / 60).toInt(); + if (mins >= 60) { + final h = mins ~/ 60; + final m = mins % 60; + return box.read(BoxName.lang) == 'ar' + ? '$h ساعة ${m > 0 ? '$m د' : ''}' + : '${h}h ${m > 0 ? '${m}m' : ''}'; + } + return box.read(BoxName.lang) == 'ar' ? '$mins دقيقة' : '$mins min'; + } + + String _formatDistance(double meters) { + if (meters >= 1000) { + return box.read(BoxName.lang) == 'ar' + ? '${(meters / 1000).toStringAsFixed(1)} كم' + : '${(meters / 1000).toStringAsFixed(1)} km'; + } + return box.read(BoxName.lang) == 'ar' + ? '${meters.toInt()} م' + : '${meters.toInt()} m'; + } + + @override + Widget build(BuildContext context) { + final bool hasRoutes = controller.routes.isNotEmpty; + final bool isArabic = box.read(BoxName.lang) == 'ar'; + final bottomPad = MediaQuery.of(context).padding.bottom; + + if (!hasRoutes && controller.recentLocations.isEmpty) { + return const SizedBox.shrink(); + } + + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Container( + padding: EdgeInsets.only(bottom: bottomPad + 8), + decoration: BoxDecoration( + color: _kSurfaceContainerLowest.withOpacity(0.92), + boxShadow: const [ + BoxShadow( + color: Color(0x14000000), + blurRadius: 48, + offset: Offset(0, -12)) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Minimal Drag Handle + Container( + width: 32, + height: 3, + margin: const EdgeInsets.only(top: 8, bottom: 8), + decoration: BoxDecoration( + color: _kOutlineVariant.withOpacity(0.3), + borderRadius: BorderRadius.circular(10))), + + // ── Route Selection Cards ── + if (hasRoutes) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: + List.generate(controller.routes.length, (index) { + final r = controller.routes[index]; + final isSelected = + controller.selectedRouteIndex == index; + return _RouteOptionCard( + index: index, + distance: _formatDistance(r.distanceM), + duration: _formatDuration(r.durationS), + isSelected: isSelected, + isArabic: isArabic, + onTap: () => controller.selectRoute(index), + ); + }), + ), + ), + const SizedBox(height: 12), + // Start Button + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + HapticFeedback.mediumImpact(); + controller.isNavigating = true; + controller.relockCameraToUser(); + controller.update(); + }, + borderRadius: BorderRadius.circular(16), + child: Ink( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF0D47A1), Color(0xFF1565C0)], + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: + const Color(0xFF0D47A1).withOpacity(0.35), + blurRadius: 16, + offset: const Offset(0, 6)) + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.navigation_rounded, + color: Colors.white, size: 20), + const SizedBox(width: 8), + Text( + isArabic + ? 'ابدأ الملاحة' + : 'Start Navigation', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w800)), + ], + ), + ), + ), + ), + ), + ] else if (!hasRoutes) ...[ + const SizedBox.shrink(), + ], + ], + ), + ), + ), + ), + ); + } +} + +class _RouteOptionCard extends StatelessWidget { + final int index; + final String distance; + final String duration; + final bool isSelected; + final bool isArabic; + final VoidCallback onTap; + + const _RouteOptionCard({ + required this.index, + required this.distance, + required this.duration, + required this.isSelected, + required this.isArabic, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + HapticFeedback.selectionClick(); + onTap(); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: isSelected + ? const Color(0xFF0D47A1) + : _kSurfaceContainerHigh.withOpacity(0.4), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected + ? const Color(0xFF42A5F5).withOpacity(0.4) + : _kOutlineVariant.withOpacity(0.15), + width: isSelected ? 1.5 : 1, + ), + ), + child: Row( + children: [ + // Route icon + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: isSelected + ? Colors.white.withOpacity(0.15) + : _kSurfaceContainerHigh.withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + index == 0 ? Icons.route_rounded : Icons.alt_route_rounded, + color: isSelected ? Colors.white : _kOnSurfaceVariant, + size: 20, + ), + ), + const SizedBox(width: 14), + + // Route Label + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + index == 0 + ? (isArabic ? 'أفضل مسار' : 'Best Route') + : (isArabic + ? 'مسار بديل $index' + : 'Alternative $index'), + style: TextStyle( + color: isSelected ? Colors.white : _kOnSurface, + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + distance, + style: TextStyle( + color: isSelected + ? Colors.white.withOpacity(0.7) + : _kOnSurfaceVariant, + fontSize: 12, + ), + ), + ], + ), + ), + + // Duration (prominent) + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: isSelected + ? Colors.white.withOpacity(0.15) + : const Color(0xFFE3F2FD), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + duration, + style: TextStyle( + color: isSelected ? Colors.white : const Color(0xFF0D47A1), + fontSize: 14, + fontWeight: FontWeight.w800, + ), + ), + ), + + // Selection indicator + const SizedBox(width: 8), + Icon( + isSelected + ? Icons.check_circle_rounded + : Icons.radio_button_unchecked_rounded, + color: isSelected ? Colors.white : _kOutlineVariant, + size: 22, + ), + ], + ), + ), + ); + } +} + +class _CompactRecentPlace extends StatelessWidget { + final String title; + final String subtitle; + final VoidCallback onTap; + + const _CompactRecentPlace({ + required this.title, + required this.subtitle, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + onTap: onTap, + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 0), + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _kSurfaceContainerHigh.withOpacity(0.5), + shape: BoxShape.circle, + ), + child: Icon(Icons.history_rounded, size: 18, color: _kOnSurface), + ), + title: Text(title, + style: TextStyle( + fontWeight: FontWeight.w700, color: _kOnSurface, fontSize: 14)), + subtitle: Text(subtitle, + style: TextStyle(color: _kOnSurfaceVariant, fontSize: 12), + maxLines: 1, + overflow: TextOverflow.ellipsis), + ); + } +} + +// ============================================================================= +// ACTIVE NAVIGATION MODE COMPONENTS +// ============================================================================= + +class _ActiveTopInstruction extends StatelessWidget { + final NavigationController controller; + const _ActiveTopInstruction({required this.controller}); + + @override + Widget build(BuildContext context) { + return Positioned( + top: MediaQuery.of(context).padding.top + 12, + left: 16, + right: 16, + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), + child: Container( + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF1B5E20), Color(0xFF2E7D32)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + border: + Border.all(color: Colors.white.withOpacity(0.2), width: 1), + boxShadow: const [ + BoxShadow( + color: Color(0x33000000), + blurRadius: 24, + offset: Offset(0, 12)) + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + children: [ + // Direction indicator + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon(controller.currentManeuverIcon, + color: Colors.white, size: 36), + ), + const SizedBox(width: 16), + // Full instruction text + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + controller.currentInstruction, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w800, + letterSpacing: -0.5), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + controller.distanceToNextStep, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 16, + fontWeight: FontWeight.w700), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _ActiveBottomHUD extends StatelessWidget { + final NavigationController controller; + const _ActiveBottomHUD({required this.controller}); + + @override + Widget build(BuildContext context) { + final bottomPad = MediaQuery.of(context).padding.bottom; + final isArabic = box.read(BoxName.lang) == 'ar'; + + return Positioned( + bottom: bottomPad + 16, + left: 16, + right: 16, + child: ClipRRect( + borderRadius: BorderRadius.circular(32), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), + child: Container( + decoration: BoxDecoration( + color: _kSurfaceContainerLowest.withOpacity(0.85), + borderRadius: BorderRadius.circular(32), + border: + Border.all(color: Colors.white.withOpacity(0.4), width: 1.5), + boxShadow: const [ + BoxShadow( + color: Color(0x14000000), + blurRadius: 24, + offset: Offset(0, 12)) + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Stats Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + controller.arrivalTime, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + color: Color(0xFF0D47A1), + ), + ), + Container( + width: 1.5, + height: 24, + color: _kOutlineVariant.withOpacity(0.3)), + Text( + isArabic + ? '${controller.estimatedTimeRemaining} دقيقة' + : '${controller.estimatedTimeRemaining} min', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: _kOnSurface, + ), + ), + Container( + width: 1.5, + height: 24, + color: _kOutlineVariant.withOpacity(0.3)), + Text( + controller.distanceWithUnit, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: _kOnSurfaceVariant, + ), + ), + ], + ), + const SizedBox(height: 16), + // Action Buttons Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _IconBtn( + icon: Icons.report_problem_rounded, + bgColor: const Color(0xFFFFF3E0), + iconColor: Colors.orange.shade800, + onTap: () {}, + ), + _IconBtn( + icon: controller.isMuted + ? Icons.volume_off_rounded + : Icons.volume_up_rounded, + bgColor: const Color(0xFFE3F2FD), + iconColor: const Color(0xFF1976D2), + onTap: () => controller.toggleMute(), + ), + _IconBtn( + icon: Icons.add_rounded, + bgColor: const Color(0xFFE8F5E9), + iconColor: const Color(0xFF388E3C), + onTap: () => controller.togglePlaceSelectionMode(), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16), + child: GestureDetector( + onTap: () => controller.clearRoute(), + child: Container( + height: 48, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFFE53935), Color(0xFFC62828)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.red.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 6), + ) + ], + ), + child: Center( + child: Text( + isArabic ? 'إنهاء الملاحة' : 'End Route', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} + +class _IconBtn extends StatelessWidget { + final IconData icon; + final Color bgColor; + final Color iconColor; + final VoidCallback onTap; + + const _IconBtn( + {required this.icon, + required this.bgColor, + required this.iconColor, + required this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + onTap(); + }, + child: Container( + width: 48, + height: 48, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + color: bgColor, + shape: BoxShape.circle, + ), + child: Icon(icon, color: iconColor, size: 24), + ), + ); + } +} + +void _showAddPlaceFormDialog( + BuildContext context, NavigationController controller) { + final nameController = TextEditingController(); + final categoryNotifier = ValueNotifier?>(null); + final isAr = box.read(BoxName.lang) == 'ar'; + + Get.dialog( + AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + backgroundColor: _kSurfaceContainerLowest, + title: Row( + children: [ + const Icon(Icons.add_business_rounded, + color: Color(0xFF0D47A1), size: 28), + const SizedBox(width: 12), + Text(isAr ? 'إضافة مكان جديد' : 'Add New Place', + style: + TextStyle(color: _kOnSurface, fontWeight: FontWeight.bold)), + ], + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + isAr + ? "ساهم في تحسين الخريطة بإضافة الأماكن الناقصة." + : "Help improve the map by adding missing places.", + style: TextStyle(color: _kOnSurfaceVariant, fontSize: 13), + ), + const SizedBox(height: 20), + TextField( + controller: nameController, + style: TextStyle(color: _kOnSurface), + decoration: InputDecoration( + labelText: isAr ? 'اسم المكان' : 'Place Name', + labelStyle: TextStyle(color: _kOnSurfaceVariant), + prefixIcon: + Icon(Icons.label_rounded, color: _kOnSurfaceVariant), + filled: true, + fillColor: _kSurfaceContainerHigh.withOpacity(0.3), + border: + OutlineInputBorder(borderRadius: BorderRadius.circular(12)), + ), + ), + const SizedBox(height: 16), + + // Category Picker Trigger + ValueListenableBuilder?>( + valueListenable: categoryNotifier, + builder: (context, selected, _) { + return InkWell( + onTap: () => _showCategoryPicker(context, (cat) { + categoryNotifier.value = cat; + }), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 16), + decoration: BoxDecoration( + color: _kSurfaceContainerHigh.withOpacity(0.3), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _kOutlineVariant.withOpacity(0.5)), + ), + child: Row( + children: [ + Icon(Icons.category_rounded, + color: _kOnSurfaceVariant), + const SizedBox(width: 12), + Expanded( + child: Text( + selected != null + ? (isAr ? selected['ar']! : selected['en']!) + : (isAr ? 'اختر الفئة' : 'Select Category'), + style: TextStyle( + color: selected != null + ? _kOnSurface + : _kOnSurfaceVariant, + fontSize: 16, + ), + ), + ), + Icon(Icons.keyboard_arrow_down_rounded, + color: _kOnSurfaceVariant), + ], + ), + ), + ); + }), + + const SizedBox(height: 24), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0D47A1), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + onPressed: () { + if (nameController.text.isNotEmpty && + categoryNotifier.value != null) { + Get.back(); + controller.submitNewPlace( + nameController.text, categoryNotifier.value!['id']!); + } else { + mySnackbarWarning( + isAr ? 'يرجى إكمال البيانات' : 'Please fill all fields'); + } + }, + child: Text(isAr ? 'إرسال' : 'Submit', + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.bold)), + ), + TextButton( + onPressed: () => Get.back(), + child: Text(isAr ? 'إلغاء' : 'Cancel', + style: TextStyle(color: _kOnSurfaceVariant)), + ), + ], + ), + ), + ), + ); +} + +void _showCategoryPicker( + BuildContext context, Function(Map) onSelected) { + final isAr = box.read(BoxName.lang) == 'ar'; + + Get.bottomSheet( + Container( + decoration: BoxDecoration( + color: _kSurfaceContainerLowest, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + ), + padding: const EdgeInsets.only(top: 12, bottom: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: _kOutlineVariant.withOpacity(0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 16), + Text(isAr ? 'اختر الفئة' : 'Select Category', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: _kOnSurface)), + const SizedBox(height: 16), + Flexible( + child: ListView.builder( + shrinkWrap: true, + itemCount: NavigationController.placeCategories.length, + itemBuilder: (context, index) { + final cat = NavigationController.placeCategories[index]; + return ListTile( + leading: Icon(_getIconData(cat['icon']!), + color: const Color(0xFF0D47A1)), + title: Text(isAr ? cat['ar']! : cat['en']!, + style: TextStyle( + color: _kOnSurface, fontWeight: FontWeight.w600)), + onTap: () { + HapticFeedback.lightImpact(); + onSelected(cat); + Get.back(); + }, + ); + }, + ), + ), + ], + ), + ), + isScrollControlled: true, + ); +} + +IconData _getIconData(String name) { + switch (name) { + case 'restaurant': + return Icons.restaurant_rounded; + case 'coffee': + return Icons.coffee_rounded; + case 'shopping_basket': + return Icons.shopping_basket_rounded; + case 'local_pharmacy': + return Icons.local_pharmacy_rounded; + case 'local_gas_station': + return Icons.local_gas_station_rounded; + case 'atm': + return Icons.atm_rounded; + case 'account_balance': + return Icons.account_balance_rounded; + case 'mosque': + return Icons.mosque_rounded; + case 'local_hospital': + return Icons.local_hospital_rounded; + case 'school': + return Icons.school_rounded; + case 'park': + return Icons.park_rounded; + case 'hotel': + return Icons.hotel_rounded; + case 'shopping_mall': + return Icons.store_rounded; + case 'fitness_center': + return Icons.fitness_center_rounded; + case 'content_cut': + return Icons.content_cut_rounded; + case 'bakery_dining': + return Icons.bakery_dining_rounded; + case 'local_laundry_service': + return Icons.local_laundry_service_rounded; + case 'build': + return Icons.build_rounded; + case 'gavel': + return Icons.gavel_rounded; + default: + return Icons.place_rounded; + } +} + +class _LocationPickerOverlay extends StatelessWidget { + final NavigationController controller; + const _LocationPickerOverlay({required this.controller}); + + @override + Widget build(BuildContext context) { + if (!controller.isSelectingPlaceLocation) return const SizedBox.shrink(); + + final isAr = box.read(BoxName.lang) == 'ar'; + + return Stack( + children: [ + // Dim the background slightly - Non-blocking + IgnorePointer( + child: Container(color: Colors.black.withOpacity(0.1)), + ), + + // Center Crosshair/Pointer - Non-blocking + IgnorePointer( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4)) + ], + ), + child: const Icon(Icons.add_location_alt_rounded, + color: Color(0xFF0D47A1), size: 40), + ), + const SizedBox(height: 40), + ], + ), + ), + ), + + // Confirm Button + Positioned( + bottom: 110, + left: 32, + right: 32, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1B5E20), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + elevation: 8, + shadowColor: const Color(0xFF1B5E20).withOpacity(0.5), + ), + onPressed: () => _showAddPlaceFormDialog(context, controller), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.check_circle_rounded, color: Colors.white), + const SizedBox(width: 12), + Text( + isAr ? 'تأكيد الموقع' : 'Confirm Location', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w800), + ), + ], + ), + ), + ), + + // Help Tooltip + Positioned( + top: 140, + left: 40, + right: 40, + child: IgnorePointer( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(30), + ), + child: Text( + isAr + ? "حرك الخريطة لتحديد موقع المكان" + : "Move map to pick place location", + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white, fontSize: 13), + ), + ), + ), + ), + + // Cancel Button + Positioned( + top: 60, + right: 20, + child: FloatingActionButton.small( + backgroundColor: Colors.white, + elevation: 4, + child: const Icon(Icons.close_rounded, color: Colors.black87), + onPressed: () => controller.togglePlaceSelectionMode(), + ), + ), + ], + ); + } +} + +class _StatItem extends StatelessWidget { + final String label; + final String value; + final Color color; + const _StatItem( + {required this.label, required this.value, required this.color}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text(label, + style: TextStyle( + fontSize: 10, + color: _kOnSurfaceVariant, + fontWeight: FontWeight.bold)), + Text(value, + style: TextStyle( + fontSize: 16, color: color, fontWeight: FontWeight.w900)), + ], + ); + } +} + +class _ActionButton extends StatelessWidget { + final IconData icon; + final String label; + final Color color; + final Color? iconColor; + final VoidCallback onPressed; + const _ActionButton( + {required this.icon, + required this.label, + required this.color, + this.iconColor, + required this.onPressed}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + IconButton( + onPressed: onPressed, + icon: Icon(icon, color: iconColor ?? _kOnSurface), + style: IconButton.styleFrom( + backgroundColor: color, fixedSize: const Size(50, 50)), + ), + const SizedBox(height: 4), + Text(label, style: const TextStyle(fontSize: 11)), + ], + ); + } +} + +class _SpeedBadge extends StatelessWidget { + final double speed; + const _SpeedBadge({required this.speed}); + + @override + Widget build(BuildContext context) { + final int kmh = speed.toInt(); + // Dynamic border color based on speed + final Color borderColor = kmh > 80 ? const Color(0xFFFF5252) : const Color(0xFFE53935); + + // Positioned at the bottom (above the HUD) + return Positioned( + bottom: 120, // Above the _ActiveBottomHUD + left: 24, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: _kSurfaceContainerLowest, + shape: BoxShape.circle, + border: Border.all(color: borderColor, width: 4), // Red border + boxShadow: const [ + BoxShadow( + color: Color(0x0F000000), blurRadius: 32, offset: Offset(0, 8)) + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('$kmh', + style: TextStyle( + color: borderColor, // Speed number in red + fontSize: 28, + fontWeight: FontWeight.w900, + height: 1.0)), + Text('km/h', + style: TextStyle( + color: _kOnSurfaceVariant, + fontSize: 10, + fontWeight: FontWeight.w800, + letterSpacing: -0.5, + )), + ], + ), + ), + ); + } +} + +// ============================================================================= +// SHARED UTILITIES +// ============================================================================= + +class _MapFab extends StatelessWidget { + final IconData icon; + final Color iconColor; + final VoidCallback onTap; + const _MapFab( + {required this.icon, required this.iconColor, required this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + width: 50, // Slightly smaller + height: 50, + decoration: BoxDecoration( + color: _kSurfaceContainerLowest.withOpacity(0.95), + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Color(0x1A000000), + blurRadius: 16, + offset: Offset(0, 8)) + ]), + child: Icon(icon, color: iconColor, size: 24), + ), + ), + ), + ); + } +} + +class _ExploreActionRow extends StatelessWidget { + final NavigationController controller; + const _ExploreActionRow({required this.controller}); + + @override + Widget build(BuildContext context) { + final bool hasRoutes = controller.routes.isNotEmpty; + final bool hasRecents = controller.recentLocations.isNotEmpty; + final isAr = box.read(BoxName.lang) == 'ar'; + + final double safeBottom = MediaQuery.of(context).padding.bottom; + final double bottomOffset = safeBottom + 20; + + return AnimatedPositioned( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + bottom: bottomOffset, + left: 0, + right: 0, + child: Container( + color: Colors.transparent, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Row 1: Favorites + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + physics: const BouncingScrollPhysics(), + child: Row( + children: [ + _ActionCapsule( + icon: Icons.add_rounded, + label: isAr ? 'إضافة' : 'Add', + onTap: () => controller.togglePlaceSelectionMode(), + isPrimary: true, + ), + _ActionCapsule( + icon: Icons.home_rounded, + label: isAr ? 'المنزل' : 'Home', + onTap: () => controller.goToFavorite('home'), + ), + _ActionCapsule( + icon: Icons.work_rounded, + label: isAr ? 'العمل' : 'Work', + onTap: () => controller.goToFavorite('work'), + ), + _ActionCapsule( + icon: Icons.bookmark_rounded, + label: isAr ? 'المحفوظات' : 'Saved', + onTap: () {}, + ), + _ActionCapsule( + icon: Icons.flight_rounded, + label: isAr ? 'المطار' : 'Airport', + onTap: () => controller.goToFavorite('airport'), + ), + const SizedBox(width: 8), + _MapFab( + icon: Icons.my_location_rounded, + iconColor: controller.isCameraLocked + ? const Color(0xFF0D47A1) + : Colors.grey[400]!, + onTap: () { + HapticFeedback.lightImpact(); + controller.relockCameraToUser(); + }, + ), + ], + ), + ), + // Row 2: Recent History (if any) + if (hasRecents && !hasRoutes) ...[ + const SizedBox(height: 12), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + child: Row( + children: controller.recentLocations.take(5).map((place) { + return _ActionCapsule( + icon: Icons.history_rounded, + label: place['name'] ?? '', + onTap: () => controller.selectDestination(place), + isRecent: true, + ); + }).toList(), + ), + ), + ], + + const SizedBox(height: 12), + ], + ), + ), + ); + } +} + +class _ActionCapsule extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + final bool isPrimary; + final bool isRecent; + + const _ActionCapsule({ + required this.icon, + required this.label, + required this.onTap, + this.isPrimary = false, + this.isRecent = false, + }); + + @override + Widget build(BuildContext context) { + Color bgColor; + Color textColor; + + if (isPrimary) { + bgColor = const Color(0xFF0D47A1).withOpacity(0.9); + textColor = Colors.white; + } else if (isRecent) { + bgColor = _kSurfaceContainerHigh.withOpacity(0.8); + textColor = _kOnSurfaceVariant; + } else { + bgColor = _kSurfaceContainerLowest.withOpacity(0.85); + textColor = _kOnSurface; + } + + return Padding( + padding: const EdgeInsets.only(right: 8), + child: GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + onTap(); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(50), + border: Border.all( + color: isRecent + ? Colors.transparent + : Colors.white.withOpacity(0.2), + width: 1.2), + boxShadow: const [ + BoxShadow( + color: Color(0x14000000), + blurRadius: 8, + offset: Offset(0, 4)) + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 18, color: textColor), + const SizedBox(width: 8), + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: textColor, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _SearchResults extends StatelessWidget { + final NavigationController controller; + const _SearchResults({required this.controller}); + + @override + Widget build(BuildContext context) { + return Positioned( + top: MediaQuery.of(context).padding.top + 90, + left: 16, + right: 16, + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Container( + decoration: BoxDecoration( + color: _kSurfaceContainerLowest.withOpacity(0.95), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withOpacity(0.2)), + boxShadow: const [ + BoxShadow( + color: Color(0x1A000000), + blurRadius: 32, + offset: Offset(0, 16)) + ]), + padding: const EdgeInsets.symmetric(vertical: 8), + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: controller.placesDestination.length, + separatorBuilder: (_, __) => Divider( + height: 1, + color: _kOutlineVariant.withOpacity(0.2), + indent: 72), + itemBuilder: (_, i) { + final place = controller.placesDestination[i]; + final dist = place['distanceKm'] as double?; + return InkWell( + onTap: () => controller.selectDestination(place), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 16), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: _kSurfaceContainerHigh, + borderRadius: BorderRadius.circular(12)), + child: Icon(Icons.place_rounded, + color: _kOnSurfaceVariant, size: 20)), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(place['name'] ?? '', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: _kOnSurface), + maxLines: 1, + overflow: TextOverflow.ellipsis), + if ((place['address'] ?? '').isNotEmpty) + Text(place['address'], + style: TextStyle( + fontSize: 14, + color: _kOnSurfaceVariant), + maxLines: 1, + overflow: TextOverflow.ellipsis), + ], + ), + ), + if (dist != null) ...[ + const SizedBox(width: 12), + Text('${dist.toStringAsFixed(1)} km', + style: TextStyle( + color: _kOnSurfaceVariant, + fontSize: 14, + fontWeight: FontWeight.bold)), + ], + ], + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + } +} + +class _LoadingOverlay extends StatelessWidget { + const _LoadingOverlay(); + + @override + Widget build(BuildContext context) { + return Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), + child: Container( + color: _kPrimaryContainer.withOpacity(0.4), + child: Center( + child: Container( + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: _kSurfaceContainerLowest, + borderRadius: BorderRadius.circular(24), + boxShadow: const [ + BoxShadow( + color: Color(0x33000000), + blurRadius: 48, + offset: Offset(0, 16)) + ]), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.black), + strokeWidth: 4), + const SizedBox(height: 24), + Text('Routing...'.tr, + style: TextStyle( + color: _kOnSurface, + fontSize: 16, + fontWeight: FontWeight.w800)), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index cc6ba2a..a14b677 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1135,10 +1135,11 @@ packages: intaleq_maps: dependency: "direct main" description: - path: "/Users/hamzaaleghwairyeen/development/App/map-saas/packages/flutter-sdk/" - relative: false - source: path - version: "2.1.3" + name: intaleq_maps + sha256: b74c4e6f1d890f81bf253c4d3996db53149b64fcbf9869b279d417067f73128b + url: "https://pub.dev" + source: hosted + version: "2.2.0" internet_connection_checker: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index a146ed2..1bdbb62 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -81,8 +81,7 @@ dependencies: internet_connection_checker: ^3.0.1 connectivity_plus: ^6.1.5 app_links: ^7.0.0 - intaleq_maps: - path: /Users/hamzaaleghwairyeen/development/App/map-saas/packages/flutter-sdk/ + intaleq_maps: ^2.2.0 socket_io_client: 1.0.2 # home_widget: ^0.7.0+1