325 lines
11 KiB
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;
|
|
} |