base hunt hint geo
This commit is contained in:
parent
e478747724
commit
a6814dfb5e
|
|
@ -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',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
24
pubspec.lock
24
pubspec.lock
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue