From 895ea862b04f3f27e4f5576a2bb56d80f9d32ec2 Mon Sep 17 00:00:00 2001 From: mohamadmahdi jebeli Date: Wed, 17 Sep 2025 09:58:25 +0330 Subject: [PATCH] improve hunt hint ui --- assets/icons/close-circle.svg | 5 + assets/icons/gps.svg | 8 + assets/icons/location-cross.svg | 5 + lib/gen/assets.gen.dart | 14 + lib/screens/mains/hunt/hunt_clean.dart | 0 .../mains/hunt/providers/hunt_provider.dart | 1 + .../hunt/widgets/hint_camera_widget.dart | 314 +++++++++++++++--- .../mains/hunt/widgets/hunt_card_widget.dart | 6 + .../hunt/widgets/leaderboard_widget.dart | 8 +- 9 files changed, 306 insertions(+), 55 deletions(-) create mode 100644 assets/icons/close-circle.svg create mode 100644 assets/icons/gps.svg create mode 100644 assets/icons/location-cross.svg delete mode 100644 lib/screens/mains/hunt/hunt_clean.dart 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,