parser logic added

This commit is contained in:
davmar
2026-08-30 12:24:29 +02:00
parent 35d5c90d81
commit 3b698a3667
28 changed files with 3018 additions and 346 deletions
+47 -22
View File
@@ -8,6 +8,8 @@ import 'package:flutter/services.dart';
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
import 'package:image_picker/image_picker.dart';
import 'crop_receipt_screen.dart';
import 'models/line_item.dart';
import 'review_screen.dart';
import 'services/app_settings.dart';
import 'services/product_repository.dart';
@@ -17,11 +19,7 @@ import 'utils/supermarkets.dart';
import 'widgets/settings_button.dart';
class ScanScreen extends StatefulWidget {
const ScanScreen({
super.key,
required this.repository,
this.onTripSaved,
});
const ScanScreen({super.key, required this.repository, this.onTripSaved});
final ProductRepository repository;
final VoidCallback? onTripSaved;
@@ -51,7 +49,7 @@ class _ScanScreenState extends State<ScanScreen> {
if (!mounted || response.isEmpty) return;
final file = response.file;
if (file != null) {
await _runOcr(file.path);
await _cropThenOcr(file.path);
}
} on UnimplementedError {
// Desktop / platforms that do not support lost-data recovery.
@@ -59,12 +57,9 @@ class _ScanScreenState extends State<ScanScreen> {
}
Future<void> _capture(ImageSource source) async {
final file = await _picker.pickImage(
source: source,
imageQuality: 95,
);
final file = await _picker.pickImage(source: source, imageQuality: 95);
if (file == null || !mounted) return;
await _runOcr(file.path);
await _cropThenOcr(file.path);
}
Future<void> _scanSampleReceipt() async {
@@ -75,7 +70,7 @@ class _ScanScreenState extends State<ScanScreen> {
);
await file.writeAsBytes(data.buffer.asUint8List(), flush: true);
if (!mounted) return;
await _runOcr(file.path);
await _cropThenOcr(file.path);
} catch (_) {
if (!mounted) return;
setState(() {
@@ -84,6 +79,17 @@ class _ScanScreenState extends State<ScanScreen> {
}
}
Future<void> _cropThenOcr(String imagePath) async {
final croppedPath = await Navigator.of(context).push<String>(
MaterialPageRoute(
fullscreenDialog: true,
builder: (_) => CropReceiptScreen(imagePath: imagePath),
),
);
if (croppedPath == null || !mounted) return;
await _runOcr(croppedPath);
}
Future<void> _runOcr(String imagePath) async {
setState(() {
_busy = true;
@@ -127,14 +133,26 @@ class _ScanScreenState extends State<ScanScreen> {
}
}
Future<void> _enterManually() async {
await _openReview('', items: const []);
}
String _ocrFailureMessage(String? detail) {
const fallback = 'Could not read text from this image.';
if (detail == null || detail.trim().isEmpty) return fallback;
return '$fallback\n$detail';
}
Future<void> _openReview(String rawText) async {
final items = ReceiptParser.parse(rawText);
Future<void> _openReview(
String rawText, {
List<LineItem>? items,
String? imagePath,
}) async {
final parsedItems = items;
ReceiptParseResult? parsed;
if (parsedItems == null) {
parsed = ReceiptParser.parseReceipt(rawText);
}
if (!mounted) return;
final names =
SettingsScope.maybeOf(context)?.supermarkets.map((s) => s.name) ??
@@ -142,11 +160,13 @@ class _ScanScreenState extends State<ScanScreen> {
final saved = await Navigator.of(context).push<bool>(
MaterialPageRoute(
builder: (_) => ReviewScreen(
items: items,
items: parsedItems ?? parsed!.items,
rawText: rawText,
repository: widget.repository,
imagePath: _imagePath,
imagePath: imagePath ?? (items == null ? _imagePath : null),
supermarketNames: names.toList(),
needsReview: parsed != null && !parsed.validationPassed,
detectedStore: parsed?.store,
),
),
);
@@ -182,6 +202,12 @@ class _ScanScreenState extends State<ScanScreen> {
label: const Text('Pick from gallery'),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: _busy ? null : _enterManually,
icon: const Icon(Icons.edit_note_outlined),
label: const Text('Enter manually'),
),
const SizedBox(height: 8),
TextButton.icon(
onPressed: _busy ? null : _scanSampleReceipt,
icon: const Icon(Icons.receipt_long_outlined),
@@ -198,13 +224,12 @@ class _ScanScreenState extends State<ScanScreen> {
const SizedBox(height: 16),
const LinearProgressIndicator(),
const SizedBox(height: 8),
Text(
'Reading text…',
style: theme.textTheme.bodyMedium,
),
Text('Reading text…', style: theme.textTheme.bodyMedium),
],
const SizedBox(height: 16),
Expanded(child: _ResultPane(text: _extractedText, error: _error)),
Expanded(
child: _ResultPane(text: _extractedText, error: _error),
),
],
),
),
@@ -236,7 +261,7 @@ class _ResultPane extends StatelessWidget {
if (text.isEmpty) {
return Center(
child: Text(
'Take a photo of a shopping receipt, or pick one from the gallery.\n\nConfirmed trips show up in Shop log.',
'Scan a receipt or enter a trip by hand.\n\nConfirmed trips show up in Shop log.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge,
),