diff --git a/lib/screens/mains/hunt/examples/hunt_usage_examples.dart b/lib/screens/mains/hunt/examples/hunt_usage_examples.dart index b65af94..654ca88 100644 --- a/lib/screens/mains/hunt/examples/hunt_usage_examples.dart +++ b/lib/screens/mains/hunt/examples/hunt_usage_examples.dart @@ -40,10 +40,10 @@ class ExampleHuntCard extends StatelessWidget { question: 'Where coffee meets books in perfect harmony...', answer: 'Literary Café', description: 'A cozy bookstore café', - targetLatitude: 32.62501010252744, - targetLongitude: 51.72622026956878, - hintLatitude: 32.62501010252744, - hintLongitude: 51.72622026956878, + targetLatitude: 32.62503845297132, + targetLongitude: 51.72643744503593, + hintLatitude: 32.62503845297132, + hintLongitude: 51.72643744503593, hintDescription: 'Look for the AR marker near the entrance', ); diff --git a/lib/screens/mains/hunt/hunt.dart b/lib/screens/mains/hunt/hunt.dart index 7873be1..1a0aa9b 100644 --- a/lib/screens/mains/hunt/hunt.dart +++ b/lib/screens/mains/hunt/hunt.dart @@ -6,11 +6,9 @@ import 'package:provider/provider.dart'; import 'package:vibration/vibration.dart'; import 'package:lba/res/colors.dart'; import 'providers/hunt_provider.dart'; -import 'services/location_service.dart'; import 'services/game_sound_service.dart'; import 'widgets/hunt_card_widget.dart'; import 'widgets/leaderboard_widget.dart'; -import 'widgets/hint_camera_widget.dart'; import 'models/hunt_card.dart'; class Hunt extends StatelessWidget { @@ -111,26 +109,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi super.dispose(); } - void _startLocationMonitoring(HuntCard card) { - _locationTimer?.cancel(); - _locationTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { - final position = await LocationService.getCurrentPosition(); - if (position != null) { - final isNearTarget = LocationService.isWithinRange( - position.latitude, - position.longitude, - card.targetLatitude, - card.targetLongitude, - rangeInMeters: 50.0, - ); - if (isNearTarget && mounted) { - _onHuntCompleted(); - timer.cancel(); - } - } - }); - } void _onCardSelected(HuntCard card) async { final huntProvider = Provider.of(context, listen: false); @@ -145,203 +124,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi huntProvider.selectCard(card); } - void _showPermissionDialog(String message) { - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: AppColors.surface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - title: Text( - 'Permission Required', - style: TextStyle( - color: AppColors.textPrimary, - fontWeight: FontWeight.bold, - ), - ), - content: Text( - message, - style: TextStyle( - color: AppColors.textSecondary, - height: 1.4, - ), - ), - actions: [ - ElevatedButton( - onPressed: () => Navigator.of(context).pop(), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primary, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text('OK'), - ), - ], - ), - ); - } - void _openHintCamera() async { - final huntProvider = Provider.of(context, listen: false); - final selectedCard = huntProvider.selectedCard; - - if (selectedCard == null) return; - - final hasCameraPermission = await LocationService.checkCameraPermission(); - if (!hasCameraPermission) { - _showPermissionDialog('Camera permission is required for the hint feature.'); - return; - } - - huntProvider.setCameraPermissionGranted(true); - huntProvider.activateHintMode(); - - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => HintCameraWidget( - targetLatitude: selectedCard.hintLatitude, - targetLongitude: selectedCard.hintLongitude, - hintDescription: selectedCard.hintDescription, - onHintFound: () async { - await GameSoundService.playHintFoundSound(); - }, - onClose: () { - Navigator.of(context).pop(); - huntProvider.startHunt(); - }, - ), - ), - ); - } - - void _onHuntCompleted() async { - final huntProvider = Provider.of(context, listen: false); - - await GameSoundService.playSuccessSound(); - await Future.delayed(const Duration(milliseconds: 500)); - await GameSoundService.playPointsEarnedSound(); - - huntProvider.completeHunt(); - _confettiController.forward(); - - _showCompletionDialog(); - } - - void _showCompletionDialog() { - final huntProvider = Provider.of(context, listen: false); - final selectedCard = huntProvider.selectedCard; - - if (selectedCard == null) return; - - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => AlertDialog( - backgroundColor: AppColors.surface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - title: Column( - children: [ - Icon( - Icons.celebration, - color: AppColors.confirmButton, - size: 48, - ), - const SizedBox(height: 8), - Text( - 'Congratulations!', - style: TextStyle( - color: AppColors.textPrimary, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'You found ${selectedCard.answer}!', - style: TextStyle( - color: AppColors.primary, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.confirmButton.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.stars_rounded, - color: AppColors.confirmButton, - size: 24, - ), - const SizedBox(width: 8), - Text( - '+${selectedCard.points} Points', - style: TextStyle( - color: AppColors.confirmButton, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ], - ), - actions: [ - Row( - children: [ - Expanded( - child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - setState(() { - _showLeaderboard = true; - }); - }, - child: Text( - 'View Leaderboard', - style: TextStyle(color: AppColors.primary), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - huntProvider.resetGame(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primary, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text('Play Again'), - ), - ), - ], - ), - ], - ), - ); - } @override Widget build(BuildContext context) { diff --git a/lib/screens/mains/hunt/providers/hunt_provider.dart b/lib/screens/mains/hunt/providers/hunt_provider.dart index 19b867d..e4581be 100644 --- a/lib/screens/mains/hunt/providers/hunt_provider.dart +++ b/lib/screens/mains/hunt/providers/hunt_provider.dart @@ -134,9 +134,9 @@ class HuntState extends ChangeNotifier { description: 'A cozy book café in Isfahan City Center', targetLatitude: 32.62501010252744, targetLongitude: 51.72622026956878, - hintLatitude: 32.62501010252744, - hintLongitude: 51.72622026956878, - hintDescription: 'Look for the AR marker near the fountain area', + hintLatitude: 32.62498106569981, + hintLongitude: 51.725834603182015, + hintDescription: 'Our store is located on Chahar Bagh Abbasi Street', ), HuntCard( id: '2', diff --git a/lib/screens/mains/hunt/services/location_service.dart b/lib/screens/mains/hunt/services/location_service.dart index ca4b07c..f7b8d94 100644 --- a/lib/screens/mains/hunt/services/location_service.dart +++ b/lib/screens/mains/hunt/services/location_service.dart @@ -1,5 +1,6 @@ import 'package:geolocator/geolocator.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'dart:math' as math; class LocationService { static Future checkLocationPermission() async { @@ -47,4 +48,23 @@ class LocationService { double distance = calculateDistance(currentLat, currentLon, targetLat, targetLon); return distance <= rangeInMeters; } + + static double getBearing( + double startLat, double startLng, + double endLat, double endLng, + ) { + final startLatRad = startLat * (math.pi / 180); + final startLngRad = startLng * (math.pi / 180); + final endLatRad = endLat * (math.pi / 180); + final endLngRad = endLng * (math.pi / 180); + + double dLng = endLngRad - startLngRad; + + double y = math.sin(dLng) * math.cos(endLatRad); + double x = math.cos(startLatRad) * math.sin(endLatRad) - + math.sin(startLatRad) * math.cos(endLatRad) * math.cos(dLng); + + double bearing = math.atan2(y, x); + return (bearing * (180 / math.pi) + 360) % 360; // Convert to degrees + } } diff --git a/lib/screens/mains/hunt/widgets/hint_camera_widget.dart b/lib/screens/mains/hunt/widgets/hint_camera_widget.dart index 3f49471..069cb9d 100644 --- a/lib/screens/mains/hunt/widgets/hint_camera_widget.dart +++ b/lib/screens/mains/hunt/widgets/hint_camera_widget.dart @@ -1,8 +1,14 @@ +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; @@ -27,96 +33,209 @@ class HintCameraWidget extends StatefulWidget { class _HintCameraWidgetState extends State with TickerProviderStateMixin { MobileScannerController? _controller; - bool _isScanning = true; - bool _hintFound = false; + 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(); - _checkLocationPeriodically(); + _startCompassListening(); + _startLocationUpdates(); + } + + void _initializeCamera() { + _controller = MobileScannerController( + detectionSpeed: DetectionSpeed.noDuplicates, + facing: CameraFacing.back, + ); } void _setupAnimations() { _pulseController = AnimationController( - duration: const Duration(milliseconds: 1500), - vsync: this, - ); - _scanController = 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), ); - - _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 _startCompassListening() { + _compassSubscription = FlutterCompass.events?.listen((CompassEvent event) { + if (!mounted) return; + setState(() { + _deviceHeading = event.heading ?? 0.0; + }); + }); } - void _checkLocationPeriodically() async { - while (_isScanning && !_hintFound) { - await Future.delayed(const Duration(seconds: 2)); - if (!mounted) break; - + void _startLocationUpdates() { + _locationTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) async { + if (!mounted) { + timer.cancel(); + return; + } final position = await LocationService.getCurrentPosition(); - if (position != null) { - final isNearTarget = LocationService.isWithinRange( + if (position != null && mounted) { + final bearing = LocationService.getBearing( position.latitude, position.longitude, widget.targetLatitude, widget.targetLongitude, - rangeInMeters: 100.0, // 100 meters range for hints ); - - if (isNearTarget && !_hintFound) { - setState(() { - _hintFound = true; - }); + 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(); - break; } } + }); + } + + 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( @@ -126,295 +245,372 @@ class _HintCameraWidgetState extends State elevation: 0, leading: IconButton( onPressed: widget.onClose, - icon: const Icon( - Icons.close_rounded, - color: Colors.white, - size: 28, - ), + icon: const Icon(Icons.close_rounded, color: Colors.white, size: 28), ), - title: Text( - 'AR Hint Scanner', + title: const Text( + 'AR HINT', style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w600, - ), + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 2, + fontFamily: 'monospace'), ), centerTitle: true, ), body: Stack( children: [ - // Camera view if (_controller != null) - MobileScanner( - controller: _controller!, - onDetect: (capture) { - // Handle any QR code detection if needed - }, - ) + MobileScanner(controller: _controller!) else Container( color: Colors.black, - child: const Center( - child: CircularProgressIndicator(color: Colors.white), + child: Center( + child: CircularProgressIndicator(color: AppColors.primary), ), ), - - // AR Overlay + BackdropFilter( + filter: ImageFilter.blur(sigmaX: 0.5, sigmaY: 0.5), + child: Container(color: Colors.black.withOpacity(0.1)), + ), _buildAROverlay(), - - // Hint status - _buildHintStatus(), - - // Instructions - _buildInstructions(), + _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() { - return CustomPaint( - painter: AROverlayPainter( - scanAnimation: _scanAnimation, - pulseAnimation: _pulseAnimation, - hintFound: _hintFound, - ), - size: Size.infinite, + 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 _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 _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 _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, + 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, ), - 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, + 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' + ), + ), + ], + ), + ), + ), + ), ), - textAlign: TextAlign.center, ), - ], + ); + }, + ), + ); + } +} + +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 AROverlayPainter extends CustomPainter { - final Animation scanAnimation; - final Animation pulseAnimation; - final bool hintFound; +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; - AROverlayPainter({ - required this.scanAnimation, - required this.pulseAnimation, - required this.hintFound, + 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 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; + 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))); - // Draw scanning circle - if (!hintFound) { - canvas.drawCircle( - center, - radius * pulseAnimation.value, - paint..color = Colors.blue.withOpacity(0.6), - ); + // Outer glow + paint + ..color = primaryColor.withOpacity(0.1 * pulseValue) + ..strokeWidth = 20.0 + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10); + canvas.drawCircle(center, baseRadius, paint); - // Draw scanning line - const sweepAngle = math.pi / 3; - final startAngle = scanAnimation.value * 2 * math.pi; - + // 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 = Colors.cyan - ..strokeWidth = 2.0 - ..style = PaintingStyle.stroke; - + ..color = primaryColor + ..strokeWidth = 4.0; canvas.drawArc( - Rect.fromCircle(center: center, radius: radius), - startAngle, + Rect.fromCircle(center: center, radius: baseRadius - 8), sweepAngle, + math.pi / 4, 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, - ); + // 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; -} +} \ No newline at end of file diff --git a/lib/screens/mains/hunt/widgets/hunt_card_widget.dart b/lib/screens/mains/hunt/widgets/hunt_card_widget.dart index 991b597..37d879e 100644 --- a/lib/screens/mains/hunt/widgets/hunt_card_widget.dart +++ b/lib/screens/mains/hunt/widgets/hunt_card_widget.dart @@ -107,15 +107,8 @@ class _HuntCardWidgetState extends State targetLongitude: widget.card.hintLongitude, hintDescription: widget.card.hintDescription, onHintFound: () { - Navigator.pop(context); - // Show hint found feedback - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('🎯 Hint Found! You\'re getting closer!'), - backgroundColor: Colors.green, - duration: Duration(seconds: 2), - ), - ); + // TODO: Handle what happens when the hint is found. + // For example, show a success message or update state. }, onClose: () { Navigator.pop(context); diff --git a/pubspec.lock b/pubspec.lock index ddb596b..8aa49e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -478,6 +478,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_compass: + dependency: "direct main" + description: + name: flutter_compass + sha256: "1b4d7e6c95a675ec8482b5c9c9ccf1ebf0ced3dbec59dce28ad609da953de850" + url: "https://pub.dev" + source: hosted + version: "0.8.1" flutter_gen_core: dependency: transitive description: @@ -1200,6 +1208,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + sensors_plus: + dependency: "direct main" + description: + name: sensors_plus + sha256: "6898cd4490ffc27fea4de5976585e92fae55355175d46c6c3b3d719d42f9e230" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + sensors_plus_platform_interface: + dependency: transitive + description: + name: sensors_plus_platform_interface + sha256: bc472d6cfd622acb4f020e726433ee31788b038056691ba433fec80e448a094f + url: "https://pub.dev" + source: hosted + version: "1.2.0" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index dd08bfb..750096d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,8 @@ dependencies: firebase_auth: ^6.0.1 google_sign_in: ^6.1.6 firebase_core: ^4.0.0 + sensors_plus: ^5.0.1 + flutter_compass: ^0.8.1 # geocoding: ^3.0.0 dev_dependencies: