From 472b9d244b4e62de6a119ec77b34301b26c9f11c Mon Sep 17 00:00:00 2001 From: "J. A. Messias" Date: Fri, 20 Sep 2024 17:23:58 -0300 Subject: [PATCH 1/4] 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 From 56966a56d05e1db7080465e7eb31093ff5822224 Mon Sep 17 00:00:00 2001 From: "J. A. Messias" Date: Fri, 20 Sep 2024 17:46:32 -0300 Subject: [PATCH 2/4] WIP --- .../menu_component/menu_component_model.dart | 2 +- ...iter_vistor_template_component_widget.dart | 2 +- lib/flutter_flow/upload_data.dart | 173 +++- lib/test/face_detector_screen.dart | 899 ------------------ 4 files changed, 174 insertions(+), 902 deletions(-) delete mode 100644 lib/test/face_detector_screen.dart 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 5fdd9ff2..fe9f9d7e 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( - '/face', + '/provisionalSchedule', extra: { kTransitionInfoKey: const TransitionInfo( hasTransition: true, diff --git a/lib/components/templates_components/regisiter_vistor_template_component/regisiter_vistor_template_component_widget.dart b/lib/components/templates_components/regisiter_vistor_template_component/regisiter_vistor_template_component_widget.dart index f1988f78..eeb79823 100644 --- a/lib/components/templates_components/regisiter_vistor_template_component/regisiter_vistor_template_component_widget.dart +++ b/lib/components/templates_components/regisiter_vistor_template_component/regisiter_vistor_template_component_widget.dart @@ -513,7 +513,7 @@ class _RegisiterVistorTemplateComponentWidgetState child: FFButtonWidget( onPressed: () async { final selectedMedia = - await selectMediaWithSourceBottomSheet( + await selectMediaWithSourceBottomSheetandFaceDetection( context: context, // maxWidth: 300.00, // maxHeight: 300.00, diff --git a/lib/flutter_flow/upload_data.dart b/lib/flutter_flow/upload_data.dart index 1e3b8b9e..b9c3de57 100644 --- a/lib/flutter_flow/upload_data.dart +++ b/lib/flutter_flow/upload_data.dart @@ -4,7 +4,9 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; import 'package:hub/flutter_flow/nav/nav.dart'; +import 'package:hub/shared/utils/dialog_util.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mime_type/mime_type.dart'; import 'package:video_player/video_player.dart'; @@ -44,6 +46,174 @@ enum MediaSource { camera, } +Future?> selectMediaWithSourceBottomSheetandFaceDetection({ + required BuildContext context, + String? storageFolderPath, + double? maxWidth, + double? maxHeight, + int? imageQuality, + required bool allowPhoto, + bool allowVideo = false, + String pickerFontFamily = 'Roboto', + // Color textColor = const Color(0xFF111417), + // Color backgroundColor = const Color(0xFFF5F5F5), + bool includeDimensions = false, + bool includeBlurHash = false, +}) async { + final faceDetector = FaceDetector( + options: FaceDetectorOptions( + enableContours: true, + enableLandmarks: true, + ), + ); + createUploadMediaListTile( + String label, MediaSource mediaSource, IconData icon) => + ListTile( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: FlutterFlowTheme.of(context).primaryText), + const SizedBox(width: 5), + Text( + label, + textAlign: TextAlign.center, + style: GoogleFonts.getFont( + pickerFontFamily, + color: FlutterFlowTheme.of(context).primaryText, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), + ], + ), + tileColor: FlutterFlowTheme.of(context).primaryBackground, + dense: false, + onTap: () => context.pop(mediaSource), + ); + final mediaSource = await showModalBottomSheet( + context: context, + backgroundColor: FlutterFlowTheme.of(context).primaryBackground, + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!kIsWeb) ...[ + Container( + margin: const EdgeInsets.only(top: 10), + width: 40, + height: 5, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(10), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), + child: ListTile( + title: Text( + FFLocalizations.of(context).getVariableText( + ptText: "Escolha uma das opções", + enText: "Choose one of the options"), + textAlign: TextAlign.center, + style: GoogleFonts.getFont( + pickerFontFamily, + color: FlutterFlowTheme.of(context) + .primaryText + .withOpacity(0.65), + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + tileColor: FlutterFlowTheme.of(context).primaryBackground, + dense: true, + ), + ), + ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (allowPhoto && allowVideo) ...[ + Expanded( + child: createUploadMediaListTile( + FFLocalizations.of(context).getVariableText( + ptText: "Galeria (Foto)", + enText: "Gallery (Photo)"), + MediaSource.photoGallery, + Icons.camera), + ), + Expanded( + child: createUploadMediaListTile( + FFLocalizations.of(context).getVariableText( + ptText: "Galeria (Video)", + enText: "Gallery (Video)"), + MediaSource.videoGallery, + Icons.videocam), + ) + ] else if (allowPhoto) + Expanded( + child: createUploadMediaListTile( + FFLocalizations.of(context).getVariableText( + ptText: "Galeria", enText: "Gallery"), + MediaSource.photoGallery, + Icons.photo)) + else + Expanded( + child: createUploadMediaListTile( + FFLocalizations.of(context).getVariableText( + ptText: "Galeria", enText: "Gallery"), + MediaSource.videoGallery, + Icons.videocam), + ), + if (!kIsWeb) ...[ + Expanded( + child: createUploadMediaListTile( + FFLocalizations.of(context).getVariableText( + ptText: "Camera", enText: "Camera"), + MediaSource.camera, + Icons.camera_alt), + ) + ], + ], + ), + const SizedBox(height: 15), + ], + ); + }); + if (mediaSource == null) { + return null; + } + final selectedMedia = await selectMedia( + storageFolderPath: storageFolderPath, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + isVideo: mediaSource == MediaSource.videoGallery || + (mediaSource == MediaSource.camera && allowVideo && !allowPhoto), + mediaSource: mediaSource, + includeDimensions: includeDimensions, + includeBlurHash: includeBlurHash, + ); + + if (selectedMedia == null || selectedMedia.isEmpty) { + return null; + } + + // Verifique se há faces na imagem + for (final media in selectedMedia) { + final inputImage = InputImage.fromFilePath(media.filePath!); + final faces = await faceDetector.processImage(inputImage); + if (faces.isEmpty) { + final String message = FFLocalizations.of(context).getVariableText( + ptText: "Nenhuma face detectada na imagem", + enText: "No face detected in the image"); + DialogUtil.error(context, message); + return null; + } + } + + return selectedMedia; +} + Future?> selectMediaWithSourceBottomSheet({ required BuildContext context, String? storageFolderPath, @@ -58,7 +228,8 @@ Future?> selectMediaWithSourceBottomSheet({ bool includeDimensions = false, bool includeBlurHash = false, }) async { - createUploadMediaListTile(String label, MediaSource mediaSource, IconData icon) => + createUploadMediaListTile( + String label, MediaSource mediaSource, IconData icon) => ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/test/face_detector_screen.dart b/lib/test/face_detector_screen.dart deleted file mode 100644 index 2c407f42..00000000 --- a/lib/test/face_detector_screen.dart +++ /dev/null @@ -1,899 +0,0 @@ -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'; -} From 6f17d5ecfc9d6b326d68a5663eb4c5a67bf9cfc8 Mon Sep 17 00:00:00 2001 From: "J. A. Messias" Date: Fri, 20 Sep 2024 17:53:00 -0300 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20Visitante=20j=C3=A1=20possui=20visit?= =?UTF-8?q?a=20em=20aberto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/flutter_flow/nav/nav.dart | 4 ---- .../schedule_complete_visit_page_widget.dart | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/flutter_flow/nav/nav.dart b/lib/flutter_flow/nav/nav.dart index 9864a78a..a4544b8b 100644 --- a/lib/flutter_flow/nav/nav.dart +++ b/lib/flutter_flow/nav/nav.dart @@ -88,10 +88,6 @@ GoRouter createRouter(AppStateNotifier appStateNotifier) => GoRouter( ); }, ), - FFRoute( - name: 'face', - path: '/face', - builder: (context, params) => FaceDetectorView()), FFRoute( name: 'receptionPage', path: '/receptionPage', diff --git a/lib/pages/schedule_complete_visit_page/schedule_complete_visit_page_widget.dart b/lib/pages/schedule_complete_visit_page/schedule_complete_visit_page_widget.dart index 3e792ee8..4d036e82 100644 --- a/lib/pages/schedule_complete_visit_page/schedule_complete_visit_page_widget.dart +++ b/lib/pages/schedule_complete_visit_page/schedule_complete_visit_page_widget.dart @@ -1536,7 +1536,8 @@ Widget scheduleVisit(BuildContext context, await ShareUtil.showShare(value.jsonBody); } else { - await DialogUtil.errorDefault(context); + final message = value.jsonBody['error_msg']; + await DialogUtil.error(context, message); context.pop(); } }); From 994862bed59f322e02a65775dc3225ba33052e75 Mon Sep 17 00:00:00 2001 From: "J. A. Messias" Date: Mon, 23 Sep 2024 13:58:22 -0300 Subject: [PATCH 4/4] fix nav --- lib/flutter_flow/nav/nav.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/flutter_flow/nav/nav.dart b/lib/flutter_flow/nav/nav.dart index a4544b8b..3947cd2b 100644 --- a/lib/flutter_flow/nav/nav.dart +++ b/lib/flutter_flow/nav/nav.dart @@ -10,7 +10,6 @@ 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';