import 'dart:async'; import 'dart:io'; import 'dart:math' as math; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import '../../core/error/exceptions.dart'; import 'package:local_auth/local_auth.dart'; class OvalCameraCapturePage extends StatefulWidget { final bool isLogin; final Future Function(File image, {required bool localAuth}) onCapture; const OvalCameraCapturePage({ super.key, this.isLogin = true, required this.onCapture, }); @override State createState() => _OvalCameraCapturePageState(); } class _OvalCameraCapturePageState extends State { CameraController? _cameraController; bool _isCameraInitialized = false; String? _errorMessage; bool _isSuccess = false; bool _isSubmitting = false; // Timer countdown int _countdown = 3; Timer? _countdownTimer; bool _countdownStarted = false; // Local auth for 422 final LocalAuthentication _localAuth = LocalAuthentication(); bool _handlingAuth422 = false; File? _lastCapturedFile; @override void initState() { super.initState(); _initializeCamera(); } @override void dispose() { _countdownTimer?.cancel(); _cameraController?.dispose(); super.dispose(); } Future _initializeCamera() async { try { setState(() { _errorMessage = null; _isCameraInitialized = false; _isSuccess = false; _isSubmitting = false; _countdown = 3; _countdownStarted = false; }); await _cameraController?.dispose(); _cameraController = null; final cameras = await availableCameras(); if (cameras.isEmpty) { if (!mounted) return; setState(() { _errorMessage = "لا توجد كاميرات متاحة على هذا الجهاز"; _isCameraInitialized = false; }); return; } final front = cameras.where( (c) => c.lensDirection == CameraLensDirection.front, ); final selectedCamera = front.isNotEmpty ? front.first : cameras.first; _cameraController = CameraController( selectedCamera, ResolutionPreset.medium, enableAudio: false, ); await _cameraController!.initialize(); if (!mounted) return; setState(() { _isCameraInitialized = true; }); _startCountdown(); } catch (e) { if (!mounted) return; setState(() { _errorMessage = "خطأ في تهيئة الكاميرا: $e"; _isCameraInitialized = false; }); } } void _startCountdown() { if (_countdownStarted) return; _countdownStarted = true; _countdown = 3; _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (!mounted) { timer.cancel(); return; } setState(() { _countdown--; }); if (_countdown <= 0) { timer.cancel(); _captureAndSubmit(); } }); } Future _showLocalAuthDialog() { return showDialog( context: context, barrierDismissible: false, builder: (_) => AlertDialog( title: const Text('فشل التحقق بالوجه', textAlign: TextAlign.center), content: const Text( 'لم يتم التعرف على الوجه.\n\nيرجى استخدام بصمة الإصبع أو رمز القفل (PIN/النمط) للمتابعة.', textAlign: TextAlign.center, ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('إلغاء'), ), ElevatedButton( onPressed: () => Navigator.pop(context, true), child: const Text('استخدام البصمة / رمز القفل'), ), ], ), ); } Future _authenticateLocally() async { try { final isSupported = await _localAuth.isDeviceSupported(); if (!isSupported) return false; return await _localAuth.authenticate( localizedReason: 'تأكيد هويتك لإكمال تسجيل الحضور.', options: const AuthenticationOptions( biometricOnly: false, stickyAuth: true, useErrorDialogs: true, ), ); } catch (_) { return false; } } void _stopCameraCompletely() { _countdownTimer?.cancel(); try { _cameraController?.dispose(); _cameraController = null; } catch (e) { debugPrint("Error stopping camera: $e"); } } Future _captureAndSubmit() async { if (_cameraController == null) return; if (_isSubmitting || _isSuccess) return; setState(() { _isSubmitting = true; _errorMessage = null; }); try { if (_cameraController == null || !_cameraController!.value.isInitialized) { _handleScanError("الكاميرا غير جاهزة، حاول مرة أخرى"); return; } final xFile = await _cameraController!.takePicture(); final file = File(xFile.path); _lastCapturedFile = file; await widget.onCapture(file, localAuth: false); if (mounted) { setState(() { _isSuccess = true; _isSubmitting = false; }); Future.delayed(const Duration(seconds: 1), () { if (mounted) { Navigator.of(context).pop(true); } }); } } on ServerException catch (e) { final msg = e.message.toLowerCase(); if (e.statusCode == 422 || msg.contains('face verification failed')) { await _handleFaceVerificationFailed422(e); return; } if (msg.contains('already logged in') || msg.contains('مسجل دخول بالفعل')) { _stopCameraCompletely(); if (mounted) { showDialog( context: context, barrierDismissible: false, builder: (_) => AlertDialog( title: const Text('تنبيه', textAlign: TextAlign.center), content: const Text( 'أنت مسجل دخول بالفعل، لا يمكنك تسجيل الدخول مرة أخرى.', textAlign: TextAlign.center, ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); Navigator.of(context).pop(); }, child: const Text('حسناً'), ), ], ), ); } return; } _handleScanError(e.message); } on NetworkException catch (e) { _handleScanError(e.message); } on CameraException catch (e) { _handleScanError("فشل التقاط الصورة: ${e.description ?? e.code}"); } catch (e) { _handleScanError("حدث خطأ غير متوقع: $e"); } } void _handleScanError(String msg) { if (!mounted) return; setState(() { _isSubmitting = false; _errorMessage = msg; _countdown = 3; _countdownStarted = false; }); // Restart countdown after error _startCountdown(); } Future _handleFaceVerificationFailed422(ServerException e) async { if (!mounted) return; if (_handlingAuth422) return; _handlingAuth422 = true; setState(() { _isSubmitting = false; _errorMessage = null; }); final proceed = await _showLocalAuthDialog(); if (proceed != true) { _handlingAuth422 = false; _stopCameraCompletely(); if (!mounted) return; Navigator.of(context).pop(false); return; } final ok = await _authenticateLocally(); if (!ok) { _handlingAuth422 = false; _stopCameraCompletely(); if (!mounted) return; Navigator.of(context).pop("local_auth_failed"); return; } final file = _lastCapturedFile; if (file == null) { _handlingAuth422 = false; _stopCameraCompletely(); if (!mounted) return; Navigator.of(context).pop("retry_missing_file"); return; } setState(() { _isSubmitting = true; }); try { await widget.onCapture(file, localAuth: true); if (!mounted) return; setState(() { _isSuccess = true; _isSubmitting = false; }); Future.delayed(const Duration(seconds: 1), () { if (mounted) Navigator.of(context).pop(true); }); } on ServerException catch (_) { _handlingAuth422 = false; _stopCameraCompletely(); if (!mounted) return; Navigator.of(context).pop("retry_failed"); } catch (_) { _handlingAuth422 = false; _stopCameraCompletely(); if (!mounted) return; Navigator.of(context).pop("retry_failed"); } } @override Widget build(BuildContext context) { if (_errorMessage != null && !_isCameraInitialized) { return Scaffold( backgroundColor: Colors.black, body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error, color: Colors.red, size: 48), const SizedBox(height: 16), Text(_errorMessage!, style: const TextStyle(color: Colors.white)), const SizedBox(height: 16), ElevatedButton( onPressed: _initializeCamera, child: const Text("إعادة المحاولة"), ), ], ), ), ); } if (!_isCameraInitialized || _cameraController == null) { return const Scaffold( backgroundColor: Colors.black, body: Center(child: CircularProgressIndicator(color: Colors.red)), ); } return Scaffold( backgroundColor: Colors.black, body: Stack( fit: StackFit.expand, children: [ Center(child: CameraPreview(_cameraController!)), CustomPaint( painter: _OvalOverlayPainter( borderColor: _isSuccess ? Colors.greenAccent : (_countdown <= 1 ? Colors.orangeAccent : Colors.white70), progress: _countdownStarted ? ((3 - _countdown) / 3).clamp(0.0, 1.0) : 0, ), ), Positioned( top: 60, left: 0, right: 0, child: Column( children: [ Text( widget.isLogin ? "تسجيل الدخول" : "تسجيل خروج", style: const TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, fontFamily: 'Cairo', ), ), const SizedBox(height: 10), Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(20), ), child: Text( _isSubmitting ? "جاري التحقق..." : _isSuccess ? "تم بنجاح" : "التقاط الصورة خلال $_countdown ثانية", style: TextStyle( color: _isSuccess ? Colors.greenAccent : Colors.white, fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ], ), ), // Countdown number in center if (!_isSubmitting && !_isSuccess && _countdown > 0) Center( child: Text( '$_countdown', style: TextStyle( color: Colors.white.withOpacity(0.7), fontSize: 80, fontWeight: FontWeight.bold, ), ), ), if (_isSubmitting) const Center(child: CircularProgressIndicator(color: Colors.white)), if (_isSuccess) const Center( child: Icon(Icons.check_circle, color: Colors.green, size: 80), ), if (_errorMessage != null && _isCameraInitialized) Positioned( bottom: 20, left: 0, right: 0, child: Container( color: Colors.black54, padding: const EdgeInsets.all(8), child: Text( _errorMessage!, style: const TextStyle(color: Colors.red), textAlign: TextAlign.center, ), ), ), ], ), ); } } class _OvalOverlayPainter extends CustomPainter { final Color borderColor; final double progress; _OvalOverlayPainter({required this.borderColor, required this.progress}); @override void paint(Canvas canvas, Size size) { final width = size.width * 0.75; final height = size.height * 0.55; final center = Offset(size.width / 2, size.height / 2); final ovalRect = Rect.fromCenter( center: center, width: width, height: height, ); final screenPath = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height)); final ovalPath = Path()..addOval(ovalRect); final overlayPath = Path.combine( PathOperation.difference, screenPath, ovalPath, ); final bgPaint = Paint() ..color = Colors.black.withOpacity(0.6) ..style = PaintingStyle.fill; canvas.drawPath(overlayPath, bgPaint); final borderPaint = Paint() ..color = borderColor ..style = PaintingStyle.stroke ..strokeWidth = 4.0; canvas.drawOval(ovalRect, borderPaint); if (progress > 0) { final progressPaint = Paint() ..color = Colors.greenAccent ..style = PaintingStyle.stroke ..strokeWidth = 6.0 ..strokeCap = StrokeCap.round; final startAngle = -math.pi / 2; final sweepAngle = 2 * math.pi * progress; canvas.drawArc( ovalRect.inflate(10), startAngle, sweepAngle, false, progressPaint, ); } } @override bool shouldRepaint(covariant _OvalOverlayPainter oldDelegate) { return oldDelegate.borderColor != borderColor || oldDelegate.progress != progress; } }