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 initializeControllerFuture = useState?>(null); final capturedImage = useState(null); final croppedImage = useState(null); final isLoading = useState(false); final isModalOpen = useState(false); 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; } void showResultModal(BuildContext context) { isModalOpen.value = true; showModalBottomSheet( context: context, isScrollControlled: true, enableDrag: false, backgroundColor: Colors.white, builder: (context) { return Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header dengan tombol close Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Hasil Foto", style: Constant.cardText(context: context).copyWith( fontWeight: FontWeight.bold, fontSize: 18, color: Constant.textBlack, ), ), IconButton( icon: const Icon(Icons.close, color: Colors.black), onPressed: () { isModalOpen.value = false; Navigator.of(context).pop(); }, ), ], ), const SizedBox(height: 16), // Gambar hasil 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, ), ), ), const SizedBox(height: 16), // Tombol Hapus dan Upload Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Tombol Hapus ElevatedButton.icon( onPressed: () { capturedImage.value = null; croppedImage.value = null; isModalOpen.value = false; Navigator.of(context).pop(); }, icon: const Icon(Icons.delete, size: 17, color: Colors.white), label: Text( 'Hapus', style: Constant.cardText(context: context).copyWith( color: Colors.white, ), ), style: ElevatedButton.styleFrom( backgroundColor: Constant.textRed, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), elevation: 8, ), ), // Tombol Upload ElevatedButton.icon( onPressed: () { print('upload baru'); }, icon: const Icon(Icons.upload, size: 17, color: Colors.white), label: Text( 'Upload', style: Constant.cardText(context: context).copyWith( color: Colors.white, ), ), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), elevation: 8, ), ), ], ), ], ), ); }, ).whenComplete(() => isModalOpen.value = false); } 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; showResultModal(context); } catch (e) { print("Error capturing image: $e"); } finally { isLoading.value = false; } } return Scaffold( 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: () { 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; }