mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-07-04 13:23:15 +02:00
Third durability layer beside GitHub auto-sync and Android Auto Backup: a plain, human/LLM-readable Markdown file kept current on local disk. - LocalBackup (lib/sync): pure, injectable file IO. scheduleExport() debounces writes (a burst of keystrokes → one export); recover() parses the file back into notes. Reused NotesMarkdown serializer. - CaptureScreen wires it: on launch, recover into an *empty* DB only (so a stale backup never clobbers existing notes), then keep the backup current as notes change. Platform path = ~/todo/BACKLOG.md on desktop (the path the user's workflow already reads) or the app documents dir on mobile (covered by Android Auto Backup). File IO is injected in tests. - Added fake_async dev dep to unit-test the debounce with a virtual clock. 151 tests, 100% line coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
57 lines
1.9 KiB
Dart
57 lines
1.9 KiB
Dart
import 'dart:async';
|
|
|
|
import '../data/note.dart';
|
|
import 'notes_markdown.dart';
|
|
|
|
/// Keeps an always-current Markdown backup of all notes on local disk, and
|
|
/// recovers from it on launch.
|
|
///
|
|
/// This is a third durability layer alongside GitHub auto-sync and Android
|
|
/// Auto Backup: a plain human-readable file the user (or an LLM) can read
|
|
/// directly. File IO is injected ([reader]/[writer]) so the class is pure and
|
|
/// fully testable; the platform-specific path lives in the caller.
|
|
///
|
|
/// Writes are debounced so a burst of keystrokes produces a single export.
|
|
class LocalBackup {
|
|
LocalBackup({
|
|
required this.reader,
|
|
required this.writer,
|
|
this.debounce = const Duration(seconds: 2),
|
|
});
|
|
|
|
/// Reads the backup file's contents, or null if it does not exist.
|
|
final Future<String?> Function() reader;
|
|
|
|
/// Writes the given Markdown to the backup file (overwriting it).
|
|
final Future<void> Function(String markdown) writer;
|
|
|
|
/// How long to wait after the last change before writing.
|
|
final Duration debounce;
|
|
|
|
Timer? _timer;
|
|
|
|
/// Schedules a debounced export of [notes]. Repeated calls reset the timer,
|
|
/// so only the latest snapshot is written. A zero [debounce] writes
|
|
/// immediately (and schedules no timer).
|
|
void scheduleExport(List<Note> notes) {
|
|
_timer?.cancel();
|
|
final markdown = NotesMarkdown.export(notes);
|
|
if (debounce == Duration.zero) {
|
|
writer(markdown);
|
|
} else {
|
|
_timer = Timer(debounce, () => writer(markdown));
|
|
}
|
|
}
|
|
|
|
/// Reads the backup file and parses it into notes for recovery. Returns an
|
|
/// empty list when there is no (usable) backup.
|
|
Future<List<Note>> recover() async {
|
|
final contents = await reader();
|
|
if (contents == null || contents.trim().isEmpty) return const [];
|
|
return NotesMarkdown.parse(contents);
|
|
}
|
|
|
|
/// Cancels any pending write. Call from the owner's dispose.
|
|
void dispose() => _timer?.cancel();
|
|
}
|