parser logic added
This commit is contained in:
+47
-22
@@ -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,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user