Files
CityCards_Customer_Flutter/lib/postcard/blocs/postcard_creation_bloc.dart

325 lines
11 KiB
Dart

import 'dart:io';
import 'dart:typed_data';
import 'package:citycards_customer/postcard/blocs/postcard_creation_events.dart';
import 'package:citycards_customer/postcard/blocs/postcard_creation_state.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image/image.dart' as img;
import 'package:path_provider/path_provider.dart';
enum PostcardStep {
uploadPhoto,
addFilter,
writeMessage,
preview,
purchase,
checkout,
orderSuccess,
myOrders,
myOrderPostcardPreview,
}
class PostcardCreationBloc
extends Bloc<PostcardCreationEvent, PostcardCreationState> {
final ImagePicker _picker = ImagePicker();
static const int maxImageSizeInBytes = 5 * 1024 * 1024; // 5 MB
PostcardCreationBloc()
: super(
const PostcardCreationState(
currentStep: PostcardStep.uploadPhoto,
address: '',
),
) {
/* ── Navigation ──────────────────────────────────────────────────────── */
on<GoToNextStep>((event, emit) async {
if (state.currentStep == PostcardStep.uploadPhoto &&
state.imagePath != null) {
final file = File(state.imagePath!);
final fileSize = await file.length();
if (fileSize > maxImageSizeInBytes) {
final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2);
emit(state.copyWith(
errorMessage:
"Image size is too large ($sizeInMB MB). Maximum allowed size is 10 MB.",
));
return;
}
}
final next = PostcardStep.values[
(state.currentStep.index + 1)
.clamp(0, PostcardStep.values.length - 1)];
emit(state.copyWith(currentStep: next, errorMessage: null));
});
on<GoToPreviousStep>((event, emit) {
final prev = PostcardStep.values[
(state.currentStep.index - 1)
.clamp(0, PostcardStep.values.length - 1)];
emit(state.copyWith(currentStep: prev, errorMessage: null));
});
/* ── Image picking ───────────────────────────────────────────────────── */
on<UploadImage>((event, emit) async {
final file = File(event.imagePath);
final fileSize = await file.length();
if (fileSize > maxImageSizeInBytes) {
final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2);
emit(state.copyWith(
errorMessage:
"Image size is too large ($sizeInMB MB). Maximum allowed size is 5 MB.",
));
return;
}
emit(state.copyWith(
imagePath: event.imagePath,
originalImagePath: event.imagePath,
filter: 'original',
filterProcessingDone: false,
isProcessingSave: false,
errorMessage: null,
));
});
on<PickImageFromGallery>((event, emit) async {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile == null) return;
final file = File(pickedFile.path);
final fileSize = await file.length();
if (fileSize > maxImageSizeInBytes) {
final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2);
emit(state.copyWith(
errorMessage:
"Image size is too large ($sizeInMB MB). Maximum allowed size is 5 MB. Please select a smaller image.",
));
return;
}
emit(state.copyWith(
imagePath: pickedFile.path,
originalImagePath: pickedFile.path,
filter: 'original',
filterProcessingDone: false,
isProcessingSave: false,
errorMessage: null,
));
});
on<PickImageFromCamera>((event, emit) async {
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
if (pickedFile == null) return;
final file = File(pickedFile.path);
final fileSize = await file.length();
if (fileSize > maxImageSizeInBytes) {
final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2);
emit(state.copyWith(
errorMessage:
"Image size is too large ($sizeInMB MB). Maximum allowed size is 5 MB. Please try taking a photo with lower quality.",
));
return;
}
emit(state.copyWith(
imagePath: pickedFile.path,
originalImagePath: pickedFile.path,
filter: 'original',
filterProcessingDone: false,
isProcessingSave: false,
errorMessage: null,
));
});
on<ClearError>((event, emit) {
emit(state.copyWith(errorMessage: null));
});
/* ── SelectFilter ────────────────────────────────────────────────────── */
///
/// EXACT mirror of [EditImageFilterBloc.on<SelectFilter>]:
/// Only updates [filter] in state. The UI immediately shows the
/// ColorFilter widget preview — zero processing, zero delay.
on<SelectFilter>((event, emit) {
if (state.originalImagePath == null) return;
emit(state.copyWith(
filter: event.filterName,
filterProcessingDone: false,
));
});
/* ── ProcessSelectedFilter ───────────────────────────────────────────── */
///
/// EXACT mirror of [EditImageFilterBloc.on<ProcessAndSaveImage>]:
/// 1. emit isProcessingSave = true → button shows spinner
/// 2. Run heavy work in isolate via compute()
/// 3. emit imagePath = processed file path
/// 4. emit filterProcessingDone = true → BlocConsumer navigates
on<ProcessSelectedFilter>((event, emit) async {
if (state.originalImagePath == null) return;
final selectedFilter = state.filter ?? 'original';
// Original / none — no processing needed, navigate right away
if (selectedFilter == 'original' || selectedFilter == 'none') {
emit(state.copyWith(
imagePath: state.originalImagePath,
isProcessingSave: false,
filterProcessingDone: true,
));
return;
}
// Step 1 — show spinner on button
emit(state.copyWith(
isProcessingSave: true,
filterProcessingDone: false,
));
try {
// Step 2 — process in isolate (same as EditImageFilterBloc)
final String tempDir = (await getTemporaryDirectory()).path;
final Map<String, dynamic> params = {
'originalFilePath': state.originalImagePath!,
'filterName': selectedFilter,
'tempDir': tempDir,
};
final String generatedFilePath =
await compute(_processImageInIsolate, params);
// Step 3 & 4 — done, hide spinner, signal navigation
emit(state.copyWith(
imagePath: generatedFilePath,
isProcessingSave: false,
filterProcessingDone: true,
));
} catch (e) {
debugPrint("ProcessSelectedFilter error: $e");
// On failure still navigate — don't leave user stuck
emit(state.copyWith(
imagePath: state.originalImagePath,
isProcessingSave: false,
filterProcessingDone: true,
));
}
});
/* ── Other events ────────────────────────────────────────────────────── */
on<WriteMessage>((event, emit) {
emit(state.copyWith(message: event.message));
});
on<ChangeFontStyle>((event, emit) {
emit(state.copyWith(selectedFont: event.fontName));
});
on<UpdatePostcardNumber>((event, emit) {
emit(state.copyWith(pcNumber: event.pcNumber));
});
on<TogglePurchaseOption>((event, emit) {
emit(state.copyWith(isGift: event.isGift));
});
on<UpdatePurchaseFormData>((event, emit) {
emit(state.copyWith(
pcTitle: event.pcTitle,
fullName: event.fullName,
emailId: event.emailId,
phoneNumber: event.phoneNumber,
address: event.address,
city: event.city,
country: event.country,
state: event.state,
zipCode: event.zipCode,
senderName: event.senderName,
senderCity: event.senderCity,
senderCountry: event.senderCountry,
));
});
on<StoreUserProfileData>((event, emit) {
emit(state.copyWith(
userProfileFullName: event.fullName,
userProfileEmail: event.email,
userProfilePhone: event.phone,
isdCode: event.isdCode,
userProfileAddress: event.address,
userProfileCity: event.city,
userProfileState: event.state,
userProfileZipCode: event.zipCode,
userProfileCountry: event.country,
));
});
}
String getFormattedMessage() {
if (state.message == null || state.message!.isEmpty) return '';
if (state.selectedFont == null || state.selectedFont!.isEmpty) {
return '<span style="font-family: Poppins;">${state.message}</span>';
}
return '<span style="font-family: ${state.selectedFont};">${state.message}</span>';
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Top-level isolate function — identical to the one in edit_image_filter_bloc.dart
// MUST be top-level so compute() can spawn it in a separate isolate.
// ─────────────────────────────────────────────────────────────────────────────
Future<String> _processImageInIsolate(Map<String, dynamic> params) async {
final String originalFilePath = params['originalFilePath'] as String;
final String filterName = params['filterName'] as String;
final String tempDir = params['tempDir'] as String;
final File originalFile = File(originalFilePath);
final Uint8List bytes = await originalFile.readAsBytes();
img.Image? image = img.decodeImage(bytes);
if (image == null) throw Exception("Failed to decode image in Isolate");
img.Image filteredImage = image.clone();
switch (filterName) {
case "vintage":
filteredImage = img.adjustColor(filteredImage,
saturation: 0.8, gamma: 1.1, contrast: 0.9);
break;
case "bw":
filteredImage = img.grayscale(filteredImage);
break;
case "sepia":
filteredImage = img.sepia(filteredImage);
break;
case "cool":
filteredImage = img.adjustColor(filteredImage,
hue: -0.042, contrast: 1.05);
break;
case "contrast":
filteredImage = img.adjustColor(filteredImage, contrast: 1.4);
break;
case "soft":
filteredImage = img.adjustColor(filteredImage,
brightness: 0.1, gamma: 0.9, saturation: 1.1);
break;
}
final String outputPath =
"$tempDir/filtered_${filterName}_${DateTime.now().millisecondsSinceEpoch}.jpg";
final encodedBytes = img.encodeJpg(filteredImage, quality: 90);
await File(outputPath).writeAsBytes(encodedBytes);
return outputPath;
}