import 'dart:io'; import 'dart:ui'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; import '../../core/theme.dart'; import '../../core/utils/camera_utils.dart'; import '../../widgets/atoms/custom_button.dart'; import '../../widgets/atoms/scanner_overlay.dart'; class CameraCaptureScreen extends StatefulWidget { const CameraCaptureScreen({Key? key}) : super(key: key); @override State createState() => _CameraCaptureScreenState(); } class _CameraCaptureScreenState extends State with SingleTickerProviderStateMixin { CameraController? _controller; CameraDescription? _frontCamera; bool _isCameraInitialized = false; bool _isProcessingFrame = false; // Quality gate bool _isFaceDetected = false; bool _isDistanceOk = false; bool _isBrightnessOk = false; bool _isLightingUniform = false; bool _isPoseValid = false; double _faceRatio = 0.0; double _brightness = 0.0; double _lightingUniformity = 0.0; double _headEulerAngleY = 0.0; // State capture foto bool _isCaptureDone = false; XFile? _capturedPhoto; late FaceDetector _faceDetector; late AnimationController _pulseController; bool get _allChecksValid => _isFaceDetected && _isDistanceOk && _isBrightnessOk && _isLightingUniform && _isPoseValid; String get _qualityMessage { if (_isCaptureDone) return 'Pemotretan berhasil ✓'; if (!_isFaceDetected) return 'Arahkan wajah ke dalam bingkai'; if (_faceRatio < 0.25) return 'Terlalu jauh, dekatkan wajah'; if (_faceRatio > 0.75) return 'Terlalu dekat, mundur sedikit'; if (!_isBrightnessOk) return 'Pencahayaan kurang terang'; if (!_isLightingUniform) return 'Ada bayangan, hadap cahaya'; if (!_isPoseValid) return 'Hadap lurus ke kamera'; return 'Siap — ambil foto'; } @override void initState() { super.initState(); _pulseController = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), )..repeat(reverse: true); _faceDetector = FaceDetector( options: FaceDetectorOptions( enableClassification: false, enableLandmarks: true, enableContours: false, enableTracking: true, performanceMode: FaceDetectorMode.accurate, ), ); _initializeCamera(); } Future _initializeCamera() async { final cameras = await availableCameras(); _frontCamera = cameras.firstWhere( (camera) => camera.lensDirection == CameraLensDirection.front, orElse: () => cameras.first, ); _controller = CameraController( _frontCamera!, ResolutionPreset.medium, enableAudio: false, imageFormatGroup: Platform.isAndroid ? ImageFormatGroup.nv21 : ImageFormatGroup.bgra8888, ); await _controller!.initialize(); if (!mounted) return; _controller!.startImageStream((CameraImage image) { if (!mounted || _isCaptureDone) return; _processFrame(image); }); setState(() { _isCameraInitialized = true; }); } void _processFrame(CameraImage image) async { if (_isProcessingFrame) return; _isProcessingFrame = true; try { _brightness = BrightnessUtils.calculateBrightness(image, centerOnly: true); _isBrightnessOk = _brightness > 70; _lightingUniformity = BrightnessUtils.calculateFaceUniformity(image); _isLightingUniform = _lightingUniformity > 0.45; final inputImage = CameraUtils.inputImageFromCameraImage( image: image, camera: _frontCamera!, rotation: CameraUtils.rotationIntToImageRotation( _frontCamera!.sensorOrientation), ); final faces = await _faceDetector.processImage(inputImage); if (faces.isNotEmpty) { final face = faces.first; _headEulerAngleY = face.headEulerAngleY ?? 0.0; _isPoseValid = _headEulerAngleY.abs() < 18.0; final rotation = CameraUtils.rotationIntToImageRotation(_frontCamera!.sensorOrientation); final bool isRotated90or270 = rotation == InputImageRotation.rotation90deg || rotation == InputImageRotation.rotation270deg; final double effectiveWidth = isRotated90or270 ? image.height.toDouble() : image.width.toDouble(); _faceRatio = face.boundingBox.width / effectiveWidth; _isFaceDetected = true; _isDistanceOk = _faceRatio >= 0.25 && _faceRatio <= 0.75; } else { _isFaceDetected = false; _isDistanceOk = false; _isPoseValid = false; _faceRatio = 0.0; } if (mounted) setState(() {}); } catch (e) { debugPrint("Error processing frame: $e"); } finally { _isProcessingFrame = false; } } Future _capturePhoto() async { if (!_allChecksValid || _isCaptureDone) return; try { await _controller!.stopImageStream(); } catch (_) {} XFile? photo; try { photo = await _controller!.takePicture(); } catch (e) { debugPrint("Gagal mengambil foto: $e"); _controller!.startImageStream((CameraImage image) { if (!mounted || _isCaptureDone) return; _processFrame(image); }); return; } HapticFeedback.mediumImpact(); setState(() { _isCaptureDone = true; _capturedPhoto = photo; }); } void _retake() { setState(() { _isCaptureDone = false; _capturedPhoto = null; _isFaceDetected = false; _isDistanceOk = false; _isPoseValid = false; _isLightingUniform = false; _isBrightnessOk = false; }); _controller!.startImageStream((CameraImage image) { if (!mounted || _isCaptureDone) return; _processFrame(image); }); } void _usePhoto() { if (_capturedPhoto != null) { Navigator.pop(context, { 'photo': File(_capturedPhoto!.path), }); } } @override void dispose() { _pulseController.dispose(); _controller?.dispose(); _faceDetector.close(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, extendBodyBehindAppBar: true, appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, leading: IconButton( icon: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), shape: BoxShape.circle, ), child: const Icon(Icons.close, color: Colors.white, size: 16), ), onPressed: () => Navigator.pop(context), ), ), body: Stack( children: [ // Preview kamera atau Foto yang diambil if (_isCaptureDone && _capturedPhoto != null) SizedBox.expand( child: Image.file( File(_capturedPhoto!.path), fit: BoxFit.cover, ), ) else if (_isCameraInitialized) SizedBox.expand( child: FittedBox( fit: BoxFit.cover, child: SizedBox( width: _controller!.value.previewSize?.height ?? MediaQuery.of(context).size.width, height: _controller!.value.previewSize?.width ?? MediaQuery.of(context).size.height, child: CameraPreview(_controller!), ), ), ) else const Center(child: CircularProgressIndicator(color: Colors.white)), // Scanner overlay ScannerOverlay( borderColor: _isCaptureDone ? AppTheme.statusGreen : _allChecksValid ? AppTheme.statusGreen : _isFaceDetected ? AppTheme.statusYellow : AppTheme.primaryBlue, isScanning: !_isCaptureDone, ), // Header info Positioned( top: MediaQuery.of(context).padding.top + kToolbarHeight + 20, left: 20, right: 20, child: Column( children: [ Text( _isCaptureDone ? 'Pemotretan Selesai' : 'Ambil Foto Wajah', style: AppTheme.heading3.copyWith(color: Colors.white), ), const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular(20), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 10), decoration: BoxDecoration( color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.white.withOpacity(0.3)), ), child: Text( _qualityMessage, textAlign: TextAlign.center, style: AppTheme.bodyLarge.copyWith( color: _isCaptureDone ? AppTheme.statusGreen : _allChecksValid ? AppTheme.statusGreen : Colors.white, fontWeight: FontWeight.bold, ), ), ), ), ), // Quality chips (sebelum rekam) if (!_isCaptureDone) ...[ const SizedBox(height: 10), Wrap( spacing: 6, runSpacing: 6, alignment: WrapAlignment.center, children: [ _buildQualityChip( icon: Icons.straighten, label: _isFaceDetected ? (_faceRatio < 0.25 ? 'Terlalu Jauh' : _faceRatio > 0.75 ? 'Terlalu Dekat' : 'Jarak ✓') : 'Jarak', isOk: _isDistanceOk, isDetected: _isFaceDetected, ), _buildQualityChip( icon: Icons.wb_sunny_outlined, label: 'Cahaya', isOk: _isBrightnessOk, isDetected: _isFaceDetected, ), _buildQualityChip( icon: Icons.blur_on, label: 'Bayangan', isOk: _isLightingUniform, isDetected: _isFaceDetected, ), _buildQualityChip( icon: Icons.face, label: 'Hadap Depan', isOk: _isPoseValid, isDetected: _isFaceDetected, ), ], ), ], ], ), ), // Tombol bawah Positioned( bottom: 40, left: 20, right: 20, child: _isCaptureDone ? Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Expanded( child: CustomButton( text: "Pindai Ulang", type: ButtonType.secondary, onPressed: _retake, icon: Icons.refresh, ), ), const SizedBox(width: 16), Expanded( child: CustomButton( text: "Gunakan", type: ButtonType.primary, onPressed: _usePhoto, icon: Icons.check, backgroundColor: AppTheme.statusGreen, ), ), ], ) : Center( child: GestureDetector( onTap: _allChecksValid ? _capturePhoto : null, child: Container( width: 76, height: 76, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: _allChecksValid ? Colors.white.withOpacity(0.8) : Colors.white.withOpacity(0.3), width: 6, ), color: _allChecksValid ? AppTheme.primaryBlue.withOpacity(0.8) : Colors.white.withOpacity(0.05), boxShadow: _allChecksValid ? [ BoxShadow( color: AppTheme.primaryBlue.withOpacity(0.4), blurRadius: 16, ) ] : null, ), child: const Icon( Icons.face_retouching_natural, color: Colors.white, size: 36, ), ), ), ), ), ], ), ); } Widget _buildQualityChip({ required IconData icon, required String label, required bool isOk, required bool isDetected, }) { final color = !isDetected ? Colors.white.withOpacity(0.4) : isOk ? AppTheme.statusGreen : AppTheme.statusYellow; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: color.withOpacity(0.15), borderRadius: BorderRadius.circular(20), border: Border.all(color: color.withOpacity(0.4)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( isOk && isDetected ? Icons.check_circle : icon, color: color, size: 14, ), const SizedBox(width: 4), Text( label, style: TextStyle( color: color, fontSize: 11, fontWeight: FontWeight.w600, ), ), ], ), ); } }