diff --git a/assets/icons/LBA Logo.svg b/assets/icons/LBA Logo.svg new file mode 100644 index 0000000..a0b325d --- /dev/null +++ b/assets/icons/LBA Logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/Winter.svg b/assets/icons/Winter.svg new file mode 100644 index 0000000..999c14c --- /dev/null +++ b/assets/icons/Winter.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/notification-bing.svg b/assets/icons/notification-bing.svg new file mode 100644 index 0000000..21ac461 --- /dev/null +++ b/assets/icons/notification-bing.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/stash_stars-light.svg b/assets/icons/stash_stars-light.svg new file mode 100644 index 0000000..01a32c3 --- /dev/null +++ b/assets/icons/stash_stars-light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index dc393c6..69d0cf9 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -15,6 +15,9 @@ import 'package:vector_graphics/vector_graphics.dart' as _vg; class $AssetsIconsGen { const $AssetsIconsGen(); + /// File path: assets/icons/LBA Logo.svg + SvgGenImage get lBALogo => const SvgGenImage('assets/icons/LBA Logo.svg'); + /// File path: assets/icons/Line 1.svg SvgGenImage get line1 => const SvgGenImage('assets/icons/Line 1.svg'); @@ -46,6 +49,9 @@ class $AssetsIconsGen { /// File path: assets/icons/Shop2.svg SvgGenImage get shop2 => const SvgGenImage('assets/icons/Shop2.svg'); + /// File path: assets/icons/Winter.svg + SvgGenImage get winter => const SvgGenImage('assets/icons/Winter.svg'); + /// File path: assets/icons/arrow-down-black.svg SvgGenImage get arrowDownBlack => const SvgGenImage('assets/icons/arrow-down-black.svg'); @@ -165,6 +171,10 @@ class $AssetsIconsGen { /// File path: assets/icons/next.svg SvgGenImage get next => const SvgGenImage('assets/icons/next.svg'); + /// File path: assets/icons/notification-bing.svg + SvgGenImage get notificationBing => + const SvgGenImage('assets/icons/notification-bing.svg'); + /// File path: assets/icons/ph_cheese.svg SvgGenImage get phCheese => const SvgGenImage('assets/icons/ph_cheese.svg'); @@ -224,6 +234,10 @@ class $AssetsIconsGen { /// File path: assets/icons/star.svg SvgGenImage get star => const SvgGenImage('assets/icons/star.svg'); + /// File path: assets/icons/stash_stars-light.svg + SvgGenImage get stashStarsLight => + const SvgGenImage('assets/icons/stash_stars-light.svg'); + /// File path: assets/icons/tick.svg SvgGenImage get tick => const SvgGenImage('assets/icons/tick.svg'); @@ -240,6 +254,7 @@ class $AssetsIconsGen { /// List of all assets List get values => [ + lBALogo, line1, line4, mDSPublicTWButton, @@ -249,6 +264,7 @@ class $AssetsIconsGen { shape, shop, shop2, + winter, arrowDownBlack, arrowDown, arrowLeft, @@ -284,6 +300,7 @@ class $AssetsIconsGen { nearby, nearby2, next, + notificationBing, phCheese, profile2, profile, @@ -302,6 +319,7 @@ class $AssetsIconsGen { starFill, starHalf, star, + stashStarsLight, tick, timerPause, timerStart, diff --git a/lib/main.dart b/lib/main.dart index 979e0bd..a8ddfcb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'features/auth/presentation/pages/onboarding_page.dart'; +import 'widgets/animated_splash_screen.dart'; import 'injection_container.dart'; void main() { @@ -21,7 +22,7 @@ class MyApp extends StatelessWidget { title: 'LBA', theme: ThemeData( fontFamily: 'Roboto', - scaffoldBackgroundColor: Colors.white, + scaffoldBackgroundColor: Colors.white, primaryColor: const Color.fromARGB(255, 14, 63, 102), buttonTheme: const ButtonThemeData( buttonColor: Color.fromARGB(255, 14, 63, 102), @@ -29,9 +30,7 @@ class MyApp extends StatelessWidget { appBarTheme: const AppBarTheme( backgroundColor: Color.fromARGB(255, 14, 63, 102), ), - dialogTheme: DialogTheme( - backgroundColor: Colors.white, - ), + dialogTheme: DialogTheme(backgroundColor: Colors.white), dropdownMenuTheme: DropdownMenuThemeData( menuStyle: MenuStyle( backgroundColor: MaterialStatePropertyAll(Colors.white), @@ -44,11 +43,23 @@ class MyApp extends StatelessWidget { borderSide: BorderSide(color: Colors.grey), ), focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color.fromARGB(255, 14, 63, 102), width: 2), + borderSide: BorderSide( + color: Color.fromARGB(255, 14, 63, 102), + width: 2, + ), ), ), + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: CupertinoPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + }, + ), + ), + home: CoolSplashScreen( + nextScreen: const OnboardingPage(), + duration: const Duration(seconds: 6), ), - home: const OnboardingPage(), ), ); } diff --git a/lib/res/colors.dart b/lib/res/colors.dart index f9ac123..ed57c64 100644 --- a/lib/res/colors.dart +++ b/lib/res/colors.dart @@ -17,4 +17,8 @@ class LightAppColors{ static const fillOrder = Color.fromARGB(255, 200, 230, 201); static const fillOrderText = Color.fromARGB(255, 32, 74, 34); static const allReviewOpener = Color.fromARGB(255, 183, 28, 28); + static const hintTitle = Color.fromARGB(255, 73, 69, 79); + static const cardBackground = Color.fromARGB(255, 242, 242, 241); + static const offerTimer = Color.fromARGB(255, 244, 67, 54); + static const offerCardDetail = Color.fromARGB(255, 73, 69, 79); } \ No newline at end of file diff --git a/lib/screens/mains/discover/discover.dart b/lib/screens/mains/discover/discover.dart index 8ccf133..d628b7a 100644 --- a/lib/screens/mains/discover/discover.dart +++ b/lib/screens/mains/discover/discover.dart @@ -1,10 +1,1025 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:dots_indicator/dots_indicator.dart'; +import 'package:lba/gen/assets.gen.dart'; +import 'package:lba/res/colors.dart'; +import 'package:lba/widgets/customBottomSheet.dart'; +import 'package:lba/widgets/remainingTime.dart'; +import 'package:lba/widgets/search_bar.dart'; -class Discover extends StatelessWidget { +class Discover extends StatefulWidget { const Discover({super.key}); + @override + State createState() => _DiscoverState(); +} + +class _DiscoverState extends State with TickerProviderStateMixin { + final PageController _pageController = PageController(); + double _currentPage = 0; + + late AnimationController _staggeredController; + late List> _staggeredAnimations; + + 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, + ]; + + @override + void initState() { + super.initState(); + _pageController.addListener(() { + if (_pageController.hasClients) { + setState(() { + _currentPage = _pageController.page!; + }); + } + }); + + _staggeredController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + ); + + final int itemCount = 8; + _staggeredAnimations = List.generate(itemCount, (index) { + return Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _staggeredController, + curve: Interval( + (0.1 * index), + (0.5 + 0.1 * index).clamp(0.0, 1.0), + curve: Curves.easeOutCubic, + ), + ), + ); + }); + + _staggeredController.forward(); + } + + @override + void dispose() { + _pageController.dispose(); + _staggeredController.dispose(); + super.dispose(); + } + + Widget _buildAnimatedSection(Widget child, int index) { + return FadeTransition( + opacity: _staggeredAnimations[index], + child: SlideTransition( + position: Tween( + begin: const Offset(0.0, 0.3), + end: Offset.zero, + ).animate(_staggeredAnimations[index]), + child: child, + ), + ); + } + @override Widget build(BuildContext context) { - return const Placeholder(); + return Scaffold( + backgroundColor: Colors.white, + appBar: _buildAppBar(), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildAnimatedSection( + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Row( + children: [ + const Expanded(child: SearchBarWidget()), + const SizedBox(width: 10), + _buildFilterButton(), + ], + ), + ), + 0, + ), + const SizedBox(height: 16), + _buildAnimatedSection(_buildSectionTitle("what's on your mind?"), 1), + const SizedBox(height: 12), + _buildAnimatedSection(_buildCategoryIcons(), 2), + const SizedBox(height: 24), + _buildAnimatedSection(_buildSectionTitle("Top 10 Discount & Offers"), 3), + const SizedBox(height: 12), + _buildAnimatedSection(_buildTopOffersSection(), 4), + const SizedBox(height: 24), + _buildAnimatedSection(_buildSectionTitle("Flash Sale"), 5), + const SizedBox(height: 12), + _buildAnimatedSection(_buildFlashSaleSection(), 6), + const SizedBox(height: 24), + _buildAnimatedSection(_buildSectionTitle("Special Discount"), 7), + const SizedBox(height: 12), + _buildAnimatedSection(_buildSpecialDiscountSection(), 1), + const SizedBox(height: 24), + _buildAnimatedSection(_buildSectionTitle("Seasonal Discount"), 2), + const SizedBox(height: 12), + _buildAnimatedSection(_buildSeasonalDiscountSection(), 3), + const SizedBox(height: 24), + _buildAnimatedSection(_buildSectionTitle("Crafting something for you"), 4), + const SizedBox(height: 12), + _buildAnimatedSection(_buildCraftingSomethingSection(), 5), + const SizedBox(height: 24), + _buildAnimatedSection(_buildSectionTitle("First Purchase Discount"), 6), + const SizedBox(height: 12), + _buildAnimatedSection(_buildFirstPurchaseSection(), 7), + const SizedBox(height: 100), + ], + ), + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + elevation: 0, + backgroundColor: Colors.white, + title: Row( + children: [ + SvgPicture.asset(Assets.icons.lBALogo.path, height: 32), + const SizedBox(width: 8), + const Text( + "Proxibuy", + style: TextStyle( + color: LightAppColors.hintTitle, + fontWeight: FontWeight.normal, + fontSize: 24, + ), + ), + ], + ), + actions: [ + IconButton( + icon: SvgPicture.asset(Assets.icons.notificationBing.path), + onPressed: () {}, + ), + const SizedBox(width: 8), + ], + ); + } + + Widget _buildFilterButton() { + return Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color.fromARGB(255, 14, 63, 102), + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + icon: SvgPicture.asset(Assets.icons.sort.path, color: Colors.white), + onPressed: () { + CustomBottomSheet.show(context, [ + "Food & Dining", + "Entertainment & Leisure", + "Health & Fitness", + "Travel & Transportation", + ]); + }, + padding: const EdgeInsets.all(8), + ), + ); + } + + Widget _buildCategoryIcons() { + return SizedBox( + height: 60, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: categoryIcons.length, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemBuilder: (context, index) { + final isFirstIcon = index == 0; + final backgroundColor = isFirstIcon + ? const Color.fromRGBO(186, 222, 251, 1) + : const Color(0xFFF3F4F6); + final iconColor = isFirstIcon + ? const Color.fromARGB(255, 14, 63, 102) + : Colors.grey.shade600; + + return Container( + width: 60, + height: 60, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(12), + ), + child: SvgPicture.asset(categoryIcons[index], color: iconColor), + ); + }, + separatorBuilder: (context, index) => const SizedBox(width: 12), + ), + ); + } + + Widget _buildTopOffersSection() { + return Column( + children: [ + SizedBox( + height: 180, + child: PageView( + controller: _pageController, + children: [ + _buildOfferBanner(), + _buildOfferBanner(), + _buildOfferBanner(), + ], + ), + ), + const SizedBox(height: 12), + DotsIndicator( + dotsCount: 3, + position: _currentPage, + decorator: DotsDecorator( + color: Colors.grey.shade300, + activeColor: LightAppColors.primary, + size: const Size.square(8.0), + activeSize: const Size(20.0, 8.0), + activeShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0), + ), + ), + ), + ], + ); + } + + Widget _buildOfferBanner() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(15), + child: Image.asset( + Assets.images.image.path, + height: 180, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.black.withOpacity(0.6), + Colors.black.withOpacity(0.1), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.8), + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + "65% OFF", + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 8), + const Text( + "NEW COLLECTION", + style: TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + _buildTimeBox("12"), + _buildTimeSeparator(), + _buildTimeBox("25"), + _buildTimeSeparator(), + _buildTimeBox("14"), + ], + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTimeBox(String time) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + time, + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ); + } + + Widget _buildTimeSeparator() { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + ":", + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ); + } + + Widget _buildSectionTitle(String title) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.normal), + ), + const SizedBox(width: 8), + const Expanded(child: Divider(color: Colors.grey, thickness: 1)), + ], + ), + ); + } + + Widget _buildFlashSaleSection() { + return SizedBox( + height: 310, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + SizedBox( + width: 250, + child: FlashSaleCard( + imagePath: Assets.images.media.path, + title: "Amul Cheese Slices", + location: "Fresno (750m away)", + originalPrice: "70", + discountedPrice: "53", + discountPercent: "13", + expiryTimeString: DateTime.now() + .add(const Duration(hours: 8, minutes: 35)) + .millisecondsSinceEpoch + .toString(), + categoryIconPath: Assets.icons.phCheese.path, + ), + ), + const SizedBox(width: 16), + SizedBox( + width: 250, + child: FlashSaleCard( + imagePath: Assets.images.wp1929534FastFoodWallpapers1.path, + title: "Tulip Luncheon Meat", + location: "Fresno (2km away)", + originalPrice: "370", + discountedPrice: "194", + discountPercent: "50", + expiryTimeString: DateTime.now() + .add(const Duration(hours: 12, minutes: 45)) + .millisecondsSinceEpoch + .toString(), + categoryIconPath: Assets.icons.shop.path, + ), + ), + ], + ), + ); + } + + Widget _buildSpecialDiscountSection() { + return Container( + height: 180, + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + image: DecorationImage( + image: AssetImage(Assets.images.topDealsAndStores.path), + fit: BoxFit.cover, + ), + ), + ); + } + + Widget _buildSeasonalDiscountSection() { + final List> seasonalItems = [ + { + "title": "Boots", + "brand": "Columbia Sportswear", + "discount": "22 - 35% off", + "imagePath": Assets.images.image.path, + }, + { + "title": "Hoodie", + "brand": "The North Face", + "discount": "22 - 35% off", + "imagePath": Assets.images.topDealsAndStores.path, + }, + { + "title": "Hats", + "brand": "Patagonia", + "discount": "10 - 15% off", + "imagePath": Assets.images.image.path, + }, + { + "title": "Jacket", + "brand": "Arc'teryx", + "discount": "20% off", + "imagePath": Assets.images.topDealsAndStores.path, + }, + { + "title": "Backpack", + "brand": "Osprey", + "discount": "15% off", + "imagePath": Assets.images.image.path, + }, + { + "title": "Gloves", + "brand": "Nike", + "discount": "Up to 40% off", + "imagePath": Assets.images.topDealsAndStores.path, + }, + ]; + + final screenWidth = MediaQuery.of(context).size.width; + final cardWidth = screenWidth * 0.8; + final int itemCount = (seasonalItems.length / 2).ceil(); + + return SizedBox( + height: 110 * 2 + 16, // Height for two cards + spacing + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: itemCount, + itemBuilder: (context, index) { + final topIndex = index * 2; + final bottomIndex = topIndex + 1; + + final topCard = SeasonalDiscountCard( + title: seasonalItems[topIndex]['title']!, + brand: seasonalItems[topIndex]['brand']!, + discount: seasonalItems[topIndex]['discount']!, + imagePath: seasonalItems[topIndex]['imagePath']!, + width: cardWidth, + ); + + Widget bottomCard; + if (bottomIndex < seasonalItems.length) { + bottomCard = SeasonalDiscountCard( + title: seasonalItems[bottomIndex]['title']!, + brand: seasonalItems[bottomIndex]['brand']!, + discount: seasonalItems[bottomIndex]['discount']!, + imagePath: seasonalItems[bottomIndex]['imagePath']!, + width: cardWidth, + ); + } else { + bottomCard = SizedBox(width: cardWidth); // Placeholder + } + + return Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + topCard, + const SizedBox(height: 16), + bottomCard, + ], + ), + ); + }, + ), + ); + } + + Widget _buildCraftingSomethingSection() { + return Container( + height: 120, + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + image: DecorationImage( + image: AssetImage(Assets.images.image.path), + fit: BoxFit.cover, + colorFilter: ColorFilter.mode( + Colors.black.withOpacity(0.4), + BlendMode.darken, + ), + ), + ), + child: const Center( + child: Text( + "Crafting something for you\nTell us your birthday and unlock special Dineout treats!", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ); + } + + Widget _buildFirstPurchaseSection() { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFE3F2FD), + borderRadius: BorderRadius.circular(15), + ), + child: Row( + children: [ + FirstPurchaseCard( + title: "McDonald's", + category: "Fast Food", + discount: "Up to 25% Off", + rating: 4.2, + imagePath: Assets.images.wp1929534FastFoodWallpapers1.path, + ), + const SizedBox(width: 16), + FirstPurchaseCard( + title: "Cafe Monarch", + category: "Cafe", + discount: "Up to 25% Off", + rating: 4.9, + imagePath: Assets.images.media.path, + ), + // For testing with more cards: + // const SizedBox(width: 16), + // FirstPurchaseCard( + // title: "Another Cafe", + // category: "Cafe", + // discount: "15% Off", + // rating: 4.5, + // imagePath: Assets.images.media.path, + // ), + ], + ), + ), + ); + } +} + +class FlashSaleCard extends StatelessWidget { + final String imagePath; + final String title; + final String location; + final String originalPrice; + final String discountedPrice; + final String discountPercent; + final String expiryTimeString; + final String categoryIconPath; + + const FlashSaleCard({ + super.key, + required this.imagePath, + required this.title, + required this.location, + required this.originalPrice, + required this.discountedPrice, + required this.discountPercent, + required this.expiryTimeString, + required this.categoryIconPath, + }); + + @override + Widget build(BuildContext context) { + final timer = + RemainingTime() + ..initializeFromExpiry(expiryTimeString: expiryTimeString); + return Container( + decoration: BoxDecoration( + color: LightAppColors.cardBackground, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: SvgPicture.asset( + Assets.icons.timer.path, + color: LightAppColors.offerTimer, + height: 20, + ), + ), + const SizedBox(width: 10), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Limited time deal", + style: TextStyle( + color: Colors.black87, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ValueListenableBuilder( + valueListenable: timer.remainingSeconds, + builder: + (context, _, __) => Text( + timer.formatTime(), + style: const TextStyle( + color: LightAppColors.offerTimer, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 5,) + ], + ), + ], + ), + ), + Image.asset( + imagePath, + height: 100, + width: double.infinity, + fit: BoxFit.cover, + ), + Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SvgPicture.asset( + categoryIconPath, + color: Colors.grey.shade700, + width: 18, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ), + ], + ), + const SizedBox(height: 6), + Row( + children: [ + SvgPicture.asset( + Assets.icons.location.path, + color: Colors.grey.shade700, + width: 18, + ), + const SizedBox(width: 4), + Text( + location, + style: const TextStyle(color: LightAppColors.offerCardDetail, fontSize: 12), + ), + ], + ), + const SizedBox(height: 6), + Row( + children: [ + SvgPicture.asset( + Assets.icons.coin.path, + color: Colors.grey.shade700, + width: 18, + ), + const SizedBox(width: 4), + Text.rich( + TextSpan( + style: const TextStyle( + fontSize: 12, + color: LightAppColors.offerCardDetail , + ), + children: [ + TextSpan( + text: '$originalPrice\$', + style: const TextStyle( + decoration: TextDecoration.lineThrough, + ), + ), + TextSpan( + text: ' - $discountedPrice\$', + style: const TextStyle( + color: LightAppColors.offerCardDetail, + fontWeight: FontWeight.normal, + fontSize: 14, + decoration: TextDecoration.none, + ), + ), + ], + ), + ), + const SizedBox(width: 10,), + Text( + '($discountPercent% off)', + style: const TextStyle( + color: Colors.green, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + 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), + ), + ), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset(Assets.icons.shoppingCart.path,color: Colors.white,width: 20,), + const SizedBox(width: 5,), + const Text( + "Reservation", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class SeasonalDiscountCard extends StatelessWidget { + final String title; + final String brand; + final String discount; + final String imagePath; + final double width; + + const SeasonalDiscountCard({ + super.key, + required this.title, + required this.brand, + required this.discount, + required this.imagePath, + required this.width, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + height: 110, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + children: [ + Expanded( + flex: 2, + child: Container( + color: const Color(0xFFF8F9FA), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildDetailRow( + icon: Assets.icons.winter.path, + text: title, + iconColor: const Color.fromARGB(255, 23, 107, 173), + textStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + _buildDetailRow( + icon: Assets.icons.shop.path, + text: brand, + iconColor: Colors.grey.shade600, + textStyle: const TextStyle( + color: Colors.black, + fontSize: 12, + ), + ), + _buildDetailRow( + icon: Assets.icons.icRoundLocalOffer.path, + text: discount, + iconColor: Colors.grey.shade600, + textStyle: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.normal, + fontSize: 14, + ), + ), + ], + ), + ), + ), + ), + Expanded( + flex: 1, + child: Image.asset( + imagePath, + height: double.infinity, + fit: BoxFit.cover, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildDetailRow({ + required String icon, + required String text, + required Color iconColor, + required TextStyle textStyle, + }) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: SvgPicture.asset( + icon, + color: iconColor, + width: 20, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + text, + style: textStyle, + softWrap: true, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } +} + +class FirstPurchaseCard extends StatelessWidget { + final String title; + final String category; + final String discount; + final double rating; + final String imagePath; + + const FirstPurchaseCard({ + super.key, + required this.title, + required this.category, + required this.discount, + required this.rating, + required this.imagePath, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: 160, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Image.asset( + imagePath, + height: 120, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + const SizedBox(height: 5), + Text( + category, + style: const TextStyle(color: LightAppColors.nearbyPopuphint, fontSize: 14), + ), + const SizedBox(height: 5), + Text( + discount, + style: const TextStyle( + fontWeight: FontWeight.w500, + color: LightAppColors.nearbyPopuphint + ), + ), + const SizedBox(height: 5), + Row( + children: [ + SvgPicture.asset(Assets.icons.star.path, width: 16), + const SizedBox(width: 4), + Text( + rating.toString(), + style: const TextStyle( + color: LightAppColors.nearbyPopuphint, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); } } \ No newline at end of file diff --git a/lib/screens/mains/navigation/navigation.dart b/lib/screens/mains/navigation/navigation.dart index 472b2ec..29f7148 100644 --- a/lib/screens/mains/navigation/navigation.dart +++ b/lib/screens/mains/navigation/navigation.dart @@ -21,11 +21,11 @@ class _MainScreenState extends State { int _currentIndex = 0; final List _screens = [ - Nearby(), - Discover(), - Hunt(), - Planner(), - Profile(), + const Nearby(), + const Discover(), + const Hunt(), + const Planner(), + const Profile(), ]; @override @@ -33,8 +33,33 @@ class _MainScreenState extends State { return Scaffold( extendBody: true, body: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: _screens[_currentIndex], + duration: const Duration(milliseconds: 400), + transitionBuilder: (Widget child, Animation animation) { + final bool isGoingForward = + (child.key as ValueKey).value > _currentIndex; + + final slideAnimation = Tween( + begin: Offset(isGoingForward ? 1.0 : -1.0, 0.0), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: animation, + curve: Curves.easeInOutCubic, + ), + ); + + return SlideTransition( + position: slideAnimation, + child: FadeTransition( + opacity: animation, + child: child, + ), + ); + }, + child: KeyedSubtree( + key: ValueKey(_currentIndex), + child: _screens[_currentIndex], + ), ), bottomNavigationBar: CustomBottomNavigationBar( onTap: (int index) { diff --git a/lib/screens/mains/nearby/bestNearby.dart b/lib/screens/mains/nearby/bestNearby.dart index 2e1a98e..e176294 100644 --- a/lib/screens/mains/nearby/bestNearby.dart +++ b/lib/screens/mains/nearby/bestNearby.dart @@ -2,8 +2,65 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:lba/gen/assets.gen.dart'; -class Bestnearby extends StatelessWidget { - const Bestnearby({super.key,}); +class Bestnearby extends StatefulWidget { + const Bestnearby({ + super.key, + }); + + @override + State createState() => _BestnearbyState(); +} + +class _BestnearbyState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late List> _staggeredAnimations; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + ); + + // --- انیمیشن‌های مرحله‌ای برای هر بخش --- + _staggeredAnimations = List.generate(3, (index) { + return Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: Interval( + (0.2 * index), + (0.6 + 0.2 * index).clamp(0.0, 1.0), + curve: Curves.easeOutCubic, + ), + ), + ); + }); + + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + // --- ویجت انیمیشنی برای هر بخش --- + Widget _buildAnimatedSection(Widget child, int index) { + return FadeTransition( + opacity: _staggeredAnimations[index], + child: SlideTransition( + position: Tween( + begin: const Offset(0.0, 0.5), + end: Offset.zero, + ).animate(_staggeredAnimations[index]), + child: child, + ), + ); + } @override Widget build(BuildContext context) { @@ -18,16 +75,18 @@ class Bestnearby extends StatelessWidget { child: SvgPicture.asset( Assets.icons.back.path, fit: BoxFit.scaleDown, + color: Colors.black, ), ), - title: const Text("Top Deals & Stores", style: TextStyle(fontSize: 18)), + title: const Text("Top Deals & Stores", + style: TextStyle(fontSize: 18, color: Colors.black)), ), body: SingleChildScrollView( child: Column( children: [ - NearbyItems(detailsType: 0), - NearbyItems(detailsType: 1), - NearbyItems(detailsType: 2) + _buildAnimatedSection(NearbyItems(detailsType: 0), 0), + _buildAnimatedSection(NearbyItems(detailsType: 1), 1), + _buildAnimatedSection(NearbyItems(detailsType: 2), 2), ], ), ), @@ -35,49 +94,108 @@ class Bestnearby extends StatelessWidget { } } -class NearbyItems extends StatelessWidget { +class NearbyItems extends StatefulWidget { const NearbyItems({super.key, required this.detailsType}); final int detailsType; + @override + State createState() => _NearbyItemsState(); +} + +class _NearbyItemsState extends State + with SingleTickerProviderStateMixin { + late AnimationController _listController; + late List> _itemAnimations; + + @override + void initState() { + super.initState(); + _listController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1000), + ); + + _itemAnimations = List.generate(5, (index) { + return Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _listController, + curve: Interval( + (0.15 * index), + (0.5 + 0.15 * index).clamp(0.0, 1.0), + curve: Curves.easeInOut, + ), + ), + ); + }); + + _listController.forward(); + } + + @override + void dispose() { + _listController.dispose(); + super.dispose(); + } + + Widget _buildAnimatedItem(Widget child, int index) { + return FadeTransition( + opacity: _itemAnimations[index], + child: SlideTransition( + position: Tween( + begin: const Offset(0.3, 0.0), + end: Offset.zero, + ).animate(_itemAnimations[index]), + child: child, + ), + ); + } + @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ - DividerTitle(detailsType: detailsType), + DividerTitle(detailsType: widget.detailsType), SizedBox( - height: detailsType==0?180:detailsType==1?240:detailsType==2?240:200, + height: widget.detailsType == 0 + ? 180 + : widget.detailsType == 1 + ? 240 + : widget.detailsType == 2 + ? 240 + : 200, child: ListView.builder( itemCount: 5, scrollDirection: Axis.horizontal, itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 7), - Container( - width: 120, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.grey[200], + return _buildAnimatedItem( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 7), + Container( + width: 120, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.grey[200], + ), + child: Image.asset( + Assets.images.topDealsAndStores.path, + ), ), - child: Image.asset( - Assets.images.topDealsAndStores.path, + SizedBox(height: 5), + Text( + "Dubai Outlet Mall", + style: TextStyle(fontWeight: FontWeight.bold), ), - ), - SizedBox(height: 5), - Text( - "Dubai Outlet Mall", - style: TextStyle(fontWeight: FontWeight.bold), - ), - SizedBox(height: 5), - detailsType == 0 - ? Row( + SizedBox(height: 5), + if (widget.detailsType == 0) + Row( children: [ SvgPicture.asset(Assets.icons.routing.path), SizedBox(width: 4), @@ -87,15 +205,16 @@ class NearbyItems extends StatelessWidget { ), ], ) - : detailsType == 1 - ? Column( + else if (widget.detailsType == 1) + Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 2), Row( children: [ - SvgPicture.asset(Assets.icons.category2.path), + SvgPicture.asset( + Assets.icons.category2.path), SizedBox(width: 4), Text("Stationery store"), ], @@ -123,8 +242,8 @@ class NearbyItems extends StatelessWidget { ), ], ) - : detailsType == 2 - ? Column( + else if (widget.detailsType == 2) + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 2), @@ -158,9 +277,10 @@ class NearbyItems extends StatelessWidget { ), ], ) - : SizedBox(), - ], + ], + ), ), + index, ); }, ), @@ -186,7 +306,11 @@ class DividerTitle extends StatelessWidget { child: Row( children: [ Text( - detailsType==0?"Highest Discount Areas":detailsType==1?"Highest Discount Areas":detailsType==2?"Best Deal, Don't Miss":"", + detailsType == 0 + ? "Highest Discount Areas" + : detailsType == 1 + ? "Highest Discount Areas" + : "Best Deal, Don't Miss", style: TextStyle(fontSize: 15), ), const SizedBox(width: 8), @@ -197,4 +321,4 @@ class DividerTitle extends StatelessWidget { ), ); } -} +} \ 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 83bcaf2..20b48c9 100644 --- a/lib/screens/mains/nearby/mainNearby/map.dart +++ b/lib/screens/mains/nearby/mainNearby/map.dart @@ -1,5 +1,3 @@ -// ignore_for_file: library_private_types_in_public_api, avoid_print - import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/screens/mains/nearby/mainNearby/nearby.dart b/lib/screens/mains/nearby/mainNearby/nearby.dart index 6b833a1..f047164 100644 --- a/lib/screens/mains/nearby/mainNearby/nearby.dart +++ b/lib/screens/mains/nearby/mainNearby/nearby.dart @@ -16,16 +16,13 @@ class Nearby extends StatefulWidget { State createState() => _NearbyState(); } -class _NearbyState extends State { +class _NearbyState extends State with TickerProviderStateMixin { String selectedOption = "Relevance"; final List options = ["Relevance", "Newest", "Oldest", "Popularity"]; int selectedIndex = 1; List selectedOptions = []; - @override - void dispose() { - super.dispose(); - } + late AnimationController _listAnimationController; @override void initState() { @@ -35,6 +32,31 @@ class _NearbyState extends State { showGPSDialog(context); } }); + + _listAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1000), + ); + + if (selectedIndex == 1) { + _listAnimationController.forward(); + } + } + + @override + void dispose() { + _listAnimationController.dispose(); + super.dispose(); + } + + void _onToggle(int index) { + setState(() { + selectedIndex = index; + }); + + if (index == 1) { + _listAnimationController.forward(from: 0.0); + } } @override @@ -125,12 +147,29 @@ class _NearbyState extends State { color: const Color.fromARGB(255, 14, 63, 102), ), Expanded( - child: selectedIndex == 1 ? _buildListContent() : CustomMap(), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + transitionBuilder: (Widget child, Animation animation) { + final slideAnimation = Tween( + begin: Offset(child.key == const ValueKey('list') ? -1.0 : 1.0, 0.0), + end: Offset.zero, + ).animate(CurvedAnimation(parent: animation, curve: Curves.easeInOutCubic)); + + return ClipRect( + child: SlideTransition( + position: slideAnimation, + child: FadeTransition( + opacity: animation, + child: child, + ), + ), + ); + }, + child: selectedIndex == 1 + ? _buildListContent(key: const ValueKey('list')) + : const CustomMap(key: ValueKey('map')), + ), ), - // ElevatedButton( - // onPressed: () => showGPSDialog(context), - // child: Text("Test GPS Dialog"), - // ) ], ), ); @@ -179,50 +218,46 @@ class _NearbyState extends State { Positioned( left: 0, child: _buildToggleButton( - color: - selectedIndex == 1 - ? const Color.fromARGB(255, 10, 69, 117) - : const Color.fromARGB(100, 177, 177, 177), + color: selectedIndex == 1 + ? const Color.fromARGB(255, 10, 69, 117) + : const Color.fromARGB(100, 177, 177, 177), icon: Assets.icons.elementEqual.path, - iconColor: - selectedIndex == 1 - ? const Color.fromARGB(255, 234, 245, 254) - : const Color.fromARGB(255, 157, 157, 155), + iconColor: selectedIndex == 1 + ? const Color.fromARGB(255, 234, 245, 254) + : const Color.fromARGB(255, 157, 157, 155), text: 'list', isSelected: selectedIndex == 1, - onTap: () => setState(() => selectedIndex = 1), + onTap: () => _onToggle(1), borderRadius: BorderRadius.only( - topLeft: Radius.circular(12), - bottomLeft: Radius.circular(12), + topLeft: const Radius.circular(12), + bottomLeft: const Radius.circular(12), topRight: - selectedIndex == 1 ? Radius.circular(12) : Radius.zero, + selectedIndex == 1 ? const Radius.circular(12) : Radius.zero, bottomRight: - selectedIndex == 1 ? Radius.circular(12) : Radius.zero, + selectedIndex == 1 ? const Radius.circular(12) : Radius.zero, ), ), ), Positioned( right: 0, child: _buildToggleButton( - color: - selectedIndex == 0 - ? const Color.fromARGB(255, 10, 69, 117) - : const Color.fromARGB(100, 177, 177, 177), - iconColor: - selectedIndex == 0 - ? const Color.fromARGB(255, 234, 245, 254) - : const Color.fromARGB(255, 157, 157, 155), + color: selectedIndex == 0 + ? const Color.fromARGB(255, 10, 69, 117) + : const Color.fromARGB(100, 177, 177, 177), + iconColor: selectedIndex == 0 + ? const Color.fromARGB(255, 234, 245, 254) + : const Color.fromARGB(255, 157, 157, 155), icon: Assets.icons.mapSelected.path, text: 'map', isSelected: selectedIndex == 0, - onTap: () => setState(() => selectedIndex = 0), + onTap: () => _onToggle(0), borderRadius: BorderRadius.only( - topRight: Radius.circular(12), - bottomRight: Radius.circular(12), + topRight: const Radius.circular(12), + bottomRight: const Radius.circular(12), bottomLeft: - selectedIndex == 0 ? Radius.circular(12) : Radius.zero, + selectedIndex == 0 ? const Radius.circular(12) : Radius.zero, topLeft: - selectedIndex == 0 ? Radius.circular(12) : Radius.zero, + selectedIndex == 0 ? const Radius.circular(12) : Radius.zero, ), ), ), @@ -232,8 +267,32 @@ class _NearbyState extends State { ); } - Widget _buildListContent() { + Widget _buildListContent({Key? key}) { + final listItems = [ + ListScreen( + delivery: true, + pickup: false, + expiryTimeString: "2025-05-19T13:30:49.623619357Z", + ontap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const Productdetail()), + ); + }, + ), + ListScreen( + delivery: false, + pickup: true, + expiryTimeString: "2025-05-25T05:20:49.623619357Z", + ), + ListScreen( + delivery: true, + pickup: true, + expiryTimeString: "2025-05-12T13:20:37.435249520Z", + ), + ]; + return SingleChildScrollView( + key: key, child: Padding( padding: const EdgeInsets.fromLTRB(30, 5, 30, 0), child: Column( @@ -252,8 +311,7 @@ class _NearbyState extends State { child: SvgPicture.asset(Assets.icons.arrowDown.path), ), elevation: 16, - dropdownColor: - Colors.white, + dropdownColor: Colors.white, style: const TextStyle( color: Color.fromARGB(255, 33, 150, 243), fontWeight: FontWeight.w500, @@ -266,35 +324,42 @@ class _NearbyState extends State { }, items: options.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), ), ], ), const SizedBox(height: 10), - ListScreen( - delivery: true, - pickup: false, - expiryTimeString: "2025-05-19T13:30:49.623619357Z", - ontap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => Productdetail()), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: listItems.length, + itemBuilder: (context, index) { + final animation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _listAnimationController, + curve: Interval( + (0.1 * index), + (0.5 + 0.1 * index).clamp(0.0, 1.0), + curve: Curves.easeOutCubic, + ), + ), + ); + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 0.5), + end: Offset.zero, + ).animate(animation), + child: listItems[index], + ), ); }, ), - ListScreen( - delivery: false, - pickup: true, - expiryTimeString: "2025-05-25T05:20:49.623619357Z", - ), - ListScreen( - delivery: true, - pickup: true, - expiryTimeString: "2025-05-12T13:20:37.435249520Z", - ), const SizedBox(height: 90), ], ), @@ -320,21 +385,19 @@ class _NearbyState extends State { height: 50, decoration: BoxDecoration( borderRadius: borderRadius, - color: - isSelected - ? const Color.fromARGB(255, 14, 63, 102) - : const Color.fromARGB(255, 234, 234, 233), - boxShadow: - isSelected - ? [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - spreadRadius: 1, - blurRadius: 4, - offset: const Offset(0, 2), - ), - ] - : null, + color: isSelected + ? const Color.fromARGB(255, 14, 63, 102) + : const Color.fromARGB(255, 234, 234, 233), + boxShadow: isSelected + ? [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ] + : null, ), alignment: Alignment.center, child: Row( @@ -376,4 +439,4 @@ class _NearbyState extends State { ), ); } -} +} \ No newline at end of file diff --git a/lib/screens/product/item.dart b/lib/screens/product/item.dart index 19deec2..4d20642 100644 --- a/lib/screens/product/item.dart +++ b/lib/screens/product/item.dart @@ -35,6 +35,9 @@ class _ItemState extends State with TickerProviderStateMixin { bool showAllReviews = false; final GlobalKey _listKey = GlobalKey(); + late AnimationController _staggeredController; + late List> _staggeredAnimations; + final List> reviews = [ { "name": "Sara M", @@ -80,6 +83,34 @@ class _ItemState extends State with TickerProviderStateMixin { void initState() { super.initState(); displayedReviews = reviews.length > 2 ? reviews.sublist(0, 2) : List.from(reviews); + + _staggeredController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + ); + + // --- FIX: Changed itemCount from 7 to 8 --- + final int itemCount = 8; + _staggeredAnimations = List.generate(itemCount, (index) { + return Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _staggeredController, + curve: Interval( + (0.1 * index), + (0.5 + 0.1 * index).clamp(0.0, 1.0), + curve: Curves.easeOutCubic, + ), + ), + ); + }); + + _staggeredController.forward(); + } + + @override + void dispose() { + _staggeredController.dispose(); + super.dispose(); } void _showAllReviewsWithAnimation() async { @@ -95,6 +126,19 @@ class _ItemState extends State with TickerProviderStateMixin { }); } + Widget _buildAnimatedWidget(Widget child, int index) { + return FadeTransition( + opacity: _staggeredAnimations[index], + child: SlideTransition( + position: Tween( + begin: const Offset(0.0, 0.3), + end: Offset.zero, + ).animate(_staggeredAnimations[index]), + child: child, + ), + ); + } + @override Widget build(BuildContext context) { return Padding( @@ -103,58 +147,70 @@ class _ItemState extends State with TickerProviderStateMixin { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - widget.title, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + _buildAnimatedWidget( + Align( + alignment: Alignment.centerLeft, + child: Text( + widget.title, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), ), + 0, ), SizedBox(height: 5), - Row( - children: [ - Text("Brand: "), - Expanded( - child: Text( - widget.brand, - style: TextStyle(color: LightAppColors.primary), - overflow: TextOverflow.ellipsis, + _buildAnimatedWidget( + Row( + children: [ + Text("Brand: "), + Expanded( + child: Text( + widget.brand, + style: TextStyle(color: LightAppColors.primary), + overflow: TextOverflow.ellipsis, + ), ), - ), - ], + ], + ), + 1, ), SizedBox(height: 10), - Row( - children: [ - CustomStarRating(rating: 4.8), - SizedBox(width: 3), - Text( - "4.8", - style: TextStyle(color: LightAppColors.productDetailDivider), - ), - ], + _buildAnimatedWidget( + Row( + children: [ + CustomStarRating(rating: 4.8), + SizedBox(width: 3), + Text( + "4.8", + style: TextStyle(color: LightAppColors.productDetailDivider), + ), + ], + ), + 2, ), SizedBox(height: 15), - Row( - children: [ - OrderType( - icon: Assets.icons.cardPos.path, - typename: "Delivery", - fill: true, - ), - SizedBox(width: 7), - OrderType( - icon: Assets.icons.shoppingCart.path, - typename: "Pickup", - fill: true, - ), - ], + _buildAnimatedWidget( + Row( + children: [ + OrderType( + icon: Assets.icons.cardPos.path, + typename: "Delivery", + fill: true, + ), + SizedBox(width: 7), + OrderType( + icon: Assets.icons.shoppingCart.path, + typename: "Pickup", + fill: true, + ), + ], + ), + 3, ), SizedBox(height: 15), - buildWrappedInfo("Dimensions:", widget.dimensions), - buildWrappedInfo("Colour:", widget.colour), - buildWrappedInfo("Material:", widget.material), - buildWrappedInfo("Description:", widget.description), + _buildAnimatedWidget(buildWrappedInfo("Dimensions:", widget.dimensions), 4), + _buildAnimatedWidget(buildWrappedInfo("Colour:", widget.colour), 5), + _buildAnimatedWidget(buildWrappedInfo("Material:", widget.material), 6), + _buildAnimatedWidget(buildWrappedInfo("Description:", widget.description), 7), // Now this is valid SizedBox(height: 30), Center( child: Text( @@ -171,16 +227,22 @@ class _ItemState extends State with TickerProviderStateMixin { initialItemCount: displayedReviews.length, itemBuilder: (context, index, animation) { final review = displayedReviews[index]; - return SizeTransition( - sizeFactor: animation, - axisAlignment: -1, - child: Reviews( - name: review['name'], - comment: review['comment'], - rate: review['rate'], - yesCount: review['yesCount'], - noCount: review['noCount'], - date: review['date'], + return SlideTransition( + position: Tween( + begin: const Offset(-1, 0), + end: Offset.zero, + ).animate(CurvedAnimation(parent: animation, curve: Curves.easeInOut)), + child: SizeTransition( + sizeFactor: animation, + axisAlignment: -1, + child: Reviews( + name: review['name'], + comment: review['comment'], + rate: review['rate'], + yesCount: review['yesCount'], + noCount: review['noCount'], + date: review['date'], + ), ), ); }, @@ -220,4 +282,4 @@ class _ItemState extends State with TickerProviderStateMixin { ), ); } -} +} \ No newline at end of file diff --git a/lib/screens/product/productdetail.dart b/lib/screens/product/productdetail.dart index 9fe9443..31c77ee 100644 --- a/lib/screens/product/productdetail.dart +++ b/lib/screens/product/productdetail.dart @@ -1,5 +1,3 @@ -// ignore_for_file: deprecated_member_use - import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:lba/data/model/workingHours.dart'; diff --git a/lib/screens/product/shop.dart b/lib/screens/product/shop.dart index 1576905..dbb6b07 100644 --- a/lib/screens/product/shop.dart +++ b/lib/screens/product/shop.dart @@ -33,15 +33,58 @@ class Shop extends StatefulWidget { State createState() => _ShopState(); } -class _ShopState extends State { +class _ShopState extends State with TickerProviderStateMixin { late String selectedPlace; bool isActiveOffer = true; + late AnimationController _staggeredController; + late List> _staggeredAnimations; + @override void initState() { super.initState(); selectedPlace = widget.place; + + _staggeredController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + ); + + final int itemCount = 8; + _staggeredAnimations = List.generate(itemCount, (index) { + return Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _staggeredController, + curve: Interval( + (0.1 * index), + (0.5 + 0.1 * index).clamp(0.0, 1.0), + curve: Curves.easeOutCubic, + ), + ), + ); + }); + + _staggeredController.forward(); + } + + @override + void dispose() { + _staggeredController.dispose(); + super.dispose(); + } + + Widget _buildAnimatedWidget(Widget child, int index) { + return FadeTransition( + opacity: _staggeredAnimations[index], + child: SlideTransition( + position: Tween( + begin: const Offset(0.0, 0.5), + end: Offset.zero, + ).animate(_staggeredAnimations[index]), + child: child, + ), + ); } @override @@ -64,155 +107,176 @@ class _ShopState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - SvgPicture.asset( - Assets.icons.shop.path, - color: Colors.black, - height: 25, - ), - SizedBox(width: 10), - Text( - widget.shopName, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17), - ), - ], - ), - SizedBox(height: 15), - Row( - children: [ - CustomStarRating(rating: widget.star), - SizedBox(width: 5), - Text( - widget.star.toString(), - style: TextStyle(color: LightAppColors.productDetailDivider), - ), - ], - ), - SizedBox(height: 5), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgPicture.asset( - Assets.icons.location.path, - color: Colors.black, - ), - SizedBox(width: 5), - Text( - selectedPlace, - style: TextStyle( - color: LightAppColors.productDetailDivider, - ), - ), - ], - ), - PopupMenuButton( - color: Colors.white, - icon: SvgPicture.asset( - Assets.icons.arrowDown.path, + _buildAnimatedWidget( + Row( + children: [ + SvgPicture.asset( + Assets.icons.shop.path, color: Colors.black, - height: 20, + height: 25, ), - onSelected: (String newPlace) { - setState(() { - selectedPlace = newPlace; - }); - }, - itemBuilder: (BuildContext context) { - return widget.branches.map((String branch) { - return PopupMenuItem( - value: branch, - child: Text(branch), - ); - }).toList(); - }, - ), - ], - ), - SizedBox(height: 5), - Row( - children: [ - SvgPicture.asset(Assets.icons.clock.path), - SizedBox(width: 5), - Text( - isOpen ? 'Open Now' : 'Closed', - style: TextStyle(color: isOpen ? Colors.green : Colors.red), - ), - SizedBox(width: 5), - Container( - color: LightAppColors.productDetailDivider, - width: 1, - height: 13, - ), - SizedBox(width: 5), - Text( - timeRange, - style: TextStyle(color: LightAppColors.productDetailDivider), - ), - ], + SizedBox(width: 10), + Text( + widget.shopName, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17), + ), + ], + ), + 0, ), SizedBox(height: 15), - DividerTitleWidget(title: "Facilities"), - SizedBox( - height: 50, - child: ListView.builder( - physics: NeverScrollableScrollPhysics(), - itemCount: 1, - itemBuilder: (context, index) { - return Row( + _buildAnimatedWidget( + Row( + children: [ + CustomStarRating(rating: widget.star), + SizedBox(width: 5), + Text( + widget.star.toString(), + style: TextStyle(color: LightAppColors.productDetailDivider), + ), + ], + ), + 1, + ), + SizedBox(height: 5), + _buildAnimatedWidget( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( children: [ - SvgPicture.asset(Assets.icons.tick.path), + SvgPicture.asset( + Assets.icons.location.path, + color: Colors.black, + ), SizedBox(width: 5), Text( - widget.facilities, + selectedPlace, style: TextStyle( color: LightAppColors.productDetailDivider, ), ), ], - ); - }, - ), - ), - SizedBox(height: 20), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: LightAppColors.confirmButton, - minimumSize: const Size(double.infinity, 48), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - onPressed: () { - MapsLauncher.launchCoordinates(25.2399, 55.2744); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.icons.routing2.path, - color: Colors.black, ), - SizedBox(width: 5), - Text( - "Direction", - style: const TextStyle( + PopupMenuButton( + color: Colors.white, + icon: SvgPicture.asset( + Assets.icons.arrowDown.path, color: Colors.black, - fontSize: 16, + height: 20, ), + onSelected: (String newPlace) { + setState(() { + selectedPlace = newPlace; + }); + }, + itemBuilder: (BuildContext context) { + return widget.branches.map((String branch) { + return PopupMenuItem( + value: branch, + child: Text(branch), + ); + }).toList(); + }, ), ], ), + 2, + ), + SizedBox(height: 5), + _buildAnimatedWidget( + Row( + children: [ + SvgPicture.asset(Assets.icons.clock.path), + SizedBox(width: 5), + Text( + isOpen ? 'Open Now' : 'Closed', + style: TextStyle(color: isOpen ? Colors.green : Colors.red), + ), + SizedBox(width: 5), + Container( + color: LightAppColors.productDetailDivider, + width: 1, + height: 13, + ), + SizedBox(width: 5), + Text( + timeRange, + style: TextStyle(color: LightAppColors.productDetailDivider), + ), + ], + ), + 3, + ), + SizedBox(height: 15), + _buildAnimatedWidget(DividerTitleWidget(title: "Facilities"), 4), + _buildAnimatedWidget( + SizedBox( + height: 50, + child: ListView.builder( + physics: NeverScrollableScrollPhysics(), + itemCount: 1, + itemBuilder: (context, index) { + return Row( + children: [ + SvgPicture.asset(Assets.icons.tick.path), + SizedBox(width: 5), + Text( + widget.facilities, + style: TextStyle( + color: LightAppColors.productDetailDivider, + ), + ), + ], + ); + }, + ), + ), + 5, + ), + SizedBox(height: 20), + _buildAnimatedWidget( + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: LightAppColors.confirmButton, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + onPressed: () { + MapsLauncher.launchCoordinates(25.2399, 55.2744); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.icons.routing2.path, + color: Colors.black, + ), + SizedBox(width: 5), + Text( + "Direction", + style: const TextStyle( + color: Colors.black, + fontSize: 16, + ), + ), + ], + ), + ), + 6, ), SizedBox(height: 30), - Text( - "List of offers", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, + _buildAnimatedWidget( + Text( + "List of offers", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), ), + 7, ), SizedBox(height: 15), Container( diff --git a/lib/widgets/animated_splash_screen.dart b/lib/widgets/animated_splash_screen.dart new file mode 100644 index 0000000..4dbb50c --- /dev/null +++ b/lib/widgets/animated_splash_screen.dart @@ -0,0 +1,174 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import '../gen/assets.gen.dart'; + +class CoolSplashScreen extends StatefulWidget { + final Widget nextScreen; + final Duration duration; + + const CoolSplashScreen({ + super.key, + required this.nextScreen, + this.duration = const Duration(seconds: 6), + }); + + @override + State createState() => _CoolSplashScreenState(); +} + +class _CoolSplashScreenState extends State + with TickerProviderStateMixin { + late AnimationController _mainController; + late Animation _backgroundAnimation; + late Animation _particlesAnimation; + late Animation _logoAnimation; + late Animation _textAnimation; + + @override + void initState() { + super.initState(); + + _mainController = AnimationController( + duration: const Duration(milliseconds: 4000), + vsync: this, + ); + + _backgroundAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _mainController, + curve: const Interval(0.0, 0.5, curve: Curves.easeInOut), + ), + ); + + _particlesAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _mainController, + curve: const Interval(0.2, 0.7, curve: Curves.easeOut), + ), + ); + + _logoAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _mainController, + curve: const Interval(0.5, 0.9, curve: Curves.elasticOut), + ), + ); + + _textAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _mainController, + curve: const Interval(0.7, 1.0, curve: Curves.easeOut), + ), + ); + + _mainController.forward(); + + Future.delayed(widget.duration, () { + if (mounted) { + Navigator.of(context).pushReplacement( + PageRouteBuilder( + pageBuilder: (_, __, ___) => widget.nextScreen, + transitionDuration: const Duration(milliseconds: 700), + transitionsBuilder: (_, animation, __, child) { + return FadeTransition(opacity: animation, child: child); + }, + ), + ); + } + }); + } + + @override + void dispose() { + _mainController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: AnimatedBuilder( + animation: _mainController, + builder: (context, child) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color.lerp(const Color(0xFFF5F5F5), const Color(0xFFE0E0E0), _backgroundAnimation.value)!, + Color.lerp(const Color(0xFFFFFFFF), const Color(0xFFF5F5F5), _backgroundAnimation.value)!, + ], + ), + ), + child: Stack( + children: [ + for (int i = 0; i < 30; i++) + Particle( + animation: _particlesAnimation, + size: Random().nextDouble() * 5 + 2, + color: Colors.blueGrey.withOpacity(Random().nextDouble() * 0.3 + 0.1), + alignment: Alignment( + (Random().nextDouble() * 2 - 1) * (2 - _particlesAnimation.value), + (Random().nextDouble() * 2 - 1) * (2 - _particlesAnimation.value), + ), + ), + + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Transform.scale( + scale: _logoAnimation.value, + child: SvgPicture.asset( + Assets.images.logo.path, + height: 140, + width: 140, + ), + ), + const SizedBox(height: 30), + ], + ), + ), + ], + ), + ); + }, + ), + ); + } +} + +class Particle extends StatelessWidget { + final Animation animation; + final double size; + final Color color; + final Alignment alignment; + + const Particle({ + super.key, + required this.animation, + required this.size, + required this.color, + required this.alignment, + }); + + @override + Widget build(BuildContext context) { + return Align( + alignment: alignment, + child: Opacity( + opacity: animation.value, + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/gpsPopup.dart b/lib/widgets/gpsPopup.dart index 5d282fb..bb60eb0 100644 --- a/lib/widgets/gpsPopup.dart +++ b/lib/widgets/gpsPopup.dart @@ -6,7 +6,6 @@ import 'package:geolocator/geolocator.dart'; import 'package:lba/gen/assets.gen.dart'; import 'package:lba/res/colors.dart'; - Future showGPSDialog(BuildContext context) async { bool isLocationEnabled = await Geolocator.isLocationServiceEnabled(); if (!isLocationEnabled) { @@ -14,8 +13,61 @@ Future showGPSDialog(BuildContext context) async { context: context, barrierDismissible: false, builder: (BuildContext context) { - return Dialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + return const _AnimatedGpsDialog(); + }, + ); + } +} + +class _AnimatedGpsDialog extends StatefulWidget { + const _AnimatedGpsDialog({Key? key}) : super(key: key); + + @override + State<_AnimatedGpsDialog> createState() => _AnimatedGpsDialogState(); +} + +class _AnimatedGpsDialogState extends State<_AnimatedGpsDialog> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 450), + ); + + _scaleAnimation = CurvedAnimation( + parent: _controller, + curve: Curves.elasticOut, + ); + + _fadeAnimation = CurvedAnimation( + parent: _controller, + curve: Curves.easeIn, + ); + + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _fadeAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + child: Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 10, backgroundColor: Colors.white, child: Stack( @@ -23,14 +75,15 @@ Future showGPSDialog(BuildContext context) async { alignment: Alignment.topCenter, children: [ Padding( - padding: const EdgeInsets.only(top: 40, left: 20, right: 20, bottom: 20), + padding: const EdgeInsets.only( + top: 40, left: 20, right: 20, bottom: 20), child: Column( mainAxisSize: MainAxisSize.min, children: [ - SizedBox(height: 10), - Padding( - padding: const EdgeInsets.all(15.0), - child: const Text( + const SizedBox(height: 10), + const Padding( + padding: EdgeInsets.all(15.0), + child: Text( "Enable Location", style: TextStyle( fontSize: 16, @@ -60,11 +113,11 @@ Future showGPSDialog(BuildContext context) async { foregroundColor: Colors.black, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), - side: BorderSide(color: Colors.grey), + side: const BorderSide(color: Colors.grey), ), ), onPressed: () => Navigator.of(context).pop(), - child: Text("Not now"), + child: const Text("Not now"), ), ElevatedButton( style: ElevatedButton.styleFrom( @@ -72,9 +125,11 @@ Future showGPSDialog(BuildContext context) async { foregroundColor: Colors.black, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), - side: BorderSide(color: LightAppColors.inputBorder), + side: BorderSide( + color: LightAppColors.inputBorder), ), - padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 7), + padding: const EdgeInsets.symmetric( + horizontal: 35, vertical: 7), ), onPressed: () async { await Geolocator.openLocationSettings(); @@ -99,7 +154,7 @@ Future showGPSDialog(BuildContext context) async { BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 8, - offset: Offset(0, 4), + offset: const Offset(0, 4), ), ], ), @@ -108,15 +163,16 @@ Future showGPSDialog(BuildContext context) async { radius: 40, child: Padding( padding: const EdgeInsets.all(12.0), - child: SvgPicture.asset(Assets.icons.fluentColorLocationRipple16.path), + child: SvgPicture.asset( + Assets.icons.fluentColorLocationRipple16.path), ), ), ), ), ], ), - ); - }, + ), + ), ); } -} +} \ No newline at end of file diff --git a/lib/widgets/search_bar.dart b/lib/widgets/search_bar.dart new file mode 100644 index 0000000..81d57d5 --- /dev/null +++ b/lib/widgets/search_bar.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:lba/gen/assets.gen.dart'; + +class SearchBarWidget extends StatelessWidget { + const SearchBarWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 15), + child: SizedBox( + height: 50, + child: TextField( + decoration: InputDecoration( + hintText: 'What are you looking for?', + hintStyle: const TextStyle( + color: Colors.grey, + fontSize: 13, + ), + prefixIcon: Padding( + padding: const EdgeInsets.all(10.0), + child: SvgPicture.asset( + Assets.icons.riSearch2Line.path, + width: 18, + height: 18, + ), + ), + filled: true, + fillColor: const Color.fromARGB(255, 248, 248, 248), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.0), + borderSide: const BorderSide( + color: Color.fromARGB(255, 14, 63, 102), + width: 2.0, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.0), + borderSide: const BorderSide( + color: Color.fromARGB(255, 14, 63, 102), + width: 1.0, + ), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 53627da..8099960 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -241,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + dots_indicator: + dependency: "direct main" + description: + name: dots_indicator + sha256: c070af5058a084ba7b354df4b4c26c719595d70a3531eea6edd8af8716684ba3 + url: "https://pub.dev" + source: hosted + version: "4.0.1" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e2745f6..53d2b62 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,7 @@ dependencies: latlong2: ^0.9.1 location: ^8.0.0 maps_launcher: ^3.0.0+1 + dots_indicator: ^4.0.1 dev_dependencies: flutter_test: