feat(storage): notes are vault sidecar notebooks
All checks were successful
CI / Windows build (push) Successful in 12m55s
All checks were successful
CI / Windows build (push) Successful in 12m55s
Phase 4. Standalone notes move off SQLite into the vault, like the PDF annotations. - "Create notebook" makes a vault folder with a notebook.badnote.json (BadnoteSidecar docType 'notebook' + a title field), opened via SidecarRepository. - PenNoteScreen loads/saves its strokes (page 0) + title to that sidecar instead of the SQLite Note model. - note_provider lists notes from a vault scan (VaultService.scanNotes = folders with notebook.badnote.json and no source file); the doc scan still excludes them. Delete removes the folder. PDF/slide editors unchanged; pre-existing SQLite notes migrate in Phase 5. analyze clean, tests green.
This commit is contained in:
@@ -15,6 +15,8 @@ import '../../providers/note_provider.dart';
|
||||
import '../../providers/ocr_provider.dart';
|
||||
import '../engine/brush.dart';
|
||||
import '../engine/shape_geometry.dart';
|
||||
import '../engine/stroke_model.dart';
|
||||
import '../persistence/sidecar_repository.dart';
|
||||
import '../input/pen_config.dart';
|
||||
import '../input/pen_input_service.dart';
|
||||
import '../input/pressure_curve.dart' show kNaturalPressureGamma;
|
||||
@@ -85,7 +87,17 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
|
||||
bool _dirty = false;
|
||||
bool _needsCenter = true;
|
||||
|
||||
String? _noteId;
|
||||
/// The note's synthetic source path `<folder>/notebook` (also the note id).
|
||||
/// Persistence flows through this note's `notebook.badnote.json` sidecar.
|
||||
String? _notePath;
|
||||
|
||||
/// Per-file sidecar persistence sink (strokes page 0 + title), debounced and
|
||||
/// atomic — replaces the old SQLite Note/noteListProvider write path here.
|
||||
SidecarRepository? _repo;
|
||||
|
||||
/// Page index a standalone note's strokes live under in the sidecar.
|
||||
static const int _notePageIndex = 0;
|
||||
|
||||
final TextEditingController _titleController = TextEditingController();
|
||||
|
||||
PenConfigController? _penConfig;
|
||||
@@ -108,15 +120,56 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
|
||||
PenInputService.instance.start();
|
||||
final note = widget.note;
|
||||
if (note != null) {
|
||||
_noteId = note.id;
|
||||
_notePath = note.id;
|
||||
_titleController.text = note.title;
|
||||
// Seed from the in-memory note's strokes (e.g. tests) until the sidecar
|
||||
// load resolves and (if present) overrides with persisted strokes.
|
||||
_strokes = penStrokesFromInk(note.strokes, kNoteLogicalPage);
|
||||
} else {
|
||||
_titleController.text = 'Untitled';
|
||||
}
|
||||
_initPenConfig();
|
||||
if (_notePath != null) _initPersistence(_notePath!);
|
||||
}
|
||||
|
||||
/// Open the note's `notebook.badnote.json` sidecar and, if it holds persisted
|
||||
/// strokes / a title, hydrate the canvas from them. Strokes load as page-0
|
||||
/// [EditorStroke]s converted to [PenStroke] (mirrors the PDF editor).
|
||||
Future<void> _initPersistence(String notePath) async {
|
||||
final repo = await SidecarRepository.open(notePath, docType: 'notebook');
|
||||
if (!mounted) {
|
||||
repo.dispose();
|
||||
return;
|
||||
}
|
||||
_repo = repo;
|
||||
final loaded = repo.loadedStrokes[_notePageIndex];
|
||||
setState(() {
|
||||
if (loaded != null && loaded.isNotEmpty) {
|
||||
_strokes = [for (final es in loaded) _penStrokeFromEditor(es)];
|
||||
}
|
||||
final title = repo.loadedTitle;
|
||||
if (title != null && title.isNotEmpty) {
|
||||
_titleController.text = title;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// EditorStroke → live PenStroke (mirror of the PDF editor's loader). Brush
|
||||
/// is not persisted (TODO(brush-persist)); derive it from the tool.
|
||||
PenStroke _penStrokeFromEditor(EditorStroke es) => PenStroke(
|
||||
points: es.points
|
||||
.map((ep) => PenPoint(ep.x, ep.y, ep.pressure, tilt: ep.tilt))
|
||||
.toList(),
|
||||
color: es.color,
|
||||
width: es.width,
|
||||
kind: es.tool == EditorTool.highlighter
|
||||
? PenStrokeKind.highlighter
|
||||
: PenStrokeKind.pen,
|
||||
brush: es.tool == EditorTool.highlighter
|
||||
? BrushKind.highlighter
|
||||
: BrushKind.fountainPen,
|
||||
);
|
||||
|
||||
Future<void> _initPenConfig() async {
|
||||
final controller = await PenConfigController.load();
|
||||
if (!mounted) {
|
||||
@@ -136,6 +189,13 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Flush any pending sidecar write before tearing down (atomic write
|
||||
// completes off the widget tree).
|
||||
final repo = _repo;
|
||||
if (repo != null) {
|
||||
repo.flush();
|
||||
repo.dispose();
|
||||
}
|
||||
_penConfig?.removeListener(_onPenConfigChanged);
|
||||
_penConfig?.dispose();
|
||||
_titleController.dispose();
|
||||
@@ -197,8 +257,11 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
|
||||
|
||||
// ── Persistence ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Convert the live pen strokes back to InkStroke and write the note. Creates
|
||||
/// the note row on first save. Triggers local OCR for search indexing.
|
||||
/// Persist the live pen strokes + title to the note's `notebook.badnote.json`
|
||||
/// sidecar (strokes as page-0 [EditorStroke]s; title via the sidecar's title
|
||||
/// field), debounced/atomic via [SidecarRepository]. Creates the notebook
|
||||
/// folder lazily on first save when the screen was opened without a path.
|
||||
/// Refreshes the home list and triggers local OCR for search indexing.
|
||||
Future<void> _save() async {
|
||||
if (!_dirty) return;
|
||||
final notifier = ref.read(noteListProvider.notifier);
|
||||
@@ -206,40 +269,46 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
|
||||
final title = _titleController.text.trim().isEmpty
|
||||
? 'Untitled'
|
||||
: _titleController.text.trim();
|
||||
final inkStrokes = <InkStroke>[
|
||||
for (final s in _strokes)
|
||||
inkStrokeFromPen(s, kNoteLogicalPage,
|
||||
id: _uuid.v4(), createdAt: now),
|
||||
];
|
||||
|
||||
Note saved;
|
||||
if (_noteId == null) {
|
||||
// Lazily create the notebook folder + sidecar repo on first save.
|
||||
if (_repo == null) {
|
||||
final created = await notifier.createNote(title: title);
|
||||
saved = created.copyWith(strokes: inkStrokes, updatedAt: now);
|
||||
await notifier.updateNote(saved);
|
||||
_noteId = saved.id;
|
||||
} else {
|
||||
saved = (widget.note ?? await _noteById(_noteId!)).copyWith(
|
||||
title: title,
|
||||
strokes: inkStrokes,
|
||||
updatedAt: now,
|
||||
);
|
||||
await notifier.updateNote(saved);
|
||||
if (!mounted) return;
|
||||
_notePath = created.id;
|
||||
final repo =
|
||||
await SidecarRepository.open(created.id, docType: 'notebook');
|
||||
if (!mounted) {
|
||||
repo.dispose();
|
||||
return;
|
||||
}
|
||||
_repo = repo;
|
||||
}
|
||||
final repo = _repo!;
|
||||
|
||||
final editorStrokes = <EditorStroke>[
|
||||
for (final s in _strokes) EditorStroke.fromPenStroke(s),
|
||||
];
|
||||
repo.scheduleTitleSave(title);
|
||||
repo.scheduleStrokeSave(_notePageIndex, editorStrokes);
|
||||
await repo.flush();
|
||||
|
||||
// Refresh the home list so the title/recency update is visible on return.
|
||||
await notifier.loadNotes();
|
||||
if (!mounted) return;
|
||||
setState(() => _dirty = false);
|
||||
_runLocalOcr(saved);
|
||||
}
|
||||
|
||||
Future<Note> _noteById(String id) async {
|
||||
final notes = ref.read(noteListProvider).valueOrNull ?? const [];
|
||||
return notes.firstWhere((n) => n.id == id,
|
||||
orElse: () => Note(
|
||||
id: id,
|
||||
title: _titleController.text,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
));
|
||||
// Build an in-memory Note (id = note path) for OCR/FTS indexing only.
|
||||
final inkStrokes = <InkStroke>[
|
||||
for (final s in _strokes)
|
||||
inkStrokeFromPen(s, kNoteLogicalPage, id: _uuid.v4(), createdAt: now),
|
||||
];
|
||||
_runLocalOcr(Note(
|
||||
id: _notePath!,
|
||||
title: title,
|
||||
strokes: inkStrokes,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
));
|
||||
}
|
||||
|
||||
void _runLocalOcr(Note note) {
|
||||
|
||||
Reference in New Issue
Block a user