proxybuy-flutter/lib/screens/mains/nearby/location_radius_screen.dart

641 lines
21 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:location/location.dart';
import 'package:latlong2/latlong.dart';
import 'package:lba/res/colors.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:lba/gen/assets.gen.dart';
import 'package:lba/widgets/app_snackbar.dart';
import 'dart:math' as math;
class LocationRadiusScreen extends StatefulWidget {
const LocationRadiusScreen({super.key});
@override
State<LocationRadiusScreen> createState() => _LocationRadiusScreenState();
}
class _LocationRadiusScreenState extends State<LocationRadiusScreen>
with TickerProviderStateMixin {
final MapController _mapController = MapController();
LatLng _currentCenter = const LatLng(25.1972, 55.2744); // Default: Dubai
double _radiusInKm = 1.0;
bool _isCustomRadius = false;
bool _isLoadingLocation = true;
bool _mapLoadError = false;
int _currentTileProvider = 0;
Location location = Location();
late bool _serviceEnabled;
late PermissionStatus _permissionGranted;
late LocationData _locationData;
late AnimationController _animationController;
late List<Animation<double>> _animations;
final List<Map<String, dynamic>> _tileProviders = [
{
'name': 'Stamen Terrain',
'lightUrl':
'https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}{r}.png',
'darkUrl':
'https://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}{r}.png',
'subdomains': ['a', 'b', 'c', 'd'],
},
{
'name': 'CartoDB',
'lightUrl':
'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
'darkUrl':
'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
'subdomains': ['a', 'b', 'c', 'd'],
},
{
'name': 'Google Maps',
'lightUrl': 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}',
'darkUrl': 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}',
'subdomains': ['mt0', 'mt1', 'mt2', 'mt3'],
},
];
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
);
_animations = List.generate(6, (index) {
final double startTime = (index / 6) * 0.5;
final double endTime = (startTime + 0.6).clamp(0.0, 1.0);
return Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(startTime, endTime, curve: Curves.easeOutCubic),
),
);
});
_currentTileProvider =
_currentTileProvider.clamp(0, _tileProviders.length - 1);
_determinePosition();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Future<void> _determinePosition() async {
setState(() => _isLoadingLocation = true);
try {
_serviceEnabled = await location.serviceEnabled();
if (!_serviceEnabled) {
_serviceEnabled = await location.requestService();
if (!_serviceEnabled) {
_showLocationMessage(
'Location services are disabled. Please enable it in settings.',
type: SnackBarType.warning);
setState(() => _isLoadingLocation = false);
_animationController.forward();
return;
}
}
_permissionGranted = await location.hasPermission();
if (_permissionGranted == PermissionStatus.denied) {
_permissionGranted = await location.requestPermission();
if (_permissionGranted != PermissionStatus.granted) {
_showLocationMessage('Location permissions are denied.',
type: SnackBarType.error);
setState(() => _isLoadingLocation = false);
_animationController.forward();
return;
}
}
_locationData = await location.getLocation();
setState(() {
_currentCenter =
LatLng(_locationData.latitude!, _locationData.longitude!);
_isLoadingLocation = false;
});
_showLocationMessage('Location found successfully!',
type: SnackBarType.success);
_mapController.move(_currentCenter, 14.0);
} catch (e) {
_showLocationMessage('Unable to get your location. Using default location.',
type: SnackBarType.info);
setState(() {
_isLoadingLocation = false;
});
} finally {
if (mounted) {
_animationController.forward();
}
}
}
void _showLocationMessage(String message,
{SnackBarType type = SnackBarType.error}) {
if (mounted) {
AppSnackBar.show(
context: context,
message: message,
type: type,
duration: const Duration(seconds: 3),
);
}
}
Widget _buildAnimatedWidget(Widget child, int index) {
return FadeTransition(
opacity: _animations[index],
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 0.5),
end: Offset.zero,
).animate(_animations[index]),
child: child,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.scaffoldBackground,
appBar: AppBar(
title: Text(
"Choose a location to see what's available",
style: TextStyle(
color: AppColors.textPrimary,
fontWeight: FontWeight.normal,
fontSize: 15),
),
backgroundColor: AppColors.surface,
elevation: 0,
leading: IconButton(
icon: SvgPicture.asset(
Assets.icons.arrowLeft.path,
color: AppColors.textPrimary,
),
onPressed: () => Navigator.of(context).pop(),
),
),
body: Stack(
children: [
_mapLoadError
? _buildMapErrorFallback()
: Container(
padding: const EdgeInsets.only(bottom: 240),
child: FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: _currentCenter,
initialZoom: 14.0,
onPositionChanged: (position, hasGesture) {
if (hasGesture) {
setState(() {
_currentCenter = position.center;
});
}
},
),
children: [
TileLayer(
urlTemplate:
'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
userAgentPackageName: 'com.example.lba',
subdomains: const ['a', 'b', 'c', 'd'],
additionalOptions: const {
'attribution':
'© CARTO © OpenStreetMap contributors',
},
),
CircleLayer(
circles: [
CircleMarker(
point: _currentCenter,
radius: _radiusInKm * 1000,
useRadiusInMeter: true,
color: _isCustomRadius
? AppColors.primary.withOpacity(0.15)
: AppColors.offerTimer.withOpacity(0.15),
borderColor: Colors.transparent,
borderStrokeWidth: 0,
),
],
),
MarkerLayer(
markers: [
Marker(
width: _calculateCirclePixelSize(),
height: _calculateCirclePixelSize(),
point: _currentCenter,
child: CustomPaint(
painter: DashedCirclePainter(
color: _isCustomRadius
? AppColors.primary
: AppColors.offerTimer,
strokeWidth: 3.0,
dashLength: 9.0,
gapLength: 5.0,
),
),
),
],
),
],
),
),
if (_isLoadingLocation)
Container(
color: AppColors.scaffoldBackground.withOpacity(0.8),
child: Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primary),
),
),
),
_buildControlsPanel(),
],
),
);
}
Widget _buildMapErrorFallback() {
return Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: AppColors.cardBackground,
border: Border.all(color: AppColors.divider),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(height: 40),
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color:
_isCustomRadius ? AppColors.primary : AppColors.offerTimer,
width: 4,
),
color: (_isCustomRadius
? AppColors.primary
: AppColors.offerTimer)
.withOpacity(0.08),
boxShadow: [
BoxShadow(
color: AppColors.shadowColor,
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: _isCustomRadius
? AppColors.primary
: AppColors.offerTimer,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppColors.shadowColor,
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Icon(
Icons.location_on,
color: AppColors.surface,
size: 24,
),
),
const SizedBox(height: 8),
Text(
'${_radiusInKm.toStringAsFixed(1)} km',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
Text(
'radius',
style: TextStyle(
fontSize: 10,
color: AppColors.textSecondary,
),
),
],
),
),
),
const Spacer(),
],
),
);
}
Widget _buildControlsPanel() {
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
)),
child: Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
boxShadow: [
BoxShadow(
color: AppColors.shadowColor,
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildAnimatedWidget(
Text(
"Preset Radius Cards",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: AppColors.nearbyPopuphint,
),
),
0,
),
const SizedBox(height: 8),
if (_mapLoadError)
_buildAnimatedWidget(
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.errorColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.errorColor.withOpacity(0.3),
),
),
child: Row(
children: [
Icon(
Icons.warning_amber_rounded,
color: AppColors.errorColor,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Map tiles blocked. Using fallback display.',
style: TextStyle(
color: AppColors.errorColor,
fontSize: 12,
),
),
),
],
),
),
1,
),
const SizedBox(height: 12),
_buildAnimatedWidget(
Row(
children: [
_buildRadiusToggleChip(
"Nearby (1 km)", !_isCustomRadius, () {
setState(() {
_isCustomRadius = false;
_radiusInKm = 1.0;
});
}),
const SizedBox(width: 8),
_buildRadiusToggleChip("Custom Radius", _isCustomRadius,
() {
setState(() {
_isCustomRadius = true;
if (_radiusInKm == 1.0) _radiusInKm = 5.0;
});
}),
],
),
2,
),
const SizedBox(height: 16),
_buildAnimatedWidget(
Row(
children: [
Expanded(
child: Slider(
value: _radiusInKm,
min: 1.0,
max: 25.0,
divisions: 24,
activeColor: _isCustomRadius
? AppColors.primary
: AppColors.divider,
inactiveColor: AppColors.divider.withOpacity(0.5),
onChanged: _isCustomRadius
? (value) {
setState(() {
_radiusInKm = value;
});
}
: null,
),
),
const SizedBox(width: 16),
Text(
"${_radiusInKm.toStringAsFixed(1)} km",
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
],
),
3,
),
_buildAnimatedWidget(
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton.icon(
onPressed: _determinePosition,
icon: SvgPicture.asset(
Assets.icons.currentLoc.path,
color: AppColors.primary,
),
label: Text(
"Use my current location",
style: TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold),
),
),
],
),
),
4,
),
const SizedBox(height: 8),
_buildAnimatedWidget(
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.confirmButton,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(27),
),
),
child: Text(
'Apply',
style: TextStyle(
color: AppColors.surface,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
),
5,
),
const SizedBox(
height: 30,
)
],
),
),
),
);
}
Widget _buildRadiusToggleChip(
String label, bool isSelected, VoidCallback onTap) {
return ChoiceChip(
label: Text(label),
selected: isSelected,
onSelected: (selected) {
if (selected) onTap();
},
selectedColor: isSelected && !_isCustomRadius
? AppColors.primary
: AppColors.primary,
backgroundColor: AppColors.cardBackground,
labelStyle: TextStyle(
color: isSelected ? AppColors.nearbyPopup : AppColors.textSecondary,
fontWeight: FontWeight.bold,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: isSelected ? Colors.transparent : AppColors.divider,
),
),
showCheckmark: false,
);
}
double _calculateCirclePixelSize() {
try {
final double currentZoom = _mapController.camera.zoom;
final double pixelsPerMeter = 256 *
math.pow(2, currentZoom) /
(2 *
math.pi *
6378137 *
math.cos(_currentCenter.latitude * math.pi / 180));
final double radiusInMeters = _radiusInKm * 1000;
final double radiusInPixels = radiusInMeters * pixelsPerMeter;
return radiusInPixels * 2;
} catch (e) {
const double pixelsPerKmAtZoom14 = 100.0;
return _radiusInKm * pixelsPerKmAtZoom14 * 2;
}
}
}
class DashedCirclePainter extends CustomPainter {
final Color color;
final double strokeWidth;
final double dashLength;
final double gapLength;
DashedCirclePainter({
required this.color,
required this.strokeWidth,
required this.dashLength,
required this.gapLength,
});
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke;
final double radius = (size.width - strokeWidth) / 2;
final Offset center = Offset(size.width / 2, size.height / 2);
final double circumference = 2 * math.pi * radius;
final double totalDashLength = dashLength + gapLength;
final int dashCount = (circumference / totalDashLength).floor();
for (int i = 0; i < dashCount; i++) {
final double startAngle = (i * totalDashLength / radius);
final double sweepAngle = dashLength / radius;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
paint,
);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}