todo-app/lib/ui/capture_draft.dart
Krzysztof kuhy Rudnicki d4c8162e0e Split capture_screen.dart into ten files under the cap
739 -> 249 lines. As with note_editor, the ~600-line State class needed
decomposing rather than moving:

  capture_draft.dart         the note being captured and its write-through
                             to storage -- the model behind "every keystroke
                             is already persisted". Lazy row creation stays
                             here; the screen just logs that it happened.
  capture_sync_runner.dart   one sync pass, with no BuildContext. This also
                             de-duplicates the two callers: the manual button
                             and the background tick ran byte-identical
                             runSync blocks differing only in the snackbar
                             and the 'auto' log flag.
  capture_status.dart        SyncStatusStore (the persisted last outcome) and
                             describeSyncResult.
  capture_app_bar.dart       the app bar.
  capture_meta_row.dart      the priority/status pickers.
  capture_status_footer.dart the two status lines.
  capture_screen_{widget,sync}.dart
                             parts, for the same reason as note_editor's.

The mounted checks are preserved exactly, including the one in
SyncStatusStore.restore that must fire AFTER its await rather than before
(writing the notifier post-dispose is the bug it exists to prevent), which
is why it takes a stillLive callback instead of a bool.

Verified: 313 tests pass, analyze clean, every capture file at 100%
coverage. lib/ and bin/ are now down to three files over the cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 16:25:51 +02:00

112 lines
3.7 KiB
Dart

/// The capture screen's in-progress note, and how it reaches storage.
///
/// Split out of `capture_screen.dart` for file size. It is the model behind
/// the app's central invariant — every keystroke is already persisted — so it
/// is kept free of Flutter state: the screen decides when to rebuild, this
/// decides what is saved.
///
/// The row is created lazily, on the first non-empty keystroke, so an
/// untouched template never reaches storage.
library;
import 'package:flutter/foundation.dart';
import 'package:todo/data/note.dart';
import 'package:todo/data/note_repository.dart';
import 'package:todo/sync/local_backup.dart';
import 'package:todo/sync/local_backup_factory.dart';
import 'package:uuid/uuid.dart';
/// Holds the draft being captured and writes it through to [repository].
class CaptureDraft {
/// Creates a draft writing to [repository].
CaptureDraft(this.repository);
/// Where the draft is persisted on every change.
final NoteRepository repository;
final Uuid _uuid = const Uuid();
/// When the draft was last written locally; null until the first save.
final ValueNotifier<DateTime?> lastSavedAt = ValueNotifier<DateTime?>(null);
String _text = '';
String? _id;
DateTime? _createdAt;
/// The draft's priority, applied to every write.
Priority priority = Priority.defaultValue;
/// The draft's status, applied to every write.
Status status = Status.todo;
/// Persists [text], creating the note row on the first non-empty keystroke.
///
/// Returns whether the row was created by this call, so the caller can log
/// it once rather than on every keystroke.
Future<bool> write(String text, {required bool Function() live}) async {
_text = text;
var created = false;
if (_id == null) {
// A note is only created once the user actually fills something in, so
// an empty template (no section typed yet) never hits storage.
if (text.trim().isEmpty) return false;
_id = _uuid.v4();
_createdAt = DateTime.now();
created = true;
}
await _save(live: live);
return created;
}
/// Re-saves when only priority/status changed.
///
/// Does nothing before the row exists: the new value is already held here
/// and will be applied by the first keystroke that creates it.
Future<void> persistMetadata({required bool Function() live}) async {
if (_id == null) return;
await _save(live: live);
}
Future<void> _save({required bool Function() live}) async {
final now = DateTime.now();
await repository.upsert(
Note(
id: _id!,
text: _text,
priority: priority,
status: status,
createdAt: _createdAt!,
updatedAt: now,
),
);
if (live()) lastSavedAt.value = now;
}
/// Forgets the current draft so the next keystroke starts a new note.
///
/// Nothing is lost: every keystroke was already persisted, which is why
/// the field going blank is the only feedback the screen needs to give.
void reset() {
_text = '';
_id = null;
_createdAt = null;
priority = Priority.defaultValue;
status = Status.todo;
lastSavedAt.value = null;
}
/// Releases the notifier.
void dispose() => lastSavedAt.dispose();
}
// coverage:ignore-start
// Platform file IO for the local backup: BACKLOG.md under ~/todo on desktop
// (the path the user's workflow already reads), or the app documents dir on
// mobile (which Android Auto Backup includes). Exercised by running the app;
// tests inject an in-memory LocalBackup instead.
/// The local backup implementation for whichever platform this is.
LocalBackup platformLocalBackup(NoteRepository repository) =>
createLocalBackup(repository);
// coverage:ignore-end