improve parsing

This commit is contained in:
davmar
2026-09-02 12:35:35 +02:00
parent d532af6111
commit e98fb0e7f4
9 changed files with 294 additions and 680 deletions
+72 -18
View File
@@ -13,6 +13,7 @@ import 'utils/money.dart';
import 'utils/supermarkets.dart';
import 'widgets/discounted_price.dart';
import 'widgets/item_edit_sheet.dart';
import 'widgets/receipt_highlight_overlay.dart';
import 'widgets/store_tag.dart';
import 'widgets/tag_editor.dart';
@@ -26,6 +27,8 @@ class ReviewScreen extends StatefulWidget {
this.supermarketNames = kSupermarkets,
this.needsReview = false,
this.detectedStore,
this.ocrImageWidth,
this.ocrImageHeight,
});
final List<LineItem> items;
@@ -35,6 +38,8 @@ class ReviewScreen extends StatefulWidget {
final List<String> supermarketNames;
final bool needsReview;
final String? detectedStore;
final double? ocrImageWidth;
final double? ocrImageHeight;
@override
State<ReviewScreen> createState() => _ReviewScreenState();
@@ -49,6 +54,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
final _tripName = TextEditingController();
List<String> _tags = [];
List<String> _tagSuggestions = [];
String? _highlightedItemId;
@override
void initState() {
@@ -88,6 +94,11 @@ class _ReviewScreenState extends State<ReviewScreen> {
return _supermarket;
}
bool get _hasReceipt {
final path = widget.imagePath;
return path != null && path.isNotEmpty && File(path).existsSync();
}
Future<void> _edit({LineItem? existing}) async {
final result = await showItemEditSheet(context, item: existing);
if (result == null || !mounted) return;
@@ -98,9 +109,20 @@ class _ReviewScreenState extends State<ReviewScreen> {
} else {
final index = _items.indexWhere((item) => item.id == existing.id);
if (index >= 0) {
_items[index] = result;
_items[index] = result.copyWith(
sourceBounds: existing.sourceBounds,
needsManualReview: false,
);
}
}
_highlightedItemId = null;
});
}
void _toggleHighlight(LineItem item) {
if (item.sourceBounds == null || !_hasReceipt) return;
setState(() {
_highlightedItemId = _highlightedItemId == item.id ? null : item.id;
});
}
@@ -181,9 +203,12 @@ class _ReviewScreenState extends State<ReviewScreen> {
names.add(_supermarket!);
}
final receiptPath = widget.imagePath;
final hasReceipt = receiptPath != null &&
receiptPath.isNotEmpty &&
File(receiptPath).existsSync();
final highlighted = _highlightedItemId == null
? null
: _items.cast<LineItem?>().firstWhere(
(item) => item?.id == _highlightedItemId,
orElse: () => null,
);
return Scaffold(
appBar: AppBar(
@@ -207,7 +232,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
'Parsed totals do not match the receipt. Check the items before saving.',
'Item count or totals do not match the receipt, or some lines need manual review. Check highlighted items before saving.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onErrorContainer,
),
@@ -325,6 +350,29 @@ class _ReviewScreenState extends State<ReviewScreen> {
});
},
child: ListTile(
selected: _highlightedItemId == item.id,
selectedTileColor: theme.colorScheme.primaryContainer
.withValues(alpha: 0.35),
leading: item.sourceBounds != null && _hasReceipt
? IconButton(
icon: Icon(
_highlightedItemId == item.id
? Icons.visibility
: Icons.visibility_outlined,
size: 20,
),
tooltip: 'Show on receipt',
onPressed: _saving
? null
: () => _toggleHighlight(item),
)
: item.needsManualReview
? Icon(
Icons.flag_outlined,
color: theme.colorScheme.error,
size: 20,
)
: null,
title: Text(item.name),
subtitle: _itemSubtitle(item),
trailing: DiscountedPrice(item: item),
@@ -337,24 +385,30 @@ class _ReviewScreenState extends State<ReviewScreen> {
],
),
),
if (hasReceipt)
if (_hasReceipt)
ExpansionTile(
initiallyExpanded: highlighted?.sourceBounds != null,
title: const Text('See receipt'),
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: GestureDetector(
onTap: _openReceipt,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(
File(receiptPath),
fit: BoxFit.contain,
height: 220,
width: double.infinity,
),
),
),
child: highlighted?.sourceBounds != null
? ReceiptHighlightOverlay(
imagePath: receiptPath!,
bounds: highlighted!.sourceBounds!,
)
: GestureDetector(
onTap: _openReceipt,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(
File(receiptPath!),
fit: BoxFit.contain,
height: 220,
width: double.infinity,
),
),
),
),
],
),