feat(scanner): add auto-scan cadence with pause/resume controls

- Add periodic auto-capture loop for uninterrupted scanner workflow
- Start auto-scan after camera init and stop cleanly on dispose
- Gate scans by busy/camera-ready state to avoid overlapping captures
- Add AUTO ON/OFF toggle in scanner status bar
- Keep manual capture path available as fallback
This commit is contained in:
Lukas Müllner 2026-03-04 11:15:43 +01:00
parent 4ba1fe19cc
commit 13114bd63f

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -37,6 +38,8 @@ class _ScannerScreenState extends State<ScannerScreen> {
bool _scanAccepted = false; bool _scanAccepted = false;
String _statusText = 'Ready to scan'; String _statusText = 'Ready to scan';
String? _activeCollectionId; String? _activeCollectionId;
bool _autoScanEnabled = false;
Timer? _autoScanTimer;
// Matches typical Hot Wheels model IDs: 25 uppercase letters followed by // Matches typical Hot Wheels model IDs: 25 uppercase letters followed by
// 24 digits, e.g. JKF21, HCV73, GRX33, FYD83. // 24 digits, e.g. JKF21, HCV73, GRX33, FYD83.
@ -46,6 +49,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
void initState() { void initState() {
super.initState(); super.initState();
_activeCollectionId = widget.activeCollectionId; _activeCollectionId = widget.activeCollectionId;
_autoScanEnabled = widget.onDetected != null;
_textRecognizer = TextRecognizer(); _textRecognizer = TextRecognizer();
_initCamera(); _initCamera();
} }
@ -81,6 +85,22 @@ class _ScannerScreenState extends State<ScannerScreen> {
await _cameraController!.initialize(); await _cameraController!.initialize();
if (!mounted) return; if (!mounted) return;
setState(() => _cameraReady = true); setState(() => _cameraReady = true);
_startAutoScanLoop();
}
void _startAutoScanLoop() {
_autoScanTimer?.cancel();
_autoScanTimer = Timer.periodic(const Duration(milliseconds: 1400), (_) {
if (!_autoScanEnabled || _isBusy || !_cameraReady || !mounted) return;
_captureAndScan();
});
}
void _toggleAutoScan() {
setState(() {
_autoScanEnabled = !_autoScanEnabled;
_statusText = _autoScanEnabled ? 'Auto scan enabled' : 'Auto scan paused';
});
} }
/// Capture a photo, run OCR, and look for a Hot Wheels ID. /// Capture a photo, run OCR, and look for a Hot Wheels ID.
@ -187,6 +207,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
@override @override
void dispose() { void dispose() {
_autoScanTimer?.cancel();
_cameraController?.dispose(); _cameraController?.dispose();
_textRecognizer.close(); _textRecognizer.close();
super.dispose(); super.dispose();
@ -243,14 +264,31 @@ class _ScannerScreenState extends State<ScannerScreen> {
color: _scanAccepted color: _scanAccepted
? AppColors.success.withValues(alpha: 0.12) ? AppColors.success.withValues(alpha: 0.12)
: Colors.black.withValues(alpha: 0.55), : Colors.black.withValues(alpha: 0.55),
child: Text( child: Row(
_statusText, children: [
textAlign: TextAlign.center, Expanded(
style: TextStyle( child: Text(
color: _scanAccepted ? AppColors.success : Colors.white, _statusText,
fontSize: 12, textAlign: TextAlign.center,
fontWeight: FontWeight.w600, style: TextStyle(
), color: _scanAccepted ? AppColors.success : Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
if (widget.onDetected != null)
TextButton(
onPressed: _isBusy ? null : _toggleAutoScan,
style: TextButton.styleFrom(
foregroundColor:
_autoScanEnabled ? AppColors.success : Colors.white,
minimumSize: const Size(0, 26),
padding: const EdgeInsets.symmetric(horizontal: 10),
),
child: Text(_autoScanEnabled ? 'AUTO ON' : 'AUTO OFF'),
),
],
), ),
), ),
// Camera preview // Camera preview