fixes before build 2

This commit is contained in:
gitea
2026-08-31 17:05:22 +02:00
parent 14fcb9a533
commit b0708101b0
20 changed files with 72 additions and 61 deletions
+3 -5
View File
@@ -1,8 +1,6 @@
# Receipity # Groci
Single-screen Flutter app that proves **capture → OCR**: take a photo of a shopping receipt (or pick one from the gallery), run Google ML Kit Text Recognition, and show the raw extracted text. Flutter app for scanning shopping receipts, tracking products and prices, shopping lists, and bonus cards.
No database, no saving, no other screens.
## Run ## Run
@@ -11,4 +9,4 @@ flutter pub get
flutter run flutter run
``` ```
Use a physical Android device or emulator with Google Play. ML Kit text recognition is on-device and does not require a network call. Use a physical Android device or emulator with Google Play for receipt OCR.
+1 -1
View File
@@ -2,7 +2,7 @@
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/> <uses-feature android:name="android.hardware.camera" android:required="false"/>
<application <application
android:label="Receipity" android:label="Groci"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
+1 -1
View File
@@ -1,6 +1,6 @@
# Receipt parsing (implementation) # Receipt parsing (implementation)
This is the working guide for Receipity's receipt parser. Use it when a new This is the working guide for Groci's receipt parser. Use it when a new
shop's receipt does not parse well. shop's receipt does not parse well.
The original design notes live in [`receipt-parsing-spec.md`](../receipt-parsing-spec.md). The original design notes live in [`receipt-parsing-spec.md`](../receipt-parsing-spec.md).
+1 -1
View File
@@ -57,7 +57,7 @@ class _CropReceiptScreenState extends State<CropReceiptScreen> {
Future<void> _saveAndPop(Uint8List bytes) async { Future<void> _saveAndPop(Uint8List bytes) async {
try { try {
final file = File( final file = File(
'${Directory.systemTemp.path}/receipity_crop_${DateTime.now().millisecondsSinceEpoch}.jpg', '${Directory.systemTemp.path}/groci_crop_${DateTime.now().millisecondsSinceEpoch}.jpg',
); );
await file.writeAsBytes(bytes, flush: true); await file.writeAsBytes(bytes, flush: true);
if (!mounted) return; if (!mounted) return;
+5 -5
View File
@@ -13,20 +13,20 @@ import 'utils/constants.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await ReceiptParser.loadConfig(); await ReceiptParser.loadConfig();
runApp(ReceipityApp()); runApp(GrociApp());
} }
class ReceipityApp extends StatefulWidget { class GrociApp extends StatefulWidget {
ReceipityApp({super.key, ProductRepository? repository}) GrociApp({super.key, ProductRepository? repository})
: repository = repository ?? ProductRepository(); : repository = repository ?? ProductRepository();
final ProductRepository repository; final ProductRepository repository;
@override @override
State<ReceipityApp> createState() => _ReceipityAppState(); State<GrociApp> createState() => _GrociAppState();
} }
class _ReceipityAppState extends State<ReceipityApp> { class _GrociAppState extends State<GrociApp> {
late final AppSettings _settings; late final AppSettings _settings;
@override @override
+1 -1
View File
@@ -69,7 +69,7 @@ class _ScanScreenState extends State<ScanScreen> {
try { try {
final data = await rootBundle.load(kSampleReceiptAsset); final data = await rootBundle.load(kSampleReceiptAsset);
final file = File( final file = File(
'${Directory.systemTemp.path}/receipity_sample_receipt.jpeg', '${Directory.systemTemp.path}/groci_sample_receipt.jpeg',
); );
await file.writeAsBytes(data.buffer.asUint8List(), flush: true); await file.writeAsBytes(data.buffer.asUint8List(), flush: true);
if (!mounted) return; if (!mounted) return;
+7 -3
View File
@@ -5,6 +5,9 @@ import 'dart:convert';
const int kBackupVersion = 3; const int kBackupVersion = 3;
const String kBackupFormat = 'groci-backup';
const String kLegacyBackupFormat = 'receipity-backup';
class BackupDocument { class BackupDocument {
const BackupDocument({ const BackupDocument({
required this.version, required this.version,
@@ -33,7 +36,7 @@ class BackupDocument {
final Map<String, String> images; final Map<String, String> images;
Map<String, Object?> toJson() => { Map<String, Object?> toJson() => {
'format': 'receipity-backup', 'format': kBackupFormat,
'version': version, 'version': version,
'exportedAt': exportedAt, 'exportedAt': exportedAt,
'darkMode': darkMode, 'darkMode': darkMode,
@@ -54,8 +57,9 @@ class BackupDocument {
if (decoded is! Map<String, dynamic>) { if (decoded is! Map<String, dynamic>) {
throw const FormatException('Backup is not a JSON object.'); throw const FormatException('Backup is not a JSON object.');
} }
if (decoded['format'] != 'receipity-backup') { final format = decoded['format'];
throw const FormatException('This file is not a Receipity backup.'); if (format != kBackupFormat && format != kLegacyBackupFormat) {
throw const FormatException('This file is not a Groci backup.');
} }
final version = decoded['version']; final version = decoded['version'];
if (version is! int || version < 1 || version > kBackupVersion) { if (version is! int || version < 1 || version > kBackupVersion) {
+6 -6
View File
@@ -1,4 +1,4 @@
/// Build, share, and restore a Receipity JSON backup. /// Build, share, and restore a Groci JSON backup.
library; library;
import 'dart:convert'; import 'dart:convert';
@@ -68,11 +68,11 @@ class BackupService {
final document = await buildBackup(); final document = await buildBackup();
final bytes = Uint8List.fromList(utf8.encode(document.encode())); final bytes = Uint8List.fromList(utf8.encode(document.encode()));
final stamp = DateTime.now().toIso8601String().split('T').first; final stamp = DateTime.now().toIso8601String().split('T').first;
final fileName = 'receipity-backup-$stamp.json'; final fileName = 'groci-backup-$stamp.json';
try { try {
final saved = await FilePicker.saveFile( final saved = await FilePicker.saveFile(
dialogTitle: 'Save Receipity backup', dialogTitle: 'Save Groci backup',
fileName: fileName, fileName: fileName,
bytes: bytes, bytes: bytes,
mimeType: 'application/json', mimeType: 'application/json',
@@ -90,8 +90,8 @@ class BackupService {
final result = await SharePlus.instance.share( final result = await SharePlus.instance.share(
ShareParams( ShareParams(
files: [XFile(file.path, mimeType: 'application/json')], files: [XFile(file.path, mimeType: 'application/json')],
subject: 'Receipity backup', subject: 'Groci backup',
text: 'Receipity backup $stamp', text: 'Groci backup $stamp',
), ),
); );
return result.status != ShareResultStatus.dismissed; return result.status != ShareResultStatus.dismissed;
@@ -99,7 +99,7 @@ class BackupService {
Future<bool> importBackup() async { Future<bool> importBackup() async {
final picked = await FilePicker.pickFile( final picked = await FilePicker.pickFile(
dialogTitle: 'Import Receipity backup', dialogTitle: 'Import Groci backup',
type: FileType.custom, type: FileType.custom,
allowedExtensions: const ['json'], allowedExtensions: const ['json'],
); );
+11 -3
View File
@@ -31,10 +31,18 @@ class ProductRepository extends ChangeNotifier {
return _opening!; return _opening!;
} }
Future<String> _defaultDatabasePath() async {
final dir = (await getApplicationDocumentsDirectory()).path;
final groci = p.join(dir, 'groci.db');
final legacy = p.join(dir, 'receipity.db');
if (!File(groci).existsSync() && File(legacy).existsSync()) {
await File(legacy).rename(groci);
}
return groci;
}
Future<Database> _open() async { Future<Database> _open() async {
final path = final path = databasePath ?? await _defaultDatabasePath();
databasePath ??
p.join((await getApplicationDocumentsDirectory()).path, 'receipity.db');
return openDatabase( return openDatabase(
path, path,
version: 9, version: 9,
+1 -1
View File
@@ -1,7 +1,7 @@
/// Shared strings used across the app. /// Shared strings used across the app.
library; library;
const String kAppName = 'Receipity'; const String kAppName = 'Groci';
/// Bundled Jumbo receipt used to test OCR without the camera. /// Bundled Jumbo receipt used to test OCR without the camera.
const String kSampleReceiptAsset = 'assets/receipt.jpeg'; const String kSampleReceiptAsset = 'assets/receipt.jpeg';
+1 -1
View File
@@ -1,4 +1,4 @@
name: receipity name: groci
description: Capture a shopping receipt and show Google ML Kit text recognition output. description: Capture a shopping receipt and show Google ML Kit text recognition output.
publish_to: 'none' publish_to: 'none'
version: 1.0.0+1 version: 1.0.0+1
+4 -3
View File
@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/services/backup_format.dart'; import 'package:groci/services/backup_format.dart';
void main() { void main() {
test('round-trips a backup document', () { test('round-trips a backup document', () {
@@ -81,9 +81,10 @@ void main() {
expect(parsed.shoppingLists.single['name'], 'Weekly'); expect(parsed.shoppingLists.single['name'], 'Weekly');
expect(parsed.shoppingListItems.single['name'], 'Milk'); expect(parsed.shoppingListItems.single['name'], 'Milk');
expect(parsed.images['products/product_1.jpg'], 'abc123'); expect(parsed.images['products/product_1.jpg'], 'abc123');
expect(parsed.encode(), contains('"format": "groci-backup"'));
}); });
test('accepts a version 1 backup without shopping lists', () { test('accepts legacy receipity backup format', () {
const json = ''' const json = '''
{ {
"format": "receipity-backup", "format": "receipity-backup",
@@ -104,7 +105,7 @@ void main() {
expect(parsed.shopTags, isEmpty); expect(parsed.shopTags, isEmpty);
}); });
test('rejects a file that is not a Receipity backup', () { test('rejects a file that is not a Groci backup', () {
expect( expect(
() => BackupDocument.parse('{"hello":"world"}'), () => BackupDocument.parse('{"hello":"world"}'),
throwsFormatException, throwsFormatException,
+1 -1
View File
@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/models/line_item.dart'; import 'package:groci/models/line_item.dart';
import 'test_helpers.dart'; import 'test_helpers.dart';
+1 -1
View File
@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/utils/fuzzy.dart'; import 'package:groci/utils/fuzzy.dart';
void main() { void main() {
test('matches substrings and character subsequences', () { test('matches substrings and character subsequences', () {
+3 -3
View File
@@ -1,7 +1,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/models/line_item.dart'; import 'package:groci/models/line_item.dart';
import 'package:receipity/models/product.dart'; import 'package:groci/models/product.dart';
import 'package:receipity/utils/product_names.dart'; import 'package:groci/utils/product_names.dart';
import 'test_helpers.dart'; import 'test_helpers.dart';
+1 -1
View File
@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/services/receipt_parser.dart'; import 'package:groci/services/receipt_parser.dart';
void main() { void main() {
test('parses name and price at the end of a line', () { test('parses name and price at the end of a line', () {
+3 -3
View File
@@ -1,7 +1,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/models/shop.dart'; import 'package:groci/models/shop.dart';
import 'package:receipity/utils/shop_calendar.dart'; import 'package:groci/utils/shop_calendar.dart';
import 'package:receipity/utils/shop_log_filter.dart'; import 'package:groci/utils/shop_log_filter.dart';
Shop _shop({ Shop _shop({
required int id, required int id,
+2 -2
View File
@@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/models/supermarket.dart'; import 'package:groci/models/supermarket.dart';
import 'package:receipity/utils/supermarkets.dart'; import 'package:groci/utils/supermarkets.dart';
void main() { void main() {
test('detects Jumbo and Lidl from receipt text', () { test('detects Jumbo and Lidl from receipt text', () {
+2 -2
View File
@@ -3,7 +3,7 @@ import 'dart:math';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:receipity/services/product_repository.dart'; import 'package:groci/services/product_repository.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart';
var _dbSeq = 0; var _dbSeq = 0;
@@ -18,7 +18,7 @@ void ensureTestSqlite() {
ProductRepository memoryRepository() { ProductRepository memoryRepository() {
ensureTestSqlite(); ensureTestSqlite();
final dir = Directory(p.join('.dart_tool', 'receipity_test_db')) final dir = Directory(p.join('.dart_tool', 'groci_test_db'))
..createSync(recursive: true); ..createSync(recursive: true);
final name = final name =
'db_${DateTime.now().microsecondsSinceEpoch}_${_dbSeq++}_${Random().nextInt(1 << 32)}.sqlite'; 'db_${DateTime.now().microsecondsSinceEpoch}_${_dbSeq++}_${Random().nextInt(1 << 32)}.sqlite';
+17 -17
View File
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/main.dart'; import 'package:groci/main.dart';
import 'package:receipity/models/line_item.dart'; import 'package:groci/models/line_item.dart';
import 'package:receipity/review_screen.dart'; import 'package:groci/review_screen.dart';
import 'package:receipity/widgets/shop_heatmap.dart'; import 'package:groci/widgets/shop_heatmap.dart';
import 'test_helpers.dart'; import 'test_helpers.dart';
@@ -13,7 +13,7 @@ void main() {
testWidgets('home screen shows overview and bottom navigation', ( testWidgets('home screen shows overview and bottom navigation', (
tester, tester,
) async { ) async {
await tester.pumpWidget(ReceipityApp(repository: memoryRepository())); await tester.pumpWidget(GrociApp(repository: memoryRepository()));
await pumpUntilFound(tester, find.text('Nothing logged yet.')); await pumpUntilFound(tester, find.text('Nothing logged yet.'));
expect(find.text('Home'), findsWidgets); expect(find.text('Home'), findsWidgets);
@@ -27,12 +27,12 @@ void main() {
}); });
testWidgets('scan screen shows capture actions', (tester) async { testWidgets('scan screen shows capture actions', (tester) async {
await tester.pumpWidget(ReceipityApp(repository: memoryRepository())); await tester.pumpWidget(GrociApp(repository: memoryRepository()));
await pumpUntilFound(tester, find.text('Overview')); await pumpUntilFound(tester, find.text('Overview'));
await tester.tap(find.byTooltip('Add')); await tester.tap(find.byTooltip('Add'));
await tester.pump(); await tester.pump();
expect(find.text('Receipity'), findsOneWidget); expect(find.text('Groci'), findsOneWidget);
expect(find.text('Take photo'), findsOneWidget); expect(find.text('Take photo'), findsOneWidget);
expect(find.text('Pick from gallery'), findsOneWidget); expect(find.text('Pick from gallery'), findsOneWidget);
expect(find.text('Enter manually'), findsOneWidget); expect(find.text('Enter manually'), findsOneWidget);
@@ -44,7 +44,7 @@ void main() {
}); });
testWidgets('empty shop log hides edit and disables search', (tester) async { testWidgets('empty shop log hides edit and disables search', (tester) async {
await tester.pumpWidget(ReceipityApp(repository: memoryRepository())); await tester.pumpWidget(GrociApp(repository: memoryRepository()));
await pumpUntilFound(tester, find.text('Overview')); await pumpUntilFound(tester, find.text('Overview'));
await tester.tap(find.text('Shop log')); await tester.tap(find.text('Shop log'));
await pumpUntilFound( await pumpUntilFound(
@@ -80,7 +80,7 @@ void main() {
); );
}); });
await tester.pumpWidget(ReceipityApp(repository: repository)); await tester.pumpWidget(GrociApp(repository: repository));
await pumpUntilFound(tester, find.text('Overview')); await pumpUntilFound(tester, find.text('Overview'));
await tester.tap(find.text('Shop log')); await tester.tap(find.text('Shop log'));
await pumpUntilFound(tester, find.text('Weekly shop')); await pumpUntilFound(tester, find.text('Weekly shop'));
@@ -116,7 +116,7 @@ void main() {
); );
}); });
await tester.pumpWidget(ReceipityApp(repository: repository)); await tester.pumpWidget(GrociApp(repository: repository));
await pumpUntilFound(tester, find.text('Overview')); await pumpUntilFound(tester, find.text('Overview'));
await tester.tap(find.text('Products').last); await tester.tap(find.text('Products').last);
await pumpUntilFound(tester, find.text('Milk')); await pumpUntilFound(tester, find.text('Milk'));
@@ -142,7 +142,7 @@ void main() {
}); });
testWidgets('profile screen is a placeholder', (tester) async { testWidgets('profile screen is a placeholder', (tester) async {
await tester.pumpWidget(ReceipityApp(repository: memoryRepository())); await tester.pumpWidget(GrociApp(repository: memoryRepository()));
await pumpUntilFound(tester, find.text('Overview')); await pumpUntilFound(tester, find.text('Overview'));
await tester.tap(find.byTooltip('Profile')); await tester.tap(find.byTooltip('Profile'));
await pumpUntilFound(tester, find.text('Guest')); await pumpUntilFound(tester, find.text('Guest'));
@@ -172,7 +172,7 @@ void main() {
}); });
testWidgets('enter manually opens a blank trip', (tester) async { testWidgets('enter manually opens a blank trip', (tester) async {
await tester.pumpWidget(ReceipityApp(repository: memoryRepository())); await tester.pumpWidget(GrociApp(repository: memoryRepository()));
await pumpUntilFound(tester, find.text('Overview')); await pumpUntilFound(tester, find.text('Overview'));
await tester.tap(find.byTooltip('Add')); await tester.tap(find.byTooltip('Add'));
await tester.pump(); await tester.pump();
@@ -182,7 +182,7 @@ void main() {
}); });
testWidgets('lists tab shows shopping lists', (tester) async { testWidgets('lists tab shows shopping lists', (tester) async {
await tester.pumpWidget(ReceipityApp(repository: memoryRepository())); await tester.pumpWidget(GrociApp(repository: memoryRepository()));
await pumpUntilFound(tester, find.text('Overview')); await pumpUntilFound(tester, find.text('Overview'));
await tester.tap(find.text('Lists')); await tester.tap(find.text('Lists'));
await tester.pump(); await tester.pump();
@@ -264,7 +264,7 @@ void main() {
); );
}); });
await tester.pumpWidget(ReceipityApp(repository: repository)); await tester.pumpWidget(GrociApp(repository: repository));
await pumpUntilFound(tester, find.text('Overview')); await pumpUntilFound(tester, find.text('Overview'));
await tester.tap(find.text('Products').last); await tester.tap(find.text('Products').last);
await pumpUntilFound(tester, find.text('Milk')); await pumpUntilFound(tester, find.text('Milk'));
@@ -286,7 +286,7 @@ void main() {
final repository = memoryRepository(); final repository = memoryRepository();
await tester.runAsync(() => seedCaseConflictCatalog(repository)); await tester.runAsync(() => seedCaseConflictCatalog(repository));
await tester.pumpWidget(ReceipityApp(repository: repository)); await tester.pumpWidget(GrociApp(repository: repository));
await pumpUntilFound(tester, find.text('Overview')); await pumpUntilFound(tester, find.text('Overview'));
await tester.tap(find.text('Products').last); await tester.tap(find.text('Products').last);
await pumpUntilFound(tester, find.text('1 duplicate product')); await pumpUntilFound(tester, find.text('1 duplicate product'));
@@ -313,7 +313,7 @@ void main() {
); );
}); });
await tester.pumpWidget(ReceipityApp(repository: repository)); await tester.pumpWidget(GrociApp(repository: repository));
await pumpUntilFound(tester, find.text('Overview')); await pumpUntilFound(tester, find.text('Overview'));
await tester.tap(find.text('Products').last); await tester.tap(find.text('Products').last);
await pumpUntilFound(tester, find.text('1 product, multiple spellings')); await pumpUntilFound(tester, find.text('1 product, multiple spellings'));
@@ -328,7 +328,7 @@ void main() {
final repository = memoryRepository(); final repository = memoryRepository();
await tester.runAsync(() => seedCaseConflictCatalog(repository)); await tester.runAsync(() => seedCaseConflictCatalog(repository));
await tester.pumpWidget(ReceipityApp(repository: repository)); await tester.pumpWidget(GrociApp(repository: repository));
await pumpUntilFound(tester, find.text('Overview')); await pumpUntilFound(tester, find.text('Overview'));
await tester.tap(find.text('Products').last); await tester.tap(find.text('Products').last);
await pumpUntilFound(tester, find.text('1 duplicate product')); await pumpUntilFound(tester, find.text('1 duplicate product'));