import 'dart:io'; import 'dart:typed_data'; 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/constant.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 capturedImage = useState(null); final croppedImage = useState(null); final isLoading = useState(false); final initializeControllerFuture = useState?>(null); 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; } catch (e) { print("Error capturing image: $e"); } finally { isLoading.value = false; } } return GestureDetector( onTap: () { FocusManager.instance.primaryFocus!.unfocus(); }, child: Scaffold( resizeToAvoidBottomInset: true, backgroundColor: Constant.textBlack, 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(), ), ), // hasil foto if (croppedImage.value != null) Positioned.fill( child: Image.file( croppedImage.value!, fit: BoxFit.contain, ), ), // loading if (isLoading.value) Positioned.fill( child: Container( color: Colors.black.withOpacity(0.5), child: const Center( child: CircularProgressIndicator(color: Colors.white), ), ), ), ], ), bottomNavigationBar: BottomAppBar( shape: const CircularNotchedRectangle(), child: Row( children: [ ElevatedButton.icon( onPressed: () { 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), ), ), Spacer(), // clear ElevatedButton.icon( onPressed: () { capturedImage.value = null; croppedImage.value = null; }, icon: Icon( Icons.clear, size: 17, color: Constant.textWhite, ), label: Text( 'Reset', style: Constant.cardText(context: context).copyWith( color: Constant.textWhite, ), ), style: ElevatedButton.styleFrom( backgroundColor: Constant.textRed, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), elevation: 8, shadowColor: Constant.bgButton.withOpacity(0.24), ), ), SizedBox( width: Constant.getActualXPhone(context: context, x: 10), ), // 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; }