worked postcard section
This commit is contained in:
BIN
assets/icons/select_photo.png
Normal file
BIN
assets/icons/select_photo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
@@ -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),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
62
lib/core/inside_bottom_navigator.dart
Normal file
62
lib/core/inside_bottom_navigator.dart
Normal file
@@ -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<PostcardCreationBloc>(context);
|
||||
return BlocProvider.value(
|
||||
value: previousBloc,
|
||||
child: const AddFilterStepPageView(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
default:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => const Scaffold(
|
||||
body: Center(child: Text('Page not found')),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ class RouteConstants {
|
||||
static const String attractionsPage = "/attractions";
|
||||
static const String postCardPage = "/postcards";
|
||||
static const String uploadPhotoPage = "/uploadPhoto";
|
||||
static const String addFilterPage = "/addFilter";
|
||||
|
||||
/* ****************************** Profile Section **************************/
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false),
|
||||
CommonAppBar(isWhiteLogo: true, isProfilePage: false),
|
||||
SizedBox(height: 140.h),
|
||||
Text(
|
||||
"CityCards.\nSee More,\nSpend Less.",
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
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 '../../postcard/views/postcard_creation_page_view.dart';
|
||||
import '../../core/inside_bottom_navigator.dart';
|
||||
import '../../postcard/views/postcard_initial_page_view.dart';
|
||||
import 'first_time_user_home_page.dart';
|
||||
|
||||
@@ -35,9 +33,9 @@ class _HomePageState extends State<HomePage> {
|
||||
child: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
_buildOffstageNavigator(0, currentIndex, const FirstTimeUserHomePage()),
|
||||
_buildOffstageNavigator(1, currentIndex, const RegisteredUserHomePage()),
|
||||
_buildOffstageNavigator(3, currentIndex, const PostcardPage()),
|
||||
buildOffstageNavigator(0, currentIndex, const FirstTimeUserHomePage(), _navigatorKeys[0]),
|
||||
buildOffstageNavigator(1, currentIndex, const RegisteredUserHomePage(), _navigatorKeys[1]),
|
||||
buildOffstageNavigator(3, currentIndex, const PostcardPage(), _navigatorKeys[3]),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: CustomBottomNavBar(),
|
||||
@@ -46,37 +44,4 @@ class _HomePageState extends State<HomePage> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
);
|
||||
|
||||
case RouteConstants.uploadPhotoPage:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => const PostcardCreationPage(),
|
||||
);
|
||||
|
||||
default:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => const Scaffold(
|
||||
body: Center(child: Text('Page not found')),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<RegisteredUserHomePage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: true , isProfilePage: false),
|
||||
const SizedBox(height: 70),
|
||||
SizedBox(height: 30.h),
|
||||
Text(
|
||||
"Chicago",
|
||||
style: TextStyle(
|
||||
|
||||
@@ -1,95 +1,171 @@
|
||||
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:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
enum PostcardStep {
|
||||
uploadPhoto,
|
||||
addFilter,
|
||||
writeMessage,
|
||||
preview,
|
||||
purchase
|
||||
}
|
||||
|
||||
class 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 TogglePurchaseOption extends PostcardCreationEvent {
|
||||
final bool isGift;
|
||||
TogglePurchaseOption(this.isGift);
|
||||
}
|
||||
|
||||
class PostcardCreationState {
|
||||
final PostcardStep currentStep;
|
||||
final String? imagePath;
|
||||
final String? filter;
|
||||
final String? message;
|
||||
final bool isGift;
|
||||
|
||||
const PostcardCreationState({
|
||||
required this.currentStep,
|
||||
this.imagePath,
|
||||
this.filter,
|
||||
this.message,
|
||||
this.isGift = false,
|
||||
});
|
||||
|
||||
PostcardCreationState copyWith({
|
||||
PostcardStep? currentStep,
|
||||
String? imagePath,
|
||||
String? filter,
|
||||
String? message,
|
||||
bool? isGift,
|
||||
}) {
|
||||
return PostcardCreationState(
|
||||
currentStep: currentStep ?? this.currentStep,
|
||||
imagePath: imagePath ?? this.imagePath,
|
||||
filter: filter ?? this.filter,
|
||||
message: message ?? this.message,
|
||||
isGift: isGift ?? this.isGift,
|
||||
);
|
||||
}
|
||||
}
|
||||
enum PostcardStep { uploadPhoto, addFilter, writeMessage, preview }
|
||||
|
||||
class PostcardCreationBloc
|
||||
extends Bloc<PostcardCreationEvent, PostcardCreationState> {
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
|
||||
PostcardCreationBloc()
|
||||
: super(const PostcardCreationState(currentStep: PostcardStep.uploadPhoto)) {
|
||||
: super(
|
||||
const PostcardCreationState(currentStep: PostcardStep.uploadPhoto),
|
||||
) {
|
||||
/* Navigation steps */
|
||||
on<GoToNextStep>((event, emit) {
|
||||
final next = PostcardStep.values[
|
||||
(state.currentStep.index + 1).clamp(0, PostcardStep.values.length - 1)];
|
||||
final next =
|
||||
PostcardStep.values[(state.currentStep.index + 1).clamp(
|
||||
0,
|
||||
PostcardStep.values.length - 1,
|
||||
)];
|
||||
emit(state.copyWith(currentStep: next));
|
||||
});
|
||||
|
||||
/* Go to previous step */
|
||||
on<GoToPreviousStep>((event, emit) {
|
||||
final prev = PostcardStep.values[
|
||||
(state.currentStep.index - 1).clamp(0, PostcardStep.values.length - 1)];
|
||||
final prev =
|
||||
PostcardStep.values[(state.currentStep.index - 1).clamp(
|
||||
0,
|
||||
PostcardStep.values.length - 1,
|
||||
)];
|
||||
emit(state.copyWith(currentStep: prev));
|
||||
});
|
||||
|
||||
/* Upload image */
|
||||
on<UploadImage>((event, emit) {
|
||||
emit(state.copyWith(imagePath: event.imagePath));
|
||||
emit(
|
||||
state.copyWith(
|
||||
imagePath: event.imagePath,
|
||||
originalImagePath: event.imagePath,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
on<SelectFilter>((event, emit) {
|
||||
emit(state.copyWith(filter: event.filterName));
|
||||
/* Pick image from galley */
|
||||
on<PickImageFromGallery>((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<PickImageFromCamera>((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<SelectFilter>((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<WriteMessage>((event, emit) {
|
||||
emit(state.copyWith(message: event.message));
|
||||
});
|
||||
|
||||
on<ChangeFontStyle>((event, emit) {
|
||||
emit(state.copyWith(selectedFont: event.fontName));
|
||||
});
|
||||
|
||||
on<TogglePurchaseOption>((event, emit) {
|
||||
emit(state.copyWith(isGift: event.isGift));
|
||||
});
|
||||
|
||||
39
lib/postcard/blocs/postcard_creation_events.dart
Normal file
39
lib/postcard/blocs/postcard_creation_events.dart
Normal file
@@ -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);
|
||||
}
|
||||
45
lib/postcard/blocs/postcard_creation_state.dart
Normal file
45
lib/postcard/blocs/postcard_creation_state.dart
Normal file
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +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 const Placeholder();
|
||||
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
|
||||
builder: (context, state) {
|
||||
final bloc = context.read<PostcardCreationBloc>();
|
||||
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<PostcardCreationBloc>().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)),
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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});
|
||||
@@ -25,7 +26,7 @@ class PostcardCreationPage extends StatelessWidget {
|
||||
stepWidget = const AddFilterStepPageView();
|
||||
break;
|
||||
case PostcardStep.writeMessage:
|
||||
stepWidget = const WriteMessagePageView();
|
||||
stepWidget = const WriteMessageStepPageView();
|
||||
break;
|
||||
case PostcardStep.preview:
|
||||
stepWidget = const PreviewPostcardStepPageView();
|
||||
|
||||
@@ -22,8 +22,6 @@ class PostcardPage extends StatelessWidget {
|
||||
children: [
|
||||
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false),
|
||||
SizedBox(height: 12.h),
|
||||
Divider(height: 1.h, color: const Color(0xFFD9D9D9)),
|
||||
SizedBox(height: 50.h),
|
||||
|
||||
ClipRRect(
|
||||
|
||||
@@ -1,10 +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';
|
||||
|
||||
class PreviewPostcardStepPageView extends StatelessWidget {
|
||||
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<PreviewPostcardStepPageView> createState() => _PreviewPostcardStepPageViewState();
|
||||
}
|
||||
|
||||
class _PreviewPostcardStepPageViewState extends State<PreviewPostcardStepPageView> {
|
||||
|
||||
bool showImage = false; // ✅ tracks which side is visible
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold();
|
||||
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,108 +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) {
|
||||
final bloc = context.read<PostcardCreationBloc>();
|
||||
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
|
||||
builder: (context, state) {
|
||||
final bloc = context.read<PostcardCreationBloc>();
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
LinearProgressIndicator(
|
||||
value: 0.25,
|
||||
color: const Color(0xffF95F62),
|
||||
backgroundColor: const Color(0xffFEE7E7),
|
||||
minHeight: 4,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
"Upload a photo",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
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<PostcardCreationBloc>();
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
"Design your own unique postcards to cherish your unforgettable moments.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: const Color(0xff464646),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Image box
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
bloc.add(UploadImage("assets/images/sample_photo.jpg"));
|
||||
bloc.add(GoToNextStep());
|
||||
},
|
||||
child: DottedBorderContainer(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
const Divider(color: Color(0xffD9D9D9)),
|
||||
const SizedBox(height: 12),
|
||||
Text("OR",
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600], fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.camera_alt_outlined, color: Color(0xffF95F62)),
|
||||
label: const Text("Take a photo",
|
||||
style: TextStyle(color: Color(0xffF95F62))),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24),
|
||||
side: const BorderSide(color: Color(0xffF95F62)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DottedBorderContainer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 220,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: const Color(0xffF95F62).withOpacity(0.7), style: BorderStyle.solid),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.image_outlined,
|
||||
color: Color(0xffF95F62), size: 50),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"+ Add image",
|
||||
style: TextStyle(
|
||||
color: const Color(0xffF95F62),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +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 WriteMessagePageView extends StatelessWidget {
|
||||
const WriteMessagePageView({super.key});
|
||||
class WriteMessageStepPageView extends StatefulWidget {
|
||||
const WriteMessageStepPageView({super.key});
|
||||
|
||||
@override
|
||||
State<WriteMessageStepPageView> createState() =>
|
||||
_WriteMessageStepPageViewState();
|
||||
}
|
||||
|
||||
class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
|
||||
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 Scaffold();
|
||||
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
|
||||
builder: (context, state) {
|
||||
final bloc = context.read<PostcardCreationBloc>();
|
||||
_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;
|
||||
}
|
||||
|
||||
75
lib/postcard/widgets/dotted_border_container.dart
Normal file
75
lib/postcard/widgets/dotted_border_container.dart
Normal file
@@ -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;
|
||||
}
|
||||
41
lib/postcard/widgets/dotted_border_holder.dart
Normal file
41
lib/postcard/widgets/dotted_border_holder.dart
Normal file
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
113
lib/postcard/widgets/filter_option_card.dart
Normal file
113
lib/postcard/widgets/filter_option_card.dart
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
183
lib/postcard/widgets/purchase_details_bottom_sheet.dart
Normal file
183
lib/postcard/widgets/purchase_details_bottom_sheet.dart
Normal file
@@ -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<PostcardCreationBloc>(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<PostcardCreationBloc, PostcardCreationState>(
|
||||
builder: (context, state) {
|
||||
final bloc = context.read<PostcardCreationBloc>();
|
||||
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<bool>(
|
||||
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<bool>(
|
||||
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),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
33
lib/postcard/widgets/step_progressbar.dart
Normal file
33
lib/postcard/widgets/step_progressbar.dart
Normal file
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
198
lib/trail.dart
Normal file
198
lib/trail.dart
Normal file
@@ -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<PreviewPostcardStepPageView> createState() =>
|
||||
// _PreviewPostcardStepPageViewState();
|
||||
// }
|
||||
//
|
||||
// class _PreviewPostcardStepPageViewState
|
||||
// extends State<PreviewPostcardStepPageView> {
|
||||
// bool showImage = false; // ✅ tracks which side is visible
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
|
||||
// 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;
|
||||
// }
|
||||
167
pubspec.lock
167
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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user