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 'package:mobile_scanner/mobile_scanner.dart'; 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/no-login-scan/no_login_scan_provider.dart'; import '../../widget/customsnackbarwidget.dart'; import '../home/list_riwayat_scan_provider.dart'; import 'no_login_upload_scan_provider.dart'; class NoLoginScanScreen extends HookConsumerWidget { const NoLoginScanScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [ SystemUiOverlay.bottom, ]); final judulAppBar = useState("Scan QRCode"); final cameraOn = useState(true); final qrCodeStr = useState("Scan QRCode untuk dapat merekam"); final judulTombol = useState("SCAN QR CODE"); final awalan = useState("Info : "); final judulPosisiHp = useState("Posisi HP Landscape"); // final scannerCtr = useState(null); final cameraOrScanner = useState("SCAN"); final loadingScreen = useState(true); 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); Future initializeCamera() async { try { final cameras = await availableCameras(); cameraController.value = CameraController( cameras[0], selectedResolution.value, ); initializeControllerFuture.value = cameraController.value!.initialize(); await initializeControllerFuture.value; if (cameraController.value!.value.isInitialized) { await cameraController.value!.setFlashMode(FlashMode.off); } } catch (e) { print('Error initializing camera: $e'); } } useEffect(() { Future initialize() async { // delay 3 second e await Future.delayed( Duration(seconds: 3), ); await initializeCamera(); loadingScreen.value = false; // Hide loading screen untuk inisialisasi } initialize(); return null; }, []); if (loadingScreen.value) { return Scaffold( appBar: AppBar( title: (cameraOn.value == false) ? Text( judulAppBar.value, style: Constant.titlePosisiHP(context: context), ) : Text( judulPosisiHp.value, style: Constant.titlePosisiHP(context: context), ), automaticallyImplyLeading: false, ), backgroundColor: Colors.black.withOpacity(0.5), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator( color: Constant.textWhite, ), SizedBox(height: 20), Text( "Menunggu Camera Siap", style: TextStyle( color: Constant.textWhite, ), ), ], ), ), bottomNavigationBar: BottomAppBar( height: Constant.getActualYPhone( context: context, y: 100, ), shape: const CircularNotchedRectangle(), child: Row( children: [ ElevatedButton.icon( onPressed: () { capturedImage.value = null; croppedImage.value = null; ref.read(barcodeX.notifier).state = Barcode(); judulTombol.value = "SCAN QR CODE"; qrCodeStr.value = "Scan QRCode untuk dapat merekam"; cameraOn.value = false; awalan.value = "Info :"; cameraOrScanner.value = "SCAN"; 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(), ], ), ), ); } void handleBarcode(BarcodeCapture barcodes) async { if (barcodes.barcodes.isNotEmpty) { final scannedBarcode = barcodes.barcodes.firstOrNull?.displayValue ?? ""; if (scannedBarcode.isNotEmpty) { await initializeCamera(); ref.read(barcodeX.notifier).state = barcodes.barcodes.first; qrCodeStr.value = scannedBarcode; awalan.value = "QrCode : "; cameraOn.value = true; judulTombol.value = "FOTO"; cameraOrScanner.value = "CAMERA"; } } } useEffect(() { Future initializeCamera() async { try { final cameras = await availableCameras(); cameraController.value = CameraController( cameras[0], selectedResolution.value, ); initializeControllerFuture.value = cameraController.value!.initialize(); await initializeControllerFuture.value; if (cameraController.value!.value.isInitialized) { await cameraController.value!.setFlashMode(FlashMode.off); } } catch (e) { print('Error initializing camera: $e'); } } initializeCamera(); return () {}; }, []); 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(noLoginUploadScanProvider.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(noLoginUploadScanProvider, (prev, next) { if (next is NoLoginUploadScanStateLoading) { isLoadingUpload.value = true; } else if (next is NoLoginUploadScanStateError) { isLoadingUpload.value = false; // errorMessage.value = next.message; print("Err : ${next.message}"); snackbarWidget( context, next.message, snackbarType.error, Duration(seconds: 3), ); } else if (next is NoLoginUploadScanStateDone) { 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: (cameraOn.value == false) ? Text( judulAppBar.value, style: Constant.titlePosisiHP(context: context), ) : Text( judulPosisiHp.value, style: Constant.titlePosisiHP(context: context), ), automaticallyImplyLeading: false, actions: [ if (cameraOn.value == true) 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: [ // jika camera scan ktp true show if (cameraOrScanner.value == "CAMERA") ...[ 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 Center( child: CircularProgressIndicator( color: Constant.textWhite, ), ); } }, ), // 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( top: Constant.getActualYPhone( context: context, y: 40, ), child: CustomPaint( painter: OverlayPainter(), ), ), ], ], if (cameraOrScanner.value == "SCAN") ...[ SizedBox( width: double.infinity, height: double.infinity, child: MobileScanner( // controller: scannerCtr.value?.controller, onDetect: handleBarcode, ), ), ], // Info Container( width: double.infinity, height: Constant.getActualYPhone( context: context, y: 100, ), alignment: Alignment.center, padding: EdgeInsets.symmetric(vertical: 10), color: Constant.inputanGrey, child: Text( "${awalan.value} ${qrCodeStr.value}", style: TextStyle(color: Colors.white), ), ), // 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; ref.read(barcodeX.notifier).state = Barcode(); judulTombol.value = "SCAN QR CODE"; qrCodeStr.value = "Scan QRCode untuk dapat merekam"; cameraOn.value = false; awalan.value = "Info :"; cameraOrScanner.value = "SCAN"; 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( judulTombol.value, 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; }