improve hunt hint ui

This commit is contained in:
mohamadmahdi jebeli 2025-09-17 09:58:25 +03:30
parent 2c0133dff7
commit 895ea862b0
9 changed files with 306 additions and 55 deletions

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22.75C6.07 22.75 1.25 17.93 1.25 12C1.25 6.07 6.07 1.25 12 1.25C17.93 1.25 22.75 6.07 22.75 12C22.75 17.93 17.93 22.75 12 22.75ZM12 2.75C6.9 2.75 2.75 6.9 2.75 12C2.75 17.1 6.9 21.25 12 21.25C17.1 21.25 21.25 17.1 21.25 12C21.25 6.9 17.1 2.75 12 2.75Z" fill="#292D32"/>
<path d="M9.17035 15.58C8.98035 15.58 8.79035 15.51 8.64035 15.36C8.35035 15.07 8.35035 14.59 8.64035 14.3L14.3004 8.63999C14.5904 8.34999 15.0704 8.34999 15.3604 8.63999C15.6504 8.92999 15.6504 9.40998 15.3604 9.69998L9.70035 15.36C9.56035 15.51 9.36035 15.58 9.17035 15.58Z" fill="#292D32"/>
<path d="M14.8304 15.58C14.6404 15.58 14.4504 15.51 14.3004 15.36L8.64035 9.69998C8.35035 9.40998 8.35035 8.92999 8.64035 8.63999C8.93035 8.34999 9.41035 8.34999 9.70035 8.63999L15.3604 14.3C15.6504 14.59 15.6504 15.07 15.3604 15.36C15.2104 15.51 15.0204 15.58 14.8304 15.58Z" fill="#292D32"/>
</svg>

After

Width:  |  Height:  |  Size: 974 B

8
assets/icons/gps.svg Normal file
View File

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 20.25C7.45 20.25 3.75 16.55 3.75 12C3.75 7.45 7.45 3.75 12 3.75C16.55 3.75 20.25 7.45 20.25 12C20.25 16.55 16.55 20.25 12 20.25ZM12 5.25C8.28 5.25 5.25 8.28 5.25 12C5.25 15.72 8.28 18.75 12 18.75C15.72 18.75 18.75 15.72 18.75 12C18.75 8.28 15.72 5.25 12 5.25Z" fill="#292D32"/>
<path d="M12 15.75C9.93 15.75 8.25 14.07 8.25 12C8.25 9.93 9.93 8.25 12 8.25C14.07 8.25 15.75 9.93 15.75 12C15.75 14.07 14.07 15.75 12 15.75ZM12 9.75C10.76 9.75 9.75 10.76 9.75 12C9.75 13.24 10.76 14.25 12 14.25C13.24 14.25 14.25 13.24 14.25 12C14.25 10.76 13.24 9.75 12 9.75Z" fill="#292D32"/>
<path d="M12 4.75C11.59 4.75 11.25 4.41 11.25 4V2C11.25 1.59 11.59 1.25 12 1.25C12.41 1.25 12.75 1.59 12.75 2V4C12.75 4.41 12.41 4.75 12 4.75Z" fill="#292D32"/>
<path d="M4 12.75H2C1.59 12.75 1.25 12.41 1.25 12C1.25 11.59 1.59 11.25 2 11.25H4C4.41 11.25 4.75 11.59 4.75 12C4.75 12.41 4.41 12.75 4 12.75Z" fill="#292D32"/>
<path d="M12 22.75C11.59 22.75 11.25 22.41 11.25 22V20C11.25 19.59 11.59 19.25 12 19.25C12.41 19.25 12.75 19.59 12.75 20V22C12.75 22.41 12.41 22.75 12 22.75Z" fill="#292D32"/>
<path d="M22 12.75H20C19.59 12.75 19.25 12.41 19.25 12C19.25 11.59 19.59 11.25 20 11.25H22C22.41 11.25 22.75 11.59 22.75 12C22.75 12.41 22.41 12.75 22 12.75Z" fill="#292D32"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0002 22.76C10.5202 22.76 9.03018 22.2 7.87018 21.09C4.92018 18.25 1.66018 13.72 2.89018 8.33C4.00018 3.44 8.27018 1.25 12.0002 1.25C12.0002 1.25 12.0002 1.25 12.0102 1.25C15.7402 1.25 20.0102 3.44 21.1202 8.34C22.3402 13.73 19.0802 18.25 16.1302 21.09C14.9702 22.2 13.4802 22.76 12.0002 22.76ZM12.0002 2.75C9.09018 2.75 5.35018 4.3 4.36018 8.66C3.28018 13.37 6.24018 17.43 8.92018 20C10.6502 21.67 13.3602 21.67 15.0902 20C17.7602 17.43 20.7202 13.37 19.6602 8.66C18.6602 4.3 14.9102 2.75 12.0002 2.75Z" fill="#292D32"/>
<path d="M14 13.71C13.81 13.71 13.62 13.64 13.47 13.49L9.50998 9.53C9.21998 9.24 9.21998 8.76 9.50998 8.47C9.79998 8.18 10.28 8.18 10.57 8.47L14.53 12.43C14.82 12.72 14.82 13.2 14.53 13.49C14.38 13.63 14.19 13.71 14 13.71Z" fill="#292D32"/>
<path d="M9.99994 13.75C9.80994 13.75 9.61994 13.68 9.46994 13.53C9.17994 13.24 9.17994 12.76 9.46994 12.47L13.4299 8.51001C13.7199 8.22001 14.1999 8.22001 14.4899 8.51001C14.7799 8.80001 14.7799 9.28001 14.4899 9.57001L10.5299 13.53C10.3799 13.68 10.1899 13.75 9.99994 13.75Z" fill="#292D32"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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,

View File

@ -64,6 +64,7 @@ class HuntState extends ChangeNotifier {
void startHunt() {
if (_selectedCard != null) {
_huntStartTime = DateTime.now();
_gameState = HuntGameState.huntingActive;
notifyListeners();
}

View File

@ -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<HintCameraWidget>
MobileScannerController? _controller;
StreamSubscription<CompassEvent>? _compassSubscription;
Timer? _locationTimer;
Timer? _countdownTimer;
final MapController _miniMapController = MapController();
@ -45,10 +52,10 @@ class _HintCameraWidgetState extends State<HintCameraWidget>
bool _isNearTarget = false;
bool _isMapVisible = false;
late AnimationController _pulseController;
Duration _currentRemainingTime = Duration.zero;
late AnimationController _rotationController;
late AnimationController _scanController;
late Animation<double> _pulseAnimation;
late Animation<double> _rotationAnimation;
late Animation<double> _scanAnimation;
@ -61,6 +68,31 @@ class _HintCameraWidgetState extends State<HintCameraWidget>
_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<HintCameraWidget>
}
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<HintCameraWidget>
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),
);
@ -153,6 +179,24 @@ class _HintCameraWidgetState extends State<HintCameraWidget>
}
}
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<HintCameraWidget>
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<HintCameraWidget>
),
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<HintCameraWidget>
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
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,
leading: IconButton(
onPressed: widget.onClose,
icon: const Icon(Icons.close_rounded, color: Colors.white, size: 28),
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),
],
),
title: const Text(
'AR HINT',
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: 18,
fontWeight: FontWeight.bold,
letterSpacing: 2,
fontFamily: 'monospace'),
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,),
),
),
],
),
),
),
),
),
body: Stack(
children: [
@ -273,6 +426,7 @@ class _HintCameraWidgetState extends State<HintCameraWidget>
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<HintCameraWidget>
);
}
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<HintCameraWidget>
return Stack(
children: [
CustomPaint(
painter: HudPainter(animationValue: _pulseAnimation.value),
painter: HudPainter(animationValue: 1.0),
size: Size.infinite,
),
if (!isVisible)
@ -362,8 +574,6 @@ class _HintCameraWidgetState extends State<HintCameraWidget>
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),
@ -373,7 +583,6 @@ class _HintCameraWidgetState extends State<HintCameraWidget>
],
),
),
),
);
}
@ -388,7 +597,7 @@ class _HintCameraWidgetState extends State<HintCameraWidget>
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<HintCameraWidget>
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<HintCameraWidget>
),
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<PulsingMarker>
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,
),
),
),
),

View File

@ -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<HuntCardWidget>
}
void _openARHint() {
final huntState = context.read<HuntState>();
Navigator.push(
context,
MaterialPageRoute(
@ -107,6 +111,8 @@ class _HuntCardWidgetState extends State<HuntCardWidget>
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.

View File

@ -250,8 +250,8 @@ class _LeaderboardWidgetState extends State<LeaderboardWidget>
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<LeaderboardWidget>
size: 12,
),
),
const SizedBox(height: 1),
const SizedBox(height: 3),
] else
const SizedBox(height: 1),
const SizedBox(height: 3),
Container(
width: 70,
height: height,