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:
parent
cf6975b748
commit
9c449964fd
19 changed files with 559 additions and 29 deletions
42
apps/app_seeds/lib/ui/auto_backup_gate.dart
Normal file
42
apps/app_seeds/lib/ui/auto_backup_gate.dart
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../services/auto_backup_service.dart';
|
||||
|
||||
/// Drives the automatic backup off the app lifecycle: once on startup and again
|
||||
/// each time the app becomes hidden (backgrounded on mobile, window hidden on
|
||||
/// desktop). The [AutoBackupService] decides whether a copy is actually due, so
|
||||
/// these are cheap no-ops most of the time.
|
||||
///
|
||||
/// When [service] is null (widget tests, or platforms without file storage such
|
||||
/// as web) it wraps [child] untouched and does nothing.
|
||||
class AutoBackupGate extends StatefulWidget {
|
||||
const AutoBackupGate({required this.child, this.service, super.key});
|
||||
|
||||
final Widget child;
|
||||
final AutoBackupService? service;
|
||||
|
||||
@override
|
||||
State<AutoBackupGate> createState() => _AutoBackupGateState();
|
||||
}
|
||||
|
||||
class _AutoBackupGateState extends State<AutoBackupGate> {
|
||||
AppLifecycleListener? _listener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final service = widget.service;
|
||||
if (service == null) return;
|
||||
_listener = AppLifecycleListener(onHide: () => service.runIfDue());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => service.runIfDue());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_listener?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue