diff --git a/assets/icons/close-circle.svg b/assets/icons/close-circle.svg
new file mode 100644
index 0000000..b2a06c5
--- /dev/null
+++ b/assets/icons/close-circle.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/gps.svg b/assets/icons/gps.svg
new file mode 100644
index 0000000..7b0321d
--- /dev/null
+++ b/assets/icons/gps.svg
@@ -0,0 +1,8 @@
+
diff --git a/assets/icons/location-cross.svg b/assets/icons/location-cross.svg
new file mode 100644
index 0000000..f4ce111
--- /dev/null
+++ b/assets/icons/location-cross.svg
@@ -0,0 +1,5 @@
+
diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart
index eabf92f..a6563cc 100644
--- a/lib/gen/assets.gen.dart
+++ b/lib/gen/assets.gen.dart
@@ -132,6 +132,10 @@ class $AssetsIconsGen {
/// File path: assets/icons/clock.svg
SvgGenImage get clock => const SvgGenImage('assets/icons/clock.svg');
+ /// File path: assets/icons/close-circle.svg
+ SvgGenImage get closeCircle =>
+ const SvgGenImage('assets/icons/close-circle.svg');
+
/// File path: assets/icons/coin.svg
SvgGenImage get coin => const SvgGenImage('assets/icons/coin.svg');
@@ -211,6 +215,9 @@ class $AssetsIconsGen {
SvgGenImage get globalSearch2 =>
const SvgGenImage('assets/icons/global-search2.svg');
+ /// File path: assets/icons/gps.svg
+ SvgGenImage get gps => const SvgGenImage('assets/icons/gps.svg');
+
/// File path: assets/icons/healthicons_fruits-outline.svg
SvgGenImage get healthiconsFruitsOutline =>
const SvgGenImage('assets/icons/healthicons_fruits-outline.svg');
@@ -258,6 +265,10 @@ class $AssetsIconsGen {
/// File path: assets/icons/list.svg
SvgGenImage get list => const SvgGenImage('assets/icons/list.svg');
+ /// File path: assets/icons/location-cross.svg
+ SvgGenImage get locationCross =>
+ const SvgGenImage('assets/icons/location-cross.svg');
+
/// File path: assets/icons/location-tick.svg
SvgGenImage get locationTick =>
const SvgGenImage('assets/icons/location-tick.svg');
@@ -493,6 +504,7 @@ class $AssetsIconsGen {
checkAlternative,
clander,
clock,
+ closeCircle,
coin,
cup,
currentLoc,
@@ -515,6 +527,7 @@ class $AssetsIconsGen {
girlClothes,
globalSearch,
globalSearch2,
+ gps,
healthiconsFruitsOutline,
heart,
hugeiconsBabyBoyDress,
@@ -528,6 +541,7 @@ class $AssetsIconsGen {
like,
link2,
list,
+ locationCross,
locationTick,
location,
locationPopup,
diff --git a/lib/screens/mains/hunt/hunt_clean.dart b/lib/screens/mains/hunt/hunt_clean.dart
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/screens/mains/hunt/providers/hunt_provider.dart b/lib/screens/mains/hunt/providers/hunt_provider.dart
index 9eac090..85a4f19 100644
--- a/lib/screens/mains/hunt/providers/hunt_provider.dart
+++ b/lib/screens/mains/hunt/providers/hunt_provider.dart
@@ -64,6 +64,7 @@ class HuntState extends ChangeNotifier {
void startHunt() {
if (_selectedCard != null) {
+ _huntStartTime = DateTime.now();
_gameState = HuntGameState.huntingActive;
notifyListeners();
}
diff --git a/lib/screens/mains/hunt/widgets/hint_camera_widget.dart b/lib/screens/mains/hunt/widgets/hint_camera_widget.dart
index fe50b4d..dc93c0f 100644
--- a/lib/screens/mains/hunt/widgets/hint_camera_widget.dart
+++ b/lib/screens/mains/hunt/widgets/hint_camera_widget.dart
@@ -2,7 +2,9 @@ import 'dart:async';
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
import 'package:geolocator/geolocator.dart';
+import 'package:lba/gen/assets.gen.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:flutter_compass/flutter_compass.dart';
import 'package:lba/res/colors.dart';
@@ -16,6 +18,8 @@ class HintCameraWidget extends StatefulWidget {
final String hintDescription;
final VoidCallback onHintFound;
final VoidCallback onClose;
+ final Duration? remainingTime;
+ final String? cardTitle;
const HintCameraWidget({
super.key,
@@ -24,6 +28,8 @@ class HintCameraWidget extends StatefulWidget {
required this.hintDescription,
required this.onHintFound,
required this.onClose,
+ this.remainingTime,
+ this.cardTitle,
});
@override
@@ -35,6 +41,7 @@ class _HintCameraWidgetState extends State
MobileScannerController? _controller;
StreamSubscription? _compassSubscription;
Timer? _locationTimer;
+ Timer? _countdownTimer;
final MapController _miniMapController = MapController();
@@ -44,11 +51,11 @@ class _HintCameraWidgetState extends State
Position? _currentPosition;
bool _isNearTarget = false;
bool _isMapVisible = false;
+
+ Duration _currentRemainingTime = Duration.zero;
- late AnimationController _pulseController;
late AnimationController _rotationController;
late AnimationController _scanController;
- late Animation _pulseAnimation;
late Animation _rotationAnimation;
late Animation _scanAnimation;
@@ -61,6 +68,31 @@ class _HintCameraWidgetState extends State
_setupAnimations();
_startCompassListening();
_startLocationUpdates();
+ _initializeTimer();
+ }
+
+ void _initializeTimer() {
+ if (widget.remainingTime != null) {
+ _currentRemainingTime = widget.remainingTime!.inSeconds == 0
+ ? const Duration(hours: 12)
+ : widget.remainingTime!;
+ _startCountdownTimer();
+ }
+ }
+
+ void _startCountdownTimer() {
+ _countdownTimer?.cancel();
+ _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
+ if (mounted) {
+ setState(() {
+ if (_currentRemainingTime.inSeconds > 0) {
+ _currentRemainingTime = Duration(seconds: _currentRemainingTime.inSeconds - 1);
+ } else {
+ timer.cancel();
+ }
+ });
+ }
+ });
}
void _initializeCamera() {
@@ -71,10 +103,6 @@ class _HintCameraWidgetState extends State
}
void _setupAnimations() {
- _pulseController = AnimationController(
- duration: const Duration(milliseconds: 2000),
- vsync: this,
- )..repeat(reverse: true);
_rotationController = AnimationController(
duration: const Duration(seconds: 8),
vsync: this,
@@ -83,9 +111,7 @@ class _HintCameraWidgetState extends State
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),
);
@@ -153,6 +179,24 @@ class _HintCameraWidgetState extends State
}
}
+ String _formatDistance(double distance) {
+ if (distance >= 100) {
+ return '${distance.round()}m';
+ } else {
+ return '${distance.toStringAsFixed(1)}m';
+ }
+ }
+
+ double _getDistanceFontSize(double distance) {
+ if (distance >= 10000) {
+ return 20;
+ } else if (distance >= 1000) {
+ return 26;
+ } else {
+ return 32;
+ }
+ }
+
void _toggleMapVisibility() {
setState(() {
_isMapVisible = !_isMapVisible;
@@ -168,8 +212,8 @@ class _HintCameraWidgetState extends State
void dispose() {
_compassSubscription?.cancel();
_locationTimer?.cancel();
+ _countdownTimer?.cancel();
_controller?.dispose();
- _pulseController.dispose();
_rotationController.dispose();
_scanController.dispose();
_miniMapController.dispose();
@@ -208,21 +252,21 @@ class _HintCameraWidgetState extends State
),
children: [
TileLayer(
- urlTemplate: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
+ urlTemplate: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
subdomains: const ['a', 'b', 'c', 'd'],
),
MarkerLayer(
markers: [
Marker(
point: userLocation,
- width: 80,
- height: 80,
+ width: 40,
+ height: 40,
child: PulsingMarker(isUser: true),
),
Marker(
point: targetLocation,
- width: 80,
- height: 80,
+ width: 40,
+ height: 40,
child: PulsingMarker(isUser: false),
),
],
@@ -240,23 +284,132 @@ class _HintCameraWidgetState extends State
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
- appBar: AppBar(
- backgroundColor: Colors.transparent,
- elevation: 0,
- leading: IconButton(
- onPressed: widget.onClose,
- icon: const Icon(Icons.close_rounded, color: Colors.white, size: 28),
+ extendBodyBehindAppBar: true,
+ appBar: PreferredSize(
+ preferredSize: const Size.fromHeight(kToolbarHeight + 10),
+ child: Container(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: [
+ Colors.black.withOpacity(0.8),
+ Colors.black.withOpacity(0.4),
+ Colors.transparent,
+ ],
+ ),
+ ),
+ child: ClipRRect(
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
+ child: AppBar(
+ backgroundColor: Colors.transparent,
+ elevation: 0,
+ toolbarHeight: kToolbarHeight + 10,
+ leading: Container(
+ margin: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Colors.white.withOpacity(0.15),
+ Colors.white.withOpacity(0.05),
+ ],
+ ),
+ shape: BoxShape.circle,
+ border: Border.all(
+ color: Colors.white.withOpacity(0.3),
+ width: 1.5,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.3),
+ blurRadius: 8,
+ spreadRadius: 1,
+ ),
+ ],
+ ),
+ child: IconButton(
+ onPressed: widget.onClose,
+ icon: SvgPicture.asset(Assets.icons.back.path, color: Colors.white,),
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(),
+ ),
+ ),
+ title: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ AppColors.primary.withOpacity(0.3),
+ AppColors.primary.withOpacity(0.1),
+ ],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ borderRadius: BorderRadius.circular(25),
+ border: Border.all(
+ color: AppColors.primary.withOpacity(0.4),
+ width: 1.5,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: AppColors.primary.withOpacity(0.2),
+ blurRadius: 15,
+ spreadRadius: 2,
+ ),
+ BoxShadow(
+ color: Colors.black.withOpacity(0.3),
+ blurRadius: 8,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: const Text(
+ 'AR Hint',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 15,
+ fontWeight: FontWeight.w700,
+ letterSpacing: 1.2,
+ fontFamily: 'monospace'
+ ),
+ ),
+ ),
+ centerTitle: true,
+ actions: [
+ Container(
+ margin: const EdgeInsets.all(10),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Colors.white.withOpacity(0.15),
+ Colors.white.withOpacity(0.05),
+ ],
+ ),
+ shape: BoxShape.circle,
+ border: Border.all(
+ color: Colors.white.withOpacity(0.3),
+ width: 1.5,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.3),
+ blurRadius: 8,
+ spreadRadius: 1,
+ ),
+ ],
+ ),
+ child: IconButton(
+ onPressed: () {
+ },
+ icon: SvgPicture.asset(Assets.icons.infoCircle.path, color: Colors.white,),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
),
- title: const Text(
- 'AR HINT',
- style: TextStyle(
- color: Colors.white,
- fontSize: 18,
- fontWeight: FontWeight.bold,
- letterSpacing: 2,
- fontFamily: 'monospace'),
- ),
- centerTitle: true,
),
body: Stack(
children: [
@@ -273,6 +426,7 @@ class _HintCameraWidgetState extends State
filter: ImageFilter.blur(sigmaX: 0.5, sigmaY: 0.5),
child: Container(color: Colors.black.withOpacity(0.1)),
),
+ if (widget.remainingTime != null) _buildTimerWidget(),
_buildAROverlay(),
_buildMiniMap(),
],
@@ -281,6 +435,64 @@ class _HintCameraWidgetState extends State
);
}
+ Widget _buildTimerWidget() {
+ if (widget.remainingTime == null) return const SizedBox.shrink();
+
+ final Duration timeToShow = _currentRemainingTime.inSeconds <= 0
+ ? (widget.remainingTime!.inSeconds == 0 ? const Duration(hours: 12) : Duration.zero)
+ : _currentRemainingTime;
+
+ final hours = timeToShow.inHours;
+ final minutes = timeToShow.inMinutes % 60;
+ final seconds = timeToShow.inSeconds % 60;
+ final isUrgent = timeToShow.inMinutes < 5 && timeToShow.inHours == 0;
+ final isExpired = _currentRemainingTime.inSeconds <= 0 && widget.remainingTime!.inSeconds > 0;
+
+ return Positioned(
+ top: kToolbarHeight + 60,
+ right: 16,
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(20),
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 25, sigmaY: 25),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ decoration: BoxDecoration(
+ color: Colors.transparent,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(
+ color: Colors.white.withOpacity(0.2),
+ width: 0.5,
+ ),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ isExpired
+ ? 'Expired'
+ : hours > 0
+ ? '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'
+ : '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}',
+ style: TextStyle(
+ color: isExpired
+ ? const Color(0xFFFF3B30)
+ : Colors.white,
+ fontSize: 15,
+ fontWeight: FontWeight.w600,
+ letterSpacing: 0.5,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+
+
Widget _buildMapFAB() {
return ClipRRect(
borderRadius: BorderRadius.circular(18),
@@ -345,7 +557,7 @@ class _HintCameraWidgetState extends State
return Stack(
children: [
CustomPaint(
- painter: HudPainter(animationValue: _pulseAnimation.value),
+ painter: HudPainter(animationValue: 1.0),
size: Size.infinite,
),
if (!isVisible)
@@ -362,16 +574,13 @@ class _HintCameraWidgetState extends State
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),
- ],
- ),
+ 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),
+ ],
),
),
);
@@ -388,7 +597,7 @@ class _HintCameraWidgetState extends State
bottom: 0,
left: (1 + screenX) * (MediaQuery.of(context).size.width / 2) - 100,
child: AnimatedBuilder(
- animation: Listenable.merge([_pulseAnimation, _rotationController, _scanAnimation]),
+ animation: Listenable.merge([_rotationController, _scanAnimation]),
builder: (context, child) {
return SizedBox(
width: 200,
@@ -397,7 +606,7 @@ class _HintCameraWidgetState extends State
painter: AdvancedTargetPainter(
isNear: _isNearTarget,
distance: distance,
- pulseValue: _pulseAnimation.value,
+ pulseValue: 1.0,
rotationValue: _rotationController.value,
scanValue: _scanAnimation.value,
),
@@ -438,11 +647,11 @@ class _HintCameraWidgetState extends State
),
const SizedBox(height: 12),
Text(
- '${distance.toStringAsFixed(1)}m',
- style: const TextStyle(
+ _formatDistance(distance),
+ style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
- fontSize: 32,
+ fontSize: _getDistanceFontSize(distance),
letterSpacing: 0.5,
fontFamily: 'monospace'
),
@@ -501,10 +710,13 @@ class _PulsingMarkerState extends State
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,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: SvgPicture.asset(
+ widget.isUser ? Assets.icons.gps.path : Assets.icons.locationCross.path,
+ color: Colors.black,
+ width: 10,
+ ),
),
),
),
diff --git a/lib/screens/mains/hunt/widgets/hunt_card_widget.dart b/lib/screens/mains/hunt/widgets/hunt_card_widget.dart
index 6ced90f..2ff3fa2 100644
--- a/lib/screens/mains/hunt/widgets/hunt_card_widget.dart
+++ b/lib/screens/mains/hunt/widgets/hunt_card_widget.dart
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:lba/gen/assets.gen.dart';
import 'package:lba/res/colors.dart';
+import 'package:provider/provider.dart';
import '../models/hunt_card.dart';
+import '../providers/hunt_provider.dart';
import 'hunt_timer_widget.dart';
import 'hint_camera_widget.dart';
import 'dart:math';
@@ -100,6 +102,8 @@ class _HuntCardWidgetState extends State
}
void _openARHint() {
+ final huntState = context.read();
+
Navigator.push(
context,
MaterialPageRoute(
@@ -107,6 +111,8 @@ class _HuntCardWidgetState extends State
targetLatitude: widget.card.hintLatitude,
targetLongitude: widget.card.hintLongitude,
hintDescription: widget.card.hintDescription,
+ remainingTime: huntState.timeRemaining,
+ cardTitle: widget.card.category,
onHintFound: () {
// TODO: Handle what happens when the hint is found.
// For example, show a success message or update state.
diff --git a/lib/screens/mains/hunt/widgets/leaderboard_widget.dart b/lib/screens/mains/hunt/widgets/leaderboard_widget.dart
index 7420160..f4bbc79 100644
--- a/lib/screens/mains/hunt/widgets/leaderboard_widget.dart
+++ b/lib/screens/mains/hunt/widgets/leaderboard_widget.dart
@@ -250,8 +250,8 @@ class _LeaderboardWidgetState extends State
mainAxisSize: MainAxisSize.min,
children: [
Container(
- width: 4,
- height: 4,
+ width: 6,
+ height: 6,
decoration: BoxDecoration(
color: AppColors.confirmButton,
shape: BoxShape.circle,
@@ -592,9 +592,9 @@ class _LeaderboardWidgetState extends State
size: 12,
),
),
- const SizedBox(height: 1),
+ const SizedBox(height: 3),
] else
- const SizedBox(height: 1),
+ const SizedBox(height: 3),
Container(
width: 70,
height: height,