diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index fda3c0e..d6fd36a 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -35,6 +35,9 @@ android { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig = signingConfigs.getByName("debug") + isMinifyEnabled = false + isShrinkResources = false + // proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..29ffa97 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,42 @@ +# Flutter related +-keep class io.flutter.app.** { *; } +-keep class io.flutter.plugin.** { *; } +-keep class io.flutter.util.** { *; } +-keep class io.flutter.view.** { *; } +-keep class io.flutter.** { *; } +-keep class io.flutter.plugins.** { *; } +-keep class io.flutter.embedding.** { *; } + +# Mobile Scanner +-keep class com.google.mlkit.** { *; } +-keep class com.google.android.gms.** { *; } + +# Geolocator +-keep class com.baseflow.geolocator.** { *; } + +# Image Picker +-keep class io.flutter.plugins.imagepicker.** { *; } + +# Permission Handler +-keep class com.baseflow.permissionhandler.** { *; } + +# Keep native methods +-keepclasseswithmembernames class * { + native ; +} + +# Keep Parcelable implementations +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +# General warnings +-dontwarn com.google.android.gms.** +-dontwarn com.google.mlkit.** +-dontwarn java.lang.invoke.** + +# Keep annotations +-keepattributes *Annotation* +-keepattributes Signature +-keepattributes InnerClasses +-keepattributes EnclosingMethod diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 477529a..91953d5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,11 @@ + + + + + + + diff --git a/assets/icons/girl_clothes.svg b/assets/icons/girl_clothes.svg new file mode 100644 index 0000000..09871fc --- /dev/null +++ b/assets/icons/girl_clothes.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/healthicons_fruits-outline.svg b/assets/icons/healthicons_fruits-outline.svg new file mode 100644 index 0000000..4c18e23 --- /dev/null +++ b/assets/icons/healthicons_fruits-outline.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/hugeicons_baby-boy-dress.svg b/assets/icons/hugeicons_baby-boy-dress.svg new file mode 100644 index 0000000..8295abe --- /dev/null +++ b/assets/icons/hugeicons_baby-boy-dress.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/hugeicons_cheese-cake-01.svg b/assets/icons/hugeicons_cheese-cake-01.svg new file mode 100644 index 0000000..809ce8d --- /dev/null +++ b/assets/icons/hugeicons_cheese-cake-01.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ion_fast-food-outline.svg b/assets/icons/ion_fast-food-outline.svg new file mode 100644 index 0000000..40307a9 --- /dev/null +++ b/assets/icons/ion_fast-food-outline.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/ph_qr-code.svg b/assets/icons/ph_qr-code.svg new file mode 100644 index 0000000..7c90ff3 --- /dev/null +++ b/assets/icons/ph_qr-code.svg @@ -0,0 +1,3 @@ + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 9e2a5eb..0dcf69e 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -45,5 +45,9 @@ UIApplicationSupportsIndirectInputEvents + NSCameraUsageDescription + This app needs access to camera to scan QR codes + NSPhotoLibraryUsageDescription + This app needs access to photo library to select QR code images diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 35dc0a5..be8707e 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -127,9 +127,29 @@ class $AssetsIconsGen { /// File path: assets/icons/game 2.svg SvgGenImage get game2 => const SvgGenImage('assets/icons/game 2.svg'); + /// File path: assets/icons/game-icons_winter-gloves.svg + SvgGenImage get gameIconsWinterGloves => + const SvgGenImage('assets/icons/game-icons_winter-gloves.svg'); + /// File path: assets/icons/game.svg SvgGenImage get game => const SvgGenImage('assets/icons/game.svg'); + /// File path: assets/icons/girl_clothes.svg + SvgGenImage get girlClothes => + const SvgGenImage('assets/icons/girl_clothes.svg'); + + /// File path: assets/icons/healthicons_fruits-outline.svg + SvgGenImage get healthiconsFruitsOutline => + const SvgGenImage('assets/icons/healthicons_fruits-outline.svg'); + + /// File path: assets/icons/hugeicons_baby-boy-dress.svg + SvgGenImage get hugeiconsBabyBoyDress => + const SvgGenImage('assets/icons/hugeicons_baby-boy-dress.svg'); + + /// File path: assets/icons/hugeicons_cheese-cake-01.svg + SvgGenImage get hugeiconsCheeseCake01 => + const SvgGenImage('assets/icons/hugeicons_cheese-cake-01.svg'); + /// File path: assets/icons/ic_round-local-offer.svg SvgGenImage get icRoundLocalOffer => const SvgGenImage('assets/icons/ic_round-local-offer.svg'); @@ -137,6 +157,10 @@ class $AssetsIconsGen { /// File path: assets/icons/infoPic.svg SvgGenImage get infoPic => const SvgGenImage('assets/icons/infoPic.svg'); + /// File path: assets/icons/ion_fast-food-outline.svg + SvgGenImage get ionFastFoodOutline => + const SvgGenImage('assets/icons/ion_fast-food-outline.svg'); + /// File path: assets/icons/like.svg SvgGenImage get like => const SvgGenImage('assets/icons/like.svg'); @@ -197,6 +221,9 @@ class $AssetsIconsGen { /// File path: assets/icons/ph_cheese.svg SvgGenImage get phCheese => const SvgGenImage('assets/icons/ph_cheese.svg'); + /// File path: assets/icons/ph_qr-code.svg + SvgGenImage get phQrCode => const SvgGenImage('assets/icons/ph_qr-code.svg'); + /// File path: assets/icons/pick up off.svg SvgGenImage get pickUpOff => const SvgGenImage('assets/icons/pick up off.svg'); @@ -321,9 +348,15 @@ class $AssetsIconsGen { favorite, fluentColorLocationRipple16, game2, + gameIconsWinterGloves, game, + girlClothes, + healthiconsFruitsOutline, + hugeiconsBabyBoyDress, + hugeiconsCheeseCake01, icRoundLocalOffer, infoPic, + ionFastFoodOutline, like, list, location, @@ -341,6 +374,7 @@ class $AssetsIconsGen { next, notificationBing, phCheese, + phQrCode, pickUpOff, pickupOn, profile2, diff --git a/lib/res/colors.dart b/lib/res/colors.dart index 7cce8bb..c6c8319 100644 --- a/lib/res/colors.dart +++ b/lib/res/colors.dart @@ -23,4 +23,5 @@ class LightAppColors{ static const offerCardDetail = Color.fromARGB(255, 73, 69, 79); static const divider = Color.fromARGB(255, 189, 189, 188); static const textPrice = Color.fromARGB(255, 85, 84, 81); + static const deliverySelectedButton = Color.fromARGB(255, 237, 247, 238); } \ No newline at end of file diff --git a/lib/screens/mains/discover/discover.dart b/lib/screens/mains/discover/discover.dart index 89f23b1..d8d45a4 100644 --- a/lib/screens/mains/discover/discover.dart +++ b/lib/screens/mains/discover/discover.dart @@ -33,11 +33,12 @@ class _DiscoverState extends State with TickerProviderStateMixin { final List categoryIcons = [ Assets.icons.stashStarsLight.path, - Assets.icons.shoppingCart.path, - Assets.icons.elementEqual.path, - Assets.icons.shop.path, - Assets.icons.game.path, - Assets.icons.receiptDiscount.path, + Assets.icons.hugeiconsBabyBoyDress.path, + Assets.icons.girlClothes.path, + Assets.icons.gameIconsWinterGloves.path, + Assets.icons.hugeiconsCheeseCake01.path, + Assets.icons.ionFastFoodOutline.path, + Assets.icons.healthiconsFruitsOutline.path, ]; @override @@ -336,7 +337,7 @@ class _DiscoverState extends State with TickerProviderStateMixin { Widget _buildCategoryIcons() { return SizedBox( - height: 60, + height: 50, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: categoryIcons.length, @@ -351,9 +352,9 @@ class _DiscoverState extends State with TickerProviderStateMixin { : Colors.grey.shade600; return Container( - width: 60, - height: 60, - padding: const EdgeInsets.all(16), + width: 50, + height: 50, + padding: const EdgeInsets.all(11), decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(12), @@ -600,7 +601,7 @@ class _DiscoverState extends State with TickerProviderStateMixin { decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), image: DecorationImage( - image: AssetImage(Assets.images.topDealsAndStores.path), + image: AssetImage(Assets.images.wp1929534FastFoodWallpapers1.path), fit: BoxFit.cover, ), ), @@ -861,7 +862,7 @@ class FlashSaleCard extends StatelessWidget { ), Image.asset( imagePath, - height: 100, + height: 110, width: double.infinity, fit: BoxFit.cover, ), @@ -889,7 +890,7 @@ class FlashSaleCard extends StatelessWidget { ), ], ), - const SizedBox(height: 6), + const SizedBox(height: 8), Row( children: [ SvgPicture.asset( @@ -905,7 +906,7 @@ class FlashSaleCard extends StatelessWidget { ), ], ), - const SizedBox(height: 6), + const SizedBox(height: 8), Row( children: [ SvgPicture.asset( @@ -953,43 +954,67 @@ class FlashSaleCard extends StatelessWidget { ], ), const SizedBox(height: 20), - Center( - child: SizedBox( - width: 150, - height: 38, - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - elevation: 0, - backgroundColor: Colors.green, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + border: Border.all(color: const Color.fromARGB(255, 76, 175, 80), width: 1.0), + color: Colors.transparent, ), - child: Center( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ SvgPicture.asset( - Assets.icons.shoppingCart.path, - color: Colors.white, - width: 20, - ), - const SizedBox( - width: 5, + Assets.icons.cardPos.path, + color: const Color.fromARGB(255, 95, 95, 95), + height: 17, ), + const SizedBox(width: 4), const Text( - "Reservation", + "Delivery", style: TextStyle( - fontWeight: FontWeight.bold, fontSize: 15), + color: Color.fromARGB(255, 95, 95, 95), + fontWeight: FontWeight.w500, + fontSize: 12, + ), ), ], ), ), ), - ), + const SizedBox(width: 8), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + border: Border.all(color: const Color.fromARGB(255, 76, 175, 80), width: 1.0), + color: Colors.transparent, + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), + child: Row( + children: [ + SvgPicture.asset( + Assets.icons.shoppingCart.path, + color: const Color.fromARGB(255, 95, 95, 95), + height: 17, + ), + const SizedBox(width: 4), + const Text( + "Pickup", + style: TextStyle( + color: Color.fromARGB(255, 95, 95, 95), + fontWeight: FontWeight.w500, + fontSize: 12, + ), + ), + ], + ), + ), + ), + ], ), ], ), @@ -1043,7 +1068,6 @@ class SeasonalDiscountCard extends StatelessWidget { _buildDetailRow( icon: Assets.icons.winter.path, text: title, - iconColor: const Color.fromARGB(255, 23, 107, 173), textStyle: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, @@ -1052,7 +1076,6 @@ class SeasonalDiscountCard extends StatelessWidget { _buildDetailRow( icon: Assets.icons.shop.path, text: brand, - iconColor: Colors.grey.shade600, textStyle: const TextStyle( color: Colors.black, fontSize: 12, @@ -1061,7 +1084,6 @@ class SeasonalDiscountCard extends StatelessWidget { _buildDetailRow( icon: Assets.icons.icRoundLocalOffer.path, text: discount, - iconColor: Colors.grey.shade600, textStyle: const TextStyle( color: Colors.red, fontWeight: FontWeight.normal, @@ -1091,7 +1113,6 @@ class SeasonalDiscountCard extends StatelessWidget { Widget _buildDetailRow({ required String icon, required String text, - required Color iconColor, required TextStyle textStyle, }) { return Row( @@ -1101,7 +1122,6 @@ class SeasonalDiscountCard extends StatelessWidget { padding: const EdgeInsets.only(top: 2.0), child: SvgPicture.asset( icon, - color: iconColor, width: 20, ), ), diff --git a/lib/screens/product/checkout.dart b/lib/screens/product/checkout.dart index 9aedb4e..f5be5f2 100644 --- a/lib/screens/product/checkout.dart +++ b/lib/screens/product/checkout.dart @@ -2,6 +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:lba/screens/product/map_selection_screen.dart'; +import 'package:lba/widgets/add_card_bottom_sheet.dart'; +import 'package:lba/widgets/time_selection_bottom_sheet.dart'; class CheckoutPage extends StatefulWidget { const CheckoutPage({super.key}); @@ -13,6 +16,11 @@ class CheckoutPage extends StatefulWidget { class _CheckoutPageState extends State with TickerProviderStateMixin { int _selectedTabIndex = 0; + String? _selectedAddress; + String? _selectedTime; + String? _selectedPickupTime; + int _selectedDeliveryOption = 0; + String? _cardNumber; late AnimationController _deliveryController; late AnimationController _pickupController; @@ -80,6 +88,65 @@ class _CheckoutPageState extends State } } + Future _selectAddress() async { + final result = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => const MapSelectionScreen()), + ); + + if (result != null && result.isNotEmpty) { + setState(() { + _selectedAddress = result; + }); + } + } + + Future _selectTime() async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const TimeSelectionBottomSheet(), + ); + + if (result != null && result.isNotEmpty) { + setState(() { + _selectedTime = result; + _selectedDeliveryOption = 2; + }); + } + } + + Future _selectPickupTime() async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const TimeSelectionBottomSheet(), + ); + + if (result != null && result.isNotEmpty) { + setState(() { + _selectedPickupTime = result; + }); + } + } + + Future _addCard() async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const AddCardBottomSheet(), + ); + + if (result != null && result.isNotEmpty) { + setState(() { + _cardNumber = result; + }); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -97,8 +164,7 @@ class _CheckoutPageState extends State opacity: animation, child: SlideTransition( position: Tween( - begin: - Offset(_selectedTabIndex == 0 ? 0.5 : -0.5, 0), + begin: Offset(_selectedTabIndex == 0 ? 0.5 : -0.5, 0), end: Offset.zero, ).animate(animation), child: child, @@ -116,6 +182,80 @@ class _CheckoutPageState extends State ); } + Widget _deliveryTimeCards() { + bool isAsapSelected = _selectedDeliveryOption == 1; + bool isScheduleSelected = _selectedDeliveryOption == 2; + + return Column( + children: [ + _clickableCard( + onTap: () { + setState(() { + _selectedDeliveryOption = 1; + _selectedTime = null; + }); + }, + isSelected: isAsapSelected, + customChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + "ASAP", + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Colors.black), + ), + SizedBox(height: 4), + Text( + "Delivered directly to you", + style: + TextStyle(fontSize: 13, color: greyTextLight, height: 1.1), + ), + ], + ), + ), + const SizedBox(height: 12), + _clickableCard( + onTap: _selectTime, + isSelected: isScheduleSelected, + customChild: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Schedule", + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Colors.black)), + const SizedBox(height: 4), + Text( + _selectedTime != null ? _selectedTime! : "Select a time", + style: TextStyle( + fontSize: 13, + color: _selectedTime != null + ? Colors.black + : greyTextLight, + height: 1.1), + ), + ], + ), + ), + SvgPicture.asset( + Assets.icons.arrowRight.path, + height: 20, + color: Colors.black, + ), + ], + ), + ), + ], + ); + } + Widget _buildDeliveryContent() { return SingleChildScrollView( key: const ValueKey('delivery'), @@ -125,7 +265,7 @@ class _CheckoutPageState extends State children: [ const SizedBox(height: 20), _sectionHeader("Deliver to:", - trailingAction: "Select address", onAction: () {}), + trailingAction: "Select address", onAction: _selectAddress), const SizedBox(height: 10), _selectAddressRow(), const SizedBox(height: 20), @@ -141,8 +281,8 @@ class _CheckoutPageState extends State const SizedBox(height: 12), _clickableCard( leading: Assets.icons.cardAdd.path, - title: "Add a credit card", - onTap: () {}, + title: _cardNumber ?? "Add a credit card", + onTap: _addCard, ), const SizedBox(height: 24), const DottedDivider(), @@ -174,9 +314,9 @@ class _CheckoutPageState extends State const DottedDivider(), const SizedBox(height: 20), _sectionHeader("Pickup time:", - trailingAction: "Select time", onAction: () {}), + trailingAction: "Select time", onAction: _selectPickupTime), const SizedBox(height: 12), - _pickupTimeRow("Set a time for your Pickup"), + _pickupTimeRow(_selectedPickupTime ?? "Set a time for your Pickup"), const SizedBox(height: 20), const DottedDivider(), const SizedBox(height: 15), @@ -184,8 +324,8 @@ class _CheckoutPageState extends State const SizedBox(height: 12), _clickableCard( leading: Assets.icons.cardAdd.path, - title: "Add a credit card", - onTap: () {}, + title: _cardNumber ?? "Add a credit card", + onTap: _addCard, ), const SizedBox(height: 24), const DottedDivider(), @@ -364,22 +504,28 @@ class _CheckoutPageState extends State } Widget _selectAddressRow() { - return Row( - children: [ - SvgPicture.asset( - Assets.icons.location.path, - color: const Color.fromARGB(255, 85, 84, 81), - ), - const SizedBox(width: 10), - const Text( - "Select address", - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: greyTextMid, + return GestureDetector( + onTap: _selectAddress, + child: Row( + children: [ + SvgPicture.asset( + Assets.icons.location.path, + color: const Color.fromARGB(255, 85, 84, 81), ), - ), - ], + const SizedBox(width: 10), + Expanded( + child: Text( + _selectedAddress ?? "Select address", + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: _selectedAddress == null ? greyTextMid : Colors.black, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), ); } @@ -404,92 +550,42 @@ class _CheckoutPageState extends State } Widget _pickupTimeRow(String time) { + bool hasSelectedTime = _selectedPickupTime != null; return Text( time, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, - color: greyTextMid, + color: hasSelectedTime ? Colors.black : greyTextMid, ), ); } - Widget _deliveryTimeCards() { - return Column( - children: [ - _clickableCard( - customChild: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Text( - "ASAP", - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: Colors.black), - ), - SizedBox(height: 4), - Text( - "Delivered directly to you", - style: TextStyle( - fontSize: 13, color: greyTextLight, height: 1.1), - ), - ], - ), - onTap: () {}, - ), - const SizedBox(height: 12), - _clickableCard( - customChild: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Text("Schedule", - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: Colors.black)), - SizedBox(height: 4), - Text("Select a time", - style: TextStyle( - fontSize: 13, - color: greyTextLight, - height: 1.1)), - ], - ), - ), - SvgPicture.asset( - Assets.icons.arrowRight.path, - height: 20, - color: Colors.black, - ), - ], - ), - onTap: () {}, - ), - ], - ); - } - Widget _clickableCard({ String? leading, String? title, Widget? customChild, VoidCallback? onTap, + bool isSelected = false, }) { return InkWell( - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.circular(15), onTap: onTap, - child: Container( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), decoration: BoxDecoration( - color: Colors.white, + color: + isSelected ? LightAppColors.deliverySelectedButton : Colors.white, borderRadius: BorderRadius.circular(15), - border: Border.all(color: LightAppColors.divider), + border: Border.all( + color: isSelected + ? LightAppColors.confirmButton + : LightAppColors.divider, + width: isSelected ? 1.5 : 1.0, + ), ), child: customChild ?? Row( @@ -507,8 +603,7 @@ class _CheckoutPageState extends State ), ), ), - const Icon(Icons.arrow_forward_ios, - size: 16, color: greyTextLight), + const Icon(Icons.arrow_forward_ios, size: 16), ], ), ), diff --git a/lib/screens/product/map_selection_screen.dart b/lib/screens/product/map_selection_screen.dart new file mode 100644 index 0000000..da07e5f --- /dev/null +++ b/lib/screens/product/map_selection_screen.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +// import 'package:geocoding/geocoding.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:lba/res/colors.dart'; + +class MapSelectionScreen extends StatefulWidget { + const MapSelectionScreen({super.key}); + + @override + State createState() => _MapSelectionScreenState(); +} + +class _MapSelectionScreenState extends State { + final MapController _mapController = MapController(); + LatLng _currentCenter = const LatLng(25.1972, 55.2744); + String _addressText = "Loading address..."; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _getAddressFromLatLng(_currentCenter); + } + + Future _getAddressFromLatLng(LatLng latLng) async { + try { + // List placemarks =` + // await placemarkFromCoordinates(latLng.latitude, latLng.longitude); + // if (placemarks.isNotEmpty) { + // final p = placemarks.first; + // setState(() { + // _addressText = + // "${p.street ?? ''}, ${p.locality ?? ''}, ${p.country ?? ''}".trim().replaceAll(RegExp(r'^,|, $'), ''); + // }); + // } + setState(() { + _addressText = "Lat: ${latLng.latitude.toStringAsFixed(4)}, Lng: ${latLng.longitude.toStringAsFixed(4)}"; + }); + } catch (e) { + setState(() { + _addressText = "Could not get address"; + }); + print(e); + } + } + + Future _goToCurrentUserLocation() async { + setState(() => _isLoading = true); + + 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(); + setState(() => _isLoading = false); + return; + } + + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('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.'))); + setState(() => _isLoading = false); + return; + } + + try { + Position position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high); + final userLatLng = LatLng(position.latitude, position.longitude); + _mapController.move(userLatLng, 15.0); + await _getAddressFromLatLng(userLatLng); + } catch (e) { + print("Error getting current location: $e"); + } finally { + setState(() => _isLoading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Select Delivery Address', style: TextStyle(color: Colors.black, fontSize: 18)), + backgroundColor: Colors.white, + iconTheme: const IconThemeData(color: Colors.black), + elevation: 1, + ), + body: Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: _currentCenter, + initialZoom: 14.0, + onPositionChanged: (position, hasGesture) { + if (hasGesture && position.center != null) { + _currentCenter = position.center!; + _getAddressFromLatLng(_currentCenter); + } + }, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'com.example.app', + ), + ], + ), + const Center( + child: Icon( + Icons.location_pin, + color: Colors.red, + size: 50, + ), + ), + Positioned( + top: 16, + right: 16, + child: FloatingActionButton( + onPressed: _goToCurrentUserLocation, + backgroundColor: Colors.white, + child: _isLoading + ? const CircularProgressIndicator() + : const Icon(Icons.my_location, color: LightAppColors.primary), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.all(16.0), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + boxShadow: [ + BoxShadow( + color: Colors.black26, + blurRadius: 10, + offset: Offset(0, -2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Selected Address:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 8), + Text( + _addressText, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context, _addressText); + }, + style: ElevatedButton.styleFrom( + backgroundColor: LightAppColors.offerTimer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Confirm Address', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/product/productdetail.dart b/lib/screens/product/productdetail.dart index 1e06a7e..7cd190a 100644 --- a/lib/screens/product/productdetail.dart +++ b/lib/screens/product/productdetail.dart @@ -21,8 +21,8 @@ class _ProductdetailState extends State final List imageList = [ Assets.images.media.path, Assets.images.topDealsAndStores.path, + Assets.images.wp1929534FastFoodWallpapers1.path, Assets.images.image.path, - Assets.images.topDealsAndStores.path, ]; late AnimationController _controller; @@ -139,8 +139,7 @@ class _ProductdetailState extends State borderRadius: BorderRadius.circular(8), child: ColorFiltered( colorFilter: - selectedIndex == 1 && - isSelected == false + !isSelected ? ColorFilter.matrix([ 0.2126, 0.7152, diff --git a/lib/screens/product/shop.dart b/lib/screens/product/shop.dart index 65eb06c..cb28894 100644 --- a/lib/screens/product/shop.dart +++ b/lib/screens/product/shop.dart @@ -47,7 +47,7 @@ class _ShopState extends State with TickerProviderStateMixin { final List> similarBusinesses = [ { - "imagePath": Assets.images.image.path, + "imagePath": Assets.images.wp1929534FastFoodWallpapers1.path, "name": "The Odd Piece", "location": "Al Quoz 1", }, diff --git a/lib/screens/qr_scanner/qr_scanner_page.dart b/lib/screens/qr_scanner/qr_scanner_page.dart new file mode 100644 index 0000000..dde5f8f --- /dev/null +++ b/lib/screens/qr_scanner/qr_scanner_page.dart @@ -0,0 +1,451 @@ +import 'package:flutter/material.dart'; +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'; + +class QRScannerPage extends StatefulWidget { + const QRScannerPage({super.key}); + + @override + State createState() => _QRScannerPageState(); +} + +class _QRScannerPageState extends State { + MobileScannerController cameraController = MobileScannerController(); + bool _isFlashOn = false; + bool _isProcessing = false; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + cameraController.dispose(); + super.dispose(); + } + + void _toggleFlash() { + setState(() { + _isFlashOn = !_isFlashOn; + }); + cameraController.toggleTorch(); + } + + Future _requestStoragePermission() async { + final status = await Permission.photos.status; + + if (status.isGranted) { + return true; + } + + if (status.isDenied) { + final result = await Permission.photos.request(); + return result.isGranted; + } + + if (status.isPermanentlyDenied) { + _showPermissionDeniedDialog(); + return false; + } + + return false; + } + + void _showPermissionDeniedDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Permission Required'), + content: const Text( + 'This app needs access to your photos to select QR code images. Please enable the permission in Settings.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + openAppSettings(); + }, + child: const Text('Open Settings'), + ), + ], + ), + ); + } + + Future _pickImageFromGallery() async { + // چک کردن و گرفتن permission + final hasPermission = await _requestStoragePermission(); + if (!hasPermission) { + return; + } + + try { + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage( + source: ImageSource.gallery, + maxWidth: 1024, + maxHeight: 1024, + imageQuality: 85, + ); + + if (image != null) { + // نمایش اطلاعات فایل انتخاب شده + final fileName = image.name; + final fileSize = await image.length(); + final fileSizeKB = (fileSize / 1024).toStringAsFixed(1); + + _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: Colors.red, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + // Camera scanner + MobileScanner( + controller: cameraController, + onDetect: (capture) { + if (_isProcessing) return; + + final List barcodes = capture.barcodes; + if (barcodes.isNotEmpty) { + final String code = barcodes.first.rawValue ?? ''; + if (code.isNotEmpty) { + setState(() { + _isProcessing = true; + }); + _handleScanResult(code); + } + } + }, + errorBuilder: (context, error, child) { + return Container( + width: double.infinity, + height: double.infinity, + color: Colors.black87, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.camera_alt_outlined, + color: Colors.white54, + size: 64, + ), + const SizedBox(height: 16), + const Text( + 'Camera Error', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Please check camera permissions', + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white54, + fontSize: 14, + ), + ), + ], + ), + ), + ); + }, + ), + + // Top bar + SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon( + Icons.arrow_back, + color: Colors.white, + size: 28, + ), + onPressed: () => Navigator.pop(context), + ), + const Text( + 'Scan QR Code', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: Icon( + _isFlashOn ? Icons.flash_on : Icons.flash_off, + color: Colors.white, + size: 28, + ), + onPressed: _toggleFlash, + ), + ], + ), + ), + ), + + // QR Scanner overlay + Center( + child: SizedBox( + width: 250, + height: 250, + child: Stack( + children: [ + // Scanner frame + Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.white, + width: 2, + ), + borderRadius: BorderRadius.circular(12), + ), + ), + + // Corner decorations + Positioned( + top: -2, + left: -2, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: LightAppColors.primary, width: 4), + left: BorderSide(color: LightAppColors.primary, width: 4), + ), + ), + ), + ), + Positioned( + top: -2, + right: -2, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: LightAppColors.primary, width: 4), + right: BorderSide(color: LightAppColors.primary, width: 4), + ), + ), + ), + ), + Positioned( + bottom: -2, + left: -2, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: LightAppColors.primary, width: 4), + left: BorderSide(color: LightAppColors.primary, width: 4), + ), + ), + ), + ), + Positioned( + bottom: -2, + right: -2, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: LightAppColors.primary, width: 4), + right: BorderSide(color: LightAppColors.primary, width: 4), + ), + ), + ), + ), + ], + ), + ), + ), + + // Bottom instructions + Positioned( + bottom: 120, + left: 0, + right: 0, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 32.0), + child: Text( + 'Position the QR code within the frame to scan', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + + // Bottom buttons + Positioned( + bottom: 50, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Gallery button + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(30), + ), + child: IconButton( + icon: const Icon( + Icons.photo_library, + color: Colors.white, + size: 28, + ), + onPressed: _pickImageFromGallery, + ), + ), + + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(30), + ), + child: IconButton( + icon: const Icon( + Icons.keyboard, + color: Colors.white, + size: 28, + ), + onPressed: () { + _showManualInputDialog(); + }, + ), + ), + ], + ), + ), + ], + ), + ); + } + + void _showManualInputDialog() { + showDialog( + context: context, + builder: (context) { + final TextEditingController controller = TextEditingController(); + return AlertDialog( + title: const Text('Enter Code Manually'), + content: TextField( + controller: controller, + decoration: const InputDecoration( + hintText: 'Enter QR code or barcode', + border: OutlineInputBorder(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + if (controller.text.isNotEmpty) { + Navigator.pop(context); + _handleScanResult(controller.text); + } + }, + child: const Text('Submit'), + ), + ], + ); + }, + ); + } + + void _handleScanResult(String result) { + // Pause camera + cameraController.stop(); + + // Handle the scanned result + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text('QR Code Scanned!'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Scanned content:', style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + ), + child: SelectableText( + result, + style: const TextStyle(fontSize: 14), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + setState(() { + _isProcessing = false; + }); + cameraController.start(); + }, + child: const Text('Scan Again'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + child: const Text('Done'), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/add_card_bottom_sheet.dart b/lib/widgets/add_card_bottom_sheet.dart new file mode 100644 index 0000000..3b29f97 --- /dev/null +++ b/lib/widgets/add_card_bottom_sheet.dart @@ -0,0 +1,262 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:lba/res/colors.dart'; +import 'package:lba/widgets/button.dart'; + +class AddCardBottomSheet extends StatefulWidget { + const AddCardBottomSheet({super.key}); + + @override + State createState() => _AddCardBottomSheetState(); +} + +class _AddCardBottomSheetState extends State { + final TextEditingController _cardNumberController = TextEditingController(); + final TextEditingController _expiryDateController = TextEditingController(); + + bool _isCardNumberValid = false; + bool _isExpiryDateValid = false; + final FocusNode _cardNumberFocus = FocusNode(); + final FocusNode _expiryDateFocus = FocusNode(); + Color _borderColor = Colors.grey.shade400; + + @override + void initState() { + super.initState(); + _cardNumberController.addListener(_validateCardNumber); + _expiryDateController.addListener(_validateExpiryDate); + + // Change border color on focus + _cardNumberFocus.addListener(() { + setState(() { + _borderColor = _cardNumberFocus.hasFocus || _expiryDateFocus.hasFocus + ? Theme.of(context).primaryColor + : Colors.grey.shade400; + }); + }); + _expiryDateFocus.addListener(() { + setState(() { + _borderColor = _cardNumberFocus.hasFocus || _expiryDateFocus.hasFocus + ? Theme.of(context).primaryColor + : Colors.grey.shade400; + }); + }); + } + + @override + void dispose() { + _cardNumberController.removeListener(_validateCardNumber); + _expiryDateController.removeListener(_validateExpiryDate); + _cardNumberController.dispose(); + _expiryDateController.dispose(); + _cardNumberFocus.dispose(); + _expiryDateFocus.dispose(); + super.dispose(); + } + + void _validateCardNumber() { + final text = _cardNumberController.text.replaceAll('-', ''); + setState(() { + _isCardNumberValid = text.length == 16; + }); + } + + void _validateExpiryDate() { + final text = _expiryDateController.text; + if (text.length == 5) { + final parts = text.split('/'); + if (parts.length == 2) { + final month = int.tryParse(parts[0]); + final year = int.tryParse(parts[1]); + if (month != null && year != null) { + final now = DateTime.now(); + final currentYear = now.year % 100; + final currentMonth = now.month; + if (month >= 1 && + month <= 12 && + (year > currentYear || + (year == currentYear && month >= currentMonth))) { + setState(() { + _isExpiryDateValid = true; + }); + return; + } + } + } + } + setState(() { + _isExpiryDateValid = false; + }); + } + + bool get _isFormValid => _isCardNumberValid && _isExpiryDateValid; + + void _addCard() { + if (_isFormValid) { + Navigator.pop(context, _cardNumberController.text); + } + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Container( + padding: const EdgeInsets.all(24.0), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Add card', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 8), + const Divider(), + const SizedBox(height: 24), + AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 55, + decoration: BoxDecoration( + border: Border.all(color: _borderColor, width: 1.5), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: Icon(Icons.credit_card, color: Colors.grey), + ), + Expanded( + child: TextFormField( + controller: _cardNumberController, + focusNode: _cardNumberFocus, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(16), + _CardNumberInputFormatter(), + ], + decoration: const InputDecoration( + hintText: '1234-5678-1234-5678', + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 15), + ), + ), + ), + SizedBox( + width: 80, + child: TextFormField( + controller: _expiryDateController, + focusNode: _expiryDateFocus, + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(4), + _ExpiryDateInputFormatter(), + ], + decoration: const InputDecoration( + hintText: 'MM/YY', + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 32), + SizedBox( + width: double.infinity, + height: 50, + child: Button( + text: 'Add card', + onPressed: _isFormValid ? _addCard : () {}, + color: _isFormValid + ? const Color(0xFFE53935) + : Colors.red.shade200, + ), + ), + ], + ), + ), + ); + } +} + +class _CardNumberInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + var text = newValue.text; + + if (newValue.selection.baseOffset == 0) { + return newValue; + } + + var buffer = StringBuffer(); + for (int i = 0; i < text.length; i++) { + buffer.write(text[i]); + var nonZeroIndex = i + 1; + if (nonZeroIndex % 4 == 0 && nonZeroIndex != text.length) { + buffer.write('-'); + } + } + + var string = buffer.toString(); + return newValue.copyWith( + text: string, + selection: TextSelection.collapsed(offset: string.length)); + } +} + +class _ExpiryDateInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + var newText = newValue.text; + + if (newValue.selection.baseOffset == 0) { + return newValue; + } + + if (oldValue.text.endsWith('/') && + oldValue.text.length > newValue.text.length) { + newText = newText.substring(0, newText.length - 1); + } + + var buffer = StringBuffer(); + for (int i = 0; i < newText.length; i++) { + buffer.write(newText[i]); + if (i == 1 && newText.length > 2 && !newText.contains('/')) { + buffer.write('/'); + } + } + + var string = buffer.toString(); + return newValue.copyWith( + text: string.substring(0, string.length > 5 ? 5 : string.length), + selection: TextSelection.collapsed( + offset: string.length > 5 ? 5 : string.length, + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/customCard.dart b/lib/widgets/customCard.dart index bddefaa..1fb3849 100644 --- a/lib/widgets/customCard.dart +++ b/lib/widgets/customCard.dart @@ -46,11 +46,14 @@ class CustomCard extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(11), ), - child: Image.asset( - Assets.images.image.path, - width: 80, - height: 80, - fit: BoxFit.cover, + child: ClipRRect( + borderRadius: BorderRadius.circular(11), + child: Image.asset( + Assets.images.wp1929534FastFoodWallpapers1.path, + width: 80, + height: 80, + fit: BoxFit.cover, + ), ), ), SizedBox(width: 8), diff --git a/lib/widgets/search_bar.dart b/lib/widgets/search_bar.dart index 81d57d5..f7d6b55 100644 --- a/lib/widgets/search_bar.dart +++ b/lib/widgets/search_bar.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:lba/gen/assets.gen.dart'; +import 'package:lba/screens/qr_scanner/qr_scanner_page.dart'; class SearchBarWidget extends StatelessWidget { const SearchBarWidget({super.key}); @@ -26,6 +27,24 @@ class SearchBarWidget extends StatelessWidget { height: 18, ), ), + suffixIcon: Padding( + padding: const EdgeInsets.all(10.0), + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const QRScannerPage(), + ), + ); + }, + child: SvgPicture.asset( + Assets.icons.phQrCode.path, + width: 20, + height: 20, + ), + ), + ), filled: true, fillColor: const Color.fromARGB(255, 248, 248, 248), contentPadding: const EdgeInsets.symmetric( diff --git a/lib/widgets/time_selection_bottom_sheet.dart b/lib/widgets/time_selection_bottom_sheet.dart new file mode 100644 index 0000000..7a95e92 --- /dev/null +++ b/lib/widgets/time_selection_bottom_sheet.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; +import 'package:lba/res/colors.dart'; +import 'package:lba/widgets/button.dart'; +import 'dart:math' as math; + +class TimeSelectionBottomSheet extends StatefulWidget { + const TimeSelectionBottomSheet({super.key}); + + @override + State createState() => + _TimeSelectionBottomSheetState(); +} + +class _TimeSelectionBottomSheetState extends State { + final List _timeSlots = []; + String? _selectedTimeSlot; + late ScrollController _scrollController; + + static const double _itemHeight = 50.0; + static const int _visibleItems = 3; + + @override + void initState() { + super.initState(); + _generateTimeSlots(); + + final initialIndex = _timeSlots.length > 1 ? 1 : 0; + if (_timeSlots.isNotEmpty) { + _selectedTimeSlot = _timeSlots[initialIndex]; + } + + _scrollController = + ScrollController(initialScrollOffset: initialIndex * _itemHeight); + _scrollController.addListener(_onScroll); + } + + void _onScroll() { + setState(() {}); + } + + @override + void dispose() { + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + } + + void _generateTimeSlots() { + final now = DateTime.now(); + final endTime = DateTime(now.year, now.month, now.day, 22, 0); + DateTime startTime = DateTime( + now.year, + now.month, + now.day, + now.hour, + now.minute > 30 ? 60 : 30, + ); + + if (startTime.isAfter(endTime)) return; + + _timeSlots.add(''); + while (startTime.isBefore(endTime)) { + final slotEnd = startTime.add(const Duration(minutes: 30)); + final formattedStart = DateFormat('HH:mm').format(startTime); + final formattedEnd = DateFormat('HH:mm').format(slotEnd); + _timeSlots.add('$formattedStart - $formattedEnd'); + startTime = slotEnd; + } + _timeSlots.add(''); + } + + void _onScrollEnd() { + final currentOffset = _scrollController.offset; + final targetIndex = (currentOffset / _itemHeight).round(); + final targetOffset = targetIndex * _itemHeight; + + if ((targetOffset - currentOffset).abs() > 0.1) { + _scrollController.animateTo( + targetOffset, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + + if (mounted && _timeSlots.isNotEmpty) { + final finalIndex = targetIndex.clamp(0, _timeSlots.length - 1); + final newSelection = _timeSlots[finalIndex]; + + if (_selectedTimeSlot != newSelection && newSelection.isNotEmpty) { + setState(() { + _selectedTimeSlot = newSelection; + }); + HapticFeedback.lightImpact(); + } + } + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 4, + margin: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(10), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Select a time', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Divider(color: Colors.grey.shade300), + const SizedBox(height: 8), + Text( + 'Philadelphia Honey Pecan Cream Cheese Spread, 7.5 oz Tub available from Now - 10:00 PM Today', + style: TextStyle(color: Colors.grey.shade600, fontSize: 14), + ), + const SizedBox(height: 16), + _buildTimePicker(), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 50, + child: Button( + text: 'Confirm', + onPressed: () { + if (_selectedTimeSlot != null && _selectedTimeSlot!.isNotEmpty) { + Navigator.pop(context, _selectedTimeSlot); + } + }, + color: LightAppColors.offerTimer, + ), + ), + const SizedBox(height: 20), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTimePicker() { + if (_timeSlots.length <= 2) { + return const Center( + child: Padding( + padding: EdgeInsets.all(20.0), + child: Text('No available time slots for today.'), + ), + ); + } + + final listHeight = _itemHeight * _visibleItems; + + return SizedBox( + height: listHeight, + child: Stack( + alignment: Alignment.center, + children: [ + Positioned( + top: _itemHeight, + bottom: _itemHeight, + left: 0, + right: 0, + child: Container( + decoration: BoxDecoration( + color: LightAppColors.fillOrder, + borderRadius: BorderRadius.circular(8), + ), + ), + ), + + NotificationListener( + onNotification: (notification) { + if (notification is ScrollEndNotification) { + _onScrollEnd(); + } + return true; + }, + child: ListView.builder( + controller: _scrollController, + padding: EdgeInsets.symmetric(vertical: (listHeight - _itemHeight) / 2), + itemCount: _timeSlots.length, + itemExtent: _itemHeight, + itemBuilder: (context, index) { + final centerOffset = _scrollController.offset; + final itemOffset = index * _itemHeight; + final distance = (itemOffset - centerOffset).abs(); + + final isSelected = distance < _itemHeight / 2; + final scale = math.max(1.0 - (distance / listHeight) * 0.7, 0.8); + final opacity = math.max(1.0 - (distance / listHeight) * 1.2, 0.3); + + return Center( + child: Transform.scale( + scale: scale, + child: Text( + _timeSlots[index], + style: TextStyle( + fontSize: 18, + color: isSelected ? Colors.black : Colors.grey.shade600.withOpacity(opacity), + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + ), + ), + ); + }, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index dbec55e..fe11140 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) maps_launcher_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MapsLauncherPlugin"); maps_launcher_plugin_register_with_registrar(maps_launcher_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 6c56d5b..633bb62 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux maps_launcher url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 98bc016..a205f64 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,16 +5,20 @@ import FlutterMacOS import Foundation +import file_selector_macos import geolocator_apple import location import maps_launcher +import mobile_scanner import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) LocationPlugin.register(with: registry.registrar(forPlugin: "LocationPlugin")) MapsLauncherPlugin.register(with: registry.registrar(forPlugin: "MapsLauncherPlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 8099960..119ca6b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -185,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: transitive description: @@ -273,6 +281,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" + url: "https://pub.dev" + source: hosted + version: "0.9.4+4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" fixnum: dependency: transitive description: @@ -326,6 +366,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab" + url: "https://pub.dev" + source: hosted + version: "2.0.29" flutter_svg: dependency: "direct main" description: @@ -448,6 +496,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: e83b2b05141469c5e19d77e1dfa11096b6b1567d09065b2265d7c6904560050c + url: "https://pub.dev" + source: hosted + version: "0.8.13" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e + url: "https://pub.dev" + source: hosted + version: "0.8.13" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" image_size_getter: dependency: transitive description: @@ -624,6 +736,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760 + url: "https://pub.dev" + source: hosted + version: "5.2.3" nested: dependency: transitive description: @@ -680,6 +800,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + url: "https://pub.dev" + source: hosted + version: "11.4.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.dev" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: @@ -1111,4 +1279,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.7.2 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 53d2b62..b1482be 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,10 @@ dependencies: location: ^8.0.0 maps_launcher: ^3.0.0+1 dots_indicator: ^4.0.1 + mobile_scanner: ^5.1.1 + image_picker: ^1.1.2 + permission_handler: ^11.3.1 + # geocoding: ^3.0.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 9a6f8bb..3c8fc89 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,15 +6,21 @@ #include "generated_plugin_registrant.h" +#include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); GeolocatorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("GeolocatorWindows")); MapsLauncherPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("MapsLauncherPlugin")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index ae10e3f..03bad29 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,8 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows geolocator_windows maps_launcher + permission_handler_windows url_launcher_windows )