added support for face filters

This commit is contained in:
otsmr 2026-01-18 02:30:56 +01:00
parent 448a28cdbd
commit 42cc6db0e2
14 changed files with 696 additions and 15 deletions

View file

@ -1,5 +1,9 @@
# Changelog
## 0.0.87
- Added basic support for face filters
## 0.0.86
- Allows to reopen send images (if send without time limit or enabled auth)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

@ -136,6 +136,10 @@ PODS:
- google_mlkit_commons (0.11.0):
- Flutter
- MLKitVision
- google_mlkit_face_detection (0.13.1):
- Flutter
- google_mlkit_commons
- GoogleMLKit/FaceDetection (~> 7.0.0)
- GoogleAdsOnDeviceConversion (3.2.0):
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
@ -169,6 +173,9 @@ PODS:
- GoogleMLKit/BarcodeScanning (7.0.0):
- GoogleMLKit/MLKitCore
- MLKitBarcodeScanning (~> 6.0.0)
- GoogleMLKit/FaceDetection (7.0.0):
- GoogleMLKit/MLKitCore
- MLKitFaceDetection (~> 6.0.0)
- GoogleMLKit/MLKitCore (7.0.0):
- MLKitCommon (~> 12.0.0)
- GoogleToolboxForMac/Defines (4.2.1)
@ -251,6 +258,9 @@ PODS:
- GoogleUtilities/Logger (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLKitFaceDetection (6.0.0):
- MLKitCommon (~> 12.0)
- MLKitVision (~> 8.0)
- MLKitVision (8.0.0):
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
@ -357,6 +367,7 @@ DEPENDENCIES:
- gal (from `.symlinks/plugins/gal/darwin`)
- google_mlkit_barcode_scanning (from `.symlinks/plugins/google_mlkit_barcode_scanning/ios`)
- google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`)
- google_mlkit_face_detection (from `.symlinks/plugins/google_mlkit_face_detection/ios`)
- GoogleUtilities
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
@ -398,6 +409,7 @@ SPEC REPOS:
- MLImage
- MLKitBarcodeScanning
- MLKitCommon
- MLKitFaceDetection
- MLKitVision
- nanopb
- PromisesObjC
@ -454,6 +466,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/google_mlkit_barcode_scanning/ios"
google_mlkit_commons:
:path: ".symlinks/plugins/google_mlkit_commons/ios"
google_mlkit_face_detection:
:path: ".symlinks/plugins/google_mlkit_face_detection/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_purchase_storekit:
@ -518,6 +532,7 @@ SPEC CHECKSUMS:
gal: baecd024ebfd13c441269ca7404792a7152fde89
google_mlkit_barcode_scanning: 8f5987f244a43fe1167689c548342a5174108159
google_mlkit_commons: 2abe6a70e1824e431d16a51085cb475b672c8aab
google_mlkit_face_detection: 754da2113a1952f063c7c5dc347ac6ae8934fb77
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
GoogleAppMeasurement: 3bf40aff49a601af5da1c3345702fcb4991d35ee
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
@ -533,6 +548,7 @@ SPEC CHECKSUMS:
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
MLKitFaceDetection: 2a593db4837db503ad3426b565e7aab045cefea5
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
no_screenshot: 89e778ede9f1e39cc3fb9404d782a42712f2a0b2

View file

@ -37,7 +37,18 @@ class MainCameraPreview extends StatelessWidget {
.cameraController!.value.previewSize!.width,
child: CameraPreview(
mainCameraController.cameraController!,
child: mainCameraController.customPaint,
child: Stack(
children: [
if (mainCameraController.customPaint != null)
Positioned.fill(
child: mainCameraController.customPaint!,
),
if (mainCameraController.facePaint != null)
Positioned.fill(
child: mainCameraController.facePaint!,
),
],
),
),
),
),

View file

@ -21,6 +21,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';
@ -506,6 +507,36 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
});
}
Future<void> 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<void> 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<void> startVideoRecording() async {
if (mc.cameraController != null &&
mc.cameraController!.value.isRecordingVideo) {
@ -736,15 +767,19 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
children: [
if (!_isVideoRecording)
GestureDetector(
onTap: pickImageFromGallery,
onTap: pressSideButtonLeft,
child: Align(
child: Container(
height: 50,
width: 80,
padding: const EdgeInsets.all(2),
child: const Center(
child: Center(
child: FaIcon(
FontAwesomeIcons.photoFilm,
mc.isSelectingFaceFilters
? mc.currentFilterType.index == 1
? FontAwesomeIcons.xmark
: FontAwesomeIcons.arrowLeft
: FontAwesomeIcons.photoFilm,
color: Colors.white,
size: 25,
),
@ -771,10 +806,39 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
: Colors.white,
),
),
child: mc.currentFilterType.preview,
),
),
),
if (!_isVideoRecording) const SizedBox(width: 80),
if (!_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),
],
),
],

View file

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/views/camera/painters/face_filters/beard_filter_painter.dart';
import 'package:twonly/src/views/camera/painters/face_filters/dog_filter_painter.dart';
enum FaceFilterType {
none,
dogBrown,
beardUpperLip,
}
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.beardUpperLip:
return BeardFilterPainter.getPreview();
}
}
}

View file

@ -5,6 +5,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:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
@ -15,7 +16,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/painters/barcode_detector_painter.dart';
import 'package:twonly/src/views/camera/painters/face_filters/beard_filter_painter.dart';
import 'package:twonly/src/views/camera/painters/face_filters/dog_filter_painter.dart';
import 'package:twonly/src/views/camera/painters/face_filters/face_filter_painter.dart';
class ScannedVerifiedContact {
ScannedVerifiedContact({
@ -45,6 +50,22 @@ class MainCameraController {
Map<int, ScannedNewProfile> scannedNewProfiles = {};
String? scannedUrl;
GlobalKey zoomButtonKey = GlobalKey();
bool isSelectingFaceFilters = false;
final BarcodeScanner _barcodeScanner = BarcodeScanner();
final FaceDetector _faceDetector = FaceDetector(
options: FaceDetectorOptions(
enableContours: true,
enableLandmarks: true,
),
);
bool _isBusy = false;
bool _isBusyFaces = false;
CustomPaint? customPaint;
CustomPaint? facePaint;
FaceFilterType _currentFilterType = FaceFilterType.beardUpperLip;
FaceFilterType get currentFilterType => _currentFilterType;
Future<void> closeCamera() async {
contactsVerified = {};
@ -73,10 +94,9 @@ class MainCameraController {
selectedCameraDetails = opts.$1;
cameraController = opts.$2;
}
if (cameraController?.description.lensDirection ==
CameraLensDirection.back) {
isSelectingFaceFilters = false;
setFilter(FaceFilterType.none);
await cameraController?.startImageStream(_processCameraImage);
}
zoomButtonKey = GlobalKey();
setState();
return cameraController;
@ -95,13 +115,23 @@ class MainCameraController {
}
final tmp = cameraController;
cameraController = null;
facePaint = null;
customPaint = null;
await tmp!.dispose();
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false);
}
final BarcodeScanner _barcodeScanner = BarcodeScanner();
bool _isBusy = false;
CustomPaint? customPaint;
void setFilter(FaceFilterType type) {
_currentFilterType = type;
if (_currentFilterType == FaceFilterType.none) {
faceFilterPainter = null;
facePaint = null;
_isBusyFaces = false;
}
setState();
}
FaceFilterPainter? faceFilterPainter;
final Map<DeviceOrientation, int> _orientations = {
DeviceOrientation.portraitUp: 0,
@ -113,7 +143,16 @@ class MainCameraController {
void _processCameraImage(CameraImage image) {
final inputImage = _inputImageFromCameraImage(image);
if (inputImage == null) return;
_processImage(inputImage);
_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) {
@ -175,7 +214,7 @@ class MainCameraController {
);
}
Future<void> _processImage(InputImage inputImage) async {
Future<void> _processBarcode(InputImage inputImage) async {
if (_isBusy) return;
_isBusy = true;
final barcodes = await _barcodeScanner.processImage(inputImage);
@ -255,4 +294,48 @@ class MainCameraController {
_isBusy = false;
setState();
}
Future<void> _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;
if (_currentFilterType == FaceFilterType.dogBrown) {
painter = DogFilterPainter(
faces,
inputImage.metadata!.size,
inputImage.metadata!.rotation,
cameraController!.description.lensDirection,
);
} else if (_currentFilterType == FaceFilterType.beardUpperLip) {
painter = BeardFilterPainter(
faces,
inputImage.metadata!.size,
inputImage.metadata!.rotation,
cameraController!.description.lensDirection,
);
}
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();
}
}

View file

@ -0,0 +1,174 @@
import 'dart:async';
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/painters/coordinates_translator.dart';
import 'package:twonly/src/views/camera/painters/face_filters/face_filter_painter.dart';
class BeardFilterPainter extends FaceFilterPainter {
BeardFilterPainter(
super.faces,
super.imageSize,
super.rotation,
super.cameraLensDirection,
) {
_loadAssets();
}
static ui.Image? _beardImage;
static bool _loading = false;
static Future<void> _loadAssets() async {
if (_loading || _beardImage != null) return;
_loading = true;
try {
_beardImage = await _loadImage('assets/filters/beard_upper_lip.webp');
} catch (e) {
Log.error('Failed to load filter assets: $e');
} finally {
_loading = false;
}
}
static Future<ui.Image> _loadImage(String assetPath) async {
final data = await rootBundle.load(assetPath);
final list = Uint8List.view(data.buffer);
final completer = Completer<ui.Image>();
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, -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() {
return Preview(
child: Padding(
padding: const EdgeInsets.all(8),
child: Image.asset(
'assets/filters/beard_upper_lip.webp',
fit: BoxFit.contain,
),
),
);
}
}

View file

@ -0,0 +1,242 @@
import 'dart:async';
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/painters/coordinates_translator.dart';
import 'package:twonly/src/views/camera/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<void> _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<ui.Image> _loadImage(String assetPath) async {
final data = await rootBundle.load(assetPath);
final list = Uint8List.view(data.buffer);
final completer = Completer<ui.Image>();
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<int>? leftMost;
Point<int>? rightMost;
Point<int>? 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, -1);
} else {
canvas.scale(scaleX, -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,
),
),
],
),
),
],
),
);
}
}

View file

@ -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<Face> 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,
),
);
}
}

View file

@ -868,6 +868,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.11.0"
google_mlkit_face_detection:
dependency: "direct main"
description:
name: google_mlkit_face_detection
sha256: f336737d5b8a86797fd4368f42a5c26aeaa9c6dcc5243f0a16b5f6f663cfb70a
url: "https://pub.dev"
source: hosted
version: "0.13.1"
graphs:
dependency: transitive
description:

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none'
version: 0.0.85+85
version: 0.0.86+86
environment:
sdk: ^3.6.0
@ -110,6 +110,7 @@ dependencies:
hand_signature: ^3.0.3
flutter_sharing_intent: ^2.0.4
no_screenshot: ^0.3.1
google_mlkit_face_detection: ^0.13.1
dependency_overrides:
dots_indicator:
@ -203,5 +204,6 @@ flutter:
- assets/animated_icons/
- assets/animations/
- assets/passwords/
- assets/filters/
- CHANGELOG.md