Skip to content

devlargs/notion-pad

Repository files navigation

Notion Pad

Your everyday notepad — but everything saves to Notion automatically. A lightweight Windows desktop app built with WPF on .NET 8.

Why

Notion is a great place to keep things, but its editor is heavy and the desktop app eats RAM. Notion Pad gives you a fast, no-friction text window that mirrors every keystroke (debounced) into a Notion database in the background. Your notes live locally as JSON first; Notion is the mirror.

Features

  • One Notion database, one page per note (uses the database's Name title property)
  • First non-empty line of the note becomes the page title
  • 1.5 s debounced autosave with a per-note serial sync queue
  • Offline-friendly: local JSON is the source of truth; failed syncs queue and retry
  • Per-note sync indicator (Synced / Pending / Syncing / Error) with inline error banner
  • Settings dialog with Test connection so you know the integration works before saving
  • Dark, minimal UI
  • Tiny footprint compared to Electron-based notepads (no Chromium runtime)

Requirements

Notion setup (one-time)

  1. Go to https://www.notion.so/my-integrations and click + New integration.
  2. Name it (e.g. "Notion Pad"), select the workspace, and create it.
  3. Copy the Internal Integration Secret — that's your token.
  4. In Notion, create a database. The screenshot used while designing the app had a single Name (title) column — that's all you need. If you add more columns, Notion Pad just ignores them.
  5. Open the database, click the ••• menu → Connections → add the integration you just created. (Without this step, Notion will return object_not_found.)
  6. Copy the database ID from its URL. It's the 32-character string between the workspace slug and the ?v= query:
    https://www.notion.so/<workspace>/<DATABASE_ID>?v=...
    

Run from source

dotnet run

That's it — the first build restores NuGet packages and launches the window. On first launch, the Settings dialog blocks until you paste your integration token and database ID.

Hot-reload during development:

dotnet watch run

Build a distributable

Framework-dependent (small, requires .NET 8 Desktop Runtime on the target machine):

dotnet publish -c Release -r win-x64 --self-contained false

Self-contained single-file (mirrors what CI publishes — runs on any Win10/11 box):

dotnet publish -c Release -r win-x64 --self-contained true `
  -p:PublishSingleFile=true `
  -p:IncludeNativeLibrariesForSelfExtract=true `
  -p:EnableCompressionInSingleFile=true `
  -p:Version=1.2.3

Output lands in bin/Release/net8.0-windows/win-x64/publish/NotionPad.exe.

Releasing

Releases are produced by .github/workflows/release.yml whenever you push a vMAJOR.MINOR.PATCH tag:

git tag v0.1.0
git push origin v0.1.0

The workflow:

  1. Checks out the repo and installs .NET 8.
  2. Extracts the version from the tag (strips the leading v).
  3. Publishes a self-contained, single-file, compressed NotionPad.exe stamped with that version.
  4. Creates a GitHub Release named Notion Pad vX.Y.Z with auto-generated notes and uploads the exe as the only asset.

The tag must match v\d+\.\d+\.\d+ exactly — pre-release suffixes (v1.0.0-beta) are intentionally rejected so the auto-updater's Version.TryParse never sees a tag it can't compare.

Auto-update

On every launch the app calls UpdaterService.CheckAndApplyAsync (wired in MainWindow.OnLoaded):

  1. Hits GET https://api.github.com/repos/devlargs/notion-pad/releases/latest.
  2. Compares the release's tag_name against the running assembly's Version.
  3. If the release is newer, streams the NotionPad.exe asset to %TEMP%\NotionPad-update-<version>.exe.
  4. Writes a one-shot .cmd script to %TEMP% that waits ~2 s, move /Ys the new exe over the running one (retrying until the file handle is released), and relaunches it.
  5. Shows a small "will restart to apply update" message box, then Application.Shutdown()s. The script then completes the swap and the app reopens on the new version.

Safety gates in UpdaterService.ShouldRun():

  • #if DEBUG → never runs in debug builds (so dotnet run is unaffected).
  • Version.Major == 0 → never runs when the assembly version is still the local 0.0.0 placeholder.
  • Exe path contains \bin\ → never runs when launched out of a build output folder.

Failures (no network, GitHub rate-limit, JSON shape change, etc.) are caught and swallowed silently — an update problem must never block the app from launching.

How it works

+------------------ WPF UI ------------------+
|  MainWindow: sidebar (notes) + editor pane |
|  SettingsWindow (first-run + edit)         |
+--------------------------------------------+
                |
                v
+------------------ Services ----------------+
|  LocalStore   atomic JSON in %AppData%     |
|  NotionClient HttpClient -> api.notion.com |
|  SyncQueue    per-note serial Task chain   |
+--------------------------------------------+
  • Local store lives at %AppData%\NotionPad\notion-pad.json and is rewritten atomically (tmp + rename) on a 200 ms debounce.
  • Title derivation: first non-empty line of the body, trimmed and truncated to 200 chars.
  • Body → blocks: split by blank lines; each paragraph becomes a Notion paragraph block (chunked at 2000 chars to respect Notion's per-block limit).
  • Sync queue: each note has its own serial chain of Tasks — a new save waits for the previous one to finish, so the most recent body always lands last and you don't race delete children against append children.
  • Updates patch the title property, delete all existing children blocks, then append fresh ones. (Simple and correct; not the most efficient for very long notes.)
  • Delete archives the Notion page (sets archived: true) and removes the local entry.

Project layout

NotionPad.csproj
App.xaml / App.xaml.cs
app.manifest
Models/
  Note.cs        Settings.cs
Services/
  LocalStore.cs  NotionClient.cs  SyncQueue.cs
Views/
  Theme.xaml
  MainWindow.xaml(.cs)
  SettingsWindow.xaml(.cs)
  SyncStateConverters.cs

Troubleshooting

  • name is not a property that exists — your database's title column isn't called Name. Rename it in Notion or update TitleProperties in Services/NotionClient.cs.
  • object_not_found — the integration hasn't been shared with the database. Open the DB → ••• → Connections → add it.
  • API token is invalid — re-copy the integration secret; tokens start with secret_ or ntn_.
  • The build can't replace NotionPad.exe — close the running window first; the executable is locked while the app is open.

License

MIT

About

Your everyday notepad! but everything saves to Notion automatically. WPF desktop app, .NET 8.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages