diff --git a/assets/icons/current loc.svg b/assets/icons/current loc.svg new file mode 100644 index 0000000..8c4d6fb --- /dev/null +++ b/assets/icons/current loc.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index c741a12..05059b7 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -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, diff --git a/lib/screens/auth/login_page.dart b/lib/screens/auth/login_page.dart index 33ca373..5734a24 100644 --- a/lib/screens/auth/login_page.dart +++ b/lib/screens/auth/login_page.dart @@ -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 { ); } 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), ); } }, diff --git a/lib/screens/auth/otp_verification_page.dart b/lib/screens/auth/otp_verification_page.dart index ce04f68..f15e598 100644 --- a/lib/screens/auth/otp_verification_page.dart +++ b/lib/screens/auth/otp_verification_page.dart @@ -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 { ); } 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 { 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', ); } }, diff --git a/lib/screens/mains/discover/discover.dart b/lib/screens/mains/discover/discover.dart index b0bd958..2e96363 100644 --- a/lib/screens/mains/discover/discover.dart +++ b/lib/screens/mains/discover/discover.dart @@ -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, diff --git a/lib/screens/mains/nearby/location_radius_screen.dart b/lib/screens/mains/nearby/location_radius_screen.dart new file mode 100644 index 0000000..1f373d4 --- /dev/null +++ b/lib/screens/mains/nearby/location_radius_screen.dart @@ -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 createState() => _LocationRadiusScreenState(); +} + +class _LocationRadiusScreenState extends State + 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> _animations; + + final List> _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(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 _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( + 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(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( + 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; +} \ No newline at end of file diff --git a/lib/screens/mains/nearby/mainNearby/map.dart b/lib/screens/mains/nearby/mainNearby/map.dart index 23bc76b..84472ff 100644 --- a/lib/screens/mains/nearby/mainNearby/map.dart +++ b/lib/screens/mains/nearby/mainNearby/map.dart @@ -369,9 +369,12 @@ class _CustomMapState extends State ), 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), ], diff --git a/lib/screens/mains/nearby/mainNearby/nearby.dart b/lib/screens/mains/nearby/mainNearby/nearby.dart index ed93583..95bb004 100644 --- a/lib/screens/mains/nearby/mainNearby/nearby.dart +++ b/lib/screens/mains/nearby/mainNearby/nearby.dart @@ -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 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 with TickerProviderStateMixin { Container( width: 350, height: 3, - color: AppColors.selectedTextNearby, + color: AppColors.isDarkMode + ? AppColors.selectedTextNearby + : AppColors.selectedBGButton, ), Expanded( child: AnimatedSwitcher( diff --git a/lib/screens/product/map_selection_screen.dart b/lib/screens/product/map_selection_screen.dart index 785fc35..973155c 100644 --- a/lib/screens/product/map_selection_screen.dart +++ b/lib/screens/product/map_selection_screen.dart @@ -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 { 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 { 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 { ), 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', + }, ), ], ), diff --git a/lib/screens/qr_scanner/qr_scanner_page.dart b/lib/screens/qr_scanner/qr_scanner_page.dart index 3bf4e7a..32b2374 100644 --- a/lib/screens/qr_scanner/qr_scanner_page.dart +++ b/lib/screens/qr_scanner/qr_scanner_page.dart @@ -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 { _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', ); } } diff --git a/lib/utils/map_tile_providers.dart b/lib/utils/map_tile_providers.dart new file mode 100644 index 0000000..92c8f37 --- /dev/null +++ b/lib/utils/map_tile_providers.dart @@ -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> _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> 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(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; +} diff --git a/lib/widgets/app_snackbar.dart b/lib/widgets/app_snackbar.dart new file mode 100644 index 0000000..46aa78a --- /dev/null +++ b/lib/widgets/app_snackbar.dart @@ -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; + } + } +} diff --git a/lib/widgets/conversion_summary.dart b/lib/widgets/conversion_summary.dart new file mode 100644 index 0000000..18ecbdf --- /dev/null +++ b/lib/widgets/conversion_summary.dart @@ -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 جدید: +✅ طراحی زیبا و مدرن +✅ رنگ‌بندی مناسب برای هر نوع پیام +✅ انیمیشن‌های روان +✅ قابلیت افزودن دکمه عمل +✅ تنظیم زمان نمایش +✅ یکدست سازی تجربه کاربری +✅ کاهش کد تکراری +✅ نگهداری آسان‌تر +*/ diff --git a/lib/widgets/snackbar_usage_examples.dart b/lib/widgets/snackbar_usage_examples.dart new file mode 100644 index 0000000..6afd7d0 --- /dev/null +++ b/lib/widgets/snackbar_usage_examples.dart @@ -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: 'نسخه جدید موجود است'); +*/