diff --git a/lib/app/route.dart b/lib/app/route.dart index 4efb32e..098efe1 100644 --- a/lib/app/route.dart +++ b/lib/app/route.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:scanktpflutter/screen/no-login-scan/no_login_scan_screen.dart'; import '../screen/no-login-home/no_login_home_screen.dart'; import '../screen/no-login-splash/no_login_splash_screen.dart'; import '../screen/no-login/no_login_screen.dart'; @@ -97,6 +98,16 @@ class AppRoute { }); } + if (settings.name == noLoginScanRoute) { + return MaterialPageRoute(builder: (context) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear(1.0), padding: EdgeInsets.all(0)), + child: NoLoginScanScreen(), + ); + }); + } + // edit screen if (settings.name == editScanRoute) { return MaterialPageRoute(builder: (context) { diff --git a/lib/provider/no-login-scan/no_login_scan_provider.dart b/lib/provider/no-login-scan/no_login_scan_provider.dart new file mode 100644 index 0000000..cbc6ea2 --- /dev/null +++ b/lib/provider/no-login-scan/no_login_scan_provider.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:scanktpflutter/model/sex_model.dart'; + +import '../../model/edit_person_model.dart'; +import '../../model/person_ktp_model.dart'; + +// list scan +final listScanRwt = StateProvider>( + (ref) => List.empty( + growable: true, + ), +); + +final selectedPersonIdx = StateProvider((ref) => "0"); +final selectedEdit = StateProvider( + (ref) => EditPersonModel( + personID: "", + personNIK: "", + personName: "", + personDob: "", + personSex: "", + personUrl: "", + m_sexname: "", + ), +); + +// inputan edit +final eNikCtr = StateProvider( + (ref) => TextEditingController(text: ""), +); + +final eNamaCtr = StateProvider( + (ref) => TextEditingController(text: ""), +); + +final eDobCtr = StateProvider( + (ref) => TextEditingController(text: ""), +); + +final eDobDt = StateProvider( + (ref) => DateTime.now(), +); + +final eSexCtr = StateProvider( + (ref) => TextEditingController(text: ""), +); + +final eSexSelected = StateProvider( + (ref) => SexModel( + M_SexID: "", + M_SexCode: "", + m_sexname: "", + M_SexNameLang: "", + ), +); + +final barcodeX = StateProvider( + (ref) => Barcode(), +); \ No newline at end of file diff --git a/lib/provider/scan_provider.dart b/lib/provider/scan_provider.dart index 057259c..385f738 100644 --- a/lib/provider/scan_provider.dart +++ b/lib/provider/scan_provider.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:scanktpflutter/model/sex_model.dart'; -import '../model/edit_person_model.dart'; -import '../model/person_ktp_model.dart'; +import '../../model/edit_person_model.dart'; +import '../../model/person_ktp_model.dart'; // list scan final listScanRwt = StateProvider>( @@ -53,4 +54,4 @@ final eSexSelected = StateProvider( m_sexname: "", M_SexNameLang: "", ), -); +); \ No newline at end of file diff --git a/lib/repository/no-login-scan/no_login_scan_repository.dart b/lib/repository/no-login-scan/no_login_scan_repository.dart index 715b7c3..70386d1 100644 --- a/lib/repository/no-login-scan/no_login_scan_repository.dart +++ b/lib/repository/no-login-scan/no_login_scan_repository.dart @@ -36,7 +36,7 @@ class NoLoginScanRepository extends BaseRepository { required String userId, }) async { final service = - "http://${host}/one-api/scan-ktpv2-no-login/Scanktp/proses_scan"; + "http://${host}/one-api/scan-ktpv2-no-login/Scanktpv2/proses_scan"; final resp = await post(param: { "base64File": base64File, "userId": userId, @@ -56,7 +56,7 @@ class NoLoginScanRepository extends BaseRepository { required String host, }) async { // final service = "${Constant.baseUrl}xauth/login"; - final service = "http://${host}/one-api/scan-ktpv2-no-login/Scanktp/getSex"; + final service = "http://${host}/one-api/scan-ktpv2-no-login/Scanktpv2/getSex"; final resp = await post(param: {}, service: service); final result = List.empty(growable: true); @@ -79,7 +79,7 @@ class NoLoginScanRepository extends BaseRepository { required String Person_Sex, }) async { final service = - "http://${host}/one-api/scan-ktpv2-no-login/Scanktp/proses_edit"; + "http://${host}/one-api/scan-ktpv2-no-login/Scanktpv2/proses_edit"; final resp = await post( param: { "Person_ID": Person_ID, diff --git a/lib/screen/home/card_riwayat_scan.dart b/lib/screen/home/card_riwayat_scan.dart index b6d1629..74e5474 100644 --- a/lib/screen/home/card_riwayat_scan.dart +++ b/lib/screen/home/card_riwayat_scan.dart @@ -8,7 +8,7 @@ import '../../app/route.dart'; import '../../app/constant.dart'; import '../../model/edit_person_model.dart'; import '../../model/person_ktp_model.dart'; -import '../../provider/scan_provider.dart'; +import '../../provider/no-login-scan/no_login_scan_provider.dart'; class CardRiwayatScan extends HookConsumerWidget { final PersonKtp data; diff --git a/lib/screen/home/home_screen.dart b/lib/screen/home/home_screen.dart index 4cafc58..189950b 100644 --- a/lib/screen/home/home_screen.dart +++ b/lib/screen/home/home_screen.dart @@ -2,7 +2,7 @@ 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 '../../provider/scan_provider.dart'; +import '../../provider/no-login-scan/no_login_scan_provider.dart'; import '../../screen/home/card_riwayat_scan.dart'; import '../../screen/home/list_riwayat_scan_provider.dart'; diff --git a/lib/screen/home/list_riwayat_scan_provider.dart b/lib/screen/home/list_riwayat_scan_provider.dart index ea07824..1a6d32e 100644 --- a/lib/screen/home/list_riwayat_scan_provider.dart +++ b/lib/screen/home/list_riwayat_scan_provider.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../provider/scan_provider.dart'; +import '../../provider/no-login-scan/no_login_scan_provider.dart'; import '../../repository/scan_repository.dart'; import '../../model/person_ktp_model.dart'; diff --git a/lib/screen/no-login-home/no_login_card_riwayat_scan.dart b/lib/screen/no-login-home/no_login_card_riwayat_scan.dart index 1216800..f563ca4 100644 --- a/lib/screen/no-login-home/no_login_card_riwayat_scan.dart +++ b/lib/screen/no-login-home/no_login_card_riwayat_scan.dart @@ -8,7 +8,7 @@ import '../../app/route.dart'; import '../../app/constant.dart'; import '../../model/edit_person_model.dart'; import '../../model/person_ktp_model.dart'; -import '../../provider/scan_provider.dart'; +import '../../provider/no-login-scan/no_login_scan_provider.dart'; class NoLoginCardRiwayatScan extends HookConsumerWidget { final PersonKtp data; diff --git a/lib/screen/no-login-home/no_login_home_screen.dart b/lib/screen/no-login-home/no_login_home_screen.dart index fa5aaef..884602b 100644 --- a/lib/screen/no-login-home/no_login_home_screen.dart +++ b/lib/screen/no-login-home/no_login_home_screen.dart @@ -6,7 +6,7 @@ import '../../app/app_extension.dart'; import '../../screen/no-login-home/no_login_riwayat_scan_provider.dart'; // import 'package:shared_preferences/shared_preferences.dart'; import '../../provider/no-login/no_login_current_user_provider.dart'; -import '../../provider/scan_provider.dart'; +import '../../provider/no-login-scan/no_login_scan_provider.dart'; import '../../app/constant.dart'; import '../../app/route.dart'; @@ -155,7 +155,7 @@ class NoLoginHomeScreen extends HookConsumerWidget { ), child: ElevatedButton( onPressed: () { - Navigator.of(context).pushNamed(scanRoute); + Navigator.of(context).pushNamed(noLoginScanRoute); }, style: ElevatedButton.styleFrom( backgroundColor: Constant.bgButton, diff --git a/lib/screen/no-login-home/no_login_riwayat_scan_provider.dart b/lib/screen/no-login-home/no_login_riwayat_scan_provider.dart index a8ff67b..f0e5791 100644 --- a/lib/screen/no-login-home/no_login_riwayat_scan_provider.dart +++ b/lib/screen/no-login-home/no_login_riwayat_scan_provider.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:scanktpflutter/screen/no-login/no_login_refresh_token_provider.dart'; import '../../app/app_extension.dart'; import '../../repository/no-login-scan/no_login_scan_repository.dart'; -import '../../provider/scan_provider.dart'; +import '../../provider/no-login-scan/no_login_scan_provider.dart'; import '../../model/person_ktp_model.dart'; import '../../provider/dio_provider.dart'; diff --git a/lib/screen/no-login-scan/no_login_scan_screen.dart b/lib/screen/no-login-scan/no_login_scan_screen.dart new file mode 100644 index 0000000..b6f6be3 --- /dev/null +++ b/lib/screen/no-login-scan/no_login_scan_screen.dart @@ -0,0 +1,609 @@ +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; +} diff --git a/lib/screen/no-login-scan/no_login_scan_screen.txt b/lib/screen/no-login-scan/no_login_scan_screen.txt new file mode 100644 index 0000000..c6ef998 --- /dev/null +++ b/lib/screen/no-login-scan/no_login_scan_screen.txt @@ -0,0 +1,487 @@ +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 & Scan KTP"); + final qrCodeExists = useState(false); + final cameraOn = useState(false); + 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 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); + + void handleBarcode(BarcodeCapture barcodes) { + if (barcodes.barcodes.isNotEmpty) { + final scannedBarcode = + barcodes.barcodes.firstOrNull?.displayValue ?? ""; + if (scannedBarcode.isNotEmpty) { + ref.read(barcodeX.notifier).state = barcodes.barcodes.first; + qrCodeStr.value = scannedBarcode; + awalan.value = "QrCode : "; + qrCodeExists.value = true; + cameraOn.value = true; + judulTombol.value = "FOTO"; + } + } + } + + 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(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: Text( + judulAppBar.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: [ + if (awalan.value == "Info :") + SizedBox( + width: double.infinity, + height: Constant.getActualYPhone( + context: context, + y: 450, + ), + child: MobileScanner( + onDetect: handleBarcode, + ), + ), + // Show Qr code Str + // Container( + // alignment: Alignment.center, + // padding: EdgeInsets.symmetric(vertical: 10), + // color: Constant.inputanGrey, + // child: Text( + // "${awalan.value} ${qrCodeStr.value}", + // style: TextStyle(color: Colors.white), + // ), + // ), + // jika camera scan ktp true show + if (cameraOn.value == true) ...[ + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 20, + ), + ), + // show notif posisi HP + Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 10), + color: Constant.textRed, + child: Text( + judulPosisiHp.value, + style: TextStyle(color: Colors.white), + ), + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 20, + ), + ), + 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; + + ref.read(barcodeX.notifier).state = Barcode(); + qrCodeExists.value = false; + judulTombol.value = "SCAN QR CODE"; + qrCodeStr.value = "Scan QRCode untuk dapat merekam"; + cameraOn.value = false; + awalan.value = "Info :"; + 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; +} diff --git a/lib/screen/no-login-scan/no_login_upload_scan_provider.dart b/lib/screen/no-login-scan/no_login_upload_scan_provider.dart new file mode 100644 index 0000000..52cee02 --- /dev/null +++ b/lib/screen/no-login-scan/no_login_upload_scan_provider.dart @@ -0,0 +1,72 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../repository/no-login-scan/no_login_scan_repository.dart'; +import '../../model/sukses_person_model.dart'; + +import '../../provider/dio_provider.dart'; +import '../../repository/base_repository.dart'; + +// 3. state provider +final noLoginUploadScanProvider = StateNotifierProvider( + (ref) => NoLoginUploadScanNotifier(ref: ref)); + +// 2. notifier +class NoLoginUploadScanNotifier extends StateNotifier { + final Ref ref; + NoLoginUploadScanNotifier({required this.ref}) : super(NoLoginUploadScanStateInit()); + void uploadScan({ + required String host, + required String userId, + required String base64File, + }) async { + try { + state = NoLoginUploadScanStateLoading(); + final resp = await NoLoginScanRepository( + dio: ref.read(dioProvider), + ).prosesScan( + host: host, + base64File: base64File, + userId: userId, + ); + + // print(resp); + state = NoLoginUploadScanStateDone(model: resp); + } catch (e) { + if (e is BaseRepositoryException) { + state = NoLoginUploadScanStateError(message: e.message); + } else { + state = NoLoginUploadScanStateError(message: e.toString()); + } + } + } +} + +// 1. state +abstract class NoLoginUploadScanState extends Equatable { + final DateTime date; + const NoLoginUploadScanState(this.date); + @override + List get props => [date]; +} + +class NoLoginUploadScanStateInit extends NoLoginUploadScanState { + NoLoginUploadScanStateInit() : super(DateTime.now()); +} + +class NoLoginUploadScanStateLoading extends NoLoginUploadScanState { + NoLoginUploadScanStateLoading() : super(DateTime.now()); +} + +class NoLoginUploadScanStateError extends NoLoginUploadScanState { + final String message; + NoLoginUploadScanStateError({ + required this.message, + }) : super(DateTime.now()); +} + +class NoLoginUploadScanStateDone extends NoLoginUploadScanState { + final List model; + NoLoginUploadScanStateDone({ + required this.model, + }) : super(DateTime.now()); +} diff --git a/lib/screen/scan/edit_scan_screen.dart b/lib/screen/scan/edit_scan_screen.dart index ba13cef..5450ea2 100644 --- a/lib/screen/scan/edit_scan_screen.dart +++ b/lib/screen/scan/edit_scan_screen.dart @@ -6,7 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:scanktpflutter/screen/scan/edit_scan_provider.dart'; import '../../model/sex_model.dart'; import '../../screen/scan/sex_provider.dart'; -import '../../provider/scan_provider.dart'; +import '../../provider/no-login-scan/no_login_scan_provider.dart'; import '../../widget/customsnackbarwidget.dart'; import '../../app/app_extension.dart'; diff --git a/lib/screen/scan/scan_screen.dart b/lib/screen/scan/scan_screen.dart index 77c5884..aa3da87 100644 --- a/lib/screen/scan/scan_screen.dart +++ b/lib/screen/scan/scan_screen.dart @@ -12,7 +12,7 @@ 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 '../../provider/no-login-scan/no_login_scan_provider.dart'; import '../../widget/customsnackbarwidget.dart'; import '../home/list_riwayat_scan_provider.dart'; import 'upload_scan_provider.dart'; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b8e2b22..3d6169d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import mobile_scanner import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 70fbbe4..74968b9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -352,6 +352,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "91d28b825784e15572fdc39165c5733099ce0e69c6f6f0964ebdbf98a62130fd" + url: "https://pub.dev" + source: hosted + version: "6.0.6" path: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 01598c5..6eee701 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: image: ^4.1.3 permission_handler: ^11.3.0 dropdown_button2: ^2.3.9 + mobile_scanner: ^6.0.6 dev_dependencies: flutter_test: