import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'navigation_controller.dart'; // ─── Brand colours ─────────────────────────────────────────────────────────── const Color _kBlue = Color(0xFF1A73E8); const Color _kBlueDark = Color(0xFF0D47A1); const Color _kSurface = Color(0xFFFFFFFF); const Color _kText = Color(0xFF1C1C1E); const Color _kSubtext = Color(0xFF6B7280); const Color _kGreen = Color(0xFF34A853); class NavigationView extends StatelessWidget { const NavigationView({super.key}); @override Widget build(BuildContext context) { final NavigationController c = Get.put(NavigationController()); return AnnotatedRegion( value: SystemUiOverlayStyle.dark, child: Scaffold( backgroundColor: Colors.black, body: GetBuilder( builder: (_) => Stack( children: [ // ── Map ──────────────────────────────────────────────────── MapLibreMap( onMapCreated: c.onMapCreated, onStyleLoadedCallback: c.onStyleLoaded, onMapLongClick: c.onMapLongPressed, styleString: "assets/style.json", initialCameraPosition: CameraPosition( target: c.myLocation ?? const LatLng(33.5138, 36.2765), zoom: 16.0, ), myLocationEnabled: false, compassEnabled: false, trackCameraPosition: true, ), // ── Top: search bar (always visible) ────────────────────── if (!c.isNavigating) _SearchBar(controller: c), // ── Top: turn banner (navigation only) ──────────────────── if (c.isNavigating && c.currentInstruction.isNotEmpty) _TurnBanner(controller: c), // ── Right: floating map controls ────────────────────────── _MapControls(controller: c), // ── Bottom: route summary card ──────────────────────────── if (!c.isNavigating && c.destinationSymbol != null) _RouteSummaryCard(controller: c), // ── Bottom: navigation HUD ──────────────────────────────── if (c.isNavigating) _NavigationHUD(controller: c), // ── Search results overlay ──────────────────────────────── if (c.placesDestination.isNotEmpty && !c.isNavigating) _SearchResults(controller: c), // ── Speed badge (navigating) ────────────────────────────── if (c.isNavigating) _SpeedBadge(speed: c.currentSpeed), // ── Loading overlay ─────────────────────────────────────── if (c.isLoading) const _LoadingOverlay(), ], ), ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Search Bar // ───────────────────────────────────────────────────────────────────────────── class _SearchBar extends StatelessWidget { final NavigationController controller; const _SearchBar({required this.controller}); @override Widget build(BuildContext context) { return Positioned( top: 0, left: 0, right: 0, child: SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: _GlassCard( padding: EdgeInsets.zero, borderRadius: 18, child: Row( children: [ const SizedBox(width: 16), Icon(Icons.search_rounded, color: _kBlue, size: 22), const SizedBox(width: 10), Expanded( child: TextField( controller: controller.placeDestinationController, onChanged: controller.onSearchChanged, textInputAction: TextInputAction.search, style: const TextStyle( fontSize: 16, color: _kText, fontWeight: FontWeight.w500), decoration: InputDecoration( hintText: 'إلى أين تريد الذهاب؟', hintStyle: TextStyle(color: _kSubtext, fontSize: 15), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(vertical: 16), ), ), ), if (controller.placeDestinationController.text.isNotEmpty) _IconBtn( icon: Icons.close_rounded, color: _kSubtext, onTap: () { controller.placeDestinationController.clear(); controller.placesDestination = []; controller.update(); }, ) else if (controller.destinationSymbol != null) _IconBtn( icon: Icons.close_rounded, color: Colors.redAccent, onTap: () => controller.clearRoute(), ), const SizedBox(width: 4), ], ), ), ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Search Results // ───────────────────────────────────────────────────────────────────────────── 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 + 76, left: 16, right: 16, child: _GlassCard( borderRadius: 18, padding: const EdgeInsets.symmetric(vertical: 6), child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 260), child: ListView.separated( shrinkWrap: true, physics: const BouncingScrollPhysics(), padding: EdgeInsets.zero, itemCount: controller.placesDestination.length, separatorBuilder: (_, __) => Divider(height: 1, color: Colors.grey[100], indent: 56), itemBuilder: (_, i) { final place = controller.placesDestination[i]; final dist = place['distanceKm'] as double?; return InkWell( onTap: () => controller.selectDestination(place), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Row( children: [ Container( width: 34, height: 34, decoration: BoxDecoration( color: _kBlue.withOpacity(0.08), shape: BoxShape.circle, ), child: const Icon(Icons.place_rounded, color: _kBlue, size: 18), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(place['name'] ?? '', style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 14.5, color: _kText), maxLines: 1, overflow: TextOverflow.ellipsis), if ((place['address'] ?? '').isNotEmpty) Text(place['address'], style: TextStyle( fontSize: 12.5, color: _kSubtext), maxLines: 1, overflow: TextOverflow.ellipsis), ], ), ), if (dist != null) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3), decoration: BoxDecoration( color: _kBlue.withOpacity(0.08), borderRadius: BorderRadius.circular(8), ), child: Text( '${dist.toStringAsFixed(1)} كم', style: const TextStyle( color: _kBlue, fontSize: 12, fontWeight: FontWeight.w600), ), ), ], ], ), ), ); }, ), ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Turn Banner (top card during navigation — like Google Maps) // ───────────────────────────────────────────────────────────────────────────── class _TurnBanner extends StatelessWidget { final NavigationController controller; const _TurnBanner({required this.controller}); @override Widget build(BuildContext context) { return Positioned( top: 0, left: 0, right: 0, child: SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 0), child: Container( decoration: BoxDecoration( color: _kBlueDark, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: _kBlueDark.withOpacity(0.35), blurRadius: 20, offset: const Offset(0, 6)), ], ), child: Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), child: Row( children: [ // Turn arrow icon Container( width: 52, height: 52, decoration: BoxDecoration( color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(14), ), child: const Icon(Icons.turn_right_rounded, color: Colors.white, size: 30), ), const SizedBox(width: 14), // Instruction text Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( controller.distanceToNextStep, style: const TextStyle( color: Colors.white70, fontSize: 13, fontWeight: FontWeight.w500), ), const SizedBox(height: 2), Text( controller.currentInstruction, style: const TextStyle( color: Colors.white, fontSize: 19, fontWeight: FontWeight.bold, height: 1.2), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), // Close / stop navigation _IconBtn( icon: Icons.close_rounded, color: Colors.white54, size: 20, onTap: () => controller.clearRoute(), ), ], ), ), ), ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Floating map controls (right side) // ───────────────────────────────────────────────────────────────────────────── class _MapControls extends StatelessWidget { final NavigationController controller; const _MapControls({required this.controller}); @override Widget build(BuildContext context) { final bottomOffset = controller.isNavigating ? 190.0 : 100.0; return Positioned( bottom: bottomOffset, right: 14, child: Column( children: [ // Re-centre / lock camera _MapFab( icon: controller.isCameraLocked ? Icons.gps_fixed_rounded : Icons.gps_not_fixed_rounded, iconColor: controller.isCameraLocked ? _kBlue : Colors.grey[600]!, onTap: () { HapticFeedback.lightImpact(); controller.relockCameraToUser(); }, tooltip: 'موقعي', ), if (controller.isNavigating) ...[ const SizedBox(height: 10), _MapFab( icon: Icons.sync_alt_rounded, iconColor: _kBlueDark, onTap: () { HapticFeedback.mediumImpact(); controller.recalculateRoute(); }, tooltip: 'إعادة التوجيه', ), ], ], ), ); } } class _MapFab extends StatelessWidget { final IconData icon; final Color iconColor; final VoidCallback onTap; final String tooltip; const _MapFab({ required this.icon, required this.iconColor, required this.onTap, required this.tooltip, }); @override Widget build(BuildContext context) { return Tooltip( message: tooltip, child: GestureDetector( onTap: onTap, child: Container( width: 46, height: 46, decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.14), blurRadius: 12, offset: const Offset(0, 4)), ], ), child: Icon(icon, color: iconColor, size: 22), ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Route Summary Card (before navigation starts) // ───────────────────────────────────────────────────────────────────────────── class _RouteSummaryCard extends StatelessWidget { final NavigationController controller; const _RouteSummaryCard({required this.controller}); @override Widget build(BuildContext context) { return Positioned( bottom: 0, left: 0, right: 0, child: Container( decoration: const BoxDecoration( color: _kSurface, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), boxShadow: [ BoxShadow( color: Color(0x1A000000), blurRadius: 24, offset: Offset(0, -6)), ], ), padding: EdgeInsets.fromLTRB( 20, 16, 20, MediaQuery.of(context).padding.bottom + 16), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Handle Container( width: 36, height: 4, margin: const EdgeInsets.only(bottom: 18), decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(2), ), ), Row( children: [ // Info pills Expanded( child: Row( children: [ _InfoPill( icon: Icons.schedule_rounded, label: controller.estimatedTimeRemaining.isNotEmpty ? controller.estimatedTimeRemaining : '--', color: _kGreen, ), const SizedBox(width: 10), _InfoPill( icon: Icons.straighten_rounded, label: controller.totalDistanceRemaining.isNotEmpty ? controller.totalDistanceRemaining : '--', color: _kBlue, ), ], ), ), // Start button ElevatedButton.icon( onPressed: () { HapticFeedback.mediumImpact(); controller.isNavigating = true; controller.relockCameraToUser(); controller.update(); }, style: ElevatedButton.styleFrom( backgroundColor: _kBlue, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 13), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14)), elevation: 0, ), icon: const Icon(Icons.navigation_rounded, size: 18), label: const Text('ابدأ', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)), ), ], ), ], ), ), ); } } class _InfoPill extends StatelessWidget { final IconData icon; final String label; final Color color; const _InfoPill( {required this.icon, required this.label, required this.color}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.2)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: color, size: 15), const SizedBox(width: 5), Text(label, style: TextStyle( color: color, fontSize: 13.5, fontWeight: FontWeight.w700)), ], ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Navigation HUD (bottom during active navigation — like HERE Maps) // ───────────────────────────────────────────────────────────────────────────── class _NavigationHUD extends StatelessWidget { final NavigationController controller; const _NavigationHUD({required this.controller}); @override Widget build(BuildContext context) { final bottomPad = MediaQuery.of(context).padding.bottom; return Positioned( bottom: 0, left: 0, right: 0, child: Container( decoration: BoxDecoration( color: _kSurface, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.12), blurRadius: 20, offset: const Offset(0, -4)), ], ), padding: EdgeInsets.fromLTRB(20, 14, 20, bottomPad + 12), child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── Next instruction row ─────────────────────────────────── if (controller.nextInstruction.isNotEmpty) Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: const Color(0xFFF8F9FA), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.withOpacity(0.15)), ), child: Row( children: [ Icon(Icons.arrow_forward_rounded, size: 15, color: _kSubtext), const SizedBox(width: 8), Expanded( child: Text( controller.nextInstruction, style: TextStyle( color: _kSubtext, fontSize: 13, fontWeight: FontWeight.w500), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ), // ── ETA / distance strip ─────────────────────────────────── Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _InfoPill( icon: Icons.schedule_rounded, label: controller.estimatedTimeRemaining.isNotEmpty ? controller.estimatedTimeRemaining : '--', color: _kGreen, ), _InfoPill( icon: Icons.straighten_rounded, label: controller.totalDistanceRemaining.isNotEmpty ? controller.totalDistanceRemaining : '--', color: _kBlue, ), // Stop navigation GestureDetector( onTap: () { HapticFeedback.mediumImpact(); controller.clearRoute(); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9), decoration: BoxDecoration( color: Colors.red.withOpacity(0.08), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.red.withOpacity(0.2)), ), child: Row( children: [ const Icon(Icons.stop_rounded, color: Colors.redAccent, size: 16), const SizedBox(width: 5), const Text('إيقاف', style: TextStyle( color: Colors.redAccent, fontSize: 13, fontWeight: FontWeight.w700)), ], ), ), ), ], ), ], ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Speed badge (bottom-left during navigation) // ───────────────────────────────────────────────────────────────────────────── class _SpeedBadge extends StatelessWidget { final double speed; const _SpeedBadge({required this.speed}); @override Widget build(BuildContext context) { final int kmh = speed.toInt(); final bool fast = kmh > 100; return Positioned( bottom: MediaQuery.of(context).padding.bottom + 130, left: 14, child: Container( width: 62, height: 62, decoration: BoxDecoration( color: fast ? const Color(0xFFD93025) : _kSurface, shape: BoxShape.circle, border: Border.all( color: fast ? Colors.red.withOpacity(0.3) : Colors.grey[200]!, width: 2), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.12), blurRadius: 12, offset: const Offset(0, 4)), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '$kmh', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: fast ? Colors.white : _kText, height: 1), ), Text( 'كم/س', style: TextStyle( fontSize: 9, color: fast ? Colors.white70 : _kSubtext, fontWeight: FontWeight.w500), ), ], ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Loading Overlay // ───────────────────────────────────────────────────────────────────────────── class _LoadingOverlay extends StatelessWidget { const _LoadingOverlay(); @override Widget build(BuildContext context) { return Positioned.fill( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), child: Container( color: Colors.black.withOpacity(0.35), child: Center( child: Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(_kBlue), strokeWidth: 3, ), const SizedBox(height: 14), Text('جاري حساب المسار...', style: TextStyle( color: _kSubtext, fontSize: 14, fontWeight: FontWeight.w500)), ], ), ), ), ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Shared primitives // ───────────────────────────────────────────────────────────────────────────── class _GlassCard extends StatelessWidget { final Widget child; final double borderRadius; final EdgeInsets padding; const _GlassCard({ required this.child, this.borderRadius = 16, this.padding = const EdgeInsets.all(16), }); @override Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(borderRadius), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( decoration: BoxDecoration( color: Colors.white.withOpacity(0.92), borderRadius: BorderRadius.circular(borderRadius), border: Border.all(color: Colors.white.withOpacity(0.5)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.07), blurRadius: 16, offset: const Offset(0, 4)), ], ), padding: padding, child: child, ), ), ); } } class _IconBtn extends StatelessWidget { final IconData icon; final Color color; final VoidCallback onTap; final double size; const _IconBtn({ required this.icon, required this.color, required this.onTap, this.size = 22, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Padding( padding: const EdgeInsets.all(8), child: Icon(icon, color: color, size: size), ), ); } }