feat: Update camera dependencies and implement selfie verification UI
- Updated camera package version to 0.11.0+2 and camera_android_camerax to 0.6.7+2 in pubspec.lock and pubspec.yaml. - Removed unused google_ml_kit dependencies from pubspec.lock and pubspec.yaml. - Added CameraPreviewWidget for displaying camera feed in registration form. - Created CapturedSelfieView to show the captured selfie and verification status. - Implemented Debug Panel for liveness detection with real-time status updates. - Added ErrorStateWidget to handle and display error messages during verification. - Introduced InstructionBanner to guide users through the verification process. - Developed VerificationProgressWidget to visually represent the verification progress.
This commit is contained in:
parent
790eb4325d
commit
5dc7aa3cc7
|
@ -1140,3 +1140,251 @@ class FaceLivenessController extends GetxController
|
||||||
return orientation == Orientation.portrait ? "Portrait" : "Landscape";
|
return orientation == Orientation.portrait ? "Portrait" : "Landscape";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
import 'package:camera/camera.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CameraScreen extends StatefulWidget {
|
||||||
|
const CameraScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CameraScreen> createState() => _CameraScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CameraScreenState extends State<CameraScreen>
|
||||||
|
with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||||
|
CameraController? controller;
|
||||||
|
String error='';
|
||||||
|
|
||||||
|
FaceDetector faceDetector = FaceDetector(
|
||||||
|
options: FaceDetectorOptions(
|
||||||
|
enableClassification: true,
|
||||||
|
enableContours: true,
|
||||||
|
enableTracking: true,
|
||||||
|
enableLandmarks: true,
|
||||||
|
performanceMode: Platform.isAndroid
|
||||||
|
? FaceDetectorMode.fast
|
||||||
|
: FaceDetectorMode.accurate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final orientations = {
|
||||||
|
DeviceOrientation.portraitUp: 0,
|
||||||
|
DeviceOrientation.landscapeLeft: 90,
|
||||||
|
DeviceOrientation.portraitDown: 180,
|
||||||
|
DeviceOrientation.landscapeRight: 270,
|
||||||
|
};
|
||||||
|
|
||||||
|
initialize() async {
|
||||||
|
controller = CameraController(
|
||||||
|
cameras[1],
|
||||||
|
ResolutionPreset.medium,
|
||||||
|
imageFormatGroup: Platform.isAndroid
|
||||||
|
? ImageFormatGroup.nv21
|
||||||
|
: ImageFormatGroup.bgra8888,
|
||||||
|
);
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
initializeCameraController(controller!.description);
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
faceDetector.close();
|
||||||
|
if (controller != null) controller!.dispose();
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
final CameraController? cameraController = controller;
|
||||||
|
if (cameraController == null || !cameraController.value.isInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == AppLifecycleState.inactive) {
|
||||||
|
cameraController.dispose();
|
||||||
|
} else if (state == AppLifecycleState.resumed) {
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initializeCameraController() async {
|
||||||
|
try {
|
||||||
|
await controller!.initialize();
|
||||||
|
controller!.setFlashMode(FlashMode.off);
|
||||||
|
// If the controller is updated then update the UI.
|
||||||
|
controller!.addListener(() {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
controller!.startImageStream(_processCameraImage).then((value) {});
|
||||||
|
});
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
} on CameraException catch (e) {
|
||||||
|
switch (e.code) {
|
||||||
|
case 'CameraAccessDenied':
|
||||||
|
Get.back();
|
||||||
|
cameraDialog();
|
||||||
|
break;
|
||||||
|
case 'CameraAccessDeniedWithoutPrompt':
|
||||||
|
Get.back();
|
||||||
|
cameraDialog();
|
||||||
|
break;
|
||||||
|
case 'CameraAccessRestricted':
|
||||||
|
Get.back();
|
||||||
|
cameraDialog();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Get.back();
|
||||||
|
EasyLoading.showError(e.description!);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraDialog() {
|
||||||
|
Get.defaultDialog(
|
||||||
|
title: 'Camera Access',
|
||||||
|
middleText:
|
||||||
|
'Looks like you have not provided permission to camera completely. Enable it in the settings.',
|
||||||
|
onConfirm: () async {
|
||||||
|
Get.back();
|
||||||
|
await openAppSettings();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCaptured(String message) {
|
||||||
|
debugPrint(message);
|
||||||
|
imageFile = null;
|
||||||
|
error = message;
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _processCameraImage(CameraImage image) {
|
||||||
|
final inputImage = inputImageFromCameraImage(image);
|
||||||
|
if (inputImage == null) return;
|
||||||
|
checkImage(inputImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkImage(InputImage inputImage) async {
|
||||||
|
List<Face> faces = await faceDetector.processImage(inputImage);
|
||||||
|
debugPrint('faces: $faces');
|
||||||
|
if (faces.isEmpty) {
|
||||||
|
clearCaptured('No faces detected in the image.');
|
||||||
|
}
|
||||||
|
if (faces.length > 1) {
|
||||||
|
clearCaptured(
|
||||||
|
'Multiple faces detected. Please try capturing a single face.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Handle the detected faces
|
||||||
|
for (Face face in faces) {
|
||||||
|
if (face.leftEyeOpenProbability != null &&
|
||||||
|
face.rightEyeOpenProbability != null) {
|
||||||
|
final leftEyeOpen = face.leftEyeOpenProbability! > 0.5;
|
||||||
|
final rightEyeOpen = face.rightEyeOpenProbability! > 0.5;
|
||||||
|
if (leftEyeOpen && rightEyeOpen) {
|
||||||
|
error = 'Click capture to save this image';
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
return;
|
||||||
|
} else if (!leftEyeOpen && !rightEyeOpen) {
|
||||||
|
clearCaptured('Both eyes are closed!');
|
||||||
|
} else if (leftEyeOpen && !rightEyeOpen) {
|
||||||
|
clearCaptured('Left eye is open, right eye is closed.');
|
||||||
|
} else if (!leftEyeOpen && rightEyeOpen) {
|
||||||
|
clearCaptured('Right eye is open, left eye is closed.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clearCaptured(
|
||||||
|
'Looks like either one or both eyes have not been captured properly. Please try again.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
InputImage? inputImageFromCameraImage(CameraImage image) {
|
||||||
|
final camera = cameras[1];
|
||||||
|
final sensorOrientation = camera.sensorOrientation;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if (rotation == null) return null;
|
||||||
|
|
||||||
|
// get image format
|
||||||
|
final format = InputImageFormatValue.fromRawValue(image.format.raw);
|
||||||
|
|
||||||
|
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
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
centerTitle: true,
|
||||||
|
title: Text('Capture Your Selfie'),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: controller == null
|
||||||
|
? null
|
||||||
|
: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(error),
|
||||||
|
SizedBox(height:30),
|
||||||
|
ElevatedButton(
|
||||||
|
child: Text('Capture'),
|
||||||
|
onPressed: () async {
|
||||||
|
await controller!.takePicture().then((value) {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height:20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: controller == null
|
||||||
|
? Center(child: Text('Need to access camera to capture selfie'))
|
||||||
|
: CameraPreview(controller!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,75 +1,77 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Sigap</string>
|
<string>Sigap</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>sigap</string>
|
<string>sigap</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>Main</string>
|
<string>Main</string>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true />
|
||||||
<!-- Add this array for Deep Links -->
|
<!-- Add this array for Deep Links -->
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Editor</string>
|
<string>Editor</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>io.supabase.flutterquickstart</string>
|
<string>io.supabase.flutterquickstart</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<!-- ... other tags -->
|
<!-- ... other tags -->
|
||||||
<!-- <key>NSLocationWhenInUseUsageDescription</key>
|
<!-- <key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>This app needs access to location when open.</string> -->
|
<string>This app needs access to location when open.</string> -->
|
||||||
<!-- Mapbox -->
|
<!-- Mapbox -->
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>Aplikasi ini memerlukan akses lokasi untuk menunjukkan posisi Anda pada peta</string>
|
<string>Aplikasi ini memerlukan akses lokasi untuk menunjukkan posisi Anda pada peta</string>
|
||||||
<key>NSLocationAlwaysUsageDescription</key>
|
<key>NSLocationAlwaysUsageDescription</key>
|
||||||
<string>Aplikasi ini memerlukan akses lokasi untuk navigasi latar belakang</string>
|
<string>Aplikasi ini memerlukan akses lokasi untuk navigasi latar belakang</string>
|
||||||
<key>io.flutter.embedded_views_preview</key>
|
<key>io.flutter.embedded_views_preview</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>MGLMapboxAccessToken</key>
|
<key>MGLMapboxAccessToken</key>
|
||||||
<string>$(MAPBOX_ACCESS_TOKEN)</string>
|
<string>$(MAPBOX_ACCESS_TOKEN)</string>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<false/>
|
<key>NSCameraUsageDescription</key>
|
||||||
</dict>
|
<string>We need access to your camera to detect faces.</string>
|
||||||
|
<false />
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
|
@ -3,7 +3,7 @@ import 'dart:developer' as dev;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
||||||
import 'package:google_mlkit_face_mesh_detection/google_mlkit_face_mesh_detection.dart';
|
import 'package:google_mlkit_face_mesh_detection/google_mlkit_face_mesh_detection.dart';
|
||||||
|
@ -23,6 +23,13 @@ enum LivenessStatus {
|
||||||
failed,
|
failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final orientations = {
|
||||||
|
DeviceOrientation.portraitUp: 0,
|
||||||
|
DeviceOrientation.landscapeLeft: 90,
|
||||||
|
DeviceOrientation.portraitDown: 180,
|
||||||
|
DeviceOrientation.landscapeRight: 270,
|
||||||
|
};
|
||||||
|
|
||||||
class FaceLivenessController extends GetxController {
|
class FaceLivenessController extends GetxController {
|
||||||
// Camera
|
// Camera
|
||||||
CameraController? cameraController;
|
CameraController? cameraController;
|
||||||
|
@ -201,9 +208,10 @@ class FaceLivenessController extends GetxController {
|
||||||
|
|
||||||
// Detect faces
|
// Detect faces
|
||||||
final faces = await faceDetector.processImage(inputImage);
|
final faces = await faceDetector.processImage(inputImage);
|
||||||
|
|
||||||
// Process face detection results
|
// Process face detection results
|
||||||
await _processFaceDetection(faces);
|
await _processFaceDetection(faces);
|
||||||
|
|
||||||
|
dev.log('Detected ${faces.length} faces', name: 'LIVENESS_CONTROLLER');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dev.log('Error processing image: $e', name: 'LIVENESS_CONTROLLER');
|
dev.log('Error processing image: $e', name: 'LIVENESS_CONTROLLER');
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -233,7 +241,18 @@ class FaceLivenessController extends GetxController {
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
rotation = InputImageRotationValue.fromRawValue(sensorOrientation);
|
rotation = InputImageRotationValue.fromRawValue(sensorOrientation);
|
||||||
} else if (Platform.isAndroid) {
|
} else if (Platform.isAndroid) {
|
||||||
var rotationCompensation = sensorOrientation;
|
var rotationCompensation =
|
||||||
|
orientations[cameraController!.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);
|
rotation = InputImageRotationValue.fromRawValue(rotationCompensation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,14 +261,10 @@ class FaceLivenessController extends GetxController {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final format = InputImageFormatValue.fromRawValue(image.format.raw);
|
final format =
|
||||||
if (format == null) {
|
Platform.isAndroid
|
||||||
dev.log(
|
? InputImageFormat.nv21
|
||||||
'Unsupported image format: ${image.format.raw}',
|
: InputImageFormat.bgra8888;
|
||||||
name: 'LIVENESS_CONTROLLER',
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle different plane configurations
|
// Handle different plane configurations
|
||||||
if (image.planes.isEmpty) {
|
if (image.planes.isEmpty) {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,60 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
|
||||||
|
class CameraPreviewWidget extends StatelessWidget {
|
||||||
|
final FaceLivenessController controller;
|
||||||
|
final double screenWidth;
|
||||||
|
|
||||||
|
const CameraPreviewWidget({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.screenWidth,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
// Camera background
|
||||||
|
Container(
|
||||||
|
width: screenWidth * 0.85,
|
||||||
|
height: screenWidth * 0.85,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Camera preview
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
child: SizedBox(
|
||||||
|
width: screenWidth * 0.85,
|
||||||
|
height: screenWidth * 0.85,
|
||||||
|
child: controller.cameraController!.buildPreview(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Scanning animation
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
child: Container(
|
||||||
|
width: screenWidth * 0.65,
|
||||||
|
height: 2,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.transparent,
|
||||||
|
TColors.primary.withOpacity(0.8),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
import 'dart:developer' as dev;
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
|
||||||
|
class CapturedSelfieView extends StatelessWidget {
|
||||||
|
final FaceLivenessController controller;
|
||||||
|
final SelfieVerificationController? selfieController;
|
||||||
|
|
||||||
|
const CapturedSelfieView({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
this.selfieController,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [Colors.white, Colors.green.shade50],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Success icon
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.shade50,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.check_circle_outline,
|
||||||
|
color: Colors.green.shade600,
|
||||||
|
size: 48,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'Verification Successful!',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.green.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'Your identity has been verified',
|
||||||
|
style: TextStyle(fontSize: 16, color: Colors.black54),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// Display captured image
|
||||||
|
if (controller.capturedImage != null)
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(150),
|
||||||
|
border: Border.all(color: Colors.white, width: 4),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 20,
|
||||||
|
spreadRadius: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(150),
|
||||||
|
child: Image.file(
|
||||||
|
File(controller.capturedImage!.path),
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// Completed steps list
|
||||||
|
_buildCompletedStepsList(),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// Continue button - clear loading state properly
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => _handleContinueButton(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: TColors.primary,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Continue',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the completed steps list
|
||||||
|
Widget _buildCompletedStepsList() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
spreadRadius: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.verified, color: Colors.green.shade600, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'All verification steps completed',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
...controller.successfulSteps.map(
|
||||||
|
(step) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.shade50,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: Colors.green.shade600,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
step,
|
||||||
|
style: const TextStyle(fontSize: 14, color: Colors.black87),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the continue button
|
||||||
|
void _handleContinueButton() {
|
||||||
|
// Reset loading state in selfie controller before navigating back
|
||||||
|
try {
|
||||||
|
if (selfieController != null) {
|
||||||
|
dev.log(
|
||||||
|
'Found SelfieVerificationController, handling success',
|
||||||
|
name: 'LIVENESS_DEBUG',
|
||||||
|
);
|
||||||
|
// Connect with SelfieVerificationController
|
||||||
|
if (controller.capturedImage != null) {
|
||||||
|
dev.log(
|
||||||
|
'Setting captured image on SelfieVerificationController',
|
||||||
|
name: 'LIVENESS_DEBUG',
|
||||||
|
);
|
||||||
|
selfieController?.selfieImage.value = controller.capturedImage;
|
||||||
|
// selfieController._processCapturedLivenessImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
dev.log(
|
||||||
|
'Error connecting with SelfieVerificationController: $e',
|
||||||
|
name: 'LIVENESS_DEBUG',
|
||||||
|
);
|
||||||
|
// Continue without selfie controller
|
||||||
|
}
|
||||||
|
Get.back(result: controller.capturedImage);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,333 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart';
|
||||||
|
|
||||||
|
/// Shows the debug panel for liveness detection
|
||||||
|
void showLivenessDebugPanel(
|
||||||
|
BuildContext context,
|
||||||
|
FaceLivenessController controller,
|
||||||
|
SelfieVerificationController? selfieController,
|
||||||
|
) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder:
|
||||||
|
(context) => LivenessDebugPanel(
|
||||||
|
controller: controller,
|
||||||
|
selfieController: selfieController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class LivenessDebugPanel extends StatelessWidget {
|
||||||
|
final FaceLivenessController controller;
|
||||||
|
final SelfieVerificationController? selfieController;
|
||||||
|
|
||||||
|
const LivenessDebugPanel({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
this.selfieController,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(20),
|
||||||
|
height: MediaQuery.of(context).size.height * 0.8,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Liveness Detection Debug Panel',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Divider(),
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Obx(
|
||||||
|
() => Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Status section
|
||||||
|
Text(
|
||||||
|
'Camera & Detection Status',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
_debugItem(
|
||||||
|
'Camera Controller',
|
||||||
|
'${controller.cameraController?.value.isInitialized}',
|
||||||
|
),
|
||||||
|
_debugItem('Status', '${controller.status.value}'),
|
||||||
|
_debugItem(
|
||||||
|
'Face In Frame',
|
||||||
|
'${controller.isFaceInFrame.value}',
|
||||||
|
),
|
||||||
|
_debugItem('Face Left', '${controller.isFaceLeft}'),
|
||||||
|
_debugItem('Face Right', '${controller.isFaceRight}'),
|
||||||
|
_debugItem('Eyes Open', '${controller.isEyeOpen}'),
|
||||||
|
_debugItem('Smiled', '${controller.isSmiled}'),
|
||||||
|
_debugItem(
|
||||||
|
'Ready For Photo',
|
||||||
|
'${controller.isFaceReadyForPhoto.value}',
|
||||||
|
),
|
||||||
|
_debugItem('Captured', '${controller.isCaptured}'),
|
||||||
|
_debugItem(
|
||||||
|
'Steps Completed',
|
||||||
|
'${controller.successfulSteps.length}',
|
||||||
|
),
|
||||||
|
_debugItem('Steps', '${controller.successfulSteps}'),
|
||||||
|
|
||||||
|
Divider(),
|
||||||
|
Text(
|
||||||
|
'Selfie Controller',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
_debugItem('Found', '${selfieController != null}'),
|
||||||
|
|
||||||
|
if (selfieController != null) ...[
|
||||||
|
_debugItem(
|
||||||
|
'Is Performing Check',
|
||||||
|
'${selfieController?.isPerformingLivenessCheck.value}',
|
||||||
|
),
|
||||||
|
_debugItem(
|
||||||
|
'Is Selfie Valid',
|
||||||
|
'${selfieController?.isSelfieValid.value}',
|
||||||
|
),
|
||||||
|
_debugItem(
|
||||||
|
'Has Confirmed Selfie',
|
||||||
|
'${selfieController?.hasConfirmedSelfie.value}',
|
||||||
|
),
|
||||||
|
_debugItem(
|
||||||
|
'Liveness Passed',
|
||||||
|
'${selfieController?.isLivenessCheckPassed.value}',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
Divider(),
|
||||||
|
_buildQuickActionsSection(context),
|
||||||
|
|
||||||
|
SizedBox(height: 16),
|
||||||
|
// Additional test connection button
|
||||||
|
if (selfieController != null &&
|
||||||
|
controller.capturedImage != null)
|
||||||
|
OutlinedButton.icon(
|
||||||
|
icon: Icon(Icons.link),
|
||||||
|
label: Text('Test Connection to Selfie Controller'),
|
||||||
|
onPressed: () {
|
||||||
|
selfieController?.selfieImage.value =
|
||||||
|
controller.capturedImage;
|
||||||
|
Navigator.pop(context);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Test data sent to SelfieVerificationController',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick actions section
|
||||||
|
Widget _buildQuickActionsSection(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.amber.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.amber.shade200),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Quick Actions',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.amber.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Use these actions to bypass steps or debug issues:',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.black87),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Button group - layout for buttons
|
||||||
|
GridView.count(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: NeverScrollableScrollPhysics(),
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 10,
|
||||||
|
crossAxisSpacing: 10,
|
||||||
|
childAspectRatio: 2.5,
|
||||||
|
children: [
|
||||||
|
// Skip all steps button
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: Icon(Icons.skip_next, color: Colors.white, size: 18),
|
||||||
|
label: Text(
|
||||||
|
'Skip All Steps',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
controller.skipAllVerificationSteps();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Skipping all steps'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Force next step button
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: Icon(Icons.arrow_forward, color: Colors.white, size: 18),
|
||||||
|
label: Text(
|
||||||
|
'Next Step',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
controller.forceAdvanceToNextStep();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Forced next step'),
|
||||||
|
backgroundColor: Colors.amber,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.amber,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Force capture button
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: Icon(Icons.camera, color: Colors.white, size: 18),
|
||||||
|
label: Text(
|
||||||
|
'Force Capture',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (controller.cameraController != null &&
|
||||||
|
controller.cameraController!.value.isInitialized) {
|
||||||
|
controller.forceCaptureImage();
|
||||||
|
Navigator.pop(context);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Camera not ready'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Reset process button
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: Icon(Icons.refresh, color: Colors.white, size: 18),
|
||||||
|
label: Text(
|
||||||
|
'Reset Process',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
controller.resetProcess();
|
||||||
|
Navigator.pop(context);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Process reset'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug list item
|
||||||
|
Widget _debugItem(String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text('$label: ', style: TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
Expanded(child: Text(value, style: TextStyle(color: Colors.blue))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
|
||||||
|
class ErrorStateWidget extends StatelessWidget {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
const ErrorStateWidget({
|
||||||
|
Key? key,
|
||||||
|
required this.message,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
String userFriendlyMessage = message;
|
||||||
|
|
||||||
|
// Convert technical errors to user-friendly messages
|
||||||
|
if (message.contains('server_config_error') ||
|
||||||
|
message.contains('environment variables')) {
|
||||||
|
userFriendlyMessage =
|
||||||
|
'The face verification service is temporarily unavailable. Please try again later.';
|
||||||
|
} else if (message.contains('network') || message.contains('connection')) {
|
||||||
|
userFriendlyMessage =
|
||||||
|
'Network error. Please check your internet connection and try again.';
|
||||||
|
} else if (message.contains('timeout')) {
|
||||||
|
userFriendlyMessage =
|
||||||
|
'The request timed out. Please try again when you have a stronger connection.';
|
||||||
|
} else if (message.contains('Camera initialization failed')) {
|
||||||
|
userFriendlyMessage =
|
||||||
|
'Unable to access camera. Please check your camera permissions and try again.';
|
||||||
|
} else if (message.contains('decode') ||
|
||||||
|
message.contains('Body can not be decoded')) {
|
||||||
|
userFriendlyMessage =
|
||||||
|
'There was a problem processing your image. Please try again.';
|
||||||
|
} else if (message.contains('invalid_request_format')) {
|
||||||
|
userFriendlyMessage =
|
||||||
|
'There was a problem with the image format. Please try again with a different image.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline, color: Colors.red, size: 48),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Verification Error',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Text(
|
||||||
|
userFriendlyMessage,
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: TColors.primary,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: Text('Go Back'),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Reset and try again
|
||||||
|
try {
|
||||||
|
final controller = Get.find<FaceLivenessController>();
|
||||||
|
controller.resetProcess();
|
||||||
|
} catch (e) {
|
||||||
|
// Handle case where controller isn't available
|
||||||
|
Get.back();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('Try Again'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
|
||||||
|
class InstructionBanner extends StatelessWidget {
|
||||||
|
final FaceLivenessController controller;
|
||||||
|
|
||||||
|
const InstructionBanner({super.key, required this.controller});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: TColors.primary.withOpacity(0.08),
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(16),
|
||||||
|
bottomRight: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: TColors.primary.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.face_retouching_natural,
|
||||||
|
color: TColors.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Obx(
|
||||||
|
() => Text(
|
||||||
|
controller.getCurrentDirection(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: TColors.primary,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Status indicator
|
||||||
|
Obx(
|
||||||
|
() => Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getStatusColor(controller.status.value),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_getStatusText(controller.status.value),
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get status color
|
||||||
|
Color _getStatusColor(LivenessStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case LivenessStatus.preparing:
|
||||||
|
case LivenessStatus.detectingFace:
|
||||||
|
return Colors.orange;
|
||||||
|
case LivenessStatus.failed:
|
||||||
|
return Colors.red;
|
||||||
|
case LivenessStatus.completed:
|
||||||
|
case LivenessStatus.photoTaken:
|
||||||
|
return Colors.green;
|
||||||
|
default:
|
||||||
|
return TColors.primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get status text
|
||||||
|
String _getStatusText(LivenessStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case LivenessStatus.preparing:
|
||||||
|
return 'Preparing';
|
||||||
|
case LivenessStatus.detectingFace:
|
||||||
|
return 'Detecting';
|
||||||
|
case LivenessStatus.checkLeftRotation:
|
||||||
|
return 'Look Left';
|
||||||
|
case LivenessStatus.checkRightRotation:
|
||||||
|
return 'Look Right';
|
||||||
|
case LivenessStatus.checkSmile:
|
||||||
|
return 'Smile';
|
||||||
|
case LivenessStatus.checkEyesOpen:
|
||||||
|
return 'Open Eyes';
|
||||||
|
case LivenessStatus.readyForPhoto:
|
||||||
|
return 'Ready';
|
||||||
|
case LivenessStatus.photoTaken:
|
||||||
|
return 'Processing';
|
||||||
|
case LivenessStatus.completed:
|
||||||
|
return 'Success';
|
||||||
|
case LivenessStatus.failed:
|
||||||
|
return 'Failed';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
|
||||||
|
class VerificationProgressWidget extends StatelessWidget {
|
||||||
|
final FaceLivenessController controller;
|
||||||
|
|
||||||
|
const VerificationProgressWidget({super.key, required this.controller});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(20),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
spreadRadius: 0,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.verified_outlined, color: TColors.primary, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Verification Progress',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Progress indicator
|
||||||
|
Obx(
|
||||||
|
() => LinearProgressIndicator(
|
||||||
|
value: controller.successfulSteps.length / 4,
|
||||||
|
backgroundColor: Colors.grey.shade200,
|
||||||
|
color: TColors.primary,
|
||||||
|
minHeight: 6,
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Steps list
|
||||||
|
Obx(() {
|
||||||
|
if (controller.successfulSteps.isEmpty) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
'Follow the instructions to complete verification',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.black54,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children:
|
||||||
|
controller.successfulSteps
|
||||||
|
.map(
|
||||||
|
(step) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.shade50,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: Colors.green.shade600,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
step,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -109,18 +109,18 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: camera
|
name: camera
|
||||||
sha256: "413d2b34fe28496c35c69ede5b232fb9dd5ca2c3a4cb606b14efc1c7546cc8cb"
|
sha256: "26ff41045772153f222ffffecba711a206f670f5834d40ebf5eed3811692f167"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.11.0+2"
|
||||||
camera_android_camerax:
|
camera_android_camerax:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: camera_android_camerax
|
name: camera_android_camerax
|
||||||
sha256: "9fb44e73e0fea3647a904dc26d38db24055e5b74fc68fd2b6d3abfa1bd20f536"
|
sha256: "8bd9cab67551642eb33ceb33ece7acc0890014fc90ddfae637c7e2b683657e65"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.17"
|
version: "0.6.7+2"
|
||||||
camera_avfoundation:
|
camera_avfoundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -693,22 +693,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.3+1"
|
version: "0.3.3+1"
|
||||||
google_ml_kit:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: google_ml_kit
|
|
||||||
sha256: a2da12a62353a6cad71534b52ada3af14a5b842e6c9b1014ce4d243652b30f4b
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.20.0"
|
|
||||||
google_mlkit_barcode_scanning:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_mlkit_barcode_scanning
|
|
||||||
sha256: b38505df2d3fdf7830979d60fee55039c2f442d189b2e06fcb2fe494ba65d0db
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.14.1"
|
|
||||||
google_mlkit_commons:
|
google_mlkit_commons:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -717,22 +701,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.0"
|
version: "0.11.0"
|
||||||
google_mlkit_digital_ink_recognition:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_mlkit_digital_ink_recognition
|
|
||||||
sha256: "8d2b89401bdeeba97158377167429dbc5cb339ebbd21e0889dca773f1c79a884"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.14.1"
|
|
||||||
google_mlkit_entity_extraction:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_mlkit_entity_extraction
|
|
||||||
sha256: "145bc26422b7e62d50cc4eca1ac394d13ac6a97e4c09b8baf7ff058b64d2f9cc"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.15.1"
|
|
||||||
google_mlkit_face_detection:
|
google_mlkit_face_detection:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -749,70 +717,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.1"
|
version: "0.4.1"
|
||||||
google_mlkit_image_labeling:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_mlkit_image_labeling
|
|
||||||
sha256: "2cac5f7a02dcc23cd3357f89bf1a79df793ae3afce5035a896de467ffa0192e8"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.14.1"
|
|
||||||
google_mlkit_language_id:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_mlkit_language_id
|
|
||||||
sha256: fc57bca69cb1dcd8ef67b929f0315e9a8baa80c03c75f7a1226becd7ad2529ff
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.13.0"
|
|
||||||
google_mlkit_object_detection:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_mlkit_object_detection
|
|
||||||
sha256: "0f740f046d74faf81d9c44cdbe4accf33888ed9f877e30efbfad4578d45ebfcd"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.15.0"
|
|
||||||
google_mlkit_pose_detection:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_mlkit_pose_detection
|
|
||||||
sha256: "5ff5fe2a325427c49c02a884a2a888d2d10cbfe414f7ebf2af9777a5155171eb"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.14.0"
|
|
||||||
google_mlkit_selfie_segmentation:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_mlkit_selfie_segmentation
|
|
||||||
sha256: e05fc255265595a0fb11cd6a6a5393f106d6ec4d3a40cbc57ff22894eef235f1
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.10.0"
|
|
||||||
google_mlkit_smart_reply:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_mlkit_smart_reply
|
|
||||||
sha256: "0c3d737e46f20aa4d4953860ee5757e5250e58f90351f8e2afdeb1d609c7047e"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.13.0"
|
|
||||||
google_mlkit_text_recognition:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_mlkit_text_recognition
|
|
||||||
sha256: "96173ad4dd7fd06c660e22ac3f9e9f1798a517fe7e48bee68eeec83853224224"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.15.0"
|
|
||||||
google_mlkit_translation:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_mlkit_translation
|
|
||||||
sha256: "7287444a0abd994087a0b354dee952fcd198e57619ded4bba65496d418c9d84b"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.13.0"
|
|
||||||
google_sign_in:
|
google_sign_in:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
name: sigap
|
name: sigap
|
||||||
description: 'A mobile app for SIGAP Jember'
|
description: "A mobile app for SIGAP Jember"
|
||||||
# The following line prevents the package from being accidentally published to
|
# The following line prevents the package from being accidentally published to
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
# The following defines the version and build number for your application.
|
# The following defines the version and build number for your application.
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
|
@ -32,7 +32,8 @@ dependencies:
|
||||||
calendar_date_picker2:
|
calendar_date_picker2:
|
||||||
easy_date_timeline:
|
easy_date_timeline:
|
||||||
equatable: ^2.0.7
|
equatable: ^2.0.7
|
||||||
camera:
|
camera: 0.11.0+2
|
||||||
|
camera_android_camerax: 0.6.7+2
|
||||||
|
|
||||||
# --- Logging & Debugging ---
|
# --- Logging & Debugging ---
|
||||||
logger:
|
logger:
|
||||||
|
@ -58,7 +59,6 @@ dependencies:
|
||||||
dotted_border:
|
dotted_border:
|
||||||
flutter_svg: ^2.1.0
|
flutter_svg: ^2.1.0
|
||||||
|
|
||||||
|
|
||||||
# --- Input & Forms ---
|
# --- Input & Forms ---
|
||||||
flutter_otp_text_field:
|
flutter_otp_text_field:
|
||||||
image_picker:
|
image_picker:
|
||||||
|
@ -117,7 +117,6 @@ dependencies:
|
||||||
# --- Machine Learning ---
|
# --- Machine Learning ---
|
||||||
google_mlkit_face_detection: ^0.13.1
|
google_mlkit_face_detection: ^0.13.1
|
||||||
google_mlkit_face_mesh_detection: ^0.4.1
|
google_mlkit_face_mesh_detection: ^0.4.1
|
||||||
google_ml_kit: ^0.20.0
|
|
||||||
|
|
||||||
# --- Localization ---
|
# --- Localization ---
|
||||||
# (add localization dependencies here if needed)
|
# (add localization dependencies here if needed)
|
||||||
|
@ -143,9 +142,9 @@ flutter:
|
||||||
|
|
||||||
#--------------- LOCAL ASSETS ------------------#
|
#--------------- LOCAL ASSETS ------------------#
|
||||||
assets:
|
assets:
|
||||||
# Environment variables
|
# Environment variables
|
||||||
- .env
|
- .env
|
||||||
# Images
|
# Images
|
||||||
- assets/logos/
|
- assets/logos/
|
||||||
- assets/icons/brands/
|
- assets/icons/brands/
|
||||||
- assets/images/content/
|
- assets/images/content/
|
||||||
|
@ -155,7 +154,7 @@ flutter:
|
||||||
- assets/icons/categories/
|
- assets/icons/categories/
|
||||||
- assets/images/animations/
|
- assets/images/animations/
|
||||||
- assets/images/on_boarding_images/
|
- assets/images/on_boarding_images/
|
||||||
# JSON Files
|
# JSON Files
|
||||||
- public/jsons/provinsi.json
|
- public/jsons/provinsi.json
|
||||||
- public/jsons/kabupaten_kota.json
|
- public/jsons/kabupaten_kota.json
|
||||||
- public/jsons/kecamatan.json
|
- public/jsons/kecamatan.json
|
||||||
|
|
Loading…
Reference in New Issue