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.
No database, no saving, no other screens.
Flutter app for scanning shopping receipts, tracking products and prices, shopping lists, and bonus cards.
## Run
@@ -11,4 +9,4 @@ flutter pub get
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-feature android:name="android.hardware.camera" android:required="false"/>
<application
android:label="Receipity"
android:label="Groci"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
+1 -1
View File
@@ -1,6 +1,6 @@
# 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.
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 {
try {
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);
if (!mounted) return;
+5 -5
View File
@@ -13,20 +13,20 @@ import 'utils/constants.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await ReceiptParser.loadConfig();
runApp(ReceipityApp());
runApp(GrociApp());
}
class ReceipityApp extends StatefulWidget {
ReceipityApp({super.key, ProductRepository? repository})
class GrociApp extends StatefulWidget {
GrociApp({super.key, ProductRepository? repository})
: repository = repository ?? ProductRepository();
final ProductRepository repository;
@override
State<ReceipityApp> createState() => _ReceipityAppState();
State<GrociApp> createState() => _GrociAppState();
}
class _ReceipityAppState extends State<ReceipityApp> {
class _GrociAppState extends State<GrociApp> {
late final AppSettings _settings;
@override
+1 -1
View File
@@ -69,7 +69,7 @@ class _ScanScreenState extends State<ScanScreen> {
try {
final data = await rootBundle.load(kSampleReceiptAsset);
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);
if (!mounted) return;
+7 -3
View File
@@ -5,6 +5,9 @@ import 'dart:convert';
const int kBackupVersion = 3;
const String kBackupFormat = 'groci-backup';
const String kLegacyBackupFormat = 'receipity-backup';
class BackupDocument {
const BackupDocument({
required this.version,
@@ -33,7 +36,7 @@ class BackupDocument {
final Map<String, String> images;
Map<String, Object?> toJson() => {
'format': 'receipity-backup',
'format': kBackupFormat,
'version': version,
'exportedAt': exportedAt,
'darkMode': darkMode,
@@ -54,8 +57,9 @@ class BackupDocument {
if (decoded is! Map<String, dynamic>) {
throw const FormatException('Backup is not a JSON object.');
}
if (decoded['format'] != 'receipity-backup') {
throw const FormatException('This file is not a Receipity backup.');
final format = decoded['format'];
if (format != kBackupFormat && format != kLegacyBackupFormat) {
throw const FormatException('This file is not a Groci backup.');
}
final version = decoded['version'];
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;
import 'dart:convert';
@@ -68,11 +68,11 @@ class BackupService {
final document = await buildBackup();
final bytes = Uint8List.fromList(utf8.encode(document.encode()));
final stamp = DateTime.now().toIso8601String().split('T').first;
final fileName = 'receipity-backup-$stamp.json';
final fileName = 'groci-backup-$stamp.json';
try {
final saved = await FilePicker.saveFile(
dialogTitle: 'Save Receipity backup',
dialogTitle: 'Save Groci backup',
fileName: fileName,
bytes: bytes,
mimeType: 'application/json',
@@ -90,8 +90,8 @@ class BackupService {
final result = await SharePlus.instance.share(
ShareParams(
files: [XFile(file.path, mimeType: 'application/json')],
subject: 'Receipity backup',
text: 'Receipity backup $stamp',
subject: 'Groci backup',
text: 'Groci backup $stamp',
),
);
return result.status != ShareResultStatus.dismissed;
@@ -99,7 +99,7 @@ class BackupService {
Future<bool> importBackup() async {
final picked = await FilePicker.pickFile(
dialogTitle: 'Import Receipity backup',
dialogTitle: 'Import Groci backup',
type: FileType.custom,
allowedExtensions: const ['json'],
);
+11 -3
View File
@@ -31,10 +31,18 @@ class ProductRepository extends ChangeNotifier {
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 {
final path =
databasePath ??
p.join((await getApplicationDocumentsDirectory()).path, 'receipity.db');
final path = databasePath ?? await _defaultDatabasePath();
return openDatabase(
path,
version: 9,
+1 -1
View File
@@ -1,7 +1,7 @@
/// Shared strings used across the app.
library;
const String kAppName = 'Receipity';
const String kAppName = 'Groci';
/// Bundled Jumbo receipt used to test OCR without the camera.
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.
publish_to: 'none'
version: 1.0.0+1
+4 -3
View File
@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/services/backup_format.dart';
import 'package:groci/services/backup_format.dart';
void main() {
test('round-trips a backup document', () {
@@ -81,9 +81,10 @@ void main() {
expect(parsed.shoppingLists.single['name'], 'Weekly');
expect(parsed.shoppingListItems.single['name'], 'Milk');
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 = '''
{
"format": "receipity-backup",
@@ -104,7 +105,7 @@ void main() {
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(
() => BackupDocument.parse('{"hello":"world"}'),
throwsFormatException,
+1 -1
View File
@@ -1,5 +1,5 @@
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';
+1 -1
View File
@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/utils/fuzzy.dart';
import 'package:groci/utils/fuzzy.dart';
void main() {
test('matches substrings and character subsequences', () {
+3 -3
View File
@@ -1,7 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/models/line_item.dart';
import 'package:receipity/models/product.dart';
import 'package:receipity/utils/product_names.dart';
import 'package:groci/models/line_item.dart';
import 'package:groci/models/product.dart';
import 'package:groci/utils/product_names.dart';
import 'test_helpers.dart';
+1 -1
View File
@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/services/receipt_parser.dart';
import 'package:groci/services/receipt_parser.dart';
void main() {
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:receipity/models/shop.dart';
import 'package:receipity/utils/shop_calendar.dart';
import 'package:receipity/utils/shop_log_filter.dart';
import 'package:groci/models/shop.dart';
import 'package:groci/utils/shop_calendar.dart';
import 'package:groci/utils/shop_log_filter.dart';
Shop _shop({
required int id,
+2 -2
View File
@@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:receipity/models/supermarket.dart';
import 'package:receipity/utils/supermarkets.dart';
import 'package:groci/models/supermarket.dart';
import 'package:groci/utils/supermarkets.dart';
void main() {
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: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';
var _dbSeq = 0;
@@ -18,7 +18,7 @@ void ensureTestSqlite() {
ProductRepository memoryRepository() {
ensureTestSqlite();
final dir = Directory(p.join('.dart_tool', 'receipity_test_db'))
final dir = Directory(p.join('.dart_tool', 'groci_test_db'))
..createSync(recursive: true);
final name =
'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_test/flutter_test.dart';
import 'package:receipity/main.dart';
import 'package:receipity/models/line_item.dart';
import 'package:receipity/review_screen.dart';
import 'package:receipity/widgets/shop_heatmap.dart';
import 'package:groci/main.dart';
import 'package:groci/models/line_item.dart';
import 'package:groci/review_screen.dart';
import 'package:groci/widgets/shop_heatmap.dart';
import 'test_helpers.dart';
@@ -13,7 +13,7 @@ void main() {
testWidgets('home screen shows overview and bottom navigation', (
tester,
) async {
await tester.pumpWidget(ReceipityApp(repository: memoryRepository()));
await tester.pumpWidget(GrociApp(repository: memoryRepository()));
await pumpUntilFound(tester, find.text('Nothing logged yet.'));
expect(find.text('Home'), findsWidgets);
@@ -27,12 +27,12 @@ void main() {
});
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 tester.tap(find.byTooltip('Add'));
await tester.pump();
expect(find.text('Receipity'), findsOneWidget);
expect(find.text('Groci'), findsOneWidget);
expect(find.text('Take photo'), findsOneWidget);
expect(find.text('Pick from gallery'), findsOneWidget);
expect(find.text('Enter manually'), findsOneWidget);
@@ -44,7 +44,7 @@ void main() {
});
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 tester.tap(find.text('Shop log'));
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 tester.tap(find.text('Shop log'));
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 tester.tap(find.text('Products').last);
await pumpUntilFound(tester, find.text('Milk'));
@@ -142,7 +142,7 @@ void main() {
});
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 tester.tap(find.byTooltip('Profile'));
await pumpUntilFound(tester, find.text('Guest'));
@@ -172,7 +172,7 @@ void main() {
});
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 tester.tap(find.byTooltip('Add'));
await tester.pump();
@@ -182,7 +182,7 @@ void main() {
});
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 tester.tap(find.text('Lists'));
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 tester.tap(find.text('Products').last);
await pumpUntilFound(tester, find.text('Milk'));
@@ -286,7 +286,7 @@ void main() {
final repository = memoryRepository();
await tester.runAsync(() => seedCaseConflictCatalog(repository));
await tester.pumpWidget(ReceipityApp(repository: repository));
await tester.pumpWidget(GrociApp(repository: repository));
await pumpUntilFound(tester, find.text('Overview'));
await tester.tap(find.text('Products').last);
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 tester.tap(find.text('Products').last);
await pumpUntilFound(tester, find.text('1 product, multiple spellings'));
@@ -328,7 +328,7 @@ void main() {
final repository = memoryRepository();
await tester.runAsync(() => seedCaseConflictCatalog(repository));
await tester.pumpWidget(ReceipityApp(repository: repository));
await tester.pumpWidget(GrociApp(repository: repository));
await pumpUntilFound(tester, find.text('Overview'));
await tester.tap(find.text('Products').last);
await pumpUntilFound(tester, find.text('1 duplicate product'));