From 472b9d244b4e62de6a119ec77b34301b26c9f11c Mon Sep 17 00:00:00 2001 From: "J. A. Messias" Date: Fri, 20 Sep 2024 17:23:58 -0300 Subject: [PATCH] WIP --- android/app/build.gradle | 5 + android/app/src/main/AndroidManifest.xml | 1 + .../menu_component/menu_component_model.dart | 2 +- lib/flutter_flow/nav/nav.dart | 5 + lib/test/face_detector_screen.dart | 899 ++++++++++++++++++ pubspec.lock | 68 +- pubspec.yaml | 2 + 7 files changed, 979 insertions(+), 3 deletions(-) create mode 100644 lib/test/face_detector_screen.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 705112da..9d29d984 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -93,6 +93,11 @@ dependencies { implementation 'androidx.window:window:1.0.0' implementation 'androidx.window:window-java:1.0.0' + implementation ('com.google.firebase:firebase-messaging:24.0.0') { + exclude group: 'com.google.firebase', module: 'firebase-iid' + } + + } apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b8fc876e..a04b84f9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -86,6 +86,7 @@ + diff --git a/lib/components/organism_components/menu_component/menu_component_model.dart b/lib/components/organism_components/menu_component/menu_component_model.dart index fe9f9d7e..5fdd9ff2 100644 --- a/lib/components/organism_components/menu_component/menu_component_model.dart +++ b/lib/components/organism_components/menu_component/menu_component_model.dart @@ -100,7 +100,7 @@ class MenuComponentModel extends FlutterFlowModel { .then((value) => value.toString() == 'true') as bool; if (isProvisional == true) { context.push( - '/provisionalSchedule', + '/face', extra: { kTransitionInfoKey: const TransitionInfo( hasTransition: true, diff --git a/lib/flutter_flow/nav/nav.dart b/lib/flutter_flow/nav/nav.dart index 3947cd2b..9864a78a 100644 --- a/lib/flutter_flow/nav/nav.dart +++ b/lib/flutter_flow/nav/nav.dart @@ -10,6 +10,7 @@ import 'package:hub/pages/pets_page/pets_page_widget.dart'; import 'package:hub/pages/provisional_schedule_page/provisional_schedule_widget.dart'; import 'package:hub/pages/reception_page/reception_page_widget.dart'; import 'package:hub/pages/reservation_page/reservation_page_widget.dart'; +import 'package:hub/test/face_detector_screen.dart'; import 'package:provider/provider.dart'; import '/backend/schema/structs/index.dart'; @@ -87,6 +88,10 @@ GoRouter createRouter(AppStateNotifier appStateNotifier) => GoRouter( ); }, ), + FFRoute( + name: 'face', + path: '/face', + builder: (context, params) => FaceDetectorView()), FFRoute( name: 'receptionPage', path: '/receptionPage', diff --git a/lib/test/face_detector_screen.dart b/lib/test/face_detector_screen.dart new file mode 100644 index 00000000..2c407f42 --- /dev/null +++ b/lib/test/face_detector_screen.dart @@ -0,0 +1,899 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; +import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; + +double translateX( + double x, + Size canvasSize, + Size imageSize, + InputImageRotation rotation, + CameraLensDirection cameraLensDirection, +) { + switch (rotation) { + case InputImageRotation.rotation90deg: + return x * + canvasSize.width / + (Platform.isIOS ? imageSize.width : imageSize.height); + case InputImageRotation.rotation270deg: + return canvasSize.width - + x * + canvasSize.width / + (Platform.isIOS ? imageSize.width : imageSize.height); + case InputImageRotation.rotation0deg: + case InputImageRotation.rotation180deg: + switch (cameraLensDirection) { + case CameraLensDirection.back: + return x * canvasSize.width / imageSize.width; + default: + return canvasSize.width - x * canvasSize.width / imageSize.width; + } + } +} + +double translateY( + double y, + Size canvasSize, + Size imageSize, + InputImageRotation rotation, + CameraLensDirection cameraLensDirection, +) { + switch (rotation) { + case InputImageRotation.rotation90deg: + case InputImageRotation.rotation270deg: + return y * + canvasSize.height / + (Platform.isIOS ? imageSize.height : imageSize.width); + case InputImageRotation.rotation0deg: + case InputImageRotation.rotation180deg: + return y * canvasSize.height / imageSize.height; + } +} + +class FaceDetectorPainter extends CustomPainter { + FaceDetectorPainter( + this.faces, + this.imageSize, + this.rotation, + this.cameraLensDirection, + ); + + final List faces; + final Size imageSize; + final InputImageRotation rotation; + final CameraLensDirection cameraLensDirection; + + @override + void paint(Canvas canvas, Size size) { + final Paint paint1 = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1.0 + ..color = Colors.red; + final Paint paint2 = Paint() + ..style = PaintingStyle.fill + ..strokeWidth = 1.0 + ..color = Colors.green; + + for (final Face face in faces) { + final left = translateX( + face.boundingBox.left, + size, + imageSize, + rotation, + cameraLensDirection, + ); + final top = translateY( + face.boundingBox.top, + size, + imageSize, + rotation, + cameraLensDirection, + ); + final right = translateX( + face.boundingBox.right, + size, + imageSize, + rotation, + cameraLensDirection, + ); + final bottom = translateY( + face.boundingBox.bottom, + size, + imageSize, + rotation, + cameraLensDirection, + ); + + canvas.drawRect( + Rect.fromLTRB(left, top, right, bottom), + paint1, + ); + + void paintContour(FaceContourType type) { + final contour = face.contours[type]; + if (contour?.points != null) { + for (final Point point in contour!.points) { + canvas.drawCircle( + Offset( + translateX( + point.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ), + translateY( + point.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ), + ), + 1, + paint1); + } + } + } + + void paintLandmark(FaceLandmarkType type) { + final landmark = face.landmarks[type]; + if (landmark?.position != null) { + canvas.drawCircle( + Offset( + translateX( + landmark!.position.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ), + translateY( + landmark.position.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ), + ), + 2, + paint2); + } + } + + for (final type in FaceContourType.values) { + paintContour(type); + } + + for (final type in FaceLandmarkType.values) { + paintLandmark(type); + } + } + } + + @override + bool shouldRepaint(FaceDetectorPainter oldDelegate) { + return oldDelegate.imageSize != imageSize || oldDelegate.faces != faces; + } +} + +class FaceDetectorView extends StatefulWidget { + @override + State createState() => _FaceDetectorViewState(); +} + +class _FaceDetectorViewState extends State { + final FaceDetector _faceDetector = FaceDetector( + options: FaceDetectorOptions( + enableContours: true, + enableLandmarks: true, + ), + ); + bool _canProcess = true; + bool _isBusy = false; + CustomPaint? _customPaint; + String? _text; + var _cameraLensDirection = CameraLensDirection.front; + + @override + void dispose() { + _canProcess = false; + _faceDetector.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DetectorView( + title: 'Face Detector', + customPaint: _customPaint, + text: _text, + onImage: _processImage, + initialCameraLensDirection: _cameraLensDirection, + onCameraLensDirectionChanged: (value) => _cameraLensDirection = value, + ); + } + + Future _processImage(InputImage inputImage) async { + if (!_canProcess) return; + if (_isBusy) return; + _isBusy = true; + setState(() { + _text = ''; + }); + final faces = await _faceDetector.processImage(inputImage); + if (inputImage.metadata?.size != null && + inputImage.metadata?.rotation != null) { + final painter = FaceDetectorPainter( + faces, + inputImage.metadata!.size, + inputImage.metadata!.rotation, + _cameraLensDirection, + ); + _customPaint = CustomPaint(painter: painter); + } else { + String text = 'Faces found: ${faces.length}\n\n'; + for (final face in faces) { + text += 'face: ${face.boundingBox}\n\n'; + } + _text = text; + // TODO: set _customPaint to draw boundingRect on top of image + _customPaint = null; + } + _isBusy = false; + if (mounted) { + setState(() {}); + } + } +} + +enum DetectorViewMode { liveFeed, gallery } + +class DetectorView extends StatefulWidget { + DetectorView({ + Key? key, + required this.title, + required this.onImage, + this.customPaint, + this.text, + this.initialDetectionMode = DetectorViewMode.liveFeed, + this.initialCameraLensDirection = CameraLensDirection.back, + this.onCameraFeedReady, + this.onDetectorViewModeChanged, + this.onCameraLensDirectionChanged, + }) : super(key: key); + + final String title; + final CustomPaint? customPaint; + final String? text; + final DetectorViewMode initialDetectionMode; + final Function(InputImage inputImage) onImage; + final Function()? onCameraFeedReady; + final Function(DetectorViewMode mode)? onDetectorViewModeChanged; + final Function(CameraLensDirection direction)? onCameraLensDirectionChanged; + final CameraLensDirection initialCameraLensDirection; + + @override + State createState() => _DetectorViewState(); +} + +class _DetectorViewState extends State { + late DetectorViewMode _mode; + + @override + void initState() { + _mode = widget.initialDetectionMode; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return _mode == DetectorViewMode.liveFeed + ? CameraView( + customPaint: widget.customPaint, + onImage: widget.onImage, + onCameraFeedReady: widget.onCameraFeedReady, + onDetectorViewModeChanged: _onDetectorViewModeChanged, + initialCameraLensDirection: widget.initialCameraLensDirection, + onCameraLensDirectionChanged: widget.onCameraLensDirectionChanged, + ) + : GalleryView( + title: widget.title, + text: widget.text, + onImage: widget.onImage, + onDetectorViewModeChanged: _onDetectorViewModeChanged); + } + + void _onDetectorViewModeChanged() { + if (_mode == DetectorViewMode.liveFeed) { + _mode = DetectorViewMode.gallery; + } else { + _mode = DetectorViewMode.liveFeed; + } + if (widget.onDetectorViewModeChanged != null) { + widget.onDetectorViewModeChanged!(_mode); + } + setState(() {}); + } +} + +class CameraView extends StatefulWidget { + CameraView( + {Key? key, + required this.customPaint, + required this.onImage, + this.onCameraFeedReady, + this.onDetectorViewModeChanged, + this.onCameraLensDirectionChanged, + this.initialCameraLensDirection = CameraLensDirection.back}) + : super(key: key); + + final CustomPaint? customPaint; + final Function(InputImage inputImage) onImage; + final VoidCallback? onCameraFeedReady; + final VoidCallback? onDetectorViewModeChanged; + final Function(CameraLensDirection direction)? onCameraLensDirectionChanged; + final CameraLensDirection initialCameraLensDirection; + + @override + State createState() => _CameraViewState(); +} + +class _CameraViewState 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; + late BuildContext buildContext; + + @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) { + 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 + ? Center( + child: const Text('Changing camera lens'), + ) + : CameraPreview( + _controller!, + child: widget.customPaint, + ), + ), + _backButton(), + _switchLiveCameraToggle(), + _detectionViewModeToggle(), + _zoomControl(), + _exposureControl(), + ], + ), + ); + } + + Widget _backButton() => Positioned( + top: 40, + left: 8, + child: SizedBox( + height: 50.0, + width: 50.0, + child: FloatingActionButton( + heroTag: Object(), + onPressed: () => Navigator.of(buildContext).pop(), + backgroundColor: Colors.black54, + child: Icon( + Icons.arrow_back_ios_outlined, + size: 20, + ), + ), + ), + ); + + Widget _detectionViewModeToggle() => Positioned( + bottom: 8, + left: 8, + child: SizedBox( + height: 50.0, + width: 50.0, + child: FloatingActionButton( + heroTag: Object(), + onPressed: widget.onDetectorViewModeChanged, + backgroundColor: Colors.black54, + child: Icon( + Icons.photo_library_outlined, + size: 25, + ), + ), + ), + ); + + Widget _switchLiveCameraToggle() => Positioned( + bottom: 8, + right: 8, + child: SizedBox( + height: 50.0, + width: 50.0, + child: FloatingActionButton( + heroTag: Object(), + onPressed: _switchLiveCamera, + backgroundColor: Colors.black54, + child: Icon( + Platform.isIOS + ? Icons.flip_camera_ios_outlined + : Icons.flip_camera_android_outlined, + size: 25, + ), + ), + ), + ); + + Widget _zoomControl() => Positioned( + bottom: 16, + left: 0, + right: 0, + child: Align( + alignment: Alignment.bottomCenter, + child: SizedBox( + width: 250, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Slider( + value: _currentZoomLevel, + min: _minAvailableZoom, + max: _maxAvailableZoom, + activeColor: Colors.white, + inactiveColor: Colors.white30, + onChanged: (value) async { + setState(() { + _currentZoomLevel = value; + }); + await _controller?.setZoomLevel(value); + }, + ), + ), + Container( + width: 50, + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(10.0), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Text( + '${_currentZoomLevel.toStringAsFixed(1)}x', + style: TextStyle(color: Colors.white), + ), + ), + ), + ), + ], + ), + ), + ), + ); + + Widget _exposureControl() => Positioned( + top: 40, + right: 8, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: 250, + ), + child: Column(children: [ + Container( + width: 55, + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(10.0), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Text( + '${_currentExposureOffset.toStringAsFixed(1)}x', + style: TextStyle(color: Colors.white), + ), + ), + ), + ), + Expanded( + child: RotatedBox( + quarterTurns: 3, + child: SizedBox( + height: 30, + child: Slider( + value: _currentExposureOffset, + min: _minAvailableExposureOffset, + max: _maxAvailableExposureOffset, + activeColor: Colors.white, + inactiveColor: Colors.white30, + onChanged: (value) async { + setState(() { + _currentExposureOffset = value; + }); + await _controller?.setExposureOffset(value); + }, + ), + ), + ), + ) + ]), + ), + ); + + 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, + ); + _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; + } + + Future _switchLiveCamera() async { + setState(() => _changingCameraLens = true); + _cameraIndex = (_cameraIndex + 1) % _cameras.length; + + await _stopLiveFeed(); + await _startLiveFeed(); + setState(() => _changingCameraLens = false); + } + + 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); + // 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 GalleryView extends StatefulWidget { + GalleryView( + {Key? key, + required this.title, + this.text, + required this.onImage, + required this.onDetectorViewModeChanged}) + : super(key: key); + + final String title; + final String? text; + final Function(InputImage inputImage) onImage; + final Function()? onDetectorViewModeChanged; + + @override + State createState() => _GalleryViewState(); +} + +class _GalleryViewState extends State { + File? _image; + String? _path; + ImagePicker? _imagePicker; + late BuildContext buildContext; + + @override + void initState() { + super.initState(); + + _imagePicker = ImagePicker(); + } + + @override + Widget build(BuildContext context) { + buildContext = this.context; + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + actions: [ + Padding( + padding: EdgeInsets.only(right: 20.0), + child: GestureDetector( + onTap: widget.onDetectorViewModeChanged, + child: Icon( + Platform.isIOS ? Icons.camera_alt_outlined : Icons.camera, + ), + ), + ), + ], + ), + body: _galleryBody()); + } + + Widget _galleryBody() { + return ListView(shrinkWrap: true, children: [ + _image != null + ? SizedBox( + height: 400, + width: 400, + child: Stack( + fit: StackFit.expand, + children: [ + Image.file(_image!), + ], + ), + ) + : Icon( + Icons.image, + size: 200, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: ElevatedButton( + onPressed: _getImageAsset, + child: Text('From Assets'), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: ElevatedButton( + child: Text('From Gallery'), + onPressed: () => _getImage(ImageSource.gallery), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: ElevatedButton( + child: Text('Take a picture'), + onPressed: () => _getImage(ImageSource.camera), + ), + ), + if (_image != null) + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + '${_path == null ? '' : 'Image path: $_path'}\n\n${widget.text ?? ''}'), + ), + ]); + } + + Future _getImage(ImageSource source) async { + setState(() { + _image = null; + _path = null; + }); + final pickedFile = await _imagePicker?.pickImage(source: source); + if (pickedFile != null) { + _processFile(pickedFile.path); + } + } + + Future _getImageAsset() async { + final manifestContent = await rootBundle.loadString('AssetManifest.json'); + final Map manifestMap = json.decode(manifestContent); + final assets = manifestMap.keys + .where((String key) => key.contains('images/')) + .where((String key) => + key.contains('.jpg') || + key.contains('.jpeg') || + key.contains('.png') || + key.contains('.webp')) + .toList(); + + showDialog( + context: buildContext, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Select image', + style: TextStyle(fontSize: 20), + ), + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7), + child: SingleChildScrollView( + child: Column( + children: [ + for (final path in assets) + GestureDetector( + onTap: () async { + Navigator.of(context).pop(); + _processFile(await getAssetPath(path)); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Image.asset(path), + ), + ), + ], + ), + ), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Cancel')), + ], + ), + ), + ); + }); + } + + Future _processFile(String path) async { + setState(() { + _image = File(path); + }); + _path = path; + final inputImage = InputImage.fromFilePath(path); + widget.onImage(inputImage); + } +} + +Future getAssetPath(String asset) async { + final path = await getLocalPath(asset); + await Directory(dirname(path)).create(recursive: true); + final file = File(path); + if (!await file.exists()) { + final byteData = await rootBundle.load(asset); + await file.writeAsBytes(byteData.buffer + .asUint8List(byteData.offsetInBytes, byteData.lengthInBytes)); + } + return file.path; +} + +Future getLocalPath(String path) async { + return '${(await getApplicationSupportDirectory()).path}/$path'; +} diff --git a/pubspec.lock b/pubspec.lock index 34dfa4d3..23421e70 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + camera: + dependency: "direct main" + description: + name: camera + sha256: "26ff41045772153f222ffffecba711a206f670f5834d40ebf5eed3811692f167" + url: "https://pub.dev" + source: hosted + version: "0.11.0+2" + camera_android_camerax: + dependency: transitive + description: + name: camera_android_camerax + sha256: "7cd93578ad201dcc6bb5810451fb00d76a86bab9b68dceb68b8cbd7038ac5846" + url: "https://pub.dev" + source: hosted + version: "0.6.8+3" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "7c28969a975a7eb2349bc2cb2dfe3ad218a33dba9968ecfb181ce08c87486655" + url: "https://pub.dev" + source: hosted + version: "0.9.17+3" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: b3ede1f171532e0d83111fe0980b46d17f1aa9788a07a2fbed07366bbdbb9061 + url: "https://pub.dev" + source: hosted + version: "2.8.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" + url: "https://pub.dev" + source: hosted + version: "0.3.5" characters: dependency: transitive description: @@ -701,6 +741,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.1" + google_mlkit_commons: + dependency: transitive + description: + name: google_mlkit_commons + sha256: "9990a65f407a3ef6bae646bf10143faa93fec126683771465bc6c0b43fb0e6e9" + url: "https://pub.dev" + source: hosted + version: "0.8.1" + google_mlkit_face_detection: + dependency: "direct main" + description: + name: google_mlkit_face_detection + sha256: "0aeab4f39204f7a235ed4cccedfe7e61401b43f4ef139a868c01fa29fdc225ab" + url: "https://pub.dev" + source: hosted + version: "0.11.1" html: dependency: transitive description: @@ -1346,6 +1402,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" string_scanner: dependency: transitive description: @@ -1627,5 +1691,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 148e4c8b..3295e84e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,11 +58,13 @@ dependencies: image_picker_platform_interface: 2.10.0 local_auth: ^2.2.0 intl: ^0.19.0 + camera: ^0.11.0+2 json_path: 0.7.2 mime_type: 1.0.0 page_transition: 2.1.0 path_provider: 2.1.3 path_provider_android: 2.2.5 + google_mlkit_face_detection: ^0.11.1 path_provider_foundation: 2.4.0 path_provider_platform_interface: 2.1.2 percent_indicator: 4.2.2