Lower the bulk-digitization cliff with two more routes on top of the already-landed CSV import and "save and add another": - Photo-first drafts (capture now, catalogue later): burst-capture photos (camera or multi-gallery) into unnamed draft varieties, shown in a "to catalogue" tray, hidden from the main list until named. Adds Variety.isDraft (schema), addDraftVariety/watchDrafts/nameDraft, the triage sheet and the inventory banner. - On-device OCR label suggestion (Tesseract, offline, no Google): a "Suggest name from photo" button in the naming dialog behind a LabelTextExtractor interface (Tesseract on Android/iOS, no-op elsewhere). Reads the largest print via hOCR bounding boxes, drops boilerplate/low-confidence noise, preprocesses (grayscale, contrast, upscale) and sweeps rotations (0-315 deg) so tilted packets still read. Bundles tessdata_fast eng+spa; validated on-device against real packets. The photo is written to a temp file deleted immediately in a finally block (the plugin needs a path) - a bounded, documented exception to no-plaintext-at-rest. This commit also carries the co-developed schema evolution v5 to v8 that shares these files (organic flag, species viability years, crop calendar, lot provenance/abundance/preservation format, condition checks) plus their exports/migrations and i18n. Tests: CSV/draft/OCR unit + widget + migration green in isolation. Note: the full widget suite currently hangs (>10 min) - under investigation.
99 lines
4 KiB
Dart
99 lines
4 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:commons_core/commons_core.dart';
|
|
import 'package:flutter/services.dart' show rootBundle;
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:get_it/get_it.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import '../data/species_catalog.dart';
|
|
import '../data/species_repository.dart';
|
|
import '../data/variety_repository.dart';
|
|
import '../db/database.dart';
|
|
import '../db/encrypted_executor.dart';
|
|
import '../i18n/strings.g.dart';
|
|
import '../security/secret_store.dart';
|
|
import '../security/secure_key_store.dart';
|
|
import '../services/export_import_service.dart';
|
|
import '../services/file_picker_file_service.dart';
|
|
import '../services/file_service.dart';
|
|
import '../services/ocr/label_text_extractor.dart';
|
|
import '../services/ocr/ocr_language.dart';
|
|
import '../services/ocr/tesseract_label_extractor.dart';
|
|
import '../services/onboarding_store.dart';
|
|
|
|
/// The app's service locator. Kept to the composition root — widgets get their
|
|
/// repositories from here (or via BlocProvider), never by reaching into it deep
|
|
/// in the tree.
|
|
final GetIt getIt = GetIt.instance;
|
|
|
|
/// Wires the encrypted DB, keystore and repositories. Call once from `main`
|
|
/// before `runApp`; the DB key must exist before the DB opens.
|
|
Future<void> configureDependencies() async {
|
|
final secretStore = FlutterSecretStore();
|
|
final keyStore = SecureKeyStore(store: secretStore);
|
|
final dbKeyHex = await keyStore.databaseKeyHex();
|
|
final rootSeedHex = await keyStore.rootSeedHex();
|
|
|
|
final database = AppDatabase(
|
|
openEncryptedExecutor(await _databaseFile(), dbKeyHex),
|
|
);
|
|
|
|
// Until real key derivation lands, the node/author id is a stable per-install
|
|
// slice of the root seed. It becomes the user's public key in the social layer.
|
|
final nodeId = rootSeedHex.substring(0, 16);
|
|
|
|
// Seed the bundled species catalog (idempotent) before the UI opens.
|
|
final speciesRepository = SpeciesRepository(database, idGen: IdGen());
|
|
await speciesRepository.seedBundled(await loadBundledSpecies());
|
|
|
|
final varietyRepository = VarietyRepository(
|
|
database,
|
|
idGen: IdGen(),
|
|
nodeId: nodeId,
|
|
);
|
|
|
|
const fileService = FilePickerFileService();
|
|
|
|
// OCR label suggestions only where a native Tesseract engine exists; every
|
|
// other platform (desktop, web) degrades to the no-op and hides the button.
|
|
// The language pack(s) follow the user's locale(s) — never a fixed region —
|
|
// limited to what we actually bundle, always unioned with the Latin baseline.
|
|
final LabelTextExtractor labelExtractor =
|
|
(Platform.isAndroid || Platform.isIOS)
|
|
? TesseractLabelExtractor(language: await _resolveOcrLanguage())
|
|
: const NoOpLabelTextExtractor();
|
|
|
|
getIt
|
|
..registerSingleton<SecureKeyStore>(keyStore)
|
|
..registerSingleton<AppDatabase>(database)
|
|
..registerSingleton<SpeciesRepository>(speciesRepository)
|
|
..registerSingleton<VarietyRepository>(varietyRepository)
|
|
..registerSingleton<FileService>(fileService)
|
|
..registerSingleton<LabelTextExtractor>(labelExtractor)
|
|
..registerSingleton<OnboardingStore>(OnboardingStore(secretStore))
|
|
..registerSingleton<ExportImportService>(
|
|
ExportImportService(repository: varietyRepository, files: fileService),
|
|
);
|
|
}
|
|
|
|
Future<File> _databaseFile() async {
|
|
final dir = await getApplicationDocumentsDirectory();
|
|
return File(p.join(dir.path, 'tane_inventory.sqlite'));
|
|
}
|
|
|
|
/// Picks the Tesseract language pack(s) for the current locale(s), limited to
|
|
/// the bundled packs listed in `tessdata_config.json`. Prefers the app's chosen
|
|
/// language, then the device's ordered locales.
|
|
Future<String> _resolveOcrLanguage() async {
|
|
final available = parseAvailableOcrLanguages(
|
|
await rootBundle.loadString('assets/tessdata_config.json'),
|
|
);
|
|
final preferred = <String>[
|
|
LocaleSettings.currentLocale.languageCode,
|
|
for (final l in WidgetsBinding.instance.platformDispatcher.locales)
|
|
l.languageCode,
|
|
];
|
|
return resolveOcrLanguages(preferredLocales: preferred, available: available);
|
|
}
|