diff --git a/ios/Podfile b/ios/Podfile index a353ca412..62c4173b1 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -38,18 +38,33 @@ target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end + +$iOSVersion = '14.0' + post_install do |installer| + # Google's ML Kit Barcode Scanning setup + installer.pods_project.build_configurations.each do |config| + config.build_settings["EXCLUDED_ARCHS[sdk=*]"] = "armv7" + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = $iOSVersion + end + installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) - # polygonid-setup + # polygonid-setup target.build_configurations.each do |config| cflags = config.build_settings['OTHER_CFLAGS'] || ['$(inherited)'] cflags << '-fembed-bitcode' config.build_settings['OTHER_CFLAGS'] = cflags config.build_settings['SWIFT_VERSION'] = '5.0' config.build_settings['ENABLE_BITCODE'] = 'NO' - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = $iOSVersion end + # Google's ML Kit Barcode Scanning setup + target.build_configurations.each do |config| + if Gem::Version.new($iOSVersion) > Gem::Version.new(config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']) + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = $iOSVersion + end + end if target.name == "Pods-Runner" puts "Updating #{target.name} OTHER_LDFLAGS" target.build_configurations.each do |config| diff --git a/lib/dashboard/qr_code/qr_code_scan/qr_code_scan.dart b/lib/dashboard/qr_code/qr_code_scan/qr_code_scan.dart index 71a05684e..0859da49d 100644 --- a/lib/dashboard/qr_code/qr_code_scan/qr_code_scan.dart +++ b/lib/dashboard/qr_code/qr_code_scan/qr_code_scan.dart @@ -1,4 +1,5 @@ export 'cubit/qr_code_scan_cubit.dart'; export 'model/siopv2_param.dart'; +export 'view/qr_camera_view.dart'; export 'view/qr_code_scan_page.dart'; export 'view/qr_scanner_page.dart'; diff --git a/lib/dashboard/qr_code/qr_code_scan/view/qr_camera_view.dart b/lib/dashboard/qr_code/qr_code_scan/view/qr_camera_view.dart new file mode 100644 index 000000000..0e79dd2fc --- /dev/null +++ b/lib/dashboard/qr_code/qr_code_scan/view/qr_camera_view.dart @@ -0,0 +1,242 @@ +import 'dart:io'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_mlkit_commons/google_mlkit_commons.dart'; + +class QrCameraView extends StatefulWidget { + const QrCameraView( + {super.key, + required this.onImage, + this.onCameraFeedReady, + this.onDetectorViewModeChanged, + this.onCameraLensDirectionChanged, + this.initialCameraLensDirection = CameraLensDirection.back}); + + final Function(InputImage inputImage) onImage; + final VoidCallback? onCameraFeedReady; + final VoidCallback? onDetectorViewModeChanged; + final Function(CameraLensDirection direction)? onCameraLensDirectionChanged; + final CameraLensDirection initialCameraLensDirection; + + @override + State createState() => _QrCameraViewState(); +} + +class _QrCameraViewState extends State { + static List _cameras = []; + CameraController? _controller; + int _cameraIndex = -1; + double _currentZoomLevel = 1.0; + double _minAvailableZoom = 1.0; + double _maxAvailableZoom = 1.0; + double _minAvailableExposureOffset = 0.0; + double _maxAvailableExposureOffset = 0.0; + double _currentExposureOffset = 0.0; + bool _changingCameraLens = false; + + @override + void initState() { + super.initState(); + + _initialize(); + } + + void _initialize() async { + if (_cameras.isEmpty) { + _cameras = await availableCameras(); + } + for (var i = 0; i < _cameras.length; i++) { + if (_cameras[i].lensDirection == widget.initialCameraLensDirection) { + _cameraIndex = i; + break; + } + } + if (_cameraIndex != -1) { + _startLiveFeed(); + } + } + + @override + void dispose() { + _stopLiveFeed(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold(body: _liveFeedBody()); + } + + Widget _liveFeedBody() { + if (_cameras.isEmpty) return Container(); + if (_controller == null) return Container(); + if (_controller?.value.isInitialized == false) return Container(); + return ColoredBox( + color: Colors.black, + child: Stack( + fit: StackFit.expand, + children: [ + Center( + child: _changingCameraLens + ? Container() + : CameraPreview( + _controller!, + child: null, + ), + ) + ], + ), + ); + } + + Future _startLiveFeed() async { + final camera = _cameras[_cameraIndex]; + _controller = CameraController( + camera, + // Set to ResolutionPreset.high. Do NOT set it to ResolutionPreset.max because for some phones does NOT work. + ResolutionPreset.high, + enableAudio: false, + imageFormatGroup: Platform.isAndroid + ? ImageFormatGroup.nv21 + : ImageFormatGroup.bgra8888, + ); + await _controller?.initialize().then((_) { + if (!mounted) { + return; + } + _controller?.getMinZoomLevel().then((value) { + _currentZoomLevel = value; + _minAvailableZoom = value; + }); + _controller?.getMaxZoomLevel().then((value) { + _maxAvailableZoom = value; + }); + _currentExposureOffset = 0.0; + _controller?.getMinExposureOffset().then((value) { + _minAvailableExposureOffset = value; + }); + _controller?.getMaxExposureOffset().then((value) { + _maxAvailableExposureOffset = value; + }); + _controller?.startImageStream(_processCameraImage).then((value) { + if (widget.onCameraFeedReady != null) { + widget.onCameraFeedReady!(); + } + if (widget.onCameraLensDirectionChanged != null) { + widget.onCameraLensDirectionChanged!(camera.lensDirection); + } + }); + setState(() {}); + }); + } + + Future _stopLiveFeed() async { + await _controller?.stopImageStream(); + await _controller?.dispose(); + _controller = null; + } + + void _processCameraImage(CameraImage image) { + final inputImage = _inputImageFromCameraImage(image); + if (inputImage == null) return; + widget.onImage(inputImage); + } + + final _orientations = { + DeviceOrientation.portraitUp: 0, + DeviceOrientation.landscapeLeft: 90, + DeviceOrientation.portraitDown: 180, + DeviceOrientation.landscapeRight: 270, + }; + + InputImage? _inputImageFromCameraImage(CameraImage image) { + if (_controller == null) return null; + + // get image rotation + // it is used in android to convert the InputImage from Dart to Java: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/google_mlkit_commons/android/src/main/java/com/google_mlkit_commons/InputImageConverter.java + // `rotation` is not used in iOS to convert the InputImage from Dart to Obj-C: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/google_mlkit_commons/ios/Classes/MLKVisionImage%2BFlutterPlugin.m + // in both platforms `rotation` and `camera.lensDirection` can be used to compensate `x` and `y` coordinates on a canvas: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/example/lib/vision_detector_views/painters/coordinates_translator.dart + final camera = _cameras[_cameraIndex]; + final sensorOrientation = camera.sensorOrientation; + // print( + // 'lensDirection: ${camera.lensDirection}, sensorOrientation: $sensorOrientation, ${_controller?.value.deviceOrientation} ${_controller?.value.lockedCaptureOrientation} ${_controller?.value.isCaptureOrientationLocked}'); + InputImageRotation? rotation; + if (Platform.isIOS) { + rotation = InputImageRotationValue.fromRawValue(sensorOrientation); + } else if (Platform.isAndroid) { + var rotationCompensation = + _orientations[_controller!.value.deviceOrientation]; + if (rotationCompensation == null) return null; + if (camera.lensDirection == CameraLensDirection.front) { + // front-facing + rotationCompensation = (sensorOrientation + rotationCompensation) % 360; + } else { + // back-facing + rotationCompensation = + (sensorOrientation - rotationCompensation + 360) % 360; + } + rotation = InputImageRotationValue.fromRawValue(rotationCompensation); + // print('rotationCompensation: $rotationCompensation'); + } + if (rotation == null) return null; + // print('final rotation: $rotation'); + + // get image format + final format = InputImageFormatValue.fromRawValue(image.format.raw as int); + // validate format depending on platform + // only supported formats: + // * nv21 for Android + // * bgra8888 for iOS + if (format == null || + (Platform.isAndroid && format != InputImageFormat.nv21) || + (Platform.isIOS && format != InputImageFormat.bgra8888)) return null; + + // since format is constraint to nv21 or bgra8888, both only have one plane + if (image.planes.length != 1) return null; + final plane = image.planes.first; + + // compose InputImage using bytes + return InputImage.fromBytes( + bytes: plane.bytes, + metadata: InputImageMetadata( + size: Size(image.width.toDouble(), image.height.toDouble()), + rotation: rotation, // used only in Android + format: format, // used only in iOS + bytesPerRow: plane.bytesPerRow, // used only in iOS + ), + ); + } +} + +class SquarePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final double squareLength = 100; + final double borderWidth = 3; + final double borderRadius = 5; + + final Offset center = size.center(Offset.zero); + final Rect squareRect = Rect.fromCenter( + center: center, + width: squareLength, + height: squareLength, + ); + + final Paint borderPaint = Paint() + ..color = Colors.black + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth; + + final RRect squareRRect = + RRect.fromRectAndRadius(squareRect, Radius.circular(borderRadius)); + + canvas.drawRRect(squareRRect, borderPaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} diff --git a/lib/dashboard/qr_code/qr_code_scan/view/qr_code_scan_page.dart b/lib/dashboard/qr_code/qr_code_scan/view/qr_code_scan_page.dart index 48526b034..a0428fd67 100644 --- a/lib/dashboard/qr_code/qr_code_scan/view/qr_code_scan_page.dart +++ b/lib/dashboard/qr_code/qr_code_scan/view/qr_code_scan_page.dart @@ -2,9 +2,10 @@ import 'package:altme/app/app.dart'; import 'package:altme/dashboard/dashboard.dart'; import 'package:altme/l10n/l10n.dart'; import 'package:altme/scan/scan.dart'; +import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; class QrCodeScanPage extends StatefulWidget { const QrCodeScanPage({super.key}); @@ -19,20 +20,18 @@ class QrCodeScanPage extends StatefulWidget { } class _QrCodeScanPageState extends State { - final qrKey = GlobalKey(debugLabel: 'QR'); - - MobileScannerController scannerController = MobileScannerController( - formats: [BarcodeFormat.qrCode], - ); - - bool isScanned = false; + final BarcodeScanner _barcodeScannerController = + BarcodeScanner(formats: [BarcodeFormat.qrCode]); @override void dispose() { + _barcodeScannerController.close(); super.dispose(); - scannerController.dispose(); } + bool isScanned = false; + var _cameraLensDirection = CameraLensDirection.back; + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -68,64 +67,35 @@ class _QrCodeScanPageState extends State { scrollView: false, extendBelow: true, titleLeading: const BackLeadingButton(), - titleTrailing: IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: scannerController.torchState, - builder: (context, state, child) { - switch (state) { - case TorchState.off: - return const Icon(Icons.flash_off, color: Colors.grey); - case TorchState.on: - return const Icon(Icons.flash_on, color: Colors.yellow); + useSafeArea: false, + backgroundColor: Colors.black, + body: Center( + child: QrCameraView( + onImage: (InputImage inputImage) async { + if (!isScanned) { + final barcodes = + await _barcodeScannerController.processImage(inputImage); + if (barcodes.isEmpty) { + return; + } + + if (isScanned) return; + isScanned = true; + + await context + .read() + .process(scannedResponse: barcodes.first.rawValue); } }, - ), - iconSize: 32, - onPressed: () => scannerController.toggleTorch(), - ), - body: SafeArea( - child: Center( - child: Container( - margin: const EdgeInsets.only(bottom: Sizes.appBarHeight), - child: ClipRRect( - borderRadius: BorderRadius.circular(15), - child: SizedBox.square( - dimension: MediaQuery.of(context).size.shortestSide * 0.8, - child: MobileScanner( - key: qrKey, - controller: scannerController, - onDetect: (capture) { - final List qrcodes = capture.barcodes; - final Barcode qrcode = qrcodes[0]; - for (final barcode in qrcodes) { - debugPrint('Barcode found! ${barcode.rawValue}'); - } - if (qrcode.rawValue == null) { - context.read().emitError( - ResponseMessage( - ResponseString - .RESPONSE_STRING_SOMETHING_WENT_WRONG_TRY_AGAIN_LATER, // ignore: lines_longer_than_80_chars - ), - ); - } else { - if (!isScanned) { - isScanned = true; - final String code = qrcode.rawValue!; - context - .read() - .process(scannedResponse: code); - scannerController.stop(); - } - } - }, - ), - ), - ), - ), + initialCameraLensDirection: _cameraLensDirection, + onCameraLensDirectionChanged: (value) => + _cameraLensDirection = value, ), ), ), ); } } + + +//qr is scanning twice diff --git a/lib/dashboard/qr_code/qr_code_scan/view/qr_scanner_page.dart b/lib/dashboard/qr_code/qr_code_scan/view/qr_scanner_page.dart index e301b4602..d828eca25 100644 --- a/lib/dashboard/qr_code/qr_code_scan/view/qr_scanner_page.dart +++ b/lib/dashboard/qr_code/qr_code_scan/view/qr_scanner_page.dart @@ -1,7 +1,9 @@ import 'package:altme/app/app.dart'; +import 'package:altme/dashboard/dashboard.dart'; import 'package:altme/l10n/l10n.dart'; +import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; class QrScannerPage extends StatefulWidget { const QrScannerPage({super.key}); @@ -16,18 +18,18 @@ class QrScannerPage extends StatefulWidget { } class _QrScannerPageState extends State { - final qrKey = GlobalKey(debugLabel: 'TokenQR'); - - MobileScannerController scannerController = MobileScannerController( - formats: [BarcodeFormat.qrCode], - ); + final BarcodeScanner _barcodeScannerController = + BarcodeScanner(formats: [BarcodeFormat.qrCode]); @override void dispose() { + _barcodeScannerController.close(); super.dispose(); - scannerController.dispose(); } + bool isScanned = false; + var _cameraLensDirection = CameraLensDirection.back; + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -37,22 +39,6 @@ class _QrScannerPageState extends State { scrollView: false, extendBelow: true, titleLeading: const BackLeadingButton(), - titleTrailing: IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: scannerController.torchState, - builder: (context, state, child) { - switch (state) { - case TorchState.off: - return const Icon(Icons.flash_off, color: Colors.grey); - case TorchState.on: - return const Icon(Icons.flash_on, color: Colors.yellow); - } - }, - ), - iconSize: 32, - onPressed: () => scannerController.toggleTorch(), - ), body: SafeArea( child: Center( child: Container( @@ -61,21 +47,25 @@ class _QrScannerPageState extends State { borderRadius: BorderRadius.circular(15), child: SizedBox.square( dimension: MediaQuery.of(context).size.shortestSide * 0.8, - child: MobileScanner( - key: qrKey, - fit: BoxFit.cover, - controller: scannerController, - onDetect: (capture) { - final List qrcodes = capture.barcodes; - final Barcode qrcode = qrcodes[0]; - if (qrcode.rawValue == null) { - Navigator.of(context).pop(); - } else { - final String code = qrcode.rawValue!; - scannerController.stop(); - Navigator.of(context).pop(code); + child: QrCameraView( + onImage: (InputImage inputImage) async { + if (!isScanned) { + final barcodes = await _barcodeScannerController + .processImage(inputImage); + if (barcodes.isEmpty) { + return; + } + + if (isScanned) return; + isScanned = true; + + await _barcodeScannerController.close(); + Navigator.of(context).pop(barcodes.first.rawValue); } }, + initialCameraLensDirection: _cameraLensDirection, + onCameraLensDirectionChanged: (value) => + _cameraLensDirection = value, ), ), ), diff --git a/pubspec.lock b/pubspec.lock index ba152ad59..cae2140c1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1114,6 +1114,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.5" + google_mlkit_barcode_scanning: + dependency: "direct main" + description: + name: google_mlkit_barcode_scanning + sha256: "99f5e35e104da1da4ee1cfd02e2ecddecb4ec7a2a1c2bbae4117982ce57d1cea" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + google_mlkit_commons: + dependency: transitive + description: + name: google_mlkit_commons + sha256: "42173a8ba89f386486cc5b8249e84da4a4b861daa70498373627d985eb418689" + url: "https://pub.dev" + source: hosted + version: "0.5.0" graphs: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d2d856a35..f8402afd5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,13 +56,14 @@ dependencies: flutter_local_notifications: ^14.1.0 flutter_localizations: sdk: flutter - flutter_markdown: ^0.6.9 #lutter_markdown ^0.6.14 requires markdown ^7.0.0 + flutter_markdown: ^0.6.9 #flutter_markdown ^0.6.14 requires markdown ^7.0.0 flutter_native_timezone: ^2.0.0 flutter_olm: ^1.2.0 flutter_openssl_crypto: ^0.1.0 flutter_svg: ^2.0.6 google_fonts: ^4.0.5 #google_mlkit_face_detection: ^0.5.0 + google_mlkit_barcode_scanning: ^0.8.0 image: ^4.0.17 image_picker: ^0.8.7+5 jose: ^0.3.3