273 lines
7.7 KiB
Dart
273 lines
7.7 KiB
Dart
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<CameraDataSource>((ref) {
|
|
return CameraDataSourceImpl();
|
|
});
|
|
|
|
final scannerRepositoryProvider = Provider<ScannerRepositoryImpl>((ref) {
|
|
return ScannerRepositoryImpl(ref.read(cameraDataSourceProvider));
|
|
});
|
|
|
|
final scanFingerprintUseCaseProvider = Provider<ScanFingerprint>((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<ScannerState> {
|
|
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<void> initialize() async {
|
|
// Default to initializing camera
|
|
await tryCamera();
|
|
}
|
|
|
|
Future<void> 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<void> 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<void> 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, ScannerState>(ScannerNotifier.new); |