import 'dart:convert'; import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image/image.dart' as img; import '../../app/route.dart'; import '../../app/constant.dart'; import '../../model/edit_person_model.dart'; import '../../model/sex_model.dart'; import '../../provider/current_user_provider.dart'; import '../../provider/scan_provider.dart'; import '../../widget/customsnackbarwidget.dart'; import '../home/list_riwayat_scan_provider.dart'; import 'upload_scan_provider.dart'; class ScanScreen extends HookConsumerWidget { const ScanScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [ SystemUiOverlay.bottom, ]); final cameraController = useState(null); final initializeControllerFuture = useState?>(null); final capturedImage = useState(null); final croppedImage = useState(null); final isLoading = useState(false); final isLoadingUpload = useState(false); final currentUser = ref.watch(currentUserProvider); final host = currentUser?.host ?? ""; final userId = currentUser?.model.userId ?? ""; final selectedResolution = useState(ResolutionPreset.medium); useEffect(() { Future initializeCamera() async { final cameras = await availableCameras(); cameraController.value = CameraController( cameras[0], // ResolutionPreset.max, // ResolutionPreset.medium, selectedResolution.value); initializeControllerFuture.value = cameraController.value!.initialize(); await initializeControllerFuture.value; await cameraController.value!.setFlashMode(FlashMode.off); } initializeCamera(); return null; }, []); Future initializeCameraAfterChangeResolution() async { final cameras = await availableCameras(); cameraController.value = CameraController( cameras[0], // ResolutionPreset.max, // ResolutionPreset.medium, selectedResolution.value); initializeControllerFuture.value = cameraController.value!.initialize(); await initializeControllerFuture.value; await cameraController.value!.setFlashMode(FlashMode.off); } Future rotateAndCropImage(File imageFile) async { Uint8List imageBytes = await imageFile.readAsBytes(); img.Image? original = img.decodeImage(imageBytes); if (original == null) return imageFile; img.Image rotated = img.bakeOrientation(original); int width = rotated.width; int height = rotated.height; bool isPortrait = height > width; int cropWidth, cropHeight; if (isPortrait) { cropHeight = (height * 0.7).toInt(); cropWidth = (cropHeight ~/ 1.59).toInt(); } else { cropWidth = (width * 0.7).toInt(); cropHeight = (cropWidth ~/ 1.59).toInt(); } int left = ((width - cropWidth) ~/ 2).toInt(); int top = ((height - cropHeight) ~/ 2).toInt(); img.Image cropped = img.copyCrop(rotated, x: left, y: top, width: cropWidth, height: cropHeight); File croppedFile = File('${imageFile.path}_cropped.jpg'); await croppedFile.writeAsBytes(img.encodeJpg(cropped)); return croppedFile; } Future rotateImage(File imageFile) async { Uint8List bytes = await imageFile.readAsBytes(); img.Image? image = img.decodeImage(bytes); if (image == null) return imageFile; img.Image rotated = img.copyRotate(image, angle: -90); File rotatedFile = File('${imageFile.path}_rotated.jpg'); await rotatedFile.writeAsBytes(img.encodeJpg(rotated)); return rotatedFile; } Future captureAndCropImage() async { try { isLoading.value = true; await initializeControllerFuture.value; // Ambil gambar dari kamera final image = await cameraController.value!.takePicture(); File cropped = await rotateAndCropImage(File(image.path)); // Rotate gambar setelah cropping File rotatedImage = await rotateImage(cropped); // Convert to grayscale Uint8List bytes = await rotatedImage.readAsBytes(); img.Image? coloredImage = img.decodeImage(bytes); if (coloredImage != null) { img.Image grayscaleImage = img.grayscale(coloredImage); rotatedImage.writeAsBytesSync(img.encodeJpg(grayscaleImage)); } // Simpan hasil yang sudah di-crop dan di-rotate capturedImage.value = image; croppedImage.value = rotatedImage; // post ke BE Uint8List finalBytes = await croppedImage.value!.readAsBytes(); String base64String = base64Encode(finalBytes); ref.read(uploadScanProvider.notifier).uploadScan( host: host, base64File: base64String, userId: userId, ); } catch (e) { print("Error capturing image: $e"); } finally { isLoading.value = false; } } // listRiwayProvider ref.listen(listRiwayatScanProvider, (prev, next) { if (next is ListRiwayatScanStateLoading) { // isLoading.value = true; } else if (next is ListRiwayatScanStateError) { // isLoading.value = false; // errorMessage.value = next.message; snackbarWidget( context, next.message, snackbarType.error, Duration(seconds: 3), ); } else if (next is ListRiwayatScanStateDone) { // isLoading.value = false; Navigator.of(context).pop(); Navigator.of(context).pushNamed( editScanRoute, ); } }); // proses upload ref.listen(uploadScanProvider, (prev, next) { if (next is UploadScanStateLoading) { isLoadingUpload.value = true; } else if (next is UploadScanStateError) { isLoadingUpload.value = false; // errorMessage.value = next.message; print("Err : ${next.message}"); snackbarWidget( context, next.message, snackbarType.error, Duration(seconds: 3), ); } else if (next is UploadScanStateDone) { isLoadingUpload.value = false; ref.read(selectedPersonIdx.notifier).state = next.model[0].personID; // set SEX ref.read(eSexSelected.notifier).state = SexModel( M_SexID: next.model[0].personSex, M_SexCode: "", m_sexname: next.model[0].m_sexname, M_SexNameLang: ""); ref.read(selectedEdit.notifier).state = EditPersonModel( personID: next.model[0].personID, personNIK: next.model[0].personNIK, personName: next.model[0].personName, m_sexname: next.model[0].m_sexname, personDob: next.model[0].personDob, personSex: next.model[0].personSex, personUrl: next.model[0].personUrl, ); ref.read(listRiwayatScanProvider.notifier).listRiwayatScan( host: host, userId: userId, ); } }); // loading proses upload useEffect useEffect(() { WidgetsBinding.instance.addPostFrameCallback((timestamp) { if (isLoadingUpload.value == true) { snackbarWidget( context, 'Sedang Upload Foto...', snackbarType.warning, Duration(seconds: 3), ); } }); return () {}; }, [isLoadingUpload.value]); return Scaffold( appBar: AppBar( title: Text( 'Posisi Foto Landscape', style: Constant.titlePosisiHP(context: context), ), automaticallyImplyLeading: false, actions: [ DropdownButton( value: selectedResolution.value, onChanged: (ResolutionPreset? newValue) { if (newValue != null) { selectedResolution.value = newValue; initializeCameraAfterChangeResolution(); } }, items: ResolutionPreset.values.map((ResolutionPreset value) { return DropdownMenuItem( value: value, child: Text(value.toString().split('.').last), ); }).toList(), underline: SizedBox.shrink(), ), ], ), backgroundColor: Colors.black.withOpacity(0.5), body: Stack( children: [ if (cameraController.value != null && croppedImage.value == null) FutureBuilder( future: initializeControllerFuture.value, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return CameraPreview(cameraController.value!); } else { return const Center(child: CircularProgressIndicator()); } }, ), // jika sudah ada foto if (croppedImage.value != null) ...[ Container( width: double.infinity, height: 300, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( croppedImage.value!, fit: BoxFit.contain, ), ), ), ] else ...[ // bingkai foto Positioned.fill( child: CustomPaint( painter: OverlayPainter(), ), ), ], // loading if (isLoading.value) Positioned.fill( child: Container( color: Colors.black.withOpacity(0.5), child: const Center( child: CircularProgressIndicator( color: Colors.white, ), ), ), ), ], ), bottomNavigationBar: BottomAppBar( height: Constant.getActualYPhone( context: context, y: 100, ), shape: const CircularNotchedRectangle(), child: Row( children: [ ElevatedButton.icon( onPressed: () { capturedImage.value = null; croppedImage.value = null; Navigator.of(context).pop(); }, icon: Icon( Icons.arrow_back, size: 17, color: Constant.textWhite, ), label: Text( 'Kembali', style: Constant.cardText(context: context).copyWith( color: Constant.textWhite, ), ), style: ElevatedButton.styleFrom( backgroundColor: Constant.textCardGrey, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), elevation: 8, shadowColor: Constant.bgButton.withOpacity(0.24), ), ), const Spacer(), // scan ElevatedButton.icon( onPressed: captureAndCropImage, icon: Icon( Icons.camera, size: 17, color: Constant.textWhite, ), label: Text( 'Foto', style: Constant.cardText(context: context).copyWith( color: Constant.textWhite, ), ), style: ElevatedButton.styleFrom( backgroundColor: Constant.bgButton, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), elevation: 8, shadowColor: Constant.bgButton.withOpacity(0.24), ), ), ], ), ), ); } } class OverlayPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.black.withOpacity(0.6) ..style = PaintingStyle.fill; final borderPaint = Paint() ..color = Colors.yellow ..strokeWidth = 4 ..style = PaintingStyle.stroke; double boxHeight = size.height * 0.7; double boxWidth = boxHeight / 1.59; double left = (size.width - boxWidth) / 2; double top = (size.height - boxHeight) / 2; Path path = Path() ..addRect(Rect.fromLTWH(0, 0, size.width, size.height)) ..addRect(Rect.fromLTWH(left, top, boxWidth, boxHeight)) ..fillType = PathFillType.evenOdd; canvas.drawPath(path, paint); canvas.drawRect(Rect.fromLTWH(left, top, boxWidth, boxHeight), borderPaint); } @override bool shouldRepaint(CustomPainter oldDelegate) => false; }