feat(storage): notes are vault sidecar notebooks
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:
2026-06-24 22:48:18 +08:00
parent 2f0fda5f95
commit f4f0853eae
14 changed files with 598 additions and 70 deletions

View File

@@ -200,4 +200,58 @@ void main() {
expect(sl.scratchpad.strokes.single.id, 's');
after.dispose();
});
group('Phase 4 standalone notebook (notebook.badnote.json)', () {
late String notePath;
setUp(() {
// The synthetic note "source" is <folder>/notebook → sidecar is
// <folder>/notebook.badnote.json (no real source file on disk).
notePath = '${tmpDir.path}/notebook';
});
test('a note\'s strokes (page 0) + title round-trip through the sidecar',
() async {
final repo =
await SidecarRepository.open(notePath, debounce: _fast, docType: 'notebook');
expect(repo.sidecarFile.path, '$notePath.badnote.json');
repo.scheduleTitleSave('My Note');
repo.scheduleStrokeSave(0, [
_stroke('p', tool: EditorTool.pen),
_stroke('h', tool: EditorTool.highlighter),
]);
await repo.flush();
repo.dispose();
// Re-open the same standalone notebook sidecar.
final reopened =
await SidecarRepository.open(notePath, debounce: _fast, docType: 'notebook');
expect(reopened.loadedTitle, 'My Note');
expect(reopened.sidecar.docType, 'notebook');
final page0 = reopened.loadedStrokes[0]!;
expect(page0.map((s) => s.id), ['p', 'h']);
expect(page0.first.tool, EditorTool.pen);
expect(page0.last.tool, EditorTool.highlighter);
expect(page0.first.color, 0xFF112233);
reopened.dispose();
});
test('editing the title persists; unchanged title is a no-op', () async {
final repo =
await SidecarRepository.open(notePath, debounce: _fast, docType: 'notebook');
repo.scheduleTitleSave('First');
await repo.flush();
// Same title again: no write needed, but flush stays safe.
repo.scheduleTitleSave('First');
repo.scheduleTitleSave('Second');
await repo.flush();
repo.dispose();
final reopened =
await SidecarRepository.open(notePath, debounce: _fast, docType: 'notebook');
expect(reopened.loadedTitle, 'Second');
reopened.dispose();
});
});
}