fixes before build 2
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,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).
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'],
|
||||
);
|
||||
|
||||
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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,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,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', () {
|
||||
|
||||
@@ -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,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', () {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
@@ -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
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user