Two-way sync of the vault folder to a user-configured WebDAV server,
so annotations (which travel with the file) sync with the file.
- WebDavSyncService.syncNow: per-file decision — local-only uploads,
remote-only downloads, and when BOTH sides changed since the last
sync it keeps the loser as <file>.conflict-<mtime> on both sides
(last-write-wins by mtime) so no data is ever lost. Creates dirs as
needed; deletes are conservative.
- The decision logic is pure and unit-tested against a fake WebDAV
client; the real client is a thin http adapter (no dio dependency).
- Settings: WebDAV URL / user / password / remote folder, Test
connection, Sync now (with status + last-synced), and an auto-sync
toggle (default OFF).
Real server round-trips are device/server-validated. Credentials are in
SharedPreferences for now (TODO secure-storage). analyze clean, 432 tests.
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.
Add a bookmark tool to the PDF editor. A bookmark anchors to a precise
location: when text is selected it captures the selection's normalized
rect + the start char index in the page text (the true paragraph
anchor); with no selection it falls back to the tapped page + point.
- Bookmark model gains optional normalized anchor rect + charIndex +
label (all absent from JSON when null, so old sidecars still load).
- Bookmarks persist in the sidecar (scheduleBookmarkUpsert) and load on
open; a bookmarks panel lists them and tapping one jumps to its page.
Delete is persisted.
Scoped to the PDF editor (note bookmarks later); scroll-to-anchor is
page-level for now. analyze clean, 391 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 3. Import becomes a single top-level action beside "Create
notebook" and the library is vault-backed.
- VaultService.createNotebook copies a picked file into a fresh
(de-duplicated) notebook folder under the vault; its sidecar lives
beside it, so annotations travel with the file.
- Home screen: one "Import file" action with a multi-extension picker
(pdf / docx / pptx); routes to the editor by extension.
- The document list is now a vault scan (folders with a source file),
not the SQLite documents table — no cache, always correct.
- PPTX soffice detection fix; DOCX convert-on-import is best-effort
and fails gracefully when LibreOffice is unavailable.
analyze clean, tests green.
Phase 2 (core swap). The PDF editor and split-view scratchpad stop
writing SQLite and persist to a per-file sidecar
`<pdfPath>.badnote.json` (debounced, atomic temp+rename+.bak) — so
annotations travel with the file. The source path is the identity
(no more djb2 doc-id).
- SidecarRepository wraps the Phase-1 store with debounced autosave.
- pen_editor: per-page ink, scratch-links AND highlights now persist
to the sidecar and restore on reopen (closes persist-highlights).
- New "un-highlight" tool: tap a stored highlight to remove it — the
highlight could not be removed before.
- split_view: each anchor's scratchpad lives in the sidecar's
scratchLinks[id].scratchpad, keyed by anchor id.
Note: pre-existing SQLite annotations are migrated later (Phase 5);
note/slide editors swap in Phase 4. analyze clean, tests green.
Phase 0 of the file-based storage plan (docs/plans/
2026-06-24-file-based-storage.md). Foundation only — no editor or
DB change yet.
- VaultService (SharedPreferences): stores the vault root path,
vaultRootValid() = path set AND directory exists.
- VaultSetupScreen: first-run folder picker (file_picker, Windows).
- main.dart gates HomeScreen behind a valid vault, re-prompting if
the saved folder is gone.
- Settings: a Vault section to change the folder.
Editors still use SQLite; later phases move annotations into
per-file sidecars under the vault. analyze clean, tests green.
Replace the ad-hoc tool palette with a shared tool system
(EditorToolKind) across the PDF, note and slide editors, and add
the core writing tools.
- Multiple brushes, each remembering its OWN color (rnote-style):
selecting a brush restores its color, changing color updates only
that brush, and each brush button shows its current color.
- Select tool: tap-select a committed stroke, drag to move it,
delete it — persisted and undoable.
- Shape tool: line / rectangle / ellipse / arrow, drawn with a live
preview and committed as generated PenStrokes (shape_geometry.dart)
so they reuse stroke rendering, erase, persistence and undo.
- Highlighter + eraser fold into the same tool system.
Text/bookmark/search+OCR/backgrounds/Windows-Ink are later batches
(TODO). Brush opacity still deferred. analyze clean, 302 tests.
Replace the rejected standalone sticky-card board with the real
feature: place a link anchor anywhere on a PDF page, tap it to open
split view whose right pane is THAT anchor's own infinite scratchpad
(keyed by anchor id) — like a paper sticky-note tab.
- ScratchLink model + scratch_links table (id, doc, page, nx, ny).
- PDF editor: "place link" tool drops/loads/shows tappable markers;
tap opens SplitViewScreen for that anchor; long-press deletes.
- SplitViewScreen rebuilt on pdfrx (was syncfusion), right scratchpad
keyed by scratchLinkId, new brush palette (was AnnotationToolbar).
- Remove board_screen + its test + the home board entry.
analyze clean, tests green.
Wire the F7 双链 + 无限便利贴 model (Board/LinkGraph) into a
reachable screen. Previously the model existed but had no UI and
no entry point.
board_screen.dart: an infinite InteractiveViewer canvas of
draggable, editable sticky cards. Card text renders [[links]] as
tappable chips that pan to the target card (dangling links styled
apart). A backlinks panel lists "linked from" via backlinksOf.
"Add card" FAB drops a card at the viewport center.
Persistence: a board_cards table (DB v7), one row per card,
debounced 800ms like the ink editors, loaded on open — boards
survive restart. Entry added to the home screen app bar
(dashboard_customize icon).
Ink-on-cards, multi-board management and link autocomplete are
deferred (TODO board-ink / board-multi / board-link-autocomplete).
analyze clean, 285 tests green.
Replace the 2-tool ink system with a data-driven, Krita-style
BrushProfile (lib/editor/engine/brush.dart). Adding a brush is a
const map entry, not render-path branching.
Four presets from the rnote/krita spec:
- fountain pen: quadratic (p^2) pressure, wide dynamic width
- ballpoint: near-constant width (thinning 0.15)
- highlighter: flat width, square caps
- pencil: sqrt(p) pressure, moderate width
Pressure is pre-warped per brush via PressureCurve(gamma) before
perfect_freehand; geometry fields (thinning/streamline/smoothing/
caps) flow through the shared stroke recipe so the PDF overlay and
the note/slide PenCanvas both honor the brush. Brush kind is now
persisted on the stroke model. Picker added to all three toolbars.
Opacity/multiply and pencil grain are carried as data but not yet
composited (TODO brush-opacity / brush-texture); this increment is
width + pressure-curve differentiation. analyze clean, 283 tests.
Replace the single-page PdfPageView bitmap with a pdfrx
PdfViewer: real vector text, continuous scroll, native
pinch-zoom (no custom zoom solver, so no zoom-jump here).
Ink is glued per-page via pageOverlaysBuilder; the pen is
captured at the viewer level by PenCaptureRegion while touch
falls through to scroll / pinch / text-select.
Add select-text -> highlight via PdfTextSelectionParams: the
selection's fragment rects are stored as normalized page rects
and drawn under the ink. In-memory only for now.
Per-page persistence, undo/redo, tools, colors, thumbnails and
pen settings are reused verbatim. analyze clean, 270 tests green.
Extend the en/zh localization to the two screens reached from the home
app bar (the "全都用 + 多语言" ask):
- settings: title, theme-mode segments (System/Light/Dark), seed-color
description, color-picker + clear-data dialogs.
- search: hint, error, empty/no-results states, Notes/Documents section
headers, page label.
New ARB keys regenerated; l10n_test now asserts the new keys resolve in
both English and Chinese.
flutter analyze: 0 issues. l10n_test: 3/3.
The app had zero localization ("软件多语言做了吗" — no). Add Flutter's
official gen-l10n pipeline and localize the core flow the user sees.
- pubspec: flutter_localizations + intl + generate: true
- l10n.yaml + lib/l10n/app_en.arb + app_zh.arb (37 strings)
- main.dart: localizationsDelegates + supportedLocales (follows OS locale)
- pen editor: all tool tooltips, page pill, error states localized
- home: app bar actions + empty-state buttons localized
Proven end-to-end: l10n_test pumps the same widget under Locale('en')
and Locale('zh') and asserts English vs Chinese strings resolve.
flutter analyze: 0 issues. l10n_test: 3/3 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>