diff --git a/lib/scanner_screen.dart b/lib/scanner_screen.dart index ada97ec..e42e89c 100644 --- a/lib/scanner_screen.dart +++ b/lib/scanner_screen.dart @@ -30,7 +30,8 @@ class ScannerScreen extends StatefulWidget { State createState() => _ScannerScreenState(); } -class _ScannerScreenState extends State { +class _ScannerScreenState extends State + with WidgetsBindingObserver { static const _autoScanTickInterval = Duration(milliseconds: 700); static const _scanCooldownSuccess = Duration(milliseconds: 1500); static const _scanCooldownNoMatch = Duration(milliseconds: 2200); @@ -48,48 +49,103 @@ class _ScannerScreenState extends State { bool _autoScanEnabled = false; Timer? _autoScanTimer; DateTime _nextScanAllowedAt = DateTime.fromMillisecondsSinceEpoch(0); + bool _isInitializingCamera = false; + String? _cameraError; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); _activeCollectionId = widget.activeCollectionId; _autoScanEnabled = widget.onDetected != null; _textRecognizer = TextRecognizer(); _initCamera(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.inactive || + state == AppLifecycleState.paused || + state == AppLifecycleState.detached) { + _disposeCamera(); + return; + } + + if (state == AppLifecycleState.resumed) { + _initCamera(force: true); + } + } + void _handleCollectionChanged(String? value) { if (value == null) return; setState(() => _activeCollectionId = value); widget.onCollectionChanged?.call(value); } - Future _initCamera() async { - final cameras = await availableCameras(); - if (cameras.isEmpty) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No camera available')), + Future _initCamera({bool force = false}) async { + if (_isInitializingCamera) return; + if (_cameraReady && !force) return; + + _isInitializingCamera = true; + try { + await _disposeCamera(); + + final cameras = await availableCameras(); + if (cameras.isEmpty) { + throw Exception('No camera available'); + } + + final backCamera = cameras.firstWhere( + (c) => c.lensDirection == CameraLensDirection.back, + orElse: () => cameras.first, ); - return; + + final controller = CameraController( + backCamera, + ResolutionPreset.medium, + enableAudio: false, + ); + + await controller.initialize(); + if (!mounted) { + await controller.dispose(); + return; + } + + _cameraController = controller; + setState(() { + _cameraReady = true; + _cameraError = null; + _statusText = 'Ready to scan'; + }); + _startAutoScanLoop(); + } catch (e) { + if (!mounted) return; + setState(() { + _cameraReady = false; + _cameraError = 'Camera unavailable. Please retry.'; + _statusText = 'Camera unavailable'; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Camera init failed: $e'), + backgroundColor: Colors.red, + ), + ); + } finally { + _isInitializingCamera = false; } + } - // Use the first back-facing camera. - final backCamera = cameras.firstWhere( - (c) => c.lensDirection == CameraLensDirection.back, - orElse: () => cameras.first, - ); - - _cameraController = CameraController( - backCamera, - ResolutionPreset.medium, - enableAudio: false, - ); - - await _cameraController!.initialize(); - if (!mounted) return; - setState(() => _cameraReady = true); - _startAutoScanLoop(); + Future _disposeCamera() async { + _autoScanTimer?.cancel(); + _autoScanTimer = null; + final controller = _cameraController; + _cameraController = null; + _cameraReady = false; + if (controller != null) { + await controller.dispose(); + } } void _startAutoScanLoop() { @@ -210,8 +266,13 @@ class _ScannerScreenState extends State { @override void dispose() { + WidgetsBinding.instance.removeObserver(this); _autoScanTimer?.cancel(); - _cameraController?.dispose(); + final controller = _cameraController; + _cameraController = null; + if (controller != null) { + controller.dispose(); + } _textRecognizer.close(); super.dispose(); } @@ -302,7 +363,32 @@ class _ScannerScreenState extends State { ), // ── Camera preview ── Expanded( - child: _cameraReady + child: _cameraError != null + ? Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.camera_alt_outlined, + size: 46, color: AppColors.error), + const SizedBox(height: 12), + Text( + _cameraError!, + textAlign: TextAlign.center, + style: const TextStyle(color: AppColors.textSecondary), + ), + const SizedBox(height: 12), + OutlinedButton.icon( + onPressed: () => _initCamera(force: true), + icon: const Icon(Icons.refresh), + label: const Text('Retry Camera'), + ), + ], + ), + ), + ) + : _cameraReady ? Stack( fit: StackFit.expand, children: [ @@ -349,7 +435,7 @@ class _ScannerScreenState extends State { ), ], ) - : const Center(child: CircularProgressIndicator()), + : const Center(child: CircularProgressIndicator()), ), // ── Detected ID confirmation area ── @@ -440,7 +526,7 @@ class _ScannerScreenState extends State { _isBusy ? 'Scanning…' : 'Capture & Scan', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600), ), - onPressed: _isBusy ? null : _captureAndScan, + onPressed: _isBusy || !_cameraReady ? null : _captureAndScan, style: ElevatedButton.styleFrom( backgroundColor: Colors.transparent, shadowColor: Colors.transparent,