Buttons (likely fix + ground truth): device diag showed ptr=12577 pen=10056 — WM_POINTER reaches the observer and GetPointerPenInfo succeeds, so the buttons were just read from the wrong field. Native now resolves the barrel from BOTH penFlags(PEN_FLAG_BARREL) AND pointerInfo.pointerFlags(POINTER_FLAG_SECONDBUTTON) — many pens use the latter. It also emits the full raw set (pointerFlags, penFlags, penMask, ButtonChangeType, tilt) plus OR-accumulated flags so a single session reveals exactly which field each button sets. Comprehensive logging (per user request "用好用的log库 / 我手动开启日志再记录"): new DiagnosticLogger emits through dart:developer log(name 'badnote.input') — capturable via `flutter run` / DevTools / `flutter logs` — AND mirrors to a file (path shown in the overlay) for the packaged GUI build that has no console. Manually enabled by the toolbar diagnostic toggle; off by default. PEN lines log on raw-field change; ZOOM lines log every scale frame + rebaselines. Zoom: scale-only glitch rejection didn't stop the jumping, so add focal/position glitch rejection — drop a 2-finger frame whose focal jumps >250px (a touch misread). The full per-frame trace (raw scale, pointerCount, applied change, focal jump, drops) is now logged so the residual cause is unambiguous. InputDiagnostics singleton accumulates the stats; the overlay shows summary + last trace lines + log path + reset. Removed the ad-hoc inline zoom min/max. Dart: analyze clean, 66/66 tests, linux build green. Native compiles on CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BadNote
Local-first Surface Pen note-taking app with PDF/PPT annotation.
All notes, documents, search, and OCR run on your device. No server is required to use the app.
Features
- Ink notes with Surface Pen (pressure, stabilizer, undo/redo)
- PDF and PPT import with page-level annotation
- Full-text search over note titles, typed text, and OCR results
- Local OCR — pluggable, fully on-device. An embedded ONNX recognition backend (cross-platform, CPU/iGPU) with a graceful fallback to the platform's built-in OCR (Windows). See Local OCR.
Build (Windows)
Prerequisites:
- Flutter SDK (3.10+)
- Visual Studio Build Tools with Desktop development with C++
- Developer Mode enabled (for Flutter plugin symlinks)
flutter pub get
flutter build windows --release
Output: build\windows\x64\runner\Release\badnote.exe
The sqlite3 native binary is vendored under vendor/sqlite3/ (configured via
hooks.user_defines in pubspec.yaml), so the build does not download anything
from GitHub — it works fully offline / behind a firewall. To add another
platform or architecture, drop its official release binary from
sqlite3.dart releases into
vendor/sqlite3/ (the build validates each file's SHA-256).
In mainland China, point pub/Flutter at the local mirrors:
$env:PUB_HOSTED_URL="https://pub.flutter-io.cn"
$env:FLUTTER_STORAGE_BASE_URL="https://storage.flutter-io.cn"
flutter pub get
flutter build windows --release
Continuous integration
.gitea/workflows/ci.yml builds a Windows release on a self-hosted Windows
runner. Format/analyze/test run but are non-blocking, so a usable .exe is
produced whenever the compile itself succeeds. It is written for runners behind
the GFW: the checkout action comes from the gitea.com mirror, Flutter is
expected to be pre-installed on the runner, pub uses flutter-io.cn, and the
sqlite3 native binary is vendored.
Self-hosted runner prerequisites
These must hold on the runner machine (they can't be set from the workflow):
- Flutter SDK on
PATHin the runner's shell (the first build step printsflutter --versionand fails fast if it isn't). - Visual Studio Build Tools with Desktop development with C++ (MSVC + Windows SDK) — required to compile the Windows runner and the ONNX Runtime.
- The local proxy running (default
http://127.0.0.1:7890) so theflutter_onnxruntimebuild can fetch the ONNX Runtime native lib. Override via repo secretsHTTP_PROXY/HTTPS_PROXY, or install ONNX Runtime system-wide to skip the download. gitea.comreachable (for the checkout action). If it isn't, switch to the manual-checkout fallback shown inci.yml(it clones from your own Gitea instance), or setDEFAULT_ACTIONS_URL=https://gitea.comin the runner config and use bareactions/checkout@v4.- Runner in host mode with a sane work directory — a malformed workspace
path (e.g.
C:\C:\...) is anact_runnerconfig problem, not a workflow one.
Architecture
lib/
├── screens/ # UI (notes, PDF/PPT annotator, search, settings)
├── services/ # Local business logic
│ ├── database_service.dart # SQLite + FTS5
│ ├── ocr_service.dart # Local OCR orchestration
│ ├── stroke_rasterizer.dart # Ink → PNG for OCR
│ ├── ocr_engine.dart # OCR entry point (delegates to a backend)
│ └── ocr/ # Pluggable OCR backends
│ ├── ocr_backend.dart # Backend interface
│ ├── ocr_backends.dart # Backend selector (ONNX → native)
│ ├── onnx_recognition_backend.dart # Embedded ONNX (cross-platform)
│ ├── native_ocr_backend.dart # OS OCR (Windows WinRT)
│ └── ctc_decoder.dart # Pure-Dart CTC greedy decode
├── providers/ # Riverpod state
└── widgets/ # Ink canvas, toolbars, thumbnails
OCR flow on save:
- Extract typed text from text-tool strokes
- Rasterize handwriting strokes to PNG
- Recognize via the active local OCR backend (embedded ONNX if a model is bundled, otherwise the platform's native OCR)
- Merge recognized text into the local FTS index for search
Local OCR
OCR runs entirely on-device through a pluggable backend (lib/services/ocr/).
OcrBackends selects, in order:
OnnxRecognitionBackend— embedded, cross-platform recognition viaflutter_onnxruntime(CPU/iGPU; suited to low-power APUs). Active only when an ONNX model is bundled.NativeOcrBackend— the OS built-in OCR (Windows WinRT today).
If no backend is available, OCR is a clean no-op — the app still works.
Enabling the embedded ONNX model
The model is not committed (it is large). Fetch it onto your dev machine before building so it bundles as an asset:
tool/fetch_ocr_model.sh # downloads PP-OCRv4 rec ONNX + ppocr_keys_v1.txt
# into assets/models/ocr/ (proxy hint inside)
See assets/models/ocr/README.md. The recognition
geometry / CTC-blank assumptions (PP-OCRv4 mobile rec, 3×48×W, blank=0) are
documented in onnx_recognition_backend.dart and should be verified on-device
against your exact exported model.
Windows build note: the
flutter_onnxruntimeplugin downloads the ONNX Runtime native library (v1.22.0) from GitHub at build time. Behind a firewall, setHTTPS_PROXYfor the build (CMake honours it), or install ONNX Runtime system-wide and build with-DUSE_SYSTEM_ONNXRUNTIME=ON -DONNXRUNTIME_ROOT_DIR=<path>.
Optional server
The server/ directory contains an experimental FastAPI backend (sync + EasyOCR). It is not required for the desktop app and is kept separately for future multi-device sync experiments. See server/README.md.
Development
flutter run -d windows
flutter test