Add notebook.json containers with multi-member pages, fix PDF text
editing (size/bold/drag/double-tap), index SidecarText in search, and
share keyboard page shortcuts plus a PDF scrubber.
Co-authored-by: Cursor <cursoragent@cursor.com>
Search now covers handwriting, the PDF text layer, AND scanned
(rasterized) PDFs.
- PdfTextIndexer runs at import: sums the embedded text layer across
pages; if present it stores that as the document body, otherwise the
PDF is rasterized and its rendered pages are OCR'd in the background.
The result lands in the sidecar `pageText` field (distinct from
`ocrText`, the handwriting OCR). Idempotent (skips a sidecar that
already has pageText); degrades gracefully with no OCR engine.
- pdfrx_page_text_source abstracts text/render so it's testable.
- VaultSearchIndex now harvests title + typed text + handwriting OCR +
PDF pageText, so search finds notes, typed PDFs and scanned PDFs.
analyze clean, 409 tests green.
Add a text-annotation tool to the PDF editor. With the text tool a
pen-tap, or a mouse double-click, drops a text box at that normalized
page point and focuses a real Flutter TextField — so the OS IME and the
Windows-Ink handwriting panel feed it (device-validated). Tapping an
existing box re-opens it; clearing it deletes it.
- SidecarText {nx, ny, text, fontSize (page-relative), color} per page,
glued under zoom; stored in the sidecar `texts` field (back-compat
missing -> none), saved via scheduleTextsSave and loaded on open.
- Rendered in pageOverlaysBuilder at the scaled position.
PDF editor only for now (note text later). analyze clean, 397 tests.
A blank note can show a page-background template painted behind the
ink, picked from the toolbar and persisted per notebook.
- NoteBackground: blank / dots / ruled / grid / cornell, drawn in
page space (scales with zoom), subtle grey. Cornell = left margin +
bottom summary rule over a ruled body.
- Stored as the enum name in the notebook sidecar (back-compat:
missing/unknown -> blank), saved/loaded via SidecarRepository so it
restores on reopen.
- Picker added to the note tool palette.
PDF backgrounds skipped (PDFs have their own page content). analyze
clean, 386 tests green.
Phase 6 (final storage phase).
- SidecarRepositoryRegistry tracks every open repo; SidecarFlushObserver
(a WidgetsBindingObserver in main) flushes them all on
inactive/hidden/paused/detached, awaiting each flush — the last
strokes can't be lost on app close, not just on the 800ms timer.
- VaultSearchIndex rebuilds by scanning vault sidecars (the source of
truth) — note titles, OCR text and document names — and search_provider
queries it, so search spans notes + PDFs. Rebuilt on launch / after
import.
The vault file-based storage migration (Phases 0-6) is complete:
annotations travel with the file, picked vault folder, atomic autosave,
one Import-file entry, SQLite migrated to sidecars. analyze clean,
tests green.
Phase 5. On first launch with a valid vault, migrate legacy SQLite
data into vault sidecars so nothing is lost on upgrade.
- SqliteToSidecarMigrator: documents (+ per-page ink, scratch-links
+ scratchpads, bookmarks) -> notebook folder + <file>.badnote.json;
notes (+ strokes) -> notebook.badnote.json. Reuses existing JSON.
- Idempotent (skips already-migrated targets); missing source files
still get their annotations migrated.
- DB relocates to <vault>/.badnote/index.sqlite; the legacy DB is
renamed to .premigration ONLY after a successful pass, so a failed
migration leaves data intact and the run-once flag unset.
- main.dart runs it once, gated on vaultMigrationDone.
Golden migrator tests (seeded legacy DB -> sidecars, idempotent
re-run, legacy preserved). analyze clean, tests green.
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.
Phase 1 of the file-based storage plan. Pure library, no runtime
behavior change yet (editors still use SQLite).
- BadnoteSidecar: per-file annotation document (schema-versioned)
holding per-page ink (EditorStroke JSON), text highlights,
scratch-link anchors (ScratchLink JSON) each with its own
scratchpad (InkStroke world-coord JSON), and bookmarks. Reuses the
existing toJson formats — no parallel stroke format.
- SidecarStore.writeAtomic: temp-file + rename atomic write keeping a
.bak; read() falls back to .bak on a missing/corrupt primary.
Round-trip + atomic-write + .bak-recovery tests. analyze clean,
322 tests green.