diff --git a/assets/filters/beard_upper_lip_green.webp b/assets/filters/beard_upper_lip_green.webp new file mode 100644 index 0000000..4046f65 Binary files /dev/null and b/assets/filters/beard_upper_lip_green.webp differ diff --git a/assets/filters/dog_brown_ear.webp b/assets/filters/dog_brown_ear.webp new file mode 100644 index 0000000..0dfec38 Binary files /dev/null and b/assets/filters/dog_brown_ear.webp differ diff --git a/assets/filters/dog_brown_nose.webp b/assets/filters/dog_brown_nose.webp new file mode 100644 index 0000000..7716995 Binary files /dev/null and b/assets/filters/dog_brown_nose.webp differ diff --git a/lib/src/views/camera/camera_preview_components/camera_preview.dart b/lib/src/views/camera/camera_preview_components/camera_preview.dart index ec2d40e..1f196ea 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview.dart @@ -46,6 +46,10 @@ class MainCameraPreview extends StatelessWidget { Positioned.fill( child: mainCameraController.customPaint!, ), + if (mainCameraController.facePaint != null) + Positioned.fill( + child: mainCameraController.facePaint!, + ), ], ), ), diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index 3c9608c..5e48ce4 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -22,6 +22,7 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.dart'; import 'package:twonly/src/utils/screenshot.dart'; import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/face_filters.dart'; import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:twonly/src/views/camera/camera_preview_components/permissions_view.dart'; import 'package:twonly/src/views/camera/camera_preview_components/send_to.dart'; @@ -457,6 +458,36 @@ class _CameraPreviewViewState extends State { }); } + Future pressSideButtonLeft() async { + if (!mc.isSelectingFaceFilters) { + return pickImageFromGallery(); + } + if (mc.currentFilterType.index == 1) { + mc.setFilter(FaceFilterType.none); + setState(() { + mc.isSelectingFaceFilters = false; + }); + return; + } + mc.setFilter(mc.currentFilterType.goLeft()); + } + + Future pressSideButtonRight() async { + if (!mc.isSelectingFaceFilters) { + setState(() { + mc.isSelectingFaceFilters = true; + }); + } + if (mc.currentFilterType.index == FaceFilterType.values.length - 1) { + mc.setFilter(FaceFilterType.none); + setState(() { + mc.isSelectingFaceFilters = false; + }); + return; + } + mc.setFilter(mc.currentFilterType.goRight()); + } + Future startVideoRecording() async { if (mc.cameraController != null && mc.cameraController!.value.isRecordingVideo) { @@ -693,27 +724,84 @@ class _CameraPreviewViewState extends State { ), ), const SizedBox(height: 30), - GestureDetector( - onTap: takePicture, - // onLongPress: startVideoRecording, - key: keyTriggerButton, - child: Align( - child: Container( - height: 100, - width: 100, - clipBehavior: Clip.antiAliasWithSaveLayer, - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - width: 7, - color: mc.isVideoRecording - ? Colors.red - : Colors.white, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!mc.isVideoRecording) + GestureDetector( + onTap: pressSideButtonLeft, + child: Align( + child: Container( + height: 50, + width: 80, + padding: const EdgeInsets.all(2), + child: Center( + child: FaIcon( + mc.isSelectingFaceFilters + ? mc.currentFilterType.index == 1 + ? FontAwesomeIcons.xmark + : FontAwesomeIcons.arrowLeft + : FontAwesomeIcons.photoFilm, + color: Colors.white, + size: 25, + ), + ), + ), + ), + ), + GestureDetector( + onTap: takePicture, + // onLongPress: startVideoRecording, + key: keyTriggerButton, + child: Align( + child: Container( + height: 100, + width: 100, + clipBehavior: Clip.antiAliasWithSaveLayer, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 7, + color: mc.isVideoRecording + ? Colors.red + : Colors.white, + ), + ), + child: mc.currentFilterType.preview, ), ), ), - ), + if (!mc.isVideoRecording) + if (isFront) + GestureDetector( + onTap: pressSideButtonRight, + child: Align( + child: Container( + height: 50, + width: 80, + padding: const EdgeInsets.all(2), + child: Center( + child: FaIcon( + mc.isSelectingFaceFilters + ? mc.currentFilterType.index == + FaceFilterType + .values.length - + 1 + ? FontAwesomeIcons.xmark + : FontAwesomeIcons.arrowRight + : FontAwesomeIcons + .faceGrinTongueSquint, + color: Colors.white, + size: 25, + ), + ), + ), + ), + ) + else + const SizedBox(width: 80), + ], ), ], ), diff --git a/lib/src/views/camera/camera_preview_components/face_filters.dart b/lib/src/views/camera/camera_preview_components/face_filters.dart new file mode 100644 index 0000000..179f679 --- /dev/null +++ b/lib/src/views/camera/camera_preview_components/face_filters.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/dog_filter_painter.dart'; + +enum FaceFilterType { + none, + dogBrown, + beardUpperLipGreen, +} + +extension FaceFilterTypeExtension on FaceFilterType { + FaceFilterType goRight() { + final nextIndex = (index + 1) % FaceFilterType.values.length; + return FaceFilterType.values[nextIndex]; + } + + FaceFilterType goLeft() { + final prevIndex = (index - 1 + FaceFilterType.values.length) % + FaceFilterType.values.length; + return FaceFilterType.values[prevIndex]; + } + + Widget get preview { + switch (this) { + case FaceFilterType.none: + return Container(); + case FaceFilterType.dogBrown: + return DogFilterPainter.getPreview(); + case FaceFilterType.beardUpperLipGreen: + return BeardFilterPainter.getPreview(this); + } + } +} diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index a6332f9..73d1cb0 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -7,6 +7,7 @@ import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; +import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; @@ -18,7 +19,11 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.dart'; import 'package:twonly/src/utils/screenshot.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/face_filters.dart'; import 'package:twonly/src/views/camera/camera_preview_components/painters/barcode_detector_painter.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/dog_filter_painter.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart'; class ScannedVerifiedContact { ScannedVerifiedContact({required this.contact, required this.verificationOk}); @@ -44,6 +49,7 @@ class MainCameraController { GlobalKey zoomButtonKey = GlobalKey(); GlobalKey cameraPreviewKey = GlobalKey(); + bool isSelectingFaceFilters = false; bool isSharePreviewIsShown = false; bool isVideoRecording = false; DateTime? timeSharedLinkWasSetWithQr; @@ -61,10 +67,21 @@ class MainCameraController { } final BarcodeScanner _barcodeScanner = BarcodeScanner(); + final FaceDetector _faceDetector = FaceDetector( + options: FaceDetectorOptions( + enableContours: true, + enableLandmarks: true, + ), + ); bool _isBusy = false; + bool _isBusyFaces = false; CustomPaint? customPaint; + CustomPaint? facePaint; Offset? focusPointOffset; + FaceFilterType _currentFilterType = FaceFilterType.none; + FaceFilterType get currentFilterType => _currentFilterType; + Future closeCamera() async { contactsVerified = {}; scannedNewProfiles = {}; @@ -154,7 +171,10 @@ class MainCameraController { ..cameraLoaded = true ..cameraId = cameraId; + facePaint = null; customPaint = null; + isSelectingFaceFilters = false; + setFilter(FaceFilterType.none); zoomButtonKey = GlobalKey(); setState(); } @@ -191,6 +211,18 @@ class MainCameraController { setState(); } + void setFilter(FaceFilterType type) { + _currentFilterType = type; + if (_currentFilterType == FaceFilterType.none) { + faceFilterPainter = null; + facePaint = null; + _isBusyFaces = false; + } + setState(); + } + + FaceFilterPainter? faceFilterPainter; + final Map _orientations = { DeviceOrientation.portraitUp: 0, DeviceOrientation.landscapeLeft: 90, @@ -205,6 +237,15 @@ class MainCameraController { final inputImage = _inputImageFromCameraImage(image); if (inputImage == null) return; _processBarcode(inputImage); + // check if front camera is selected + if (cameraController?.description.lensDirection == + CameraLensDirection.front) { + if (_currentFilterType != FaceFilterType.none) { + _processFaces(inputImage); + } + } else { + _processBarcode(inputImage); + } } InputImage? _inputImageFromCameraImage(CameraImage image) { @@ -360,4 +401,52 @@ class MainCameraController { _isBusy = false; setState(); } + + Future _processFaces(InputImage inputImage) async { + if (_isBusyFaces) return; + _isBusyFaces = true; + final faces = await _faceDetector.processImage(inputImage); + if (inputImage.metadata?.size != null && + inputImage.metadata?.rotation != null && + cameraController != null) { + if (faces.isNotEmpty) { + CustomPainter? painter; + switch (_currentFilterType) { + case FaceFilterType.dogBrown: + painter = DogFilterPainter( + faces, + inputImage.metadata!.size, + inputImage.metadata!.rotation, + cameraController!.description.lensDirection, + ); + case FaceFilterType.beardUpperLipGreen: + painter = BeardFilterPainter( + _currentFilterType, + faces, + inputImage.metadata!.size, + inputImage.metadata!.rotation, + cameraController!.description.lensDirection, + ); + case FaceFilterType.none: + break; + } + + if (painter != null) { + facePaint = CustomPaint(painter: painter); + // Also set the correct FaceFilterPainter reference if needed for other logic, + // though currently facePaint is what's used for display. + if (painter is FaceFilterPainter) { + faceFilterPainter = painter; + } + } else { + facePaint = null; + faceFilterPainter = null; + } + } else { + facePaint = null; + } + } + _isBusyFaces = false; + setState(); + } } diff --git a/lib/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart b/lib/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart new file mode 100644 index 0000000..635f3a2 --- /dev/null +++ b/lib/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart @@ -0,0 +1,191 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/face_filters.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/coordinates_translator.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart'; + +class BeardFilterPainter extends FaceFilterPainter { + BeardFilterPainter( + FaceFilterType beardType, + super.faces, + super.imageSize, + super.rotation, + super.cameraLensDirection, + ) { + _loadAssets(beardType); + } + + static FaceFilterType? _lastLoadedBeardType; + static ui.Image? _beardImage; + static bool _loading = false; + + static String getAssetPath(FaceFilterType beardType) { + switch (beardType) { + case FaceFilterType.beardUpperLipGreen: + return 'assets/filters/beard_upper_lip_green.webp'; + case FaceFilterType.dogBrown: + case FaceFilterType.none: + return ''; + } + } + + static Future _loadAssets(FaceFilterType beardType) async { + if ((_loading || _beardImage != null) && + _lastLoadedBeardType == beardType) { + return; + } + _loading = true; + try { + _beardImage = await _loadImage(getAssetPath(beardType)); + } catch (e) { + Log.error('Failed to load filter assets: $e'); + } finally { + _loading = false; + } + } + + static Future _loadImage(String assetPath) async { + final data = await rootBundle.load(assetPath); + final list = Uint8List.view(data.buffer); + final completer = Completer(); + ui.decodeImageFromList(list, completer.complete); + return completer.future; + } + + @override + void paint(Canvas canvas, Size size) { + if (_beardImage == null) return; + + for (final face in faces) { + final noseBase = face.landmarks[FaceLandmarkType.noseBase]; + final mouthLeft = face.landmarks[FaceLandmarkType.leftMouth]; + final mouthRight = face.landmarks[FaceLandmarkType.rightMouth]; + final bottomMouth = face.landmarks[FaceLandmarkType.bottomMouth]; + + if (noseBase != null && + mouthLeft != null && + mouthRight != null && + bottomMouth != null) { + final noseX = translateX( + noseBase.position.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + final noseY = translateY( + noseBase.position.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + + final mouthLeftX = translateX( + mouthLeft.position.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + final mouthLeftY = translateY( + mouthLeft.position.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + + final mouthRightX = translateX( + mouthRight.position.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + final mouthRightY = translateY( + mouthRight.position.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + + final mouthCenterX = (mouthLeftX + mouthRightX) / 2; + final mouthCenterY = (mouthLeftY + mouthRightY) / 2; + + final beardCenterX = (noseX + mouthCenterX) / 2; + final beardCenterY = (noseY + mouthCenterY) / 2; + + final dx = mouthRightX - mouthLeftX; + final dy = mouthRightY - mouthLeftY; + final angle = atan2(dy, dx); + + final mouthWidth = sqrt(dx * dx + dy * dy); + final beardWidth = mouthWidth * 1.5; + + final yaw = face.headEulerAngleY ?? 0; + final scaleX = cos(yaw * pi / 180).abs(); + + _drawImage( + canvas, + _beardImage!, + Offset(beardCenterX, beardCenterY), + beardWidth, + angle, + scaleX, + ); + } + } + } + + void _drawImage( + Canvas canvas, + ui.Image image, + Offset position, + double width, + double rotation, + double scaleX, + ) { + canvas + ..save() + ..translate(position.dx, position.dy) + ..rotate(rotation) + ..scale(scaleX, Platform.isAndroid ? -1 : 1); + + final srcRect = + Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); + + final aspectRatio = image.width / image.height; + final dstWidth = width; + final dstHeight = width / aspectRatio; + + final dstRect = Rect.fromCenter( + center: Offset.zero, + width: dstWidth, + height: dstHeight, + ); + + canvas + ..drawImageRect(image, srcRect, dstRect, Paint()) + ..restore(); + } + + static Widget getPreview(FaceFilterType beardType) { + return Preview( + child: Padding( + padding: const EdgeInsets.all(8), + child: Image.asset( + getAssetPath(beardType), + fit: BoxFit.contain, + ), + ), + ); + } +} diff --git a/lib/src/views/camera/camera_preview_components/painters/face_filters/dog_filter_painter.dart b/lib/src/views/camera/camera_preview_components/painters/face_filters/dog_filter_painter.dart new file mode 100644 index 0000000..3643c33 --- /dev/null +++ b/lib/src/views/camera/camera_preview_components/painters/face_filters/dog_filter_painter.dart @@ -0,0 +1,243 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/coordinates_translator.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart'; + +class DogFilterPainter extends FaceFilterPainter { + DogFilterPainter( + super.faces, + super.imageSize, + super.rotation, + super.cameraLensDirection, + ) { + _loadAssets(); + } + + static ui.Image? _earImage; + static ui.Image? _noseImage; + static bool _loading = false; + + static Future _loadAssets() async { + if (_loading || (_earImage != null && _noseImage != null)) return; + _loading = true; + try { + _earImage = await _loadImage('assets/filters/dog_brown_ear.webp'); + _noseImage = await _loadImage('assets/filters/dog_brown_nose.webp'); + } catch (e) { + Log.error('Failed to load filter assets: $e'); + } finally { + _loading = false; + } + } + + static Future _loadImage(String assetPath) async { + final data = await rootBundle.load(assetPath); + final list = Uint8List.view(data.buffer); + final completer = Completer(); + ui.decodeImageFromList(list, completer.complete); + return completer.future; + } + + @override + void paint(Canvas canvas, Size size) { + if (_earImage == null || _noseImage == null) return; + + for (final face in faces) { + final faceContour = face.contours[FaceContourType.face]; + final noseBase = face.landmarks[FaceLandmarkType.noseBase]; + + if (faceContour != null && noseBase != null) { + final points = faceContour.points; + if (points.isEmpty) continue; + + final upperPoints = + points.where((p) => p.y < noseBase.position.y).toList(); + + if (upperPoints.isEmpty) continue; + + Point? leftMost; + Point? rightMost; + Point? topMost; + + for (final point in upperPoints) { + if (leftMost == null || point.x < leftMost.x) { + leftMost = point; + } + if (rightMost == null || point.x > rightMost.x) { + rightMost = point; + } + if (topMost == null || point.y < topMost.y) { + topMost = point; + } + } + + if (leftMost == null || rightMost == null || topMost == null) continue; + + final leftEarX = translateX( + leftMost.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + final leftEarY = translateY( + topMost.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + + final rightEarX = translateX( + rightMost.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + final rightEarY = translateY( + topMost.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + + final noseX = translateX( + noseBase.position.x.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + final noseY = translateY( + noseBase.position.y.toDouble(), + size, + imageSize, + rotation, + cameraLensDirection, + ); + + final dx = rightEarX - leftEarX; + final dy = rightEarY - leftEarY; + + final faceWidth = sqrt(dx * dx + dy * dy) * 1.5; + final angle = atan2(dy, dx); + + final yaw = face.headEulerAngleY ?? 0; + final scaleX = cos(yaw * pi / 180).abs(); + + final earSize = faceWidth / 2.5; + + _drawImage( + canvas, + _earImage!, + Offset(leftEarX, leftEarY + earSize * 0.3), + earSize, + angle, + scaleX, + ); + + _drawImage( + canvas, + _earImage!, + Offset(rightEarX, rightEarY + earSize * 0.3), + earSize, + angle, + scaleX, + isFlipped: true, + ); + + final noseSize = faceWidth * 0.4; + _drawImage( + canvas, + _noseImage!, + Offset(noseX, noseY + noseSize * 0.1), + noseSize, + angle, + scaleX, + ); + } + } + } + + void _drawImage( + Canvas canvas, + ui.Image image, + Offset position, + double size, + double rotation, + double scaleX, { + bool isFlipped = false, + }) { + canvas + ..save() + ..translate(position.dx, position.dy) + ..rotate(rotation); + if (isFlipped) { + canvas.scale(-scaleX, Platform.isAndroid ? -1 : 1); + } else { + canvas.scale(scaleX, Platform.isAndroid ? -1 : 1); + } + + final srcRect = + Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); + final aspectRatio = image.width / image.height; + final dstWidth = size; + final dstHeight = size / aspectRatio; + + final dstRect = Rect.fromCenter( + center: Offset.zero, + width: dstWidth, + height: dstHeight, + ); + + canvas + ..drawImageRect(image, srcRect, dstRect, Paint()) + ..restore(); + } + + static Widget getPreview() { + return Preview( + child: Stack( + alignment: Alignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 25), + child: Image.asset( + 'assets/filters/dog_brown_nose.webp', + width: 25, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/filters/dog_brown_ear.webp', + width: 20, + ), + const SizedBox(width: 15), + Transform.scale( + scaleX: -1, + child: Image.asset( + 'assets/filters/dog_brown_ear.webp', + width: 20, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart b/lib/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart new file mode 100644 index 0000000..b32ec7d --- /dev/null +++ b/lib/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart @@ -0,0 +1,44 @@ +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; + +abstract class FaceFilterPainter extends CustomPainter { + FaceFilterPainter( + this.faces, + this.imageSize, + this.rotation, + this.cameraLensDirection, + ); + + final List faces; + final Size imageSize; + final InputImageRotation rotation; + final CameraLensDirection cameraLensDirection; + + @override + bool shouldRepaint(covariant FaceFilterPainter oldDelegate) { + return oldDelegate.imageSize != imageSize || + oldDelegate.faces != faces || + oldDelegate.rotation != rotation || + oldDelegate.cameraLensDirection != cameraLensDirection; + } +} + +class Preview extends StatelessWidget { + const Preview({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.withValues(alpha: 0.2), + ), + child: Center( + child: child, + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index e0fcc73..e918588 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -868,6 +868,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.1" + google_mlkit_face_detection: + dependency: "direct main" + description: + name: google_mlkit_face_detection + sha256: "7b6ddcc69dbd6fbfa313fb2d974ad0f0c3a0d1657560f0da6be465baf1889687" + url: "https://pub.dev" + source: hosted + version: "0.13.2" graphs: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index caaf36c..ea64295 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -105,6 +105,7 @@ dependencies: flutter_volume_controller: ^1.3.4 gal: ^2.3.1 google_mlkit_barcode_scanning: ^0.14.1 + google_mlkit_face_detection: ^0.13.1 pro_video_editor: ^1.6.1 dependency_overrides: @@ -201,6 +202,7 @@ flutter: - assets/icons/ - assets/animated_icons/ - assets/animations/ + - assets/filters/ - assets/passwords/ - CHANGELOG.md