import 'dart:io'; import 'package:commons_core/commons_core.dart'; import 'package:flutter/foundation.dart' show kIsWeb; 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/auto_backup_service.dart'; import '../services/auto_backup_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'; import '../services/recovery_sheet_service.dart'; import '../services/share_catalog_service.dart'; import '../services/offer_outbox.dart'; import '../services/social_service.dart'; import '../services/social_settings.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 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), ); // CRDT author id: a stable per-install slice of the root seed. Kept distinct // from the social-layer public key on purpose — unifying the two would rewrite // authorship of existing rows, a data-model decision for later. final nodeId = rootSeedHex.substring(0, 16); // Block 2 social identity: a secp256k1 Nostr key derived (one-way) from the // SAME root seed, so it needs no extra backup. Cheap + offline; no relay is // contacted here (local-first — the social layer only enriches). final socialService = await SocialService.fromRootSeedHex(rootSeedHex); // 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(keyStore) ..registerSingleton(database) ..registerSingleton(speciesRepository) ..registerSingleton(varietyRepository) ..registerSingleton(fileService) ..registerSingleton(labelExtractor) ..registerSingleton(OnboardingStore(secretStore)) ..registerSingleton(socialService) ..registerSingleton(SocialSettings(secretStore)) ..registerSingleton(OfferOutbox(secretStore)) ..registerSingleton( ExportImportService( repository: varietyRepository, files: fileService, keys: keyStore, ), ) ..registerSingleton( ShareCatalogService(files: fileService), ) ..registerSingleton( RecoverySheetService(files: fileService), ); // Automatic silent backups need real file storage; the web build has none, so // it simply goes without (the manual "save a copy" still works there). if (!kIsWeb) { getIt.registerSingleton( AutoBackupService( exporter: getIt(), store: AutoBackupStore(secretStore), directory: _backupsDir, ), ); } } Future _backupsDir() async { final dir = await getApplicationSupportDirectory(); return Directory(p.join(dir.path, 'backups')); } Future _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 _resolveOcrLanguage() async { final available = parseAvailableOcrLanguages( await rootBundle.loadString('assets/tessdata_config.json'), ); final preferred = [ LocaleSettings.currentLocale.languageCode, for (final l in WidgetsBinding.instance.platformDispatcher.locales) l.languageCode, ]; return resolveOcrLanguages(preferredLocales: preferred, available: available); }