mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 20:27:01 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
New optional note field created_by (kuhy|claude), stored like by/repo and omitted when unset so older builds stay wire-compatible. App UI stamps kuhy; MCP add_task and the CLI stamp claude. list/get/search output includes it. Human notes sort above Claude's; Claude notes are hidden behind a per-device eye toggle. CLI --backfill-created-by [--dry-run] classifies old notes by the node that wrote created_at, writing only the created_by field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
138 lines
4.6 KiB
Dart
138 lines
4.6 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;
|
|
|
|
/// Whether [_id] was handed out by [reserveId] and no write has used it
|
|
/// yet, so the first write still reports the note as created.
|
|
bool _reserved = false;
|
|
|
|
/// Whether a note with content has been persisted for this draft, i.e.
|
|
/// whether committing it is a real capture.
|
|
bool get hasCapture => _id != null && !_reserved && _text.trim().isNotEmpty;
|
|
|
|
/// 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;
|
|
} else if (_reserved) {
|
|
if (text.trim().isEmpty) return false;
|
|
_reserved = false;
|
|
created = true;
|
|
}
|
|
await _save(live: live);
|
|
return created;
|
|
}
|
|
|
|
/// The draft's note id, reserving one (without saving anything) when the
|
|
/// draft has none yet: an attached image is filed under its note's id
|
|
/// before the link that creates the note is inserted.
|
|
String reserveId() {
|
|
if (_id == null) {
|
|
_id = _uuid.v4();
|
|
_createdAt = DateTime.now();
|
|
_reserved = true;
|
|
}
|
|
return _id!;
|
|
}
|
|
|
|
/// 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 || _reserved) 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,
|
|
createdBy: 'kuhy',
|
|
),
|
|
);
|
|
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;
|
|
_reserved = false;
|
|
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 ~/src/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
|