/// Single screen: capture or pick a receipt photo, then show raw OCR text. library; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; import 'package:image_picker/image_picker.dart'; import 'products_screen.dart'; import 'review_screen.dart'; import 'services/product_repository.dart'; import 'services/receipt_parser.dart'; import 'utils/constants.dart'; class ScanScreen extends StatefulWidget { const ScanScreen({super.key, required this.repository}); final ProductRepository repository; @override State createState() => _ScanScreenState(); } class _ScanScreenState extends State { final ImagePicker _picker = ImagePicker(); String _extractedText = ''; bool _busy = false; String? _error; @override void initState() { super.initState(); _recoverLostImage(); } /// Android may kill the activity while the camera app is open. Future _recoverLostImage() async { try { final response = await _picker.retrieveLostData(); if (!mounted || response.isEmpty) return; final file = response.file; if (file != null) { await _runOcr(file.path); } } on UnimplementedError { // Desktop / platforms that do not support lost-data recovery. } } Future _capture(ImageSource source) async { final file = await _picker.pickImage( source: source, imageQuality: 95, ); if (file == null || !mounted) return; await _runOcr(file.path); } Future _scanSampleReceipt() async { try { final data = await rootBundle.load(kSampleReceiptAsset); final file = File( '${Directory.systemTemp.path}/receipity_sample_receipt.jpeg', ); await file.writeAsBytes(data.buffer.asUint8List(), flush: true); if (!mounted) return; await _runOcr(file.path); } catch (_) { if (!mounted) return; setState(() { _error = 'Could not load the sample receipt.'; }); } } Future _runOcr(String imagePath) async { setState(() { _busy = true; _error = null; _extractedText = ''; }); final recognizer = TextRecognizer(script: TextRecognitionScript.latin); try { final inputImage = InputImage.fromFilePath(imagePath); final recognized = await recognizer.processImage(inputImage); if (!mounted) return; setState(() { _extractedText = recognized.text.trim(); if (_extractedText.isEmpty) { _error = 'No text found in this image.'; } }); } catch (error) { if (!mounted) return; setState(() { _error = 'Could not read text from this image.'; }); } finally { await recognizer.close(); if (mounted) { setState(() => _busy = false); } } if (mounted && _extractedText.isNotEmpty) { await _openReview(_extractedText); } } Future _openReview(String rawText) async { final items = ReceiptParser.parse(rawText); if (!mounted) return; await Navigator.of(context).push( MaterialPageRoute( builder: (_) => ReviewScreen( items: items, rawText: rawText, repository: widget.repository, ), ), ); } void _openProducts() { Navigator.of(context).push( MaterialPageRoute( builder: (_) => ProductsScreen(repository: widget.repository), ), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: const Text(kAppName), actions: [ IconButton( tooltip: 'Products', onPressed: _openProducts, icon: const Icon(Icons.inventory_2_outlined), ), ], ), body: SafeArea( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ FilledButton.icon( onPressed: _busy ? null : () => _capture(ImageSource.camera), icon: const Icon(Icons.photo_camera_outlined), label: const Text('Take photo'), ), const SizedBox(height: 8), OutlinedButton.icon( onPressed: _busy ? null : () => _capture(ImageSource.gallery), icon: const Icon(Icons.photo_library_outlined), label: const Text('Pick from gallery'), ), const SizedBox(height: 8), TextButton.icon( onPressed: _busy ? null : _scanSampleReceipt, icon: const Icon(Icons.receipt_long_outlined), label: const Text('Use sample receipt'), ), if (_extractedText.isNotEmpty) ...[ const SizedBox(height: 8), OutlinedButton( onPressed: _busy ? null : () => _openReview(_extractedText), child: const Text('Review items'), ), ], if (_busy) ...[ const SizedBox(height: 16), const LinearProgressIndicator(), const SizedBox(height: 8), Text( 'Reading text…', style: theme.textTheme.bodyMedium, ), ], const SizedBox(height: 16), Expanded(child: _ResultPane(text: _extractedText, error: _error)), ], ), ), ), ); } } class _ResultPane extends StatelessWidget { const _ResultPane({required this.text, required this.error}); final String text; final String? error; @override Widget build(BuildContext context) { final theme = Theme.of(context); if (error != null && text.isEmpty) { return Center( child: Text( error!, textAlign: TextAlign.center, style: theme.textTheme.bodyLarge, ), ); } if (text.isEmpty) { return Center( child: Text( 'Take a photo of a shopping receipt, or pick one from the gallery.\n\nExtracted text will show up here.', textAlign: TextAlign.center, style: theme.textTheme.bodyLarge, ), ); } return SingleChildScrollView( child: SelectableText( text, style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface, height: 1.4, ), ), ); } }