import 'dart:async'; import 'dart:math' as math; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:flutter_compass/flutter_compass.dart'; import 'package:lba/res/colors.dart'; import '../services/location_service.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; class HintCameraWidget extends StatefulWidget { final double targetLatitude; final double targetLongitude; final String hintDescription; final VoidCallback onHintFound; final VoidCallback onClose; const HintCameraWidget({ super.key, required this.targetLatitude, required this.targetLongitude, required this.hintDescription, required this.onHintFound, required this.onClose, }); @override State createState() => _HintCameraWidgetState(); } class _HintCameraWidgetState extends State with TickerProviderStateMixin { MobileScannerController? _controller; StreamSubscription? _compassSubscription; Timer? _locationTimer; final MapController _miniMapController = MapController(); double _deviceHeading = 0.0; double _bearingToTarget = 0.0; double _distanceToTarget = 0.0; Position? _currentPosition; bool _isNearTarget = false; bool _isMapVisible = false; late AnimationController _pulseController; late AnimationController _rotationController; late AnimationController _scanController; late Animation _pulseAnimation; late Animation _rotationAnimation; late Animation _scanAnimation; final double _cameraHorizontalFov = 60.0; @override void initState() { super.initState(); _initializeCamera(); _setupAnimations(); _startCompassListening(); _startLocationUpdates(); } void _initializeCamera() { _controller = MobileScannerController( detectionSpeed: DetectionSpeed.noDuplicates, facing: CameraFacing.back, ); } void _setupAnimations() { _pulseController = AnimationController( duration: const Duration(milliseconds: 2000), vsync: this, )..repeat(reverse: true); _rotationController = AnimationController( duration: const Duration(seconds: 8), vsync: this, )..repeat(); _scanController = AnimationController( duration: const Duration(milliseconds: 3500), vsync: this, )..repeat(); _pulseAnimation = Tween(begin: 0.95, end: 1.05).animate( CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), ); _rotationAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _rotationController, curve: Curves.linear), ); _scanAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _scanController, curve: Curves.easeInOutSine), ); } void _startCompassListening() { _compassSubscription = FlutterCompass.events?.listen((CompassEvent event) { if (!mounted) return; setState(() { _deviceHeading = event.heading ?? 0.0; }); }); } void _startLocationUpdates() { _locationTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) async { if (!mounted) { timer.cancel(); return; } final position = await LocationService.getCurrentPosition(); if (position != null && mounted) { final bearing = LocationService.getBearing( position.latitude, position.longitude, widget.targetLatitude, widget.targetLongitude, ); final distance = LocationService.calculateDistance( position.latitude, position.longitude, widget.targetLatitude, widget.targetLongitude, ); final isNear = distance <= 10.0; setState(() { _currentPosition = position; _bearingToTarget = bearing; _distanceToTarget = distance; _isNearTarget = isNear; }); if (_isMapVisible) { _updateMiniMapCamera(); } if (isNear) { widget.onHintFound(); } } }); } void _updateMiniMapCamera() { if (_currentPosition != null) { final userLocation = LatLng(_currentPosition!.latitude, _currentPosition!.longitude); final targetLocation = LatLng(widget.targetLatitude, widget.targetLongitude); _miniMapController.fitCamera( CameraFit.bounds( bounds: LatLngBounds(userLocation, targetLocation), padding: const EdgeInsets.all(60.0), ), ); } } void _toggleMapVisibility() { setState(() { _isMapVisible = !_isMapVisible; if (_isMapVisible) { Future.delayed(const Duration(milliseconds: 100), () { _updateMiniMapCamera(); }); } }); } @override void dispose() { _compassSubscription?.cancel(); _locationTimer?.cancel(); _controller?.dispose(); _pulseController.dispose(); _rotationController.dispose(); _scanController.dispose(); _miniMapController.dispose(); super.dispose(); } Widget _buildMiniMap() { if (_currentPosition == null) return const SizedBox.shrink(); final userLocation = LatLng(_currentPosition!.latitude, _currentPosition!.longitude); final targetLocation = LatLng(widget.targetLatitude, widget.targetLongitude); return AnimatedPositioned( duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic, bottom: _isMapVisible ? 100 : -220, left: 20, right: 20, child: ClipRRect( borderRadius: BorderRadius.circular(25), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), child: Container( height: 200, decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(25), border: Border.all(color: Colors.white.withOpacity(0.2)), ), child: ClipRRect( borderRadius: BorderRadius.circular(24), child: FlutterMap( mapController: _miniMapController, options: MapOptions( initialCenter: userLocation, initialZoom: 15, interactionOptions: const InteractionOptions(flags: InteractiveFlag.none), ), children: [ TileLayer( urlTemplate: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', subdomains: const ['a', 'b', 'c', 'd'], ), MarkerLayer( markers: [ Marker( point: userLocation, width: 80, height: 80, child: PulsingMarker(isUser: true), ), Marker( point: targetLocation, width: 80, height: 80, child: PulsingMarker(isUser: false), ), ], ), ], ), ), ), ), ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, leading: IconButton( onPressed: widget.onClose, icon: const Icon(Icons.close_rounded, color: Colors.white, size: 28), ), title: const Text( 'AR HINT', style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 2, fontFamily: 'monospace'), ), centerTitle: true, ), body: Stack( children: [ if (_controller != null) MobileScanner(controller: _controller!) else Container( color: Colors.black, child: Center( child: CircularProgressIndicator(color: AppColors.primary), ), ), BackdropFilter( filter: ImageFilter.blur(sigmaX: 0.5, sigmaY: 0.5), child: Container(color: Colors.black.withOpacity(0.1)), ), _buildAROverlay(), _buildMiniMap(), ], ), floatingActionButton: _buildMapFAB(), ); } Widget _buildMapFAB() { return ClipRRect( borderRadius: BorderRadius.circular(18), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: AnimatedContainer( duration: const Duration(milliseconds: 300), decoration: BoxDecoration( color: _isMapVisible ? AppColors.primary.withOpacity(0.5) : Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(18), border: Border.all(color: Colors.white.withOpacity(0.2)), ), child: FloatingActionButton( onPressed: _toggleMapVisibility, backgroundColor: Colors.transparent, elevation: 0, highlightElevation: 0, child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), transitionBuilder: (child, animation) { return ScaleTransition(scale: animation, child: child); }, child: Icon( _isMapVisible ? Icons.map_outlined : Icons.map, key: ValueKey(_isMapVisible), color: Colors.white, ), ), ), ), ), ); } Widget _buildAROverlay() { if (_currentPosition == null) { return const Center( child: Text( 'ACQUIRING GPS SIGNAL...', style: TextStyle( color: Colors.white70, fontSize: 16, fontWeight: FontWeight.bold, letterSpacing: 1.5, ), ), ); } double normalizeAngle(double angle) => (angle + 180) % 360 - 180; double deviceHeadingNormalized = normalizeAngle(_deviceHeading); double bearingToTargetNormalized = normalizeAngle(_bearingToTarget); double angleDifference = bearingToTargetNormalized - deviceHeadingNormalized; if (angleDifference > 180) angleDifference -= 360; if (angleDifference < -180) angleDifference += 360; bool isVisible = angleDifference.abs() < (_cameraHorizontalFov / 2); return Stack( children: [ CustomPaint( painter: HudPainter(animationValue: _pulseAnimation.value), size: Size.infinite, ), if (!isVisible) _buildOffScreenIndicator(angleDifference) else _buildOnScreenHint(angleDifference), ], ); } Widget _buildOffScreenIndicator(double angleDifference) { final isLeft = angleDifference < 0; return Align( alignment: isLeft ? Alignment.centerLeft : Alignment.centerRight, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: ScaleTransition( scale: _pulseAnimation, child: Icon( isLeft ? Icons.arrow_back_ios_new_rounded : Icons.arrow_forward_ios_rounded, color: AppColors.primary.withOpacity(0.8), size: 48, shadows: [ Shadow(color: AppColors.primary.withOpacity(0.5), blurRadius: 15), ], ), ), ), ); } Widget _buildOnScreenHint(double angleDifference) { final double screenX = angleDifference / (_cameraHorizontalFov / 2); final double distance = _distanceToTarget; return AnimatedPositioned( duration: const Duration(milliseconds: 100), curve: Curves.easeOut, top: 0, bottom: 0, left: (1 + screenX) * (MediaQuery.of(context).size.width / 2) - 100, child: AnimatedBuilder( animation: Listenable.merge([_pulseAnimation, _rotationController, _scanAnimation]), builder: (context, child) { return SizedBox( width: 200, height: 200, child: CustomPaint( painter: AdvancedTargetPainter( isNear: _isNearTarget, distance: distance, pulseValue: _pulseAnimation.value, rotationValue: _rotationController.value, scanValue: _scanAnimation.value, ), child: Center( child: ClipRRect( borderRadius: BorderRadius.circular(100), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( width: 140, height: 140, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.05), shape: BoxShape.circle, border: Border.all( color: (_isNearTarget ? const Color(0xFF00E676) : const Color(0xFF2196F3)) .withOpacity(0.4), width: 1, ), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _isNearTarget ? "TARGET ACQUIRED" : "SCANNING", style: TextStyle( color: _isNearTarget ? const Color(0xFF00E676) : const Color(0xFF2196F3), fontWeight: FontWeight.w600, fontSize: 12, letterSpacing: 1.5, fontFamily: 'monospace' ), ), const SizedBox(height: 12), Text( '${distance.toStringAsFixed(1)}m', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w800, fontSize: 32, letterSpacing: 0.5, fontFamily: 'monospace' ), ), ], ), ), ), ), ), ), ); }, ), ); } } class PulsingMarker extends StatefulWidget { final bool isUser; const PulsingMarker({super.key, required this.isUser}); @override _PulsingMarkerState createState() => _PulsingMarkerState(); } class _PulsingMarkerState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(seconds: 2), )..repeat(); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final color = widget.isUser ? Colors.blue.shade400 : Colors.yellow.shade700; return FadeTransition( opacity: Tween(begin: 1.0, end: 0.3).animate(_animationController), child: ScaleTransition( scale: Tween(begin: 0.5, end: 1.0).animate(_animationController), child: Container( decoration: BoxDecoration( shape: BoxShape.circle, color: color.withOpacity(0.5), border: Border.all(color: color, width: 2), ), child: Icon( widget.isUser ? Icons.my_location : Icons.star, color: Colors.white, size: 20, ), ), ), ); } } class HudPainter extends CustomPainter { final double animationValue; HudPainter({required this.animationValue}); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = AppColors.primary.withOpacity(0.3 + (animationValue * 0.2)) ..style = PaintingStyle.stroke ..strokeWidth = 2.0; const bracketSize = 30.0; const margin = 20.0; canvas.drawLine(Offset(margin, margin), Offset(margin + bracketSize, margin), paint); canvas.drawLine(Offset(margin, margin), Offset(margin, margin + bracketSize), paint); canvas.drawLine(Offset(size.width - margin, margin), Offset(size.width - margin - bracketSize, margin), paint); canvas.drawLine(Offset(size.width - margin, margin), Offset(size.width - margin, margin + bracketSize), paint); canvas.drawLine(Offset(margin, size.height - margin), Offset(margin + bracketSize, size.height - margin), paint); canvas.drawLine(Offset(margin, size.height - margin), Offset(margin, size.height - margin - bracketSize), paint); canvas.drawLine(Offset(size.width - margin, size.height - margin), Offset(size.width - margin - bracketSize, size.height - margin), paint); canvas.drawLine(Offset(size.width - margin, size.height - margin), Offset(size.width - margin, size.height - margin - bracketSize), paint); } @override bool shouldRepaint(covariant HudPainter oldDelegate) => oldDelegate.animationValue != animationValue; } class AdvancedTargetPainter extends CustomPainter { final bool isNear; final double distance; final double pulseValue; final double rotationValue; final double scanValue; AdvancedTargetPainter({ required this.isNear, required this.distance, required this.pulseValue, required this.rotationValue, required this.scanValue, }); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final paint = Paint()..style = PaintingStyle.stroke; final primaryColor = isNear ? const Color(0xFF00E676) : const Color(0xFF2196F3); final baseRadius = math.max(60.0, math.min(85.0, 100 - (distance / 2))); // Outer glow paint ..color = primaryColor.withOpacity(0.1 * pulseValue) ..strokeWidth = 20.0 ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10); canvas.drawCircle(center, baseRadius, paint); // Main rotating rings paint ..color = primaryColor.withOpacity(0.8) ..strokeWidth = 2.5 ..maskFilter = null; canvas.drawArc(Rect.fromCircle(center: center, radius: baseRadius), rotationValue * 2 * math.pi, 2.5, false, paint); canvas.drawArc(Rect.fromCircle(center: center, radius: baseRadius), rotationValue * 2 * math.pi + 3, 2.5, false, paint); // Inner static ring paint ..color = primaryColor.withOpacity(0.4) ..strokeWidth = 1.5; canvas.drawCircle(center, baseRadius - 15, paint); // Scanning arc if (!isNear) { final sweepAngle = scanValue * 2 * math.pi; paint ..color = primaryColor ..strokeWidth = 4.0; canvas.drawArc( Rect.fromCircle(center: center, radius: baseRadius - 8), sweepAngle, math.pi / 4, false, paint, ); } // Success animation if (isNear) { paint ..color = primaryColor.withOpacity(1.0 - pulseValue) ..strokeWidth = 4.0 ..style = PaintingStyle.stroke; canvas.drawCircle(center, baseRadius + (pulseValue * 15), paint); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }