base hunt hint geo

This commit is contained in:
mohamadmahdi jebeli 2025-09-14 16:46:03 +03:30
parent e478747724
commit a6814dfb5e
8 changed files with 534 additions and 516 deletions

View File

@ -40,10 +40,10 @@ class ExampleHuntCard extends StatelessWidget {
question: 'Where coffee meets books in perfect harmony...', question: 'Where coffee meets books in perfect harmony...',
answer: 'Literary Café', answer: 'Literary Café',
description: 'A cozy bookstore café', description: 'A cozy bookstore café',
targetLatitude: 32.62501010252744, targetLatitude: 32.62503845297132,
targetLongitude: 51.72622026956878, targetLongitude: 51.72643744503593,
hintLatitude: 32.62501010252744, hintLatitude: 32.62503845297132,
hintLongitude: 51.72622026956878, hintLongitude: 51.72643744503593,
hintDescription: 'Look for the AR marker near the entrance', hintDescription: 'Look for the AR marker near the entrance',
); );

View File

@ -6,11 +6,9 @@ import 'package:provider/provider.dart';
import 'package:vibration/vibration.dart'; import 'package:vibration/vibration.dart';
import 'package:lba/res/colors.dart'; import 'package:lba/res/colors.dart';
import 'providers/hunt_provider.dart'; import 'providers/hunt_provider.dart';
import 'services/location_service.dart';
import 'services/game_sound_service.dart'; import 'services/game_sound_service.dart';
import 'widgets/hunt_card_widget.dart'; import 'widgets/hunt_card_widget.dart';
import 'widgets/leaderboard_widget.dart'; import 'widgets/leaderboard_widget.dart';
import 'widgets/hint_camera_widget.dart';
import 'models/hunt_card.dart'; import 'models/hunt_card.dart';
class Hunt extends StatelessWidget { class Hunt extends StatelessWidget {
@ -111,26 +109,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
super.dispose(); 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 { void _onCardSelected(HuntCard card) async {
final huntProvider = Provider.of<HuntState>(context, listen: false); final huntProvider = Provider.of<HuntState>(context, listen: false);
@ -145,203 +124,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
huntProvider.selectCard(card); 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<HuntState>(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<HuntState>(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<HuntState>(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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -134,9 +134,9 @@ class HuntState extends ChangeNotifier {
description: 'A cozy book café in Isfahan City Center', description: 'A cozy book café in Isfahan City Center',
targetLatitude: 32.62501010252744, targetLatitude: 32.62501010252744,
targetLongitude: 51.72622026956878, targetLongitude: 51.72622026956878,
hintLatitude: 32.62501010252744, hintLatitude: 32.62498106569981,
hintLongitude: 51.72622026956878, hintLongitude: 51.725834603182015,
hintDescription: 'Look for the AR marker near the fountain area', hintDescription: 'Our store is located on Chahar Bagh Abbasi Street',
), ),
HuntCard( HuntCard(
id: '2', id: '2',

View File

@ -1,5 +1,6 @@
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'dart:math' as math;
class LocationService { class LocationService {
static Future<bool> checkLocationPermission() async { static Future<bool> checkLocationPermission() async {
@ -47,4 +48,23 @@ class LocationService {
double distance = calculateDistance(currentLat, currentLon, targetLat, targetLon); double distance = calculateDistance(currentLat, currentLon, targetLat, targetLon);
return distance <= rangeInMeters; 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
}
} }

View File

@ -1,8 +1,14 @@
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:flutter_compass/flutter_compass.dart';
import 'package:lba/res/colors.dart'; import 'package:lba/res/colors.dart';
import '../services/location_service.dart'; import '../services/location_service.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
class HintCameraWidget extends StatefulWidget { class HintCameraWidget extends StatefulWidget {
final double targetLatitude; final double targetLatitude;
@ -27,96 +33,209 @@ class HintCameraWidget extends StatefulWidget {
class _HintCameraWidgetState extends State<HintCameraWidget> class _HintCameraWidgetState extends State<HintCameraWidget>
with TickerProviderStateMixin { with TickerProviderStateMixin {
MobileScannerController? _controller; MobileScannerController? _controller;
bool _isScanning = true; StreamSubscription<CompassEvent>? _compassSubscription;
bool _hintFound = false; 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 _pulseController;
late AnimationController _rotationController;
late AnimationController _scanController; late AnimationController _scanController;
late Animation<double> _pulseAnimation; late Animation<double> _pulseAnimation;
late Animation<double> _rotationAnimation;
late Animation<double> _scanAnimation; late Animation<double> _scanAnimation;
final double _cameraHorizontalFov = 60.0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializeCamera(); _initializeCamera();
_setupAnimations(); _setupAnimations();
_checkLocationPeriodically(); _startCompassListening();
_startLocationUpdates();
}
void _initializeCamera() {
_controller = MobileScannerController(
detectionSpeed: DetectionSpeed.noDuplicates,
facing: CameraFacing.back,
);
} }
void _setupAnimations() { void _setupAnimations() {
_pulseController = AnimationController( _pulseController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_scanController = AnimationController(
duration: const Duration(milliseconds: 2000), duration: const Duration(milliseconds: 2000),
vsync: this, 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<double>(begin: 0.95, end: 1.05).animate(
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
);
_rotationAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _rotationController, curve: Curves.linear),
);
_scanAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _scanController, curve: Curves.easeInOutSine),
); );
_pulseAnimation = Tween<double>(
begin: 0.8,
end: 1.2,
).animate(CurvedAnimation(
parent: _pulseController,
curve: Curves.easeInOut,
));
_scanAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _scanController,
curve: Curves.linear,
));
_pulseController.repeat(reverse: true);
_scanController.repeat();
} }
void _initializeCamera() async { void _startCompassListening() {
try { _compassSubscription = FlutterCompass.events?.listen((CompassEvent event) {
_controller = MobileScannerController( if (!mounted) return;
detectionSpeed: DetectionSpeed.noDuplicates, setState(() {
facing: CameraFacing.back, _deviceHeading = event.heading ?? 0.0;
); });
} catch (e) { });
// Handle camera initialization error
}
} }
void _checkLocationPeriodically() async { void _startLocationUpdates() {
while (_isScanning && !_hintFound) { _locationTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) async {
await Future.delayed(const Duration(seconds: 2)); if (!mounted) {
if (!mounted) break; timer.cancel();
return;
}
final position = await LocationService.getCurrentPosition(); final position = await LocationService.getCurrentPosition();
if (position != null) { if (position != null && mounted) {
final isNearTarget = LocationService.isWithinRange( final bearing = LocationService.getBearing(
position.latitude, position.latitude,
position.longitude, position.longitude,
widget.targetLatitude, widget.targetLatitude,
widget.targetLongitude, widget.targetLongitude,
rangeInMeters: 100.0, // 100 meters range for hints
); );
final distance = LocationService.calculateDistance(
if (isNearTarget && !_hintFound) { position.latitude,
setState(() { position.longitude,
_hintFound = true; 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(); 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 @override
void dispose() { void dispose() {
_compassSubscription?.cancel();
_locationTimer?.cancel();
_controller?.dispose(); _controller?.dispose();
_pulseController.dispose(); _pulseController.dispose();
_rotationController.dispose();
_scanController.dispose(); _scanController.dispose();
_miniMapController.dispose();
super.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -126,295 +245,372 @@ class _HintCameraWidgetState extends State<HintCameraWidget>
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
onPressed: widget.onClose, onPressed: widget.onClose,
icon: const Icon( icon: const Icon(Icons.close_rounded, color: Colors.white, size: 28),
Icons.close_rounded,
color: Colors.white,
size: 28,
),
), ),
title: Text( title: const Text(
'AR Hint Scanner', 'AR HINT',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.bold,
), letterSpacing: 2,
fontFamily: 'monospace'),
), ),
centerTitle: true, centerTitle: true,
), ),
body: Stack( body: Stack(
children: [ children: [
// Camera view
if (_controller != null) if (_controller != null)
MobileScanner( MobileScanner(controller: _controller!)
controller: _controller!,
onDetect: (capture) {
// Handle any QR code detection if needed
},
)
else else
Container( Container(
color: Colors.black, color: Colors.black,
child: const Center( child: Center(
child: CircularProgressIndicator(color: Colors.white), child: CircularProgressIndicator(color: AppColors.primary),
), ),
), ),
BackdropFilter(
// AR Overlay filter: ImageFilter.blur(sigmaX: 0.5, sigmaY: 0.5),
child: Container(color: Colors.black.withOpacity(0.1)),
),
_buildAROverlay(), _buildAROverlay(),
_buildMiniMap(),
// Hint status
_buildHintStatus(),
// Instructions
_buildInstructions(),
], ],
), ),
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<bool>(_isMapVisible),
color: Colors.white,
),
),
),
),
),
); );
} }
Widget _buildAROverlay() { Widget _buildAROverlay() {
return CustomPaint( if (_currentPosition == null) {
painter: AROverlayPainter( return const Center(
scanAnimation: _scanAnimation, child: Text(
pulseAnimation: _pulseAnimation, 'ACQUIRING GPS SIGNAL...',
hintFound: _hintFound, style: TextStyle(
), color: Colors.white70,
size: Size.infinite, 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() { Widget _buildOffScreenIndicator(double angleDifference) {
return Positioned( final isLeft = angleDifference < 0;
top: 100, return Align(
left: 20, alignment: isLeft ? Alignment.centerLeft : Alignment.centerRight,
right: 20, child: Padding(
child: Container( padding: const EdgeInsets.symmetric(horizontal: 24.0),
padding: const EdgeInsets.all(16), child: ScaleTransition(
decoration: BoxDecoration( scale: _pulseAnimation,
color: Colors.black.withOpacity(0.7), child: Icon(
borderRadius: BorderRadius.circular(12), isLeft ? Icons.arrow_back_ios_new_rounded : Icons.arrow_forward_ios_rounded,
), color: AppColors.primary.withOpacity(0.8),
child: Column( size: 48,
children: [ shadows: [
Icon( Shadow(color: AppColors.primary.withOpacity(0.5), blurRadius: 15),
_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() { Widget _buildOnScreenHint(double angleDifference) {
return Positioned( final double screenX = angleDifference / (_cameraHorizontalFov / 2);
bottom: 50, final double distance = _distanceToTarget;
left: 20,
right: 20, return AnimatedPositioned(
child: Container( duration: const Duration(milliseconds: 100),
padding: const EdgeInsets.all(20), curve: Curves.easeOut,
decoration: BoxDecoration( top: 0,
color: Colors.black.withOpacity(0.8), bottom: 0,
borderRadius: BorderRadius.circular(16), left: (1 + screenX) * (MediaQuery.of(context).size.width / 2) - 100,
), child: AnimatedBuilder(
child: Column( animation: Listenable.merge([_pulseAnimation, _rotationController, _scanAnimation]),
mainAxisSize: MainAxisSize.min, builder: (context, child) {
children: [ return SizedBox(
Icon( width: 200,
Icons.center_focus_strong, height: 200,
color: AppColors.primary, child: CustomPaint(
size: 40, painter: AdvancedTargetPainter(
), isNear: _isNearTarget,
const SizedBox(height: 12), distance: distance,
Text( pulseValue: _pulseAnimation.value,
'Point your camera at the environment', rotationValue: _rotationController.value,
style: const TextStyle( scanValue: _scanAnimation.value,
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
), ),
textAlign: TextAlign.center, child: Center(
), child: ClipRRect(
const SizedBox(height: 8), borderRadius: BorderRadius.circular(100),
Text( child: BackdropFilter(
'Move closer to the target location\nto discover the AR hint marker', filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
style: const TextStyle( child: Container(
color: Colors.white70, width: 140,
fontSize: 14, height: 140,
height: 1.4, 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<PulsingMarker>
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<double>(begin: 1.0, end: 0.3).animate(_animationController),
child: ScaleTransition(
scale: Tween<double>(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 { class HudPainter extends CustomPainter {
final Animation<double> scanAnimation; final double animationValue;
final Animation<double> pulseAnimation; HudPainter({required this.animationValue});
final bool hintFound;
@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({ const bracketSize = 30.0;
required this.scanAnimation, const margin = 20.0;
required this.pulseAnimation,
required this.hintFound, 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 @override
void paint(Canvas canvas, Size size) { 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 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 // Outer glow
if (!hintFound) { paint
canvas.drawCircle( ..color = primaryColor.withOpacity(0.1 * pulseValue)
center, ..strokeWidth = 20.0
radius * pulseAnimation.value, ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10);
paint..color = Colors.blue.withOpacity(0.6), canvas.drawCircle(center, baseRadius, paint);
);
// Draw scanning line // Main rotating rings
const sweepAngle = math.pi / 3; paint
final startAngle = scanAnimation.value * 2 * math.pi; ..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 paint
..color = Colors.cyan ..color = primaryColor
..strokeWidth = 2.0 ..strokeWidth = 4.0;
..style = PaintingStyle.stroke;
canvas.drawArc( canvas.drawArc(
Rect.fromCircle(center: center, radius: radius), Rect.fromCircle(center: center, radius: baseRadius - 8),
startAngle,
sweepAngle, sweepAngle,
math.pi / 4,
false, false,
paint, 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 // Success animation
_drawCornerBrackets(canvas, size); if (isNear) {
} paint
..color = primaryColor.withOpacity(1.0 - pulseValue)
void _drawCornerBrackets(Canvas canvas, Size size) { ..strokeWidth = 4.0
final paint = Paint() ..style = PaintingStyle.stroke;
..color = Colors.white.withOpacity(0.8) canvas.drawCircle(center, baseRadius + (pulseValue * 15), paint);
..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 @override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true; bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
} }

View File

@ -107,15 +107,8 @@ class _HuntCardWidgetState extends State<HuntCardWidget>
targetLongitude: widget.card.hintLongitude, targetLongitude: widget.card.hintLongitude,
hintDescription: widget.card.hintDescription, hintDescription: widget.card.hintDescription,
onHintFound: () { onHintFound: () {
Navigator.pop(context); // TODO: Handle what happens when the hint is found.
// Show hint found feedback // For example, show a success message or update state.
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('🎯 Hint Found! You\'re getting closer!'),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
),
);
}, },
onClose: () { onClose: () {
Navigator.pop(context); Navigator.pop(context);

View File

@ -478,6 +478,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.1.1" 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: flutter_gen_core:
dependency: transitive dependency: transitive
description: description:
@ -1200,6 +1208,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" 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: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -59,6 +59,8 @@ dependencies:
firebase_auth: ^6.0.1 firebase_auth: ^6.0.1
google_sign_in: ^6.1.6 google_sign_in: ^6.1.6
firebase_core: ^4.0.0 firebase_core: ^4.0.0
sensors_plus: ^5.0.1
flutter_compass: ^0.8.1
# geocoding: ^3.0.0 # geocoding: ^3.0.0
dev_dependencies: dev_dependencies: