added location radius Screen

This commit is contained in:
mohamadmahdi jebeli 2025-08-26 16:18:41 +03:30
parent 9b944369fb
commit 049e037933
14 changed files with 1234 additions and 45 deletions

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.30624 9.92045L16.9662 4.36845C18.6662 3.55845 20.4402 5.33345 19.6312 7.03445L14.0792 18.6934C13.3202 20.2864 11.0202 20.1884 10.4002 18.5354L9.37424 15.7964C9.27399 15.5292 9.11771 15.2866 8.91591 15.0848C8.7141 14.883 8.47145 14.7267 8.20424 14.6264L5.46424 13.5994C3.81224 12.9794 3.71324 10.6794 5.30624 9.92045Z" stroke="#2196F3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 518 B

View File

@ -135,6 +135,10 @@ class $AssetsIconsGen {
/// File path: assets/icons/coin.svg
SvgGenImage get coin => const SvgGenImage('assets/icons/coin.svg');
/// File path: assets/icons/current loc.svg
SvgGenImage get currentLoc =>
const SvgGenImage('assets/icons/current loc.svg');
/// File path: assets/icons/delivery off.svg
SvgGenImage get deliveryOff =>
const SvgGenImage('assets/icons/delivery off.svg');
@ -463,6 +467,7 @@ class $AssetsIconsGen {
clander,
clock,
coin,
currentLoc,
deliveryOff,
deliveryOn,
dislike,

View File

@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:lba/screens/auth/bloc/auth_bloc.dart';
import 'package:lba/screens/auth/user_info_page.dart';
import 'package:lba/widgets/app_snackbar.dart';
import '../../extension/screenSize.dart';
import '../../gen/assets.gen.dart';
import '../../res/colors.dart';
@ -185,12 +186,10 @@ class _LoginPageState extends State<LoginPage> {
);
}
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 2),
backgroundColor: AppColors.errorColor,
content: Text(state.message),
),
AppSnackBar.showError(
context: context,
message: state.message,
duration: const Duration(seconds: 2),
);
}
},

View File

@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:lba/res/colors.dart';
import 'package:lba/screens/auth/bloc/auth_bloc.dart';
import 'package:lba/widgets/app_snackbar.dart';
import '../../gen/assets.gen.dart';
import '../../widgets/button.dart';
import '../../widgets/remainingTime.dart';
@ -155,12 +156,10 @@ class _OTPVerificationPageState extends State<OTPVerificationPage> {
);
}
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 2),
backgroundColor: AppColors.errorColor,
content: Text(state.message),
),
AppSnackBar.showError(
context: context,
message: state.message,
duration: const Duration(seconds: 2),
);
}
},
@ -183,12 +182,9 @@ class _OTPVerificationPageState extends State<OTPVerificationPage> {
phoneNumber: widget.phoneNumber,
));
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: AppColors.errorColor,
content: Text(
'Please enter the complete OTP code'),
),
AppSnackBar.showWarning(
context: context,
message: 'Please enter the complete OTP code',
);
}
},

View File

@ -826,15 +826,6 @@ class FlashSaleCard extends StatelessWidget {
decoration: BoxDecoration(
color: AppColors.cardBackground,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.divider),
boxShadow: [
BoxShadow(
color: AppColors.shadowColor.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -0,0 +1,641 @@
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;
}

View File

@ -369,9 +369,12 @@ class _CustomMapState extends State<CustomMap>
),
children: [
TileLayer(
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
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',
},
),
MarkerLayer(markers: _markers),
],

View File

@ -3,6 +3,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:lba/gen/assets.gen.dart';
import 'package:lba/screens/mains/nearby/location_radius_screen.dart';
import 'package:lba/screens/mains/nearby/mainNearby/listScreen.dart';
import 'package:lba/screens/mains/nearby/mainNearby/map.dart';
import 'package:lba/screens/product/productdetail.dart';
@ -131,7 +132,14 @@ class _NearbyState extends State<Nearby> with TickerProviderStateMixin {
const SizedBox(width: 8),
_buildIconButton(
icon: Assets.icons.location.path,
onPressed: () {},
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LocationRadiusScreen(),
),
);
},
),
],
),
@ -146,7 +154,9 @@ class _NearbyState extends State<Nearby> with TickerProviderStateMixin {
Container(
width: 350,
height: 3,
color: AppColors.selectedTextNearby,
color: AppColors.isDarkMode
? AppColors.selectedTextNearby
: AppColors.selectedBGButton,
),
Expanded(
child: AnimatedSwitcher(

View File

@ -4,6 +4,7 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
import 'package:lba/res/colors.dart';
import 'package:lba/widgets/app_snackbar.dart';
class MapSelectionScreen extends StatefulWidget {
const MapSelectionScreen({super.key});
@ -51,9 +52,12 @@ class _MapSelectionScreenState extends State<MapSelectionScreen> {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Location services are disabled. Please enable them in settings.')));
await Geolocator.openLocationSettings();
AppSnackBar.showWarning(
context: context,
message: 'Location services are disabled. Please enable them in settings.',
actionLabel: 'Settings',
onActionPressed: () => Geolocator.openLocationSettings(),
);
setState(() => _isLoading = false);
return;
}
@ -62,17 +66,21 @@ class _MapSelectionScreenState extends State<MapSelectionScreen> {
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Location permissions are denied.')));
AppSnackBar.showError(
context: context,
message: 'Location permissions are denied.',
);
setState(() => _isLoading = false);
return;
}
}
if (permission == LocationPermission.deniedForever) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text(
'Location permissions are permanently denied, we cannot request permissions.')));
AppSnackBar.showError(
context: context,
message: 'Location permissions are permanently denied, we cannot request permissions.',
duration: const Duration(seconds: 5),
);
setState(() => _isLoading = false);
return;
}
@ -116,8 +124,12 @@ class _MapSelectionScreenState extends State<MapSelectionScreen> {
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
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',
},
),
],
),

View File

@ -3,6 +3,7 @@ import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:lba/res/colors.dart';
import 'package:lba/widgets/app_snackbar.dart';
class QRScannerPage extends StatefulWidget {
const QRScannerPage({super.key});
@ -102,11 +103,9 @@ class _QRScannerPageState extends State<QRScannerPage> {
_handleScanResult('Image selected:\nFile: $fileName\nSize: ${fileSizeKB}KB\nPath: ${image.path}');
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error selecting image: $e'),
backgroundColor: AppColors.errorColor,
),
AppSnackBar.showError(
context: context,
message: 'Error selecting image: $e',
);
}
}

View File

@ -0,0 +1,74 @@
import 'package:flutter_map/flutter_map.dart';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:lba/utils/theme_manager.dart';
class MapTileProviders {
static const 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'],
},
];
static List<Map<String, dynamic>> get providers => _tileProviders;
static TileLayer createTileLayer({
required BuildContext context,
int currentProvider = 0,
Function(int)? onProviderChange,
void Function(Object, dynamic, dynamic)? errorCallback,
}) {
final themeManager = Provider.of<ThemeManager>(context, listen: false);
final isDarkMode = themeManager.isDarkMode;
final safeProviderIndex = currentProvider.clamp(0, _tileProviders.length - 1);
return TileLayer(
urlTemplate: isDarkMode
? _tileProviders[safeProviderIndex]['darkUrl']
: _tileProviders[safeProviderIndex]['lightUrl'],
userAgentPackageName: 'com.example.lba',
subdomains: _tileProviders[safeProviderIndex]['subdomains'],
additionalOptions: const {
'attribution': '© Map contributors',
},
errorTileCallback: errorCallback ?? _defaultErrorCallback(onProviderChange),
);
}
static void Function(Object, dynamic, dynamic) _defaultErrorCallback(Function(int)? onProviderChange) {
bool _isSwitching = false;
return (tile, error, stackTrace) {
if (!_isSwitching && onProviderChange != null) {
_isSwitching = true;
Future.delayed(const Duration(milliseconds: 500), () {
onProviderChange(1);
_isSwitching = false;
});
}
};
}
static String getProviderName(int index) {
final safeIndex = index.clamp(0, _tileProviders.length - 1);
return _tileProviders[safeIndex]['name'];
}
static int get providerCount => _tileProviders.length;
}

View File

@ -0,0 +1,262 @@
import 'package:flutter/material.dart';
enum SnackBarType {
success,
error,
warning,
info,
}
class AppSnackBar {
static void show({
required BuildContext context,
required String message,
SnackBarType type = SnackBarType.info,
Duration duration = const Duration(seconds: 3),
String? actionLabel,
VoidCallback? onActionPressed,
}) {
// Remove any existing snackbars
ScaffoldMessenger.of(context).hideCurrentSnackBar();
final snackBar = SnackBar(
content: _SnackBarContent(
message: message,
type: type,
),
backgroundColor: Colors.transparent,
elevation: 0,
duration: duration,
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
padding: EdgeInsets.zero,
action: actionLabel != null
? SnackBarAction(
label: actionLabel,
textColor: _getActionColor(type),
onPressed: onActionPressed ?? () {},
)
: null,
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
static void showSuccess({
required BuildContext context,
required String message,
Duration duration = const Duration(seconds: 3),
String? actionLabel,
VoidCallback? onActionPressed,
}) {
show(
context: context,
message: message,
type: SnackBarType.success,
duration: duration,
actionLabel: actionLabel,
onActionPressed: onActionPressed,
);
}
static void showError({
required BuildContext context,
required String message,
Duration duration = const Duration(seconds: 4),
String? actionLabel,
VoidCallback? onActionPressed,
}) {
show(
context: context,
message: message,
type: SnackBarType.error,
duration: duration,
actionLabel: actionLabel,
onActionPressed: onActionPressed,
);
}
static void showWarning({
required BuildContext context,
required String message,
Duration duration = const Duration(seconds: 3),
String? actionLabel,
VoidCallback? onActionPressed,
}) {
show(
context: context,
message: message,
type: SnackBarType.warning,
duration: duration,
actionLabel: actionLabel,
onActionPressed: onActionPressed,
);
}
static void showInfo({
required BuildContext context,
required String message,
Duration duration = const Duration(seconds: 3),
String? actionLabel,
VoidCallback? onActionPressed,
}) {
show(
context: context,
message: message,
type: SnackBarType.info,
duration: duration,
actionLabel: actionLabel,
onActionPressed: onActionPressed,
);
}
static Color _getActionColor(SnackBarType type) {
switch (type) {
case SnackBarType.success:
return Colors.green.shade100;
case SnackBarType.error:
return Colors.red.shade100;
case SnackBarType.warning:
return Colors.orange.shade100;
case SnackBarType.info:
return Colors.blue.shade100;
}
}
}
class _SnackBarContent extends StatelessWidget {
final String message;
final SnackBarType type;
const _SnackBarContent({
required this.message,
required this.type,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: _getBackgroundColor(),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
border: Border.all(
color: _getBorderColor(),
width: 1,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _getIconBackgroundColor(),
shape: BoxShape.circle,
),
child: Icon(
_getIcon(),
color: _getIconColor(),
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
message,
style: TextStyle(
color: _getTextColor(),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
Color _getBackgroundColor() {
switch (type) {
case SnackBarType.success:
return Colors.green.shade50;
case SnackBarType.error:
return Colors.red.shade50;
case SnackBarType.warning:
return Colors.orange.shade50;
case SnackBarType.info:
return Colors.blue.shade50;
}
}
Color _getBorderColor() {
switch (type) {
case SnackBarType.success:
return Colors.green.shade200;
case SnackBarType.error:
return Colors.red.shade200;
case SnackBarType.warning:
return Colors.orange.shade200;
case SnackBarType.info:
return Colors.blue.shade200;
}
}
Color _getIconBackgroundColor() {
switch (type) {
case SnackBarType.success:
return Colors.green.shade100;
case SnackBarType.error:
return Colors.red.shade100;
case SnackBarType.warning:
return Colors.orange.shade100;
case SnackBarType.info:
return Colors.blue.shade100;
}
}
Color _getIconColor() {
switch (type) {
case SnackBarType.success:
return Colors.green.shade700;
case SnackBarType.error:
return Colors.red.shade700;
case SnackBarType.warning:
return Colors.orange.shade700;
case SnackBarType.info:
return Colors.blue.shade700;
}
}
Color _getTextColor() {
switch (type) {
case SnackBarType.success:
return Colors.green.shade800;
case SnackBarType.error:
return Colors.red.shade800;
case SnackBarType.warning:
return Colors.orange.shade800;
case SnackBarType.info:
return Colors.blue.shade800;
}
}
IconData _getIcon() {
switch (type) {
case SnackBarType.success:
return Icons.check_circle;
case SnackBarType.error:
return Icons.error;
case SnackBarType.warning:
return Icons.warning;
case SnackBarType.info:
return Icons.info;
}
}
}

View File

@ -0,0 +1,122 @@
// مثال عملی استفاده از AppSnackBar در فایلهای مختلف اپ
// Practical examples of AppSnackBar usage in different app files
import 'package:flutter/material.dart';
import 'package:lba/widgets/app_snackbar.dart';
import 'package:geolocator/geolocator.dart';
/*
تبدیل موفق تمام SnackBarهای قدیمی به AppSnackBar جدید
Successfully converted all old SnackBars to new AppSnackBar
📂 فایلهای تبدیل شده / Converted Files:
1. 📍 lib/screens/mains/nearby/location_radius_screen.dart
- پیام موفقیت: "Location found successfully!"
- پیام هشدار: "Location services disabled"
- پیام خطا: "Location permissions denied"
- پیام اطلاعاتی: "Using default location"
2. 📍 lib/screens/product/map_selection_screen.dart
- پیام هشدار: "Location services disabled" + دکمه Settings
- پیام خطا: "Location permissions denied"
- پیام خطا: "Permanently denied permissions"
3. 📍 lib/screens/qr_scanner/qr_scanner_page.dart
- پیام خطا: "Error selecting image"
4. 📍 lib/screens/auth/otp_verification_page.dart
- پیام خطا: Authentication errors
- پیام هشدار: "Please enter complete OTP"
5. 📍 lib/screens/auth/login_page.dart
- پیام خطا: Authentication errors
🎨 ویژگیهای جدید AppSnackBar:
*/
// کد قدیمی (Old Code):
/*
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: AppColors.errorColor,
duration: Duration(seconds: 3),
),
);
*/
// کد جدید (New Code):
/*
AppSnackBar.showError(
context: context,
message: message,
duration: Duration(seconds: 3),
);
// یا با دکمه عمل
AppSnackBar.showWarning(
context: context,
message: 'Location services disabled',
actionLabel: 'Settings',
onActionPressed: () => Geolocator.openLocationSettings(),
);
*/
// 🎯 نحوه استفاده در صفحات جدید:
class ExampleUsage {
static void showLocationSuccess(context) {
AppSnackBar.showSuccess(
context: context,
message: 'Successful',
);
}
static void showConnectionError(context) {
AppSnackBar.showError(
context: context,
message: 'Connection to the internet is not established',
actionLabel: 'Retry',
onActionPressed: () {
// Retry operation
},
);
}
static void showFileUploadInfo(context) {
AppSnackBar.showInfo(
context: context,
message: 'File is uploading...',
duration: Duration(seconds: 5),
);
}
static void showDeleteWarning(context) {
AppSnackBar.showWarning(
context: context,
message: 'This action is irreversible',
actionLabel: 'Cancel',
onActionPressed: () {
// Cancel operation
},
);
}
}
/*
📊 آمار تبدیل:
- تعداد فایلهای بررسی شده: 5+
- تعداد SnackBar های تبدیل شده: 7+
- نوع پیامهای پیادهسازی شده: 4 نوع (Success, Error, Warning, Info)
- ویژگیهای اضافه شده: Action Button, Custom Duration, Beautiful Design
🚀 مزایای AppSnackBar جدید:
طراحی زیبا و مدرن
رنگبندی مناسب برای هر نوع پیام
انیمیشنهای روان
قابلیت افزودن دکمه عمل
تنظیم زمان نمایش
یکدست سازی تجربه کاربری
کاهش کد تکراری
نگهداری آسانتر
*/

View File

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:lba/widgets/app_snackbar.dart';
class SnackBarUsageExamples {
// پیام موفقیت
static void showSuccessExample(BuildContext context) {
AppSnackBar.showSuccess(
context: context,
message: 'عملیات با موفقیت انجام شد!',
);
}
// پیام خطا
static void showErrorExample(BuildContext context) {
AppSnackBar.showError(
context: context,
message: 'خطایی رخ داده است. لطفاً دوباره تلاش کنید.',
actionLabel: 'تلاش مجدد',
onActionPressed: () {
// عملیات تلاش مجدد
},
);
}
// پیام هشدار
static void showWarningExample(BuildContext context) {
AppSnackBar.showWarning(
context: context,
message: 'توجه: این عمل قابل بازگشت نیست.',
duration: const Duration(seconds: 5),
);
}
// پیام اطلاعاتی
static void showInfoExample(BuildContext context) {
AppSnackBar.showInfo(
context: context,
message: 'اطلاعات جدید دریافت شد.',
);
}
// استفاده عمومی با انتخاب نوع
static void showCustomExample(BuildContext context) {
AppSnackBar.show(
context: context,
message: 'پیام سفارشی',
type: SnackBarType.info,
duration: const Duration(seconds: 4),
actionLabel: 'بستن',
onActionPressed: () {
// عملیات بستن
},
);
}
}
/*
نحوه استفاده در صفحات مختلف:
1. برای نمایش پیام موفقیت:
AppSnackBar.showSuccess(context: context, message: 'با موفقیت ذخیره شد');
2. برای نمایش پیام خطا:
AppSnackBar.showError(context: context, message: 'اتصال به اینترنت برقرار نیست');
3. برای نمایش پیام هشدار:
AppSnackBar.showWarning(context: context, message: 'فایل حذف خواهد شد');
4. برای نمایش پیام اطلاعاتی:
AppSnackBar.showInfo(context: context, message: 'نسخه جدید موجود است');
*/