Merge branch 'main' into spike/block2-derisking

This commit is contained in:
vjrj 2026-07-10 15:27:01 +02:00
commit 1a2e404365
4 changed files with 112 additions and 9 deletions

View file

@ -45,10 +45,19 @@ class AutoBackupService {
/// a backup must never crash startup or the move to the background).
Future<bool> runIfDue({Duration interval = const Duration(days: 7)}) async {
try {
final now = _now();
final last = await _store.lastBackupAt();
if (last != null && now.difference(last) < interval) return false;
if (last != null && _now().difference(last) < interval) return false;
} on Object {
return false;
}
return backupNow();
}
/// Makes a copy right now, ignoring the schedule for an explicit "back up
/// now" tap. Returns true on success, false on any failure (never throws).
Future<bool> backupNow() async {
try {
final now = _now();
final dir = await _directory();
await dir.create(recursive: true);
final sealed = await _exporter.buildSealedBackup();

View file

@ -313,21 +313,48 @@ class BackupSection extends StatelessWidget {
}
}
/// A quiet reassurance line: the app already keeps copies on its own, and this
/// says when the last one was made. Hidden entirely when there is no automatic
/// backup on this platform.
class _AutoBackupTile extends StatelessWidget {
/// The automatic-copies line: the app keeps sealed copies on its own, and this
/// says when the last one was made. Tapping it makes one right now (handy after
/// a big change, and so the entry visibly *does* something). Hidden entirely
/// when there is no automatic backup on this platform.
class _AutoBackupTile extends StatefulWidget {
const _AutoBackupTile({required this.service});
final AutoBackupService? service;
@override
State<_AutoBackupTile> createState() => _AutoBackupTileState();
}
class _AutoBackupTileState extends State<_AutoBackupTile> {
late Future<DateTime?> _lastBackup = _read();
bool _busy = false;
Future<DateTime?> _read() async => widget.service?.lastBackupAt();
Future<void> _backupNow() async {
final service = widget.service;
if (service == null || _busy) return;
final t = context.t;
final messenger = ScaffoldMessenger.of(context);
setState(() => _busy = true);
final ok = await service.backupNow();
if (!mounted) return;
setState(() {
_busy = false;
_lastBackup = _read();
});
messenger.showSnackBar(
SnackBar(content: Text(ok ? t.backup.exportSaved : t.backup.failed)),
);
}
@override
Widget build(BuildContext context) {
final service = this.service;
if (service == null) return const SizedBox.shrink();
if (widget.service == null) return const SizedBox.shrink();
final t = context.t;
return FutureBuilder<DateTime?>(
future: service.lastBackupAt(),
future: _lastBackup,
builder: (context, snapshot) {
final last = snapshot.data;
final subtitle = last == null
@ -341,6 +368,14 @@ class _AutoBackupTile extends StatelessWidget {
leading: const Icon(Icons.shield_outlined),
title: Text(t.backup.autoBackupTitle),
subtitle: Text(subtitle),
trailing: _busy
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.backup_outlined),
onTap: _busy ? null : _backupNow,
);
},
);