diff --git a/assets/icons/select_photo.png b/assets/icons/select_photo.png new file mode 100644 index 0000000..3e86ce0 Binary files /dev/null and b/assets/icons/select_photo.png differ diff --git a/lib/common_packages/app_bar.dart b/lib/common_packages/app_bar.dart index 85c9106..80e33f3 100644 --- a/lib/common_packages/app_bar.dart +++ b/lib/common_packages/app_bar.dart @@ -11,39 +11,51 @@ class CommonAppBar extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return Column( children: [ - Image.asset( - isWhiteLogo ? "assets/logo/logo_city_cards_white.png" :"assets/logo/logo_city_cards.png", - scale: 4,), Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: Image.asset( - "assets/icons/shopping_cart.png", - height: 20.h, - ), + Image.asset( + isWhiteLogo ? "assets/logo/logo_city_cards_white.png" :"assets/logo/logo_city_cards.png", + scale: 4,), + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Image.asset( + "assets/icons/shopping_cart.png", + height: 20.h, + ), + ), + SizedBox(width: 8.w), + if(!isProfilePage) + GestureDetector( + onTap: (){ + Navigator.of(context, rootNavigator: true) + .pushNamed(RouteConstants.profile); + }, + child: CircleAvatar( + backgroundColor: Color(0xffFFDFDF), + backgroundImage: + AssetImage("assets/images/profile_img.png")), + ), + ], ), - SizedBox(width: 8.w), - if(!isProfilePage) - GestureDetector( - onTap: (){ - Navigator.of(context, rootNavigator: true) - .pushNamed(RouteConstants.profile); - }, - child: CircleAvatar( - backgroundColor: Color(0xffFFDFDF), - backgroundImage: - AssetImage("assets/images/profile_img.png")), - ), ], ), + if(!isWhiteLogo) + Column( + children: [ + SizedBox(height: 12.h), + Divider(height: 1.h, color: Color(0xFFD9D9D9)), + SizedBox(height: 22.h), + ], + ) ], ); } diff --git a/lib/core/inside_bottom_navigator.dart b/lib/core/inside_bottom_navigator.dart new file mode 100644 index 0000000..cba13a3 --- /dev/null +++ b/lib/core/inside_bottom_navigator.dart @@ -0,0 +1,62 @@ +import 'package:citycards_customer/core/route_constants.dart'; +import 'package:citycards_customer/postcard/views/add_filter_step_page_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../attractions/views/attractions_page_view.dart'; +import '../postcard/blocs/postcard_creation_bloc.dart'; +import '../postcard/views/postcard_creation_page_view.dart'; + +Widget buildOffstageNavigator( + int index, + int currentIndex, + Widget child, + Key key, + ) { + return Offstage( + offstage: currentIndex != index, + child: Navigator( + key: key, + onGenerateRoute: (RouteSettings settings) { + switch (settings.name) { + case '/': + return MaterialPageRoute(builder: (_) => child); + + // 🔹 Attractions Page + case RouteConstants.attractionsPage: + return MaterialPageRoute( + builder: (_) => const AttractionsPage(), + ); + + // 🔹 Upload Photo Page (start of postcard creation flow) + case RouteConstants.uploadPhotoPage: + return MaterialPageRoute( + builder: (_) => BlocProvider( + create: (_) => PostcardCreationBloc(), + child: const PostcardCreationPage(), + ), + ); + + // 🔹 Add Filter Page (uses same bloc instance) + case RouteConstants.addFilterPage: + return MaterialPageRoute( + builder: (context) { + final previousBloc = BlocProvider.of(context); + return BlocProvider.value( + value: previousBloc, + child: const AddFilterStepPageView(), + ); + }, + ); + + default: + return MaterialPageRoute( + builder: (_) => const Scaffold( + body: Center(child: Text('Page not found')), + ), + ); + } + }, + ), + ); +} diff --git a/lib/core/route_constants.dart b/lib/core/route_constants.dart index 935ee64..80b884f 100644 --- a/lib/core/route_constants.dart +++ b/lib/core/route_constants.dart @@ -3,6 +3,9 @@ class RouteConstants { /****************************** HOME SECTION ************************************/ static const String home = '/home'; static const String attractionsPage = "/attractions"; + static const String postCardPage = "/postcards"; + static const String uploadPhotoPage = "/uploadPhoto"; + static const String addFilterPage = "/addFilter"; /* ****************************** Profile Section **************************/ diff --git a/lib/esim_offer/esim_offer_view.dart b/lib/esim_offer/esim_offer_view.dart index 71317f6..f15ec33 100644 --- a/lib/esim_offer/esim_offer_view.dart +++ b/lib/esim_offer/esim_offer_view.dart @@ -311,7 +311,6 @@ class EsimOfferPage extends StatelessWidget { ), ], ), - SizedBox(height: 150.h,) ], ), ), diff --git a/lib/home/views/first_time_user_home_page.dart b/lib/home/views/first_time_user_home_page.dart index c918ee0..db9e87e 100644 --- a/lib/home/views/first_time_user_home_page.dart +++ b/lib/home/views/first_time_user_home_page.dart @@ -81,183 +81,186 @@ class _FirstTimeUserHomePageState extends State { @override Widget build(BuildContext context) { return SafeArea( - child: SingleChildScrollView( - child: Stack( - children: [ - Image.asset("assets/images/home_bg.png"), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: false), - SizedBox(height: 140.h), - Text( - "CityCards.\nSee More,\nSpend Less.", - style: TextStyle( - fontSize: 44.sp, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - SizedBox(height: 8.h), - const Text( - "Instant QR access to 40+ attractions,\nexclusive perks, and savings up to 30%", - style: TextStyle(color: Colors.white), - ), - SizedBox(height: 20.h), - ElevatedButton( - style: ElevatedButton.styleFrom( - fixedSize: const Size(200, 50), - padding: EdgeInsets.symmetric( - horizontal: 15.w, - vertical: 15.h, - ), - backgroundColor: const Color(0xffF95F62), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(25.r), + child: Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + child: Stack( + children: [ + Image.asset("assets/images/home_bg.png"), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: true, isProfilePage: false), + SizedBox(height: 140.h), + Text( + "CityCards.\nSee More,\nSpend Less.", + style: TextStyle( + fontSize: 44.sp, + fontWeight: FontWeight.bold, + color: Colors.white, ), ), - onPressed: () {}, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - "Get You'r CityCard", - style: TextStyle(color: Colors.white), - ), - SizedBox(width: 10.w), - Image.asset("assets/icons/arrow.png", height: 13.h), - ], + SizedBox(height: 8.h), + const Text( + "Instant QR access to 40+ attractions,\nexclusive perks, and savings up to 30%", + style: TextStyle(color: Colors.white), ), - ), - SizedBox(height: 80.h), - Text.rich( - TextSpan( - children: [ - TextSpan( - text: "Explore ", - style: TextStyle( - fontSize: 24.sp, - fontWeight: FontWeight.w500, - color: Color(0xffF95F62), + SizedBox(height: 20.h), + ElevatedButton( + style: ElevatedButton.styleFrom( + fixedSize: const Size(200, 50), + padding: EdgeInsets.symmetric( + horizontal: 15.w, + vertical: 15.h, + ), + backgroundColor: const Color(0xffF95F62), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25.r), + ), + ), + onPressed: () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "Get You'r CityCard", + style: TextStyle(color: Colors.white), ), - ), - TextSpan( - text: "Cities", - style: TextStyle( - fontSize: 24, - color: Colors.black, - fontWeight: FontWeight.w500, + SizedBox(width: 10.w), + Image.asset("assets/icons/arrow.png", height: 13.h), + ], + ), + ), + SizedBox(height: 80.h), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Explore ", + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.w500, + color: Color(0xffF95F62), + ), ), - ), - ], + TextSpan( + text: "Cities", + style: TextStyle( + fontSize: 24, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + ], + ), ), - ), - SizedBox(height: 8.h), - const Text( - "Explore your dream destination and experience various attractions.", - style: TextStyle(color: Color(0xff676D75)), - ), - SizedBox(height: 16.sp), - - // Horizontal cards - SizedBox( - height: 270.h, - child: ListView.builder( - controller: _scrollController, - scrollDirection: Axis.horizontal, - itemCount: featuredCities.length, - itemBuilder: (context, index) { - final city = featuredCities[index]; - return ExploreCitiesCard( - name: city['name']!, - description: city['description']!, - imageUrl: city['image']!, - individualPrice: city['individualTicket']!, - cityCardPrice: city['cityCard']!, - savingsText: city['savings']!, - ); - }, + SizedBox(height: 8.h), + const Text( + "Explore your dream destination and experience various attractions.", + style: TextStyle(color: Color(0xff676D75)), ), - ), + SizedBox(height: 16.sp), - SizedBox(height: 10.h), - SizedBox(height: 10.h), - Align( - alignment: Alignment.center, - child: SizedBox( - width: 200.w, - child: ClipRRect( - borderRadius: BorderRadius.circular(10.r), - child: LinearProgressIndicator( - value: _scrollProgress, - minHeight: 6.h, - backgroundColor: Color(0xffFEE7E7), - color: const Color(0xffF95F62), + // Horizontal cards + SizedBox( + height: 270.h, + child: ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + itemCount: featuredCities.length, + itemBuilder: (context, index) { + final city = featuredCities[index]; + return ExploreCitiesCard( + name: city['name']!, + description: city['description']!, + imageUrl: city['image']!, + individualPrice: city['individualTicket']!, + cityCardPrice: city['cityCard']!, + savingsText: city['savings']!, + ); + }, + ), + ), + + SizedBox(height: 10.h), + SizedBox(height: 10.h), + Align( + alignment: Alignment.center, + child: SizedBox( + width: 200.w, + child: ClipRRect( + borderRadius: BorderRadius.circular(10.r), + child: LinearProgressIndicator( + value: _scrollProgress, + minHeight: 6.h, + backgroundColor: Color(0xffFEE7E7), + color: const Color(0xffF95F62), + ), ), ), ), - ), - SizedBox(height: 40.h), - Text.rich( - TextSpan( - children: [ - TextSpan( - text: "Upcoming ", - style: TextStyle( - fontSize: 24.sp, - fontWeight: FontWeight.w500, - color: Color(0xffF95F62), + SizedBox(height: 40.h), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Upcoming ", + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.w500, + color: Color(0xffF95F62), + ), ), - ), - TextSpan( - text: "Cities", - style: TextStyle( - fontSize: 24.sp, - color: Colors.black, - fontWeight: FontWeight.w500, + TextSpan( + text: "Cities", + style: TextStyle( + fontSize: 24.sp, + color: Colors.black, + fontWeight: FontWeight.w500, + ), ), - ), - ], + ], + ), ), - ), - SizedBox(height: 8.h), - Text( - "Explore your dream destination and experience various attractions.", - style: TextStyle(color: Colors.grey[600]), - ), - SizedBox(height: 16.h), - SizedBox( - height: 80.h, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: upcomingCities.length, - separatorBuilder: (_, __) => SizedBox(width: 16.w), - itemBuilder: (context, index) { - return Column( - children: [ - CircleAvatar( - radius: 28.r, - backgroundImage: AssetImage( - upcomingCities[index]["image"] ?? "", + SizedBox(height: 8.h), + Text( + "Explore your dream destination and experience various attractions.", + style: TextStyle(color: Colors.grey[600]), + ), + SizedBox(height: 16.h), + SizedBox( + height: 80.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: upcomingCities.length, + separatorBuilder: (_, __) => SizedBox(width: 16.w), + itemBuilder: (context, index) { + return Column( + children: [ + CircleAvatar( + radius: 28.r, + backgroundImage: AssetImage( + upcomingCities[index]["image"] ?? "", + ), ), - ), - SizedBox(height: 4.h), - Text( - upcomingCities[index]["name"] ?? "", - style: TextStyle(fontSize: 12.sp), - ), - ], - ); - }, + SizedBox(height: 4.h), + Text( + upcomingCities[index]["name"] ?? "", + style: TextStyle(fontSize: 12.sp), + ), + ], + ); + }, + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/home/views/home_page_view.dart b/lib/home/views/home_page_view.dart index f1ad21f..45b25ae 100644 --- a/lib/home/views/home_page_view.dart +++ b/lib/home/views/home_page_view.dart @@ -1,10 +1,10 @@ import 'package:citycards_customer/home/views/registered_user_home_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../attractions/views/attractions_page_view.dart'; import '../../common_bloc/bottom_navigation_bloc.dart'; import '../../common_packages/custom_bottom_navbar.dart'; -import '../../core/route_constants.dart'; +import '../../core/inside_bottom_navigator.dart'; +import '../../postcard/views/postcard_initial_page_view.dart'; import 'first_time_user_home_page.dart'; class HomePage extends StatefulWidget { @@ -18,6 +18,8 @@ class _HomePageState extends State { final _navigatorKeys = [ GlobalKey(), // tab 0 GlobalKey(), // tab 1 + GlobalKey(), // tab 2 + GlobalKey(), // tab 2 ]; @override @@ -31,8 +33,9 @@ class _HomePageState extends State { child: Scaffold( body: Stack( children: [ - _buildOffstageNavigator(0, currentIndex, const FirstTimeUserHomePage()), - _buildOffstageNavigator(1, currentIndex, const RegisteredUserHomePage()), + buildOffstageNavigator(0, currentIndex, const FirstTimeUserHomePage(), _navigatorKeys[0]), + buildOffstageNavigator(1, currentIndex, const RegisteredUserHomePage(), _navigatorKeys[1]), + buildOffstageNavigator(3, currentIndex, const PostcardPage(), _navigatorKeys[3]), ], ), bottomNavigationBar: CustomBottomNavBar(), @@ -41,32 +44,4 @@ class _HomePageState extends State { }, ); } - - Widget _buildOffstageNavigator(int index, int currentIndex, Widget child) { - return Offstage( - offstage: currentIndex != index, - child: Navigator( - key: _navigatorKeys[index], - onGenerateRoute: (settings) { - switch (settings.name) { - case '/': - return MaterialPageRoute(builder: (_) => child); - - case RouteConstants.attractionsPage: - return MaterialPageRoute( - builder: (_) => const AttractionsPage(), - ); - - default: - return MaterialPageRoute( - builder: (_) => const Scaffold( - body: Center(child: Text('Page not found')), - ), - ); - } - }, - ), - ); - } - } diff --git a/lib/home/views/registered_user_home_page.dart b/lib/home/views/registered_user_home_page.dart index 7861625..e8a387b 100644 --- a/lib/home/views/registered_user_home_page.dart +++ b/lib/home/views/registered_user_home_page.dart @@ -1,6 +1,7 @@ import 'package:citycards_customer/home/widgets/e_sim_offer_section.dart'; import 'package:citycards_customer/home/widgets/hotel_offers_section.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../common_packages/app_bar.dart'; @@ -68,7 +69,7 @@ class _RegisteredUserHomePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ CommonAppBar(isWhiteLogo: true , isProfilePage: false), - const SizedBox(height: 70), + SizedBox(height: 30.h), Text( "Chicago", style: TextStyle( @@ -226,7 +227,7 @@ class _RegisteredUserHomePageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Left side text + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -261,7 +262,7 @@ class _RegisteredUserHomePageState extends State { color: Color(0xffFDCDCE), shape: BoxShape.circle, ), - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(12), child: Image.asset( "assets/icons/arrow_angle_up.png", scale: 4, diff --git a/lib/home/widgets/e_sim_offer_section.dart b/lib/home/widgets/e_sim_offer_section.dart index 7c28654..85db7a7 100644 --- a/lib/home/widgets/e_sim_offer_section.dart +++ b/lib/home/widgets/e_sim_offer_section.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../core/route_constants.dart'; + class ESimOfferSection extends StatelessWidget { const ESimOfferSection({super.key}); @@ -116,16 +118,22 @@ class ESimOfferSection extends StatelessWidget { fontWeight: FontWeight.w600, ), ), - Container( - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - padding: const EdgeInsets.all(12), - child: Image.asset( - "assets/icons/arrow_angle_up.png", - color: Color(0xFFF95F62), - scale: 4, + InkWell( + onTap: (){ + Navigator.of(context, rootNavigator: true) + .pushNamed(RouteConstants.esimOffer); + }, + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + padding: const EdgeInsets.all(12), + child: Image.asset( + "assets/icons/arrow_angle_up.png", + color: Color(0xFFF95F62), + scale: 4, + ), ), ) ], diff --git a/lib/home/widgets/hotel_offers_section.dart b/lib/home/widgets/hotel_offers_section.dart index 10c7a64..9710724 100644 --- a/lib/home/widgets/hotel_offers_section.dart +++ b/lib/home/widgets/hotel_offers_section.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../core/route_constants.dart'; + class HotelOffersSection extends StatelessWidget { const HotelOffersSection({super.key}); @@ -140,16 +142,22 @@ class HotelOffersSection extends StatelessWidget { fontWeight: FontWeight.w600, ), ), - Container( - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - padding: const EdgeInsets.all(12), - child: Image.asset( - "assets/icons/arrow_angle_up.png", - color: Color(0xFFF95F62), - scale: 4, + InkWell( + onTap: (){ + Navigator.of(context, rootNavigator: true) + .pushNamed(RouteConstants.hotelOffer); + }, + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + padding: const EdgeInsets.all(12), + child: Image.asset( + "assets/icons/arrow_angle_up.png", + color: Color(0xFFF95F62), + scale: 4, + ), ), ) ], diff --git a/lib/main.dart b/lib/main.dart index 3487656..2f2b1c8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,7 +29,7 @@ class MyApp extends StatelessWidget { builder: (context, child) { return MaterialApp( onGenerateRoute: _appRouter.onGenerateRoute, - initialRoute: RouteConstants.profile, + initialRoute: RouteConstants.home, debugShowCheckedModeBanner: false, title: 'City Cards', theme: ThemeData( diff --git a/lib/postcard/blocs/postcard_creation_bloc.dart b/lib/postcard/blocs/postcard_creation_bloc.dart new file mode 100644 index 0000000..85ee218 --- /dev/null +++ b/lib/postcard/blocs/postcard_creation_bloc.dart @@ -0,0 +1,173 @@ +import 'dart:io'; +import 'package:citycards_customer/postcard/blocs/postcard_creation_events.dart'; +import 'package:citycards_customer/postcard/blocs/postcard_creation_state.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:image/image.dart' as img; + +enum PostcardStep { uploadPhoto, addFilter, writeMessage, preview } + +class PostcardCreationBloc + extends Bloc { + final ImagePicker _picker = ImagePicker(); + + PostcardCreationBloc() + : super( + const PostcardCreationState(currentStep: PostcardStep.uploadPhoto), + ) { + /* Navigation steps */ + on((event, emit) { + final next = + PostcardStep.values[(state.currentStep.index + 1).clamp( + 0, + PostcardStep.values.length - 1, + )]; + emit(state.copyWith(currentStep: next)); + }); + + /* Go to previous step */ + on((event, emit) { + final prev = + PostcardStep.values[(state.currentStep.index - 1).clamp( + 0, + PostcardStep.values.length - 1, + )]; + emit(state.copyWith(currentStep: prev)); + }); + + /* Upload image */ + on((event, emit) { + emit( + state.copyWith( + imagePath: event.imagePath, + originalImagePath: event.imagePath, + ), + ); + }); + + /* Pick image from galley */ + on((event, emit) async { + final pickedFile = await _picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + emit( + state.copyWith( + imagePath: pickedFile.path, + originalImagePath: pickedFile.path, + ), + ); + } + }); + + /* Pick image from camera */ + on((event, emit) async { + final pickedFile = await _picker.pickImage(source: ImageSource.camera); + if (pickedFile != null) { + emit( + state.copyWith( + imagePath: pickedFile.path, + originalImagePath: pickedFile.path, + ), + ); + } + }); + + /* Select filter */ + on((event, emit) async { + // 1️⃣ No image? Exit early. + if (state.originalImagePath == null) return; + + // 2️⃣ Handle "Original" immediately. + if (event.filterName == "none" || event.filterName == "original") { + emit( + state.copyWith( + imagePath: state.originalImagePath, + // revert to the untouched original + filter: "none", + isProcessing: false, + ), + ); + return; + } + + // 3️⃣ Start loader + emit(state.copyWith(isProcessing: true)); + + try { + // Always base filters on the ORIGINAL image, not the last filtered one + final originalFile = File(state.originalImagePath!); + final bytes = await originalFile.readAsBytes(); + img.Image? image = img.decodeImage(bytes); + + if (image == null) { + emit(state.copyWith(isProcessing: false)); + return; + } + + // 4️⃣ Apply chosen filter + switch (event.filterName) { + case "vintage": + image = img.adjustColor( + image, + saturation: 0.8, + gamma: 1.1, + contrast: 0.9, + ); + break; + case "bw": + image = img.grayscale(image); + break; + case "sepia": + image = img.sepia(image); + break; + case "cool": + image = img.adjustColor(image, hue: -15, contrast: 1.05); + break; + case "contrast": + image = img.adjustColor(image, contrast: 1.4); + break; + case "soft": + image = img.adjustColor( + image, + brightness: 0.1, + gamma: 0.9, + saturation: 1.1, + ); + break; + default: + emit(state.copyWith(filter: "none", isProcessing: false)); + return; + } + + // 5️⃣ Save filtered image to a new temporary file + final filteredFile = File( + "${originalFile.parent.path}/filtered_${event.filterName}.jpg", + )..writeAsBytesSync(img.encodeJpg(image, quality: 95)); + + // 6️⃣ Emit new state + emit( + state.copyWith( + imagePath: filteredFile.path, + filter: event.filterName, + isProcessing: false, + ), + ); + } catch (e) { + debugPrint("❌ Error applying filter: $e"); + emit(state.copyWith(isProcessing: false)); + } + }); + + on((event, emit) { + emit(state.copyWith(message: event.message)); + }); + + on((event, emit) { + emit(state.copyWith(selectedFont: event.fontName)); + }); + + on((event, emit) { + emit(state.copyWith(isGift: event.isGift)); + }); + } +} diff --git a/lib/postcard/blocs/postcard_creation_events.dart b/lib/postcard/blocs/postcard_creation_events.dart new file mode 100644 index 0000000..737885f --- /dev/null +++ b/lib/postcard/blocs/postcard_creation_events.dart @@ -0,0 +1,39 @@ +class PostcardCreationEvent {} + +class PickImageFromGallery extends PostcardCreationEvent {} + +class PickImageFromCamera extends PostcardCreationEvent {} + +class GoToNextStep extends PostcardCreationEvent {} + +class GoToPreviousStep extends PostcardCreationEvent {} + +class UploadImage extends PostcardCreationEvent { + final String imagePath; + + UploadImage(this.imagePath); +} + +class SelectFilter extends PostcardCreationEvent { + final String filterName; + + SelectFilter(this.filterName); +} + +class WriteMessage extends PostcardCreationEvent { + final String message; + + WriteMessage(this.message); +} + +class ChangeFontStyle extends PostcardCreationEvent { + final String fontName; + ChangeFontStyle(this.fontName); +} + + +class TogglePurchaseOption extends PostcardCreationEvent { + final bool isGift; + + TogglePurchaseOption(this.isGift); +} \ No newline at end of file diff --git a/lib/postcard/blocs/postcard_creation_state.dart b/lib/postcard/blocs/postcard_creation_state.dart new file mode 100644 index 0000000..d8fec4a --- /dev/null +++ b/lib/postcard/blocs/postcard_creation_state.dart @@ -0,0 +1,45 @@ +import 'package:citycards_customer/postcard/blocs/postcard_creation_bloc.dart'; + +class PostcardCreationState { + final PostcardStep currentStep; + final String? imagePath; + final String? originalImagePath; + final String? filter; + final String? message; + final bool isGift; + final bool isProcessing; + final String? selectedFont; + + const PostcardCreationState({ + required this.currentStep, + this.imagePath, + this.originalImagePath, + this.filter, + this.message, + this.isGift = false, + this.isProcessing = false, + this.selectedFont + }); + + PostcardCreationState copyWith({ + PostcardStep? currentStep, + String? imagePath, + String? originalImagePath, + String? filter, + String? message, + bool? isGift, + bool? isProcessing, + String? selectedFont, + }) { + return PostcardCreationState( + currentStep: currentStep ?? this.currentStep, + imagePath: imagePath ?? this.imagePath, + originalImagePath: originalImagePath ?? this.originalImagePath, + filter: filter ?? this.filter, + message: message ?? this.message, + isGift: isGift ?? this.isGift, + isProcessing: isProcessing ?? this.isProcessing, + selectedFont: selectedFont ?? this.selectedFont + ); + } +} \ No newline at end of file diff --git a/lib/postcard/models/postcard_model.dart b/lib/postcard/models/postcard_model.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/postcard/repository/postcard_repository.dart b/lib/postcard/repository/postcard_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/postcard/views/add_filter_step_page_view.dart b/lib/postcard/views/add_filter_step_page_view.dart new file mode 100644 index 0000000..79939be --- /dev/null +++ b/lib/postcard/views/add_filter_step_page_view.dart @@ -0,0 +1,166 @@ +import 'dart:io'; +import 'package:citycards_customer/postcard/widgets/dotted_border_holder.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../common_packages/app_bar.dart'; +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_events.dart'; +import '../blocs/postcard_creation_state.dart'; +import '../widgets/filter_option_card.dart'; +import '../widgets/step_progressbar.dart'; + +class AddFilterStepPageView extends StatelessWidget { + const AddFilterStepPageView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + final imageFile = File(state.imagePath!); + + return SafeArea( + child: Stack( + children: [ + SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + StepProgressBar(totalSteps: 4, currentStep: 2), + const SizedBox(height: 24), + Text( + "Add a Filter", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 6), + Text( + "Choose your favorite filter and enhance your postcard.", + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff2D3134), + ), + ), + const SizedBox(height: 10), + if (state.imagePath != null) + DottedBorderContainerHolder( + imagePath: state.imagePath!, + filter: state.filter ?? "", + ), + const SizedBox(height: 20), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + buildFilterOption( + context, + bloc, + "Original", + File(state.originalImagePath!), + "original", + state.filter, + ), + buildFilterOption( + context, + bloc, + "Black & White", + imageFile, + "bw", + state.filter, + ), + buildFilterOption( + context, + bloc, + "Sepia", + imageFile, + "sepia", + state.filter, + ), + buildFilterOption( + context, + bloc, + "Vintage", + imageFile, + "vintage", + state.filter, + ), + buildFilterOption( + context, + bloc, + "Cool Tone", + imageFile, + "cool", + state.filter, + ), + buildFilterOption( + context, + bloc, + "Contrast", + imageFile, + "contrast", + state.filter, + ), + buildFilterOption( + context, + bloc, + "Soft Glow", + imageFile, + "soft", + state.filter, + ), + ], + ), + + ), + SizedBox( + height: 20.h, + ), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + onPressed: () { + context.read().add(GoToNextStep()); + }, + child: Text( + "Write your message", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + + if (state.isProcessing) + Container( + color: Colors.black.withOpacity(0.4), + child: const Center( + child: CircularProgressIndicator(color: Color(0xffF95F62)), + ), + ), + + ], + ), + ); + }, + ); + } +} diff --git a/lib/postcard/views/postcard_creation_page_view.dart b/lib/postcard/views/postcard_creation_page_view.dart new file mode 100644 index 0000000..76b54a3 --- /dev/null +++ b/lib/postcard/views/postcard_creation_page_view.dart @@ -0,0 +1,46 @@ +import 'package:citycards_customer/postcard/views/add_filter_step_page_view.dart'; +import 'package:citycards_customer/postcard/views/preview_postcard_step_page_view.dart'; +import 'package:citycards_customer/postcard/views/upload_photo_step_page_view.dart'; +import 'package:citycards_customer/postcard/views/write_message_step_page_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_state.dart'; + +class PostcardCreationPage extends StatelessWidget { + const PostcardCreationPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => PostcardCreationBloc(), + child: BlocBuilder( + builder: (context, state) { + Widget stepWidget; + switch (state.currentStep) { + case PostcardStep.uploadPhoto: + stepWidget = const UploadPhotoStepPageView(); + break; + case PostcardStep.addFilter: + stepWidget = const AddFilterStepPageView(); + break; + case PostcardStep.writeMessage: + stepWidget = const WriteMessageStepPageView(); + break; + case PostcardStep.preview: + stepWidget = const PreviewPostcardStepPageView(); + break; + default: + stepWidget = const UploadPhotoStepPageView(); + } + + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea(child: stepWidget), + ); + }, + ), + ); + } +} diff --git a/lib/postcard/views/postcard_initial_page_view.dart b/lib/postcard/views/postcard_initial_page_view.dart index b9a9ff5..8594299 100644 --- a/lib/postcard/views/postcard_initial_page_view.dart +++ b/lib/postcard/views/postcard_initial_page_view.dart @@ -2,6 +2,9 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../common_packages/app_bar.dart'; +import '../../core/route_constants.dart'; + class PostcardPage extends StatelessWidget { const PostcardPage({super.key}); @@ -12,43 +15,44 @@ class PostcardPage extends StatelessWidget { backgroundColor: Colors.white, body: SafeArea( child: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 24.h), + padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ - // 🖼️ Postcard image + + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + SizedBox(height: 50.h), + ClipRRect( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(8), child: Image.asset( - "assets/images/postcard_bg.png", // <-- your image + "assets/images/post_card_intro.png", width: double.infinity, - height: 200.h, fit: BoxFit.cover, ), ), - SizedBox(height: 32.h), + SizedBox(height: 50.h), - // 🌴📮💌 emojis const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("🌴", style: TextStyle(fontSize: 24)), - SizedBox(width: 8), - Text("📮", style: TextStyle(fontSize: 24)), - SizedBox(width: 8), - Text("💌", style: TextStyle(fontSize: 24)), + Text("🌴", style: TextStyle(fontSize: 16)), + SizedBox(width: 4), + Text("📮", style: TextStyle(fontSize: 16)), + SizedBox(width: 4), + Text("💌", style: TextStyle(fontSize: 16)), ], ), SizedBox(height: 24.h), - // 📝 Title and subtitle Text( "Make the most of your trip", - style: GoogleFonts.poppins( + style: TextStyle( fontSize: 20.sp, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w500, color: const Color(0xffF95F62), ), textAlign: TextAlign.center, @@ -56,10 +60,10 @@ class PostcardPage extends StatelessWidget { SizedBox(height: 8.h), Text( "Design your own unique postcards to\ncherish your unforgettable moments.", - style: GoogleFonts.poppins( - fontSize: 13.sp, + style: TextStyle( + fontSize: 14.sp, fontWeight: FontWeight.w400, - color: const Color(0xff464646), + color: const Color(0xff707070), height: 1.5, ), textAlign: TextAlign.center, @@ -67,7 +71,7 @@ class PostcardPage extends StatelessWidget { SizedBox(height: 36.h), - // 🟥 CTA button + SizedBox( width: double.infinity, child: ElevatedButton( @@ -79,7 +83,7 @@ class PostcardPage extends StatelessWidget { ), ), onPressed: () { - // Add navigation or bloc event here + Navigator.of(context).pushNamed(RouteConstants.uploadPhotoPage); }, child: Text( "Lets Create", diff --git a/lib/postcard/views/preview_postcard_step_page_view.dart b/lib/postcard/views/preview_postcard_step_page_view.dart new file mode 100644 index 0000000..fd1b9e5 --- /dev/null +++ b/lib/postcard/views/preview_postcard_step_page_view.dart @@ -0,0 +1,201 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../../common_packages/app_bar.dart'; +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_state.dart'; +import '../widgets/purchase_details_bottom_sheet.dart'; +import '../widgets/step_progressbar.dart'; + +class PreviewPostcardStepPageView extends StatefulWidget { + const PreviewPostcardStepPageView({super.key}); + + @override + State createState() => _PreviewPostcardStepPageViewState(); +} + +class _PreviewPostcardStepPageViewState extends State { + + bool showImage = false; // ✅ tracks which side is visible + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + StepProgressBar(totalSteps: 4, currentStep: 4), + const SizedBox(height: 24), + + Text( + "Preview your Postcard", + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w600, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 20), + + + showImage ? + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + "assets/images/post_card_intro.png", + width: double.infinity, + fit: BoxFit.cover, + ), + ): + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 🖼️ Image section + if (state.imagePath != null) + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(state.imagePath!), + height: 140.h, + width: 140.w, + fit: BoxFit.cover, + ), + ), + const SizedBox(height: 12), + + // 📝 Message section with lines and font + CustomPaint( + painter: _LinedPaperPainter(), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 12, + ), + child: Text( + state.message ?? "", + style: TextStyle( + fontFamily: state.selectedFont ?? + GoogleFonts.poppins().fontFamily, + fontSize: 14.sp, + color: const Color(0xff1A1A1A), + height: 1.8, + ), + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // 🔁 Flip Buttons + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton.icon( + onPressed: showImage + ? () => setState(() => showImage = false) : null, + icon: Icon(Icons.arrow_back, + color: showImage + ? const Color(0xffF95F62) + : const Color(0xffC8C8C8), size: 18), + label: Text( + "Flip", + style: TextStyle( + color: showImage + ? const Color(0xffF95F62) + : const Color(0xffC8C8C8), + fontWeight: FontWeight.w500, + ), + ), + ), + TextButton.icon( + onPressed: showImage + ? null + : () => setState(() => showImage = true), + icon: Text( + "Flip", + style: TextStyle( + color: !showImage + ? const Color(0xffF95F62) + : const Color(0xffC8C8C8), + fontWeight: FontWeight.w500, + ), + ), + label: Icon(Icons.arrow_forward, + color: !showImage + ? const Color(0xffF95F62) + : const Color(0xffC8C8C8),size: 18), + ), + ], + ), + + const SizedBox(height: 16), + + // ▶ Next Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + PurchaseDetailsBottomSheet.show(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + "Next", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ) +, + ], + ), + ), + ); + }, + ); + } +} + +/// 📜 Custom Painter for lined background +class _LinedPaperPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = const Color(0xffE6DCDC) + ..strokeWidth = 1; + + const lineHeight = 30.0; + for (double y = 20; y < size.height; y += lineHeight) { + canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/postcard/views/upload_photo_step_page_view.dart b/lib/postcard/views/upload_photo_step_page_view.dart new file mode 100644 index 0000000..208f193 --- /dev/null +++ b/lib/postcard/views/upload_photo_step_page_view.dart @@ -0,0 +1,242 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../common_packages/app_bar.dart'; +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_events.dart'; +import '../blocs/postcard_creation_state.dart'; +import '../widgets/dotted_border_container.dart'; +import '../widgets/step_progressbar.dart'; + +class UploadPhotoStepPageView extends StatelessWidget { + const UploadPhotoStepPageView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + + StepProgressBar(totalSteps: 4, currentStep: 1), + const SizedBox(height: 24), + + Text( + "Upload a photo", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 6), + Text( + "Design your own unique postcards to cherish your unforgettable moments.", + textAlign: TextAlign.start, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff2D3134), + ), + ), + const SizedBox(height: 30), + + if (state.imagePath != null) + Container( + height: 300.h, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: const Color(0xFFFFF5F5), + image: DecorationImage( + image: FileImage(File(state.imagePath!)), + fit: BoxFit.cover, + ), + ), + ) + else + GestureDetector( + onTap: () => bloc.add(PickImageFromGallery()), + child: const DottedBorderContainer(), + ), + + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Container( + width: MediaQuery.of(context).size.width / 2 - 40, + height: 1.5, + color: Color(0xffD9D9D9), + ), + Text( + "OR", + style: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + Container( + width: MediaQuery.of(context).size.width / 2 - 40, + height: 1.5, + color: Color(0xffD9D9D9), + ), + ], + ), + const SizedBox(height: 12), + + if(state.imagePath == null) + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => bloc.add(PickImageFromCamera()), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 16, + ), + side: const BorderSide(color: Color(0xffF95F62)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text( + "Take a photo", + style: TextStyle( + color: Color(0xffF95F62), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 8), + Icon( + Icons.camera_alt_outlined, + color: Color(0xffF95F62), + ), + ], + ), + ), + ), + ], + ), + if(state.imagePath != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => bloc.add(PickImageFromCamera()), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 16, + ), + side: const BorderSide(color: Color(0xffF95F62)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text( + "Take a photo", + style: TextStyle( + color: Color(0xffF95F62), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 8), + Icon( + Icons.camera_alt_outlined, + color: Color(0xffF95F62), + ), + ], + ), + ), + ), + + const SizedBox(width: 16), // spacing between buttons + // 🖼️ Upload Photo button + Expanded( + child: OutlinedButton( + onPressed: () => bloc.add(PickImageFromGallery()), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 16, + ), + side: const BorderSide(color: Color(0xffF95F62)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text( + "Upload again", + style: TextStyle( + color: Color(0xffF95F62), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 8), + Icon(Icons.refresh, color: Color(0xffF95F62)), + ], + ), + ), + ), + ], + ), + + SizedBox(height: 30.h), + if(state.imagePath != null) + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + onPressed: () { + final bloc = context.read(); + if (bloc.state.imagePath != null) { + bloc.add(GoToNextStep()); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Please upload an image first")), + ); + } + // Navigator.of(context).pushNamed(RouteConstants.addFilterPage); + // Navigator.of(context).pushNamed(RouteConstants.); + }, + child: Text( + "Next", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/postcard/views/write_message_step_page_view.dart b/lib/postcard/views/write_message_step_page_view.dart new file mode 100644 index 0000000..5c50a12 --- /dev/null +++ b/lib/postcard/views/write_message_step_page_view.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../common_packages/app_bar.dart'; +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_events.dart'; +import '../blocs/postcard_creation_state.dart'; +import '../widgets/step_progressbar.dart'; + +class WriteMessageStepPageView extends StatefulWidget { + const WriteMessageStepPageView({super.key}); + + @override + State createState() => + _WriteMessageStepPageViewState(); +} + +class _WriteMessageStepPageViewState extends State { + late final TextEditingController _controller; + late final FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + _focusNode = FocusNode(); + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + _controller.text = state.message ?? ""; + _controller.selection = TextSelection.fromPosition( + TextPosition(offset: _controller.text.length)); + + final fonts = [ + {"name": "Default", "font": GoogleFonts.poppins()}, + {"name": "Classic", "font": GoogleFonts.playfairDisplay()}, + {"name": "Handwriting", "font": GoogleFonts.dancingScript()}, + {"name": "Elegant", "font": GoogleFonts.cormorantGaramond()}, + ]; + + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false), + StepProgressBar(totalSteps: 4, currentStep: 3), + const SizedBox(height: 24), + Text("Write a message", + style: + TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + const SizedBox(height: 6), + Text( + "Design your own unique postcards to cherish your unforgettable moments.", + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff2D3134), + ), + ), + const SizedBox(height: 30), + + // 📝 Lined Text Field + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(12), + ), + child: CustomPaint( + painter: _LinedPaperPainter(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: TextField( + controller: _controller, + focusNode: _focusNode, + maxLines: 8, + maxLength: 400, + cursorColor: const Color(0xffF95F62), + style: TextStyle( + fontFamily: state.selectedFont ?? + GoogleFonts.poppins().fontFamily, + fontSize: 14.sp, + color: Colors.black, + ), + decoration: InputDecoration( + border: InputBorder.none, + hintText: "Add Your Message Here", + hintStyle: TextStyle( + color: const Color(0xff999999), + fontSize: 14.sp, + ), + counterText: "", + ), + onChanged: (val) => bloc.add(WriteMessage(val)), + ), + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only(top: 6, right: 8), + child: Text( + "${_controller.text.length}/400", + style: TextStyle( + fontSize: 12.sp, + color: const Color(0xff999999), + ), + ), + ), + ), + + const SizedBox(height: 20), + + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: fonts.map((font) { + final TextStyle fontStyle = font['font'] as TextStyle; + final String fontName = font["name"] as String; + final isSelected = state.selectedFont == + fontStyle.fontFamily || + (state.selectedFont == null && + fontName == "Default"); + + return GestureDetector( + onTap: () => bloc + .add(ChangeFontStyle(fontStyle.fontFamily ?? "")), + child: Container( + margin: const EdgeInsets.only(right: 12), + padding: const EdgeInsets.all(12), + width: 90.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? const Color(0xffF95F62) + : const Color(0xffE0E0E0), + width: 1.5, + ), + ), + child: Column( + children: [ + Text("Aa", + style: fontStyle.copyWith( + fontSize: 20.sp, + color: const Color(0xff1A1A1A), + )), + const SizedBox(height: 4), + Text(fontName, + style: TextStyle( + fontSize: 12.sp, + color: isSelected + ? const Color(0xffF95F62) + : const Color(0xff2D3134), + )), + ], + ), + ), + ); + }).toList(), + ), + ), + + const SizedBox(height: 30), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: (state.message?.trim().isEmpty ?? true) + ? null + : () => bloc.add(GoToNextStep()), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + disabledBackgroundColor: const Color(0xffFEE7E7), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + "Next", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +/// 🖋 Custom Painter for horizontal lines +class _LinedPaperPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = const Color(0xffE6DCDC) + ..strokeWidth = 1; + + const lineHeight = 36.0; // distance between lines + for (double y = 28; y < size.height; y += lineHeight) { + canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/postcard/widgets/dotted_border_container.dart b/lib/postcard/widgets/dotted_border_container.dart new file mode 100644 index 0000000..7165fed --- /dev/null +++ b/lib/postcard/widgets/dotted_border_container.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class DottedBorderContainer extends StatelessWidget { + const DottedBorderContainer({super.key}); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: DottedBorderPainter(), + child: Container( + height: 300.h, + width: double.infinity, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset("assets/icons/select_photo.png", scale: 4), + SizedBox(height: 20.h), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add_circle_outline, color: Color(0xffF95F62), size: 25,), + const Text( + "Add image", + style: TextStyle( + color: Color(0xffF95F62), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +class DottedBorderPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = const Color(0xffF95F62) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke; + + const double dashWidth = 6; + const double dashSpace = 3; + final path = Path() + ..addRRect(RRect.fromRectAndRadius( + Rect.fromLTWH(0, 0, size.width, size.height), + const Radius.circular(16))); + + final pathMetrics = path.computeMetrics(); + for (final metric in pathMetrics) { + double distance = 0.0; + while (distance < metric.length) { + final segment = metric.extractPath( + distance, + distance + dashWidth, + ); + canvas.drawPath(segment, paint); + distance += dashWidth + dashSpace; + } + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/postcard/widgets/dotted_border_holder.dart b/lib/postcard/widgets/dotted_border_holder.dart new file mode 100644 index 0000000..d165dbe --- /dev/null +++ b/lib/postcard/widgets/dotted_border_holder.dart @@ -0,0 +1,41 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import 'dotted_border_container.dart'; +import 'filter_option_card.dart'; + +class DottedBorderContainerHolder extends StatelessWidget { + final String imagePath; + final String filter; + const DottedBorderContainerHolder({super.key, required this.imagePath, required this.filter}); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: DottedBorderPainter(), + child: Container( + padding: EdgeInsets.all(10.sp), + height: 300.h, + width: double.infinity, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: ColorFiltered( + colorFilter: getColorFilter(filter), + child: Image.file( + File(imagePath), + height: 300.h, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/postcard/widgets/filter_option_card.dart b/lib/postcard/widgets/filter_option_card.dart new file mode 100644 index 0000000..156dd51 --- /dev/null +++ b/lib/postcard/widgets/filter_option_card.dart @@ -0,0 +1,113 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; + +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_events.dart'; + +/// Builds a single filter preview thumbnail +Widget buildFilterOption(BuildContext context, + PostcardCreationBloc postbloc, + String label, + File imageFile, + String filter, + String? selectedFilter,) { + final isSelected = selectedFilter == filter; + + return GestureDetector( + onTap: () => postbloc.add(SelectFilter(filter)), + child: Container( + margin: const EdgeInsets.only(right: 12), + width: 90, + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: ColorFiltered( + colorFilter: getColorFilter(filter), + child: Image.file( + imageFile, + height: 70, + width: 90, + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(height: 6), + Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: isSelected + ? const Color(0xffF95F62) + : const Color(0xff2D3134), + ), + ), + ], + ), + ), + ); +} + +ColorFilter getColorFilter(String? filter) { + switch (filter) { + case "vintage": + // Muted, warm tones without overflow + return const ColorFilter.matrix([ + 0.9, 0.3, 0.1, 0, 0, + 0.2, 0.8, 0.1, 0, 0, + 0.1, 0.3, 0.7, 0, 0, + 0, 0, 0, 1, 0, + ]); + + case "bw": + // Grayscale + return const ColorFilter.matrix([ + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0, 0, 0, 1, 0, + ]); + + case "sepia": + // Classic soft brown + return const ColorFilter.matrix([ + 0.393, 0.769, 0.189, 0, 0, + 0.349, 0.686, 0.168, 0, 0, + 0.272, 0.534, 0.131, 0, 0, + 0, 0, 0, 1, 0, + ]); + + case "cool": + // Gentle blue tone — no gamma boost to avoid clipping + return const ColorFilter.matrix([ + 1.0, 0, 0, 0, 0, + 0, 1.0, 0, 0, 0, + 0, 0, 1.1, 0, 10, + 0, 0, 0, 1, 0, + ]); + + case "contrast": + // Slight contrast increase, safe range + return const ColorFilter.matrix([ + 1.1, 0, 0, 0, -10, + 0, 1.1, 0, 0, -10, + 0, 0, 1.1, 0, -10, + 0, 0, 0, 1, 0, + ]); + + case "soft": + // Gentle brightness and warmth — fixed to avoid pixelation + return const ColorFilter.matrix([ + 1.02, 0, 0, 0, 5, + 0, 1.02, 0, 0, 5, + 0, 0, 1.02, 0, 5, + 0, 0, 0, 1, 0, + ]); + + default: + return const ColorFilter.mode(Colors.transparent, BlendMode.srcOver); + } +} + diff --git a/lib/postcard/widgets/purchase_details_bottom_sheet.dart b/lib/postcard/widgets/purchase_details_bottom_sheet.dart new file mode 100644 index 0000000..9b157ce --- /dev/null +++ b/lib/postcard/widgets/purchase_details_bottom_sheet.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../blocs/postcard_creation_bloc.dart'; +import '../blocs/postcard_creation_events.dart'; +import '../blocs/postcard_creation_state.dart'; + +class PurchaseDetailsBottomSheet { + static void show(BuildContext context) { + final existingBloc = BlocProvider.of(context); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (BuildContext modalContext) { + return BlocProvider.value( + value: existingBloc, + child: BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + top: 16, + left: 16, + right: 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 45, + height: 5, + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(10), + ), + ), + const SizedBox(height: 12), + + Text( + "Purchase Details", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 24), + + // 🟥 Option 1: Buy Postcard for Myself + GestureDetector( + onTap: () => bloc.add(TogglePurchaseOption(false)), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: !state.isGift + ? const Color(0xffF95F62) + : const Color(0xffE0E0E0), + width: 1.5, + ), + ), + child: Row( + children: [ + Radio( + value: false, + groupValue: state.isGift, + onChanged: (_) => + bloc.add(TogglePurchaseOption(false)), + activeColor: const Color(0xffF95F62), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + "Buy Postcard for Myself", + style: TextStyle( + fontWeight: FontWeight.w600, + color: Color(0xffF95F62), + ), + ), + SizedBox(height: 8), + Text( + "Frank Adam", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Color(0xff1A1A1A), + ), + ), + Text( + "132 My Street, Kingston, NY\n12401", + style: TextStyle( + fontSize: 13, + color: Color(0xff5E5E5E), + ), + ), + ], + ), + ), + ElevatedButton( + onPressed: () { + // TODO: Navigate to edit details screen + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + "Edit Details", + style: TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 20), + + // 🩶 Option 2: Gift the Postcard + GestureDetector( + onTap: () => bloc.add(TogglePurchaseOption(true)), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: state.isGift + ? const Color(0xffF95F62) + : const Color(0xffE0E0E0), + width: 1.5, + ), + ), + child: Row( + children: [ + Radio( + value: true, + groupValue: state.isGift, + onChanged: (_) => + bloc.add(TogglePurchaseOption(true)), + activeColor: const Color(0xffF95F62), + ), + const SizedBox(width: 8), + const Expanded( + child: Text( + "Gift the Postcard for someone else", + style: TextStyle( + fontWeight: FontWeight.w600, + color: Color(0xffF95F62), + ), + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 32), + ], + ), + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/postcard/widgets/step_progressbar.dart b/lib/postcard/widgets/step_progressbar.dart new file mode 100644 index 0000000..785746a --- /dev/null +++ b/lib/postcard/widgets/step_progressbar.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class StepProgressBar extends StatelessWidget { + final int totalSteps; + final int currentStep; + + const StepProgressBar({ + super.key, + required this.totalSteps, + required this.currentStep, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate(totalSteps, (index) { + bool isActive = index < currentStep; + return Expanded( + child: Container( + height: 8, + margin: EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + color: isActive + ? const Color(0xffF95F62) + : const Color(0xffF95F62).withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + ), + ); + }), + ); + } +} diff --git a/lib/trail.dart b/lib/trail.dart new file mode 100644 index 0000000..1d19807 --- /dev/null +++ b/lib/trail.dart @@ -0,0 +1,198 @@ +// import 'dart:io'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:flutter_screenutil/flutter_screenutil.dart'; +// import 'package:google_fonts/google_fonts.dart'; +// import '../../common_packages/app_bar.dart'; +// import '../blocs/postcard_creation_bloc.dart'; +// import '../blocs/postcard_creation_state.dart'; +// import '../widgets/purchase_details_bottom_sheet.dart'; +// import '../widgets/step_progressbar.dart'; +// +// class PreviewPostcardStepPageView extends StatefulWidget { +// const PreviewPostcardStepPageView({super.key}); +// +// @override +// State createState() => +// _PreviewPostcardStepPageViewState(); +// } +// +// class _PreviewPostcardStepPageViewState +// extends State { +// bool showImage = false; // ✅ tracks which side is visible +// +// @override +// Widget build(BuildContext context) { +// return BlocBuilder( +// builder: (context, state) { +// return SafeArea( +// child: SingleChildScrollView( +// padding: const EdgeInsets.all(16), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// CommonAppBar(isWhiteLogo: false, isProfilePage: false), +// StepProgressBar(totalSteps: 4, currentStep: 4), +// const SizedBox(height: 24), +// +// Text( +// "Preview your Postcard", +// style: TextStyle( +// fontSize: 20.sp, +// fontWeight: FontWeight.w600, +// color: const Color(0xff1A1A1A), +// ), +// ), +// const SizedBox(height: 20), +// +// // 🖼️ Preview Section +// Container( +// padding: const EdgeInsets.all(12), +// decoration: BoxDecoration( +// color: const Color(0xFFFFF5F5), +// borderRadius: BorderRadius.circular(12), +// ), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// if (showImage) +// // ✅ Show image side +// ClipRRect( +// borderRadius: BorderRadius.circular(8), +// child: state.imagePath != null +// ? Image.file( +// File(state.imagePath!), +// height: 180.h, +// width: double.infinity, +// fit: BoxFit.cover, +// ) +// : const Center( +// child: Padding( +// padding: EdgeInsets.all(40), +// child: Text( +// "No image selected", +// style: +// TextStyle(color: Color(0xff999999)), +// ), +// ), +// ), +// ) +// else +// // ✅ Show message side +// CustomPaint( +// painter: _LinedPaperPainter(), +// child: Container( +// width: double.infinity, +// padding: const EdgeInsets.symmetric( +// horizontal: 10, +// vertical: 12, +// ), +// child: Text( +// state.message ?? "", +// style: TextStyle( +// fontFamily: state.selectedFont ?? +// GoogleFonts.poppins().fontFamily, +// fontSize: 14.sp, +// color: const Color(0xff1A1A1A), +// height: 1.8, +// ), +// ), +// ), +// ), +// ], +// ), +// ), +// +// const SizedBox(height: 24), +// +// // 🔁 Flip Buttons (toggle state) +// Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// TextButton.icon( +// onPressed: showImage +// ? () => setState(() => showImage = false) +// : null, +// icon: const Icon(Icons.arrow_back, +// color: Color(0xffF95F62), size: 18), +// label: Text( +// "Flip", +// style: TextStyle( +// color: showImage +// ? const Color(0xffF95F62) +// : const Color(0xffC8C8C8), +// fontWeight: FontWeight.w500, +// ), +// ), +// ), +// TextButton.icon( +// onPressed: showImage +// ? null +// : () => setState(() => showImage = true), +// icon: Text( +// "Flip", +// style: TextStyle( +// color: showImage +// ? const Color(0xffC8C8C8) +// : const Color(0xffF95F62), +// fontWeight: FontWeight.w500, +// ), +// ), +// label: const Icon(Icons.arrow_forward, +// color: Color(0xffF95F62), size: 18), +// ), +// ], +// ), +// +// const SizedBox(height: 16), +// +// // ▶ Next Button +// SizedBox( +// width: double.infinity, +// child: ElevatedButton( +// onPressed: () { +// PurchaseDetailsBottomSheet.show(context); +// }, +// style: ElevatedButton.styleFrom( +// backgroundColor: const Color(0xffF95F62), +// padding: EdgeInsets.symmetric(vertical: 16.h), +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(40), +// ), +// ), +// child: Text( +// "Next", +// style: TextStyle( +// color: Colors.white, +// fontSize: 14.sp, +// fontWeight: FontWeight.w600, +// ), +// ), +// ), +// ), +// ], +// ), +// ), +// ); +// }, +// ); +// } +// } +// +// /// 📜 Custom Painter for lined message area +// class _LinedPaperPainter extends CustomPainter { +// @override +// void paint(Canvas canvas, Size size) { +// final paint = Paint() +// ..color = const Color(0xffE6DCDC) +// ..strokeWidth = 1; +// +// const lineHeight = 30.0; +// for (double y = 20; y < size.height; y += lineHeight) { +// canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); +// } +// } +// +// @override +// bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +// } diff --git a/pubspec.lock b/pubspec.lock index dca9f12..a796bed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" async: dependency: transitive description: @@ -49,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.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: @@ -81,6 +97,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + 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" flutter: dependency: "direct main" description: flutter @@ -102,6 +150,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "306f0596590e077338312f38837f595c04f28d6cdeeac392d3d74df2f0003687" + url: "https://pub.dev" + source: hosted + version: "2.0.32" flutter_screenutil: dependency: "direct main" description: @@ -115,6 +171,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" google_fonts: dependency: "direct main" description: @@ -139,6 +200,78 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: "direct main" + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + 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: "58a85e6f09fe9c4484d53d18a0bd6271b72c53fce1d05e6f745ae36d8c18efca" + url: "https://pub.dev" + source: hosted + version: "0.8.13+5" + 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" intl: dependency: "direct main" description: @@ -203,6 +336,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" nested: dependency: transitive description: @@ -267,6 +408,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" platform: dependency: transitive description: @@ -283,6 +432,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" provider: dependency: transitive description: @@ -384,6 +541,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" sdks: dart: ">=3.9.0 <4.0.0" - flutter: ">=3.29.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index b66a61a..c476cc3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,16 +28,18 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: - google_fonts: ^6.3.2 - flutter_bloc: ^9.1.1 flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. + google_fonts: ^6.3.2 + flutter_bloc: ^9.1.1 cupertino_icons: ^1.0.8 flutter_screenutil: ^5.9.3 intl: ^0.20.2 + image_picker: ^1.2.0 + image: ^4.5.4 dev_dependencies: flutter_test: