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 '../../provider/current_user_provider.dart'; import '../../widget/customsnackbarwidget.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 ?? ""; useEffect(() { Future initializeCamera() async { final cameras = await availableCameras(); cameraController.value = CameraController(cameras[0], ResolutionPreset.max); initializeControllerFuture.value = cameraController.value!.initialize(); await initializeControllerFuture.value; await cameraController.value!.setFlashMode(FlashMode.off); } initializeCamera(); return null; }, []); 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); // Simpan hasil yang sudah di-crop dan di-rotate capturedImage.value = image; croppedImage.value = rotatedImage; // post ke BE Uint8List bytes = await croppedImage.value!.readAsBytes(); String base64String = base64Encode(bytes); ref.read(uploadScanProvider.notifier).uploadScan( host: host, base64File: base64String, userId: userId, ); } catch (e) { print("Error capturing image: $e"); } finally { isLoading.value = false; } } // 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; snackbarWidget( context, next.pesan, snackbarType.success, Duration(seconds: 3), ); Navigator.of(context) .pushNamedAndRemoveUntil(homeRoute, (route) => false); } }); // loading proses upload useEffect useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) { if (isLoadingUpload.value == true) { snackbarWidget( context, 'Sedang Upload Foto...', snackbarType.warning, // Duration(seconds: 3), Duration(days: 1), ); } }); return () {}; }, [isLoadingUpload.value]); // loading proses image useEffect useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) { if (isLoading.value == true) { snackbarWidget( context, 'Sedang Proses Gambar...', snackbarType.warning, // Duration(seconds: 5), Duration(days: 1), ); } }); return () {}; }, [isLoading.value]); return Scaffold( appBar: AppBar( title: Text('Posisi Foto Landscape'), automaticallyImplyLeading: false, ), backgroundColor: Colors.black.withOpacity(0.5), body: Stack( children: [ if (cameraController.value != null) FutureBuilder( future: initializeControllerFuture.value, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return CameraPreview(cameraController.value!); } else { return const Center(child: CircularProgressIndicator()); } }, ), // 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; }