import 'dart:isolate'; import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/services/image_processor_isolate.dart'; import '../../core/services/image_analysis_service.dart'; import '../../data/datasources/camera_datasource.dart'; import '../../data/repositories/scanner_repository_impl.dart'; import '../../domain/entities/scan_result.dart'; import '../../domain/entities/quality_status.dart'; import '../../domain/entities/processing_request.dart'; import '../../domain/usecases/scan_fingerprint.dart'; // --- Dependency Injection --- final cameraDataSourceProvider = Provider((ref) { return CameraDataSourceImpl(); }); final scannerRepositoryProvider = Provider((ref) { return ScannerRepositoryImpl(ref.read(cameraDataSourceProvider)); }); final scanFingerprintUseCaseProvider = Provider((ref) { return ScanFingerprint(ref.read(scannerRepositoryProvider)); }); // --- State --- abstract class ScannerState {} class ScannerInitial extends ScannerState {} class ScannerInitializing extends ScannerState {} class ScannerReady extends ScannerState { final CameraController? controller; final QualityStatus? qualityStatus; final int consecutivePasses; final bool isSimulation; bool get canCapture => consecutivePasses >= 5; ScannerReady(this.controller, { this.qualityStatus, this.consecutivePasses = 0, this.isSimulation = false }); } class ScannerScanning extends ScannerState { final CameraController? controller; ScannerScanning(this.controller); } class ScannerSuccess extends ScannerState { final ScanResult result; final CameraController? controller; ScannerSuccess(this.result, {this.controller}); } class ScannerError extends ScannerState { final String message; final CameraController? controller; ScannerError(this.message, {this.controller}); } // --- Notifier --- class ScannerNotifier extends Notifier { late final ScannerRepositoryImpl repository; late final ScanFingerprint scanUseCase; Isolate? _isolate; SendPort? _isolateSendPort; ReceivePort? _receivePort; CameraController? _activeController; // Track controller for safe disposal DateTime _lastFrameTime = DateTime.fromMillisecondsSinceEpoch(0); static const _throttleDuration = Duration(milliseconds: 150); bool _isMounted = true; @override ScannerState build() { repository = ref.read(scannerRepositoryProvider); scanUseCase = ref.read(scanFingerprintUseCaseProvider); _isMounted = true; ref.onDispose(() { _isMounted = false; _stopStreamAndDispose(); }); return ScannerInitial(); } void _stopStreamAndDispose() { if (_activeController != null) { repository.stopImageStream(_activeController!).catchError((_) {}); _activeController!.dispose(); _activeController = null; } _receivePort?.close(); _isolate?.kill(); _isolate = null; _receivePort = null; } Future initialize() async { // Default to initializing camera await tryCamera(); } Future tryCamera() async { state = ScannerInitializing(); // Dispose previous controller if exists if (_activeController != null) { await repository.stopImageStream(_activeController!).catchError((_) {}); await _activeController!.dispose(); _activeController = null; } try { final controller = await repository.initializeCamera(); _activeController = controller; if (!kIsWeb) { _receivePort = ReceivePort(); _isolate = await Isolate.spawn(ImageProcessorIsolate.spawn, _receivePort!.sendPort); _receivePort!.listen(_handleIsolateMessage); } await repository.startImageStream(controller, (image) => _onFrame(image, controller)); state = ScannerReady(controller, isSimulation: false); } catch (e) { debugPrint("Camera failed: $e"); state = ScannerError("Camera Access Denied or Unavailable. If you are on a mobile browser, ensure you are using HTTPS or have enabled the necessary flags.\n\nError: $e"); } } void startSimulation() { state = ScannerReady(null, isSimulation: true); if (kIsWeb) _startWebSimulation(); } void _startWebSimulation() async { while (_isMounted) { final currentState = state; if (currentState is! ScannerReady || !currentState.isSimulation) break; await Future.delayed(const Duration(milliseconds: 200)); if (!_isMounted) return; final result = ImageAnalysisService.analyze( bytes: Uint8List(0), width: 100, height: 100, ); _updateQuality(result); } } void _handleIsolateMessage(dynamic message) { if (message is SendPort) { _isolateSendPort = message; } else if (message is QualityStatus) { _updateQuality(message); } } void _updateQuality(QualityStatus status) { if (!_isMounted) return; final currentState = state; if (currentState is ScannerReady) { int passes = currentState.consecutivePasses; if (status.canCapture) { passes++; } else { passes = 0; } state = ScannerReady( currentState.controller, qualityStatus: status, consecutivePasses: passes, isSimulation: currentState.isSimulation ); } } void _onFrame(CameraImage image, CameraController controller) { if (!_isMounted) return; final now = DateTime.now(); if (now.difference(_lastFrameTime) < _throttleDuration) { return; } _lastFrameTime = now; if (!controller.value.isInitialized) return; final bytes = image.planes[0].bytes; if (kIsWeb) { final result = ImageAnalysisService.analyze( bytes: bytes, width: image.width, height: image.height, ); _updateQuality(result); } else { if (_isolateSendPort == null) return; _isolateSendPort!.send(ProcessingRequest( bytes: bytes, width: image.width, height: image.height, sendPort: _receivePort!.sendPort, )); } } Future scan() async { final currentState = state; if (currentState is ScannerReady) { if (!currentState.canCapture) return; state = ScannerScanning(currentState.controller); try { String imagePath; if (currentState.controller != null) { await repository.stopImageStream(currentState.controller!); final result = await scanUseCase(currentState.controller!); imagePath = result.imagePath; } else { await Future.delayed(const Duration(seconds: 1)); imagePath = "simulated_capture.jpg"; } if (_isMounted) { state = ScannerSuccess( ScanResult(imagePath: imagePath, timestamp: DateTime.now()), controller: currentState.controller ); } } catch (e) { if (_isMounted) { state = ScannerError(e.toString(), controller: currentState.controller); } } } } Future reset() async { final currentState = state; if (currentState is ScannerSuccess) { final controller = currentState.controller; if (controller != null) { try { await repository.startImageStream(controller, (image) => _onFrame(image, controller)); state = ScannerReady(controller, isSimulation: false); } catch(e) { tryCamera(); } } else { startSimulation(); } } else { tryCamera(); } } } final scannerProvider = NotifierProvider(ScannerNotifier.new);