mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-03-03 13:56:45 +00:00
added support for face filters
This commit is contained in:
parent
448a28cdbd
commit
42cc6db0e2
14 changed files with 696 additions and 15 deletions
|
|
@ -1,5 +1,9 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.0.87
|
||||||
|
|
||||||
|
- Added basic support for face filters
|
||||||
|
|
||||||
## 0.0.86
|
## 0.0.86
|
||||||
|
|
||||||
- Allows to reopen send images (if send without time limit or enabled auth)
|
- Allows to reopen send images (if send without time limit or enabled auth)
|
||||||
|
|
|
||||||
BIN
assets/filters/beard_upper_lip.webp
Normal file
BIN
assets/filters/beard_upper_lip.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
assets/filters/dog_brown_ear.webp
Normal file
BIN
assets/filters/dog_brown_ear.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
assets/filters/dog_brown_nose.webp
Normal file
BIN
assets/filters/dog_brown_nose.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
|
|
@ -136,6 +136,10 @@ PODS:
|
||||||
- google_mlkit_commons (0.11.0):
|
- google_mlkit_commons (0.11.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- MLKitVision
|
- MLKitVision
|
||||||
|
- google_mlkit_face_detection (0.13.1):
|
||||||
|
- Flutter
|
||||||
|
- google_mlkit_commons
|
||||||
|
- GoogleMLKit/FaceDetection (~> 7.0.0)
|
||||||
- GoogleAdsOnDeviceConversion (3.2.0):
|
- GoogleAdsOnDeviceConversion (3.2.0):
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/Logger (~> 8.1)
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
|
|
@ -169,6 +173,9 @@ PODS:
|
||||||
- GoogleMLKit/BarcodeScanning (7.0.0):
|
- GoogleMLKit/BarcodeScanning (7.0.0):
|
||||||
- GoogleMLKit/MLKitCore
|
- GoogleMLKit/MLKitCore
|
||||||
- MLKitBarcodeScanning (~> 6.0.0)
|
- MLKitBarcodeScanning (~> 6.0.0)
|
||||||
|
- GoogleMLKit/FaceDetection (7.0.0):
|
||||||
|
- GoogleMLKit/MLKitCore
|
||||||
|
- MLKitFaceDetection (~> 6.0.0)
|
||||||
- GoogleMLKit/MLKitCore (7.0.0):
|
- GoogleMLKit/MLKitCore (7.0.0):
|
||||||
- MLKitCommon (~> 12.0.0)
|
- MLKitCommon (~> 12.0.0)
|
||||||
- GoogleToolboxForMac/Defines (4.2.1)
|
- GoogleToolboxForMac/Defines (4.2.1)
|
||||||
|
|
@ -251,6 +258,9 @@ PODS:
|
||||||
- GoogleUtilities/Logger (~> 8.0)
|
- GoogleUtilities/Logger (~> 8.0)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||||
|
- MLKitFaceDetection (6.0.0):
|
||||||
|
- MLKitCommon (~> 12.0)
|
||||||
|
- MLKitVision (~> 8.0)
|
||||||
- MLKitVision (8.0.0):
|
- MLKitVision (8.0.0):
|
||||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||||
|
|
@ -357,6 +367,7 @@ DEPENDENCIES:
|
||||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||||
- google_mlkit_barcode_scanning (from `.symlinks/plugins/google_mlkit_barcode_scanning/ios`)
|
- google_mlkit_barcode_scanning (from `.symlinks/plugins/google_mlkit_barcode_scanning/ios`)
|
||||||
- google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`)
|
- google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`)
|
||||||
|
- google_mlkit_face_detection (from `.symlinks/plugins/google_mlkit_face_detection/ios`)
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
||||||
|
|
@ -398,6 +409,7 @@ SPEC REPOS:
|
||||||
- MLImage
|
- MLImage
|
||||||
- MLKitBarcodeScanning
|
- MLKitBarcodeScanning
|
||||||
- MLKitCommon
|
- MLKitCommon
|
||||||
|
- MLKitFaceDetection
|
||||||
- MLKitVision
|
- MLKitVision
|
||||||
- nanopb
|
- nanopb
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
|
|
@ -454,6 +466,8 @@ EXTERNAL SOURCES:
|
||||||
:path: ".symlinks/plugins/google_mlkit_barcode_scanning/ios"
|
:path: ".symlinks/plugins/google_mlkit_barcode_scanning/ios"
|
||||||
google_mlkit_commons:
|
google_mlkit_commons:
|
||||||
:path: ".symlinks/plugins/google_mlkit_commons/ios"
|
:path: ".symlinks/plugins/google_mlkit_commons/ios"
|
||||||
|
google_mlkit_face_detection:
|
||||||
|
:path: ".symlinks/plugins/google_mlkit_face_detection/ios"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
in_app_purchase_storekit:
|
in_app_purchase_storekit:
|
||||||
|
|
@ -518,6 +532,7 @@ SPEC CHECKSUMS:
|
||||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||||
google_mlkit_barcode_scanning: 8f5987f244a43fe1167689c548342a5174108159
|
google_mlkit_barcode_scanning: 8f5987f244a43fe1167689c548342a5174108159
|
||||||
google_mlkit_commons: 2abe6a70e1824e431d16a51085cb475b672c8aab
|
google_mlkit_commons: 2abe6a70e1824e431d16a51085cb475b672c8aab
|
||||||
|
google_mlkit_face_detection: 754da2113a1952f063c7c5dc347ac6ae8934fb77
|
||||||
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
|
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
|
||||||
GoogleAppMeasurement: 3bf40aff49a601af5da1c3345702fcb4991d35ee
|
GoogleAppMeasurement: 3bf40aff49a601af5da1c3345702fcb4991d35ee
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
|
|
@ -533,6 +548,7 @@ SPEC CHECKSUMS:
|
||||||
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
|
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
|
||||||
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
|
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
|
||||||
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
|
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
|
||||||
|
MLKitFaceDetection: 2a593db4837db503ad3426b565e7aab045cefea5
|
||||||
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
|
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
no_screenshot: 89e778ede9f1e39cc3fb9404d782a42712f2a0b2
|
no_screenshot: 89e778ede9f1e39cc3fb9404d782a42712f2a0b2
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,18 @@ class MainCameraPreview extends StatelessWidget {
|
||||||
.cameraController!.value.previewSize!.width,
|
.cameraController!.value.previewSize!.width,
|
||||||
child: CameraPreview(
|
child: CameraPreview(
|
||||||
mainCameraController.cameraController!,
|
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!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/qr.dart';
|
import 'package:twonly/src/utils/qr.dart';
|
||||||
import 'package:twonly/src/utils/screenshot.dart';
|
import 'package:twonly/src/utils/screenshot.dart';
|
||||||
import 'package:twonly/src/utils/storage.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/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/permissions_view.dart';
|
||||||
import 'package:twonly/src/views/camera/camera_preview_components/send_to.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 {
|
Future<void> startVideoRecording() async {
|
||||||
if (mc.cameraController != null &&
|
if (mc.cameraController != null &&
|
||||||
mc.cameraController!.value.isRecordingVideo) {
|
mc.cameraController!.value.isRecordingVideo) {
|
||||||
|
|
@ -736,15 +767,19 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
children: [
|
children: [
|
||||||
if (!_isVideoRecording)
|
if (!_isVideoRecording)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: pickImageFromGallery,
|
onTap: pressSideButtonLeft,
|
||||||
child: Align(
|
child: Align(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 50,
|
height: 50,
|
||||||
width: 80,
|
width: 80,
|
||||||
padding: const EdgeInsets.all(2),
|
padding: const EdgeInsets.all(2),
|
||||||
child: const Center(
|
child: Center(
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
FontAwesomeIcons.photoFilm,
|
mc.isSelectingFaceFilters
|
||||||
|
? mc.currentFilterType.index == 1
|
||||||
|
? FontAwesomeIcons.xmark
|
||||||
|
: FontAwesomeIcons.arrowLeft
|
||||||
|
: FontAwesomeIcons.photoFilm,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 25,
|
size: 25,
|
||||||
),
|
),
|
||||||
|
|
@ -771,10 +806,39 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
: Colors.white,
|
: 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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:drift/drift.dart' show Value;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.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/globals.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.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/qr.dart';
|
||||||
import 'package:twonly/src/utils/screenshot.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/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/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 {
|
class ScannedVerifiedContact {
|
||||||
ScannedVerifiedContact({
|
ScannedVerifiedContact({
|
||||||
|
|
@ -45,6 +50,22 @@ class MainCameraController {
|
||||||
Map<int, ScannedNewProfile> scannedNewProfiles = {};
|
Map<int, ScannedNewProfile> scannedNewProfiles = {};
|
||||||
String? scannedUrl;
|
String? scannedUrl;
|
||||||
GlobalKey zoomButtonKey = GlobalKey();
|
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 {
|
Future<void> closeCamera() async {
|
||||||
contactsVerified = {};
|
contactsVerified = {};
|
||||||
|
|
@ -73,10 +94,9 @@ class MainCameraController {
|
||||||
selectedCameraDetails = opts.$1;
|
selectedCameraDetails = opts.$1;
|
||||||
cameraController = opts.$2;
|
cameraController = opts.$2;
|
||||||
}
|
}
|
||||||
if (cameraController?.description.lensDirection ==
|
isSelectingFaceFilters = false;
|
||||||
CameraLensDirection.back) {
|
setFilter(FaceFilterType.none);
|
||||||
await cameraController?.startImageStream(_processCameraImage);
|
await cameraController?.startImageStream(_processCameraImage);
|
||||||
}
|
|
||||||
zoomButtonKey = GlobalKey();
|
zoomButtonKey = GlobalKey();
|
||||||
setState();
|
setState();
|
||||||
return cameraController;
|
return cameraController;
|
||||||
|
|
@ -95,13 +115,23 @@ class MainCameraController {
|
||||||
}
|
}
|
||||||
final tmp = cameraController;
|
final tmp = cameraController;
|
||||||
cameraController = null;
|
cameraController = null;
|
||||||
|
facePaint = null;
|
||||||
|
customPaint = null;
|
||||||
await tmp!.dispose();
|
await tmp!.dispose();
|
||||||
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false);
|
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
final BarcodeScanner _barcodeScanner = BarcodeScanner();
|
void setFilter(FaceFilterType type) {
|
||||||
bool _isBusy = false;
|
_currentFilterType = type;
|
||||||
CustomPaint? customPaint;
|
if (_currentFilterType == FaceFilterType.none) {
|
||||||
|
faceFilterPainter = null;
|
||||||
|
facePaint = null;
|
||||||
|
_isBusyFaces = false;
|
||||||
|
}
|
||||||
|
setState();
|
||||||
|
}
|
||||||
|
|
||||||
|
FaceFilterPainter? faceFilterPainter;
|
||||||
|
|
||||||
final Map<DeviceOrientation, int> _orientations = {
|
final Map<DeviceOrientation, int> _orientations = {
|
||||||
DeviceOrientation.portraitUp: 0,
|
DeviceOrientation.portraitUp: 0,
|
||||||
|
|
@ -113,7 +143,16 @@ class MainCameraController {
|
||||||
void _processCameraImage(CameraImage image) {
|
void _processCameraImage(CameraImage image) {
|
||||||
final inputImage = _inputImageFromCameraImage(image);
|
final inputImage = _inputImageFromCameraImage(image);
|
||||||
if (inputImage == null) return;
|
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) {
|
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;
|
if (_isBusy) return;
|
||||||
_isBusy = true;
|
_isBusy = true;
|
||||||
final barcodes = await _barcodeScanner.processImage(inputImage);
|
final barcodes = await _barcodeScanner.processImage(inputImage);
|
||||||
|
|
@ -255,4 +294,48 @@ class MainCameraController {
|
||||||
_isBusy = false;
|
_isBusy = false;
|
||||||
setState();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -868,6 +868,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.0"
|
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:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 0.0.85+85
|
version: 0.0.86+86
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.6.0
|
sdk: ^3.6.0
|
||||||
|
|
@ -110,6 +110,7 @@ dependencies:
|
||||||
hand_signature: ^3.0.3
|
hand_signature: ^3.0.3
|
||||||
flutter_sharing_intent: ^2.0.4
|
flutter_sharing_intent: ^2.0.4
|
||||||
no_screenshot: ^0.3.1
|
no_screenshot: ^0.3.1
|
||||||
|
google_mlkit_face_detection: ^0.13.1
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
dots_indicator:
|
dots_indicator:
|
||||||
|
|
@ -203,5 +204,6 @@ flutter:
|
||||||
- assets/animated_icons/
|
- assets/animated_icons/
|
||||||
- assets/animations/
|
- assets/animations/
|
||||||
- assets/passwords/
|
- assets/passwords/
|
||||||
|
- assets/filters/
|
||||||
- CHANGELOG.md
|
- CHANGELOG.md
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue