diff --git a/README.md b/README.md
index 9f6ea37..098b007 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 0f0f20d..94e23c3 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -2,7 +2,7 @@
{
Future _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;
diff --git a/lib/main.dart b/lib/main.dart
index dc62225..c31d6e9 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -13,20 +13,20 @@ import 'utils/constants.dart';
Future 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 createState() => _ReceipityAppState();
+ State createState() => _GrociAppState();
}
-class _ReceipityAppState extends State {
+class _GrociAppState extends State {
late final AppSettings _settings;
@override
diff --git a/lib/scan_screen.dart b/lib/scan_screen.dart
index 1dddf42..312d681 100644
--- a/lib/scan_screen.dart
+++ b/lib/scan_screen.dart
@@ -69,7 +69,7 @@ class _ScanScreenState extends State {
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;
diff --git a/lib/services/backup_format.dart b/lib/services/backup_format.dart
index b2712c3..89bd276 100644
--- a/lib/services/backup_format.dart
+++ b/lib/services/backup_format.dart
@@ -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 images;
Map toJson() => {
- 'format': 'receipity-backup',
+ 'format': kBackupFormat,
'version': version,
'exportedAt': exportedAt,
'darkMode': darkMode,
@@ -54,8 +57,9 @@ class BackupDocument {
if (decoded is! Map) {
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) {
diff --git a/lib/services/backup_service.dart b/lib/services/backup_service.dart
index 5dd4b1a..a1b8d75 100644
--- a/lib/services/backup_service.dart
+++ b/lib/services/backup_service.dart
@@ -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 importBackup() async {
final picked = await FilePicker.pickFile(
- dialogTitle: 'Import Receipity backup',
+ dialogTitle: 'Import Groci backup',
type: FileType.custom,
allowedExtensions: const ['json'],
);
diff --git a/lib/services/product_repository.dart b/lib/services/product_repository.dart
index c414e00..7f31a09 100644
--- a/lib/services/product_repository.dart
+++ b/lib/services/product_repository.dart
@@ -31,10 +31,18 @@ class ProductRepository extends ChangeNotifier {
return _opening!;
}
+ Future _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 _open() async {
- final path =
- databasePath ??
- p.join((await getApplicationDocumentsDirectory()).path, 'receipity.db');
+ final path = databasePath ?? await _defaultDatabasePath();
return openDatabase(
path,
version: 9,
diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart
index 3b76a91..4a4589b 100644
--- a/lib/utils/constants.dart
+++ b/lib/utils/constants.dart
@@ -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';
diff --git a/pubspec.yaml b/pubspec.yaml
index 4ee794d..28139ba 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -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
diff --git a/test/backup_format_test.dart b/test/backup_format_test.dart
index e7dbdef..2d57aca 100644
--- a/test/backup_format_test.dart
+++ b/test/backup_format_test.dart
@@ -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,
diff --git a/test/catalog_test.dart b/test/catalog_test.dart
index 7ea4a8c..7fc12d9 100644
--- a/test/catalog_test.dart
+++ b/test/catalog_test.dart
@@ -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';
diff --git a/test/fuzzy_test.dart b/test/fuzzy_test.dart
index 86f7880..597d3cb 100644
--- a/test/fuzzy_test.dart
+++ b/test/fuzzy_test.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', () {
diff --git a/test/product_names_test.dart b/test/product_names_test.dart
index 6f8ee87..f20d0b1 100644
--- a/test/product_names_test.dart
+++ b/test/product_names_test.dart
@@ -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';
diff --git a/test/receipt_parser_test.dart b/test/receipt_parser_test.dart
index f137add..1a652ed 100644
--- a/test/receipt_parser_test.dart
+++ b/test/receipt_parser_test.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', () {
diff --git a/test/shop_log_test.dart b/test/shop_log_test.dart
index 7bd78b9..ece9460 100644
--- a/test/shop_log_test.dart
+++ b/test/shop_log_test.dart
@@ -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,
diff --git a/test/supermarket_test.dart b/test/supermarket_test.dart
index 87b064a..41d4bdc 100644
--- a/test/supermarket_test.dart
+++ b/test/supermarket_test.dart
@@ -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', () {
diff --git a/test/test_helpers.dart b/test/test_helpers.dart
index 6bd54c5..81527b0 100644
--- a/test/test_helpers.dart
+++ b/test/test_helpers.dart
@@ -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';
diff --git a/test/widget_test.dart b/test/widget_test.dart
index cef3381..bbeca55 100644
--- a/test/widget_test.dart
+++ b/test/widget_test.dart
@@ -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'));