feat(backup): add automatic weekly encrypted backups

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.
This commit is contained in:
vjrj 2026-07-10 10:17:36 +02:00
parent cf6975b748
commit 9c449964fd
19 changed files with 559 additions and 29 deletions

View file

@ -7,12 +7,14 @@ import 'package:go_router/go_router.dart';
import 'data/species_repository.dart';
import 'data/variety_repository.dart';
import 'i18n/strings.g.dart';
import 'services/auto_backup_service.dart';
import 'services/onboarding_store.dart';
import 'services/social_service.dart';
import 'services/social_settings.dart';
import 'state/inventory_cubit.dart';
import 'state/variety_detail_cubit.dart';
import 'ui/about_screen.dart';
import 'ui/auto_backup_gate.dart';
import 'ui/home_screen.dart';
import 'ui/intro_screen.dart';
import 'ui/inventory_list_screen.dart';
@ -32,14 +34,15 @@ class TaneApp extends StatelessWidget {
this.social,
this.socialSettings,
this.showIntro = false,
this.autoBackup,
super.key,
}) : _router = _buildRouter(
repository,
onboarding,
showIntro,
social,
socialSettings,
);
repository,
onboarding,
showIntro,
social,
socialSettings,
);
final VarietyRepository repository;
final SpeciesRepository species;
@ -50,6 +53,10 @@ class TaneApp extends StatelessWidget {
final SocialService? social;
final SocialSettings? socialSettings;
final bool showIntro;
/// Drives silent periodic backups off the app lifecycle. Null in widget tests
/// and on platforms without file storage (web) the gate then does nothing.
final AutoBackupService? autoBackup;
final GoRouter _router;
static GoRouter _buildRouter(
@ -116,22 +123,25 @@ class TaneApp extends StatelessWidget {
RepositoryProvider.value(value: repository),
RepositoryProvider.value(value: species),
],
child: MaterialApp.router(
onGenerateTitle: (context) => context.t.app.title,
debugShowCheckedModeBanner: false,
theme: buildTaneTheme(),
// Let scrollables & PageViews be dragged with a mouse/trackpad on
// desktop (Flutter disables that by default), so the photo carousel and
// lists swipe there too.
scrollBehavior: const _DragScrollBehavior(),
locale: TranslationProvider.of(context).flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
routerConfig: _router,
child: AutoBackupGate(
service: autoBackup,
child: MaterialApp.router(
onGenerateTitle: (context) => context.t.app.title,
debugShowCheckedModeBanner: false,
theme: buildTaneTheme(),
// Let scrollables & PageViews be dragged with a mouse/trackpad on
// desktop (Flutter disables that by default), so the photo carousel and
// lists swipe there too.
scrollBehavior: const _DragScrollBehavior(),
locale: TranslationProvider.of(context).flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
routerConfig: _router,
),
),
);
}