import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:lba/res/colors.dart'; import '../services/location_service.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; bool _isScanning = true; bool _hintFound = false; late AnimationController _pulseController; late AnimationController _scanController; late Animation _pulseAnimation; late Animation _scanAnimation; @override void initState() { super.initState(); _initializeCamera(); _setupAnimations(); _checkLocationPeriodically(); } void _setupAnimations() { _pulseController = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, ); _scanController = AnimationController( duration: const Duration(milliseconds: 2000), vsync: this, ); _pulseAnimation = Tween( begin: 0.8, end: 1.2, ).animate(CurvedAnimation( parent: _pulseController, curve: Curves.easeInOut, )); _scanAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _scanController, curve: Curves.linear, )); _pulseController.repeat(reverse: true); _scanController.repeat(); } void _initializeCamera() async { try { _controller = MobileScannerController( detectionSpeed: DetectionSpeed.noDuplicates, facing: CameraFacing.back, ); } catch (e) { // Handle camera initialization error } } void _checkLocationPeriodically() async { while (_isScanning && !_hintFound) { await Future.delayed(const Duration(seconds: 2)); if (!mounted) break; final position = await LocationService.getCurrentPosition(); if (position != null) { final isNearTarget = LocationService.isWithinRange( position.latitude, position.longitude, widget.targetLatitude, widget.targetLongitude, rangeInMeters: 100.0, // 100 meters range for hints ); if (isNearTarget && !_hintFound) { setState(() { _hintFound = true; }); widget.onHintFound(); break; } } } } @override void dispose() { _controller?.dispose(); _pulseController.dispose(); _scanController.dispose(); super.dispose(); } @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: Text( 'AR Hint Scanner', style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600, ), ), centerTitle: true, ), body: Stack( children: [ // Camera view if (_controller != null) MobileScanner( controller: _controller!, onDetect: (capture) { // Handle any QR code detection if needed }, ) else Container( color: Colors.black, child: const Center( child: CircularProgressIndicator(color: Colors.white), ), ), // AR Overlay _buildAROverlay(), // Hint status _buildHintStatus(), // Instructions _buildInstructions(), ], ), ); } Widget _buildAROverlay() { return CustomPaint( painter: AROverlayPainter( scanAnimation: _scanAnimation, pulseAnimation: _pulseAnimation, hintFound: _hintFound, ), size: Size.infinite, ); } Widget _buildHintStatus() { return Positioned( top: 100, left: 20, right: 20, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(12), ), child: Column( children: [ Icon( _hintFound ? Icons.check_circle : Icons.search, color: _hintFound ? Colors.green : Colors.orange, size: 32, ), const SizedBox(height: 8), Text( _hintFound ? 'Hint Found!' : 'Searching for AR Marker...', style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, ), if (_hintFound) ...[ const SizedBox(height: 8), Text( widget.hintDescription, style: const TextStyle( color: Colors.white70, fontSize: 14, ), textAlign: TextAlign.center, ), ], ], ), ), ); } Widget _buildInstructions() { return Positioned( bottom: 50, left: 20, right: 20, child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.black.withOpacity(0.8), borderRadius: BorderRadius.circular(16), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.center_focus_strong, color: AppColors.primary, size: 40, ), const SizedBox(height: 12), Text( 'Point your camera at the environment', style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( 'Move closer to the target location\nto discover the AR hint marker', style: const TextStyle( color: Colors.white70, fontSize: 14, height: 1.4, ), textAlign: TextAlign.center, ), ], ), ), ); } } class AROverlayPainter extends CustomPainter { final Animation scanAnimation; final Animation pulseAnimation; final bool hintFound; AROverlayPainter({ required this.scanAnimation, required this.pulseAnimation, required this.hintFound, }); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = hintFound ? Colors.green : Colors.blue ..style = PaintingStyle.stroke ..strokeWidth = 3.0; final center = Offset(size.width / 2, size.height / 2); final radius = 80.0; // Draw scanning circle if (!hintFound) { canvas.drawCircle( center, radius * pulseAnimation.value, paint..color = Colors.blue.withOpacity(0.6), ); // Draw scanning line const sweepAngle = math.pi / 3; final startAngle = scanAnimation.value * 2 * math.pi; paint ..color = Colors.cyan ..strokeWidth = 2.0 ..style = PaintingStyle.stroke; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, paint, ); } else { // Draw found indicator paint ..color = Colors.green ..style = PaintingStyle.fill; canvas.drawCircle( center, 20 * pulseAnimation.value, paint..color = Colors.green.withOpacity(0.3), ); canvas.drawCircle( center, 10, paint..color = Colors.green, ); // Draw checkmark paint ..color = Colors.white ..strokeWidth = 3.0 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; final path = Path(); path.moveTo(center.dx - 5, center.dy); path.lineTo(center.dx - 2, center.dy + 3); path.lineTo(center.dx + 5, center.dy - 3); canvas.drawPath(path, paint); } // Draw corner brackets _drawCornerBrackets(canvas, size); } void _drawCornerBrackets(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.white.withOpacity(0.8) ..strokeWidth = 2.0 ..style = PaintingStyle.stroke; const bracketSize = 30.0; const margin = 50.0; // Top-left canvas.drawLine( Offset(margin, margin), Offset(margin + bracketSize, margin), paint, ); canvas.drawLine( Offset(margin, margin), Offset(margin, margin + bracketSize), paint, ); // Top-right 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, ); // Bottom-left 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, ); // Bottom-right 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 CustomPainter oldDelegate) => true; }