A silent safety net against in-app data loss or DB corruption, with zero friction. Driven off the app lifecycle (startup + when hidden), it writes a sealed .tanemaki copy into the app's private storage once a week, keeping the newest 3 (rotating). Reuses the manual backup's encryption via the extracted ExportImportService.buildSealedBackup(); never throws, so a failed copy can't crash startup or backgrounding. Not registered on web (no file storage). Settings shows a quiet reassurance line with the date of the last automatic copy (localised via intl), hidden where no automatic backup exists.
141 lines
5.5 KiB
Dart
141 lines
5.5 KiB
Dart
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/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<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),
|
|
);
|
|
|
|
// 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<SecureKeyStore>(keyStore)
|
|
..registerSingleton<AppDatabase>(database)
|
|
..registerSingleton<SpeciesRepository>(speciesRepository)
|
|
..registerSingleton<VarietyRepository>(varietyRepository)
|
|
..registerSingleton<FileService>(fileService)
|
|
..registerSingleton<LabelTextExtractor>(labelExtractor)
|
|
..registerSingleton<OnboardingStore>(OnboardingStore(secretStore))
|
|
..registerSingleton<SocialService>(socialService)
|
|
..registerSingleton<SocialSettings>(SocialSettings(secretStore))
|
|
..registerSingleton<ExportImportService>(
|
|
ExportImportService(
|
|
repository: varietyRepository,
|
|
files: fileService,
|
|
keys: keyStore,
|
|
),
|
|
)
|
|
..registerSingleton<ShareCatalogService>(
|
|
ShareCatalogService(files: fileService),
|
|
)
|
|
..registerSingleton<RecoverySheetService>(
|
|
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>(
|
|
AutoBackupService(
|
|
exporter: getIt<ExportImportService>(),
|
|
store: AutoBackupStore(secretStore),
|
|
directory: _backupsDir,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<Directory> _backupsDir() async {
|
|
final dir = await getApplicationSupportDirectory();
|
|
return Directory(p.join(dir.path, 'backups'));
|
|
}
|
|
|
|
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);
|
|
}
|