v1.0.0: security fixes, code review cleanup, release prep

Security:
- Add path traversal protection (safe_vault_path) for all file operations
- Sanitize markdown preview with DOMPurify to prevent XSS
- Fix encryption: decrypted content no longer written back to disk
- Harden CSP for Tauri v2 production mode

Code quality:
- Remove dead code (cache.rs, NoteView, unused tabs/activeTab state)
- Fix stale closure in debounced save using refs
- Fix image paste to use domToMarkdown instead of innerText
- Single-pass widget rendering (was 3 sequential innerHTML clobbers)
- Move snapshot logic into save callback
- Remove no-op updateWikilinks in drag-and-drop
- Wire CSS editor to Cmd+U shortcut
- Replace hardcoded vault path with portable fallback

Performance:
- LazyLock static regexes for WIKILINK_RE and TAG_RE
- Fix duplicate dirs_config_path, update CSS commands

Release:
- Bump version to 1.0.0
- Rewrite README with project documentation
- Update CHANGELOG with 1.0.0 entry
- Update .gitignore for build artifacts and vault data
- Update canvas dependency path
- Fix canvas API compatibility (onSelectionChange, removed props)
This commit is contained in:
enzotar 2026-03-09 18:15:39 -07:00
parent c1f556b86b
commit 0d26e63c9a
15 changed files with 658 additions and 505 deletions

6
.gitignore vendored
View file

@ -12,6 +12,12 @@ dist
dist-ssr dist-ssr
*.local *.local
# Rust build artifacts
src-tauri/target/
# User vault data
vault/
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json

View file

@ -4,6 +4,25 @@ All notable changes to Graph Notes will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
## [1.0.0] — 2026-03-09
### 🎉 First Stable Release
Graph Notes reaches 1.0 — a local-first, graph-based note-taking app built with Tauri, React, and Rust.
### Fixed
- **Rust compilation** — resolved duplicate `dirs_config_path()` definition and removed reference to unlinked `dirs` crate
- **Content Security Policy** — replaced `null` CSP with a proper baseline policy allowing local resources and Google Fonts
- **Canvas dependency** — updated `@blinksgg/canvas` to correct local file path
### Removed
- **Dead code** — removed unused `cache.rs` module (196 lines) that was never compiled (no `mod cache;` declaration, missing `notify` crate dependency)
### Changed
- **README** — replaced Vite template boilerplate with comprehensive project documentation
- **.gitignore** — added `src-tauri/target/` and `vault/` exclusions
- **Version** — bumped from 0.9.0 → 1.0.0 across `package.json`, `Cargo.toml`, and `tauri.conf.json`
## [0.9.0] — 2026-03-09 ## [0.9.0] — 2026-03-09
### Added ### Added
@ -173,27 +192,3 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
- **Daily Notes** — Auto-generated daily journal entries accessible from sidebar shortcut - **Daily Notes** — Auto-generated daily journal entries accessible from sidebar shortcut
- **Auto-Save** — Debounced 500ms save on every keystroke - **Auto-Save** — Debounced 500ms save on every keystroke
- **Custom Scrollbars** — Minimal 5px scrollbars matching the dark theme - **Custom Scrollbars** — Minimal 5px scrollbars matching the dark theme
## [0.4.0] - 2026-03-07 (origin)
### Added
- **VaultCache** (`src-tauri/src/cache.rs`): In-memory note cache with mtime-based invalidation
- **`init_vault_cache`/`get_cache_stats`**: Startup scan + diagnostics
- **Cache-backed commands**: `read_note`, `build_graph`, `search_vault` from cache
- **Frontend LRU cache** (`src/lib/noteCache.ts`): 20-note stale-while-revalidate
- **File watcher** (`notify` crate): FS change detection foundation
## [0.3.0] - 2026-03-07 (origin)
### Added
- **Frontmatter Parsing**: YAML metadata, `read_note_with_meta`, TOC panel, templates, attachments
## [0.2.0] - 2026-03-07 (origin)
### Added
- **Command Palette**, keyboard shortcuts, full-text search, rename/delete, graph filtering
## [0.1.0] - 2025-06-01 (origin)
### Added
- Tauri 2 + React 19, editor, wikilinks, graph, sidebar, backlinks, daily notes

View file

@ -1,7 +1,86 @@
# Tauri + React + Typescript # Graph Notes
This template should help get you started developing with Tauri, React and Typescript in Vite. A local-first, graph-based note-taking app built with Tauri, React, and Rust. Think Obsidian meets Roam — your notes live as plain Markdown files on disk, connected by `[[wikilinks]]` and visualized as an interactive knowledge graph.
## Recommended IDE Setup ## Features
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) **Core Editing**
- Rich contenteditable editor with inline Markdown styling
- `[[Wikilink]]` autocomplete with note creation on the fly
- Slash commands, heading folding, split-pane editing
- Tabbed editor with drag-reorder
**Knowledge Graph**
- Force-directed graph visualization with semantic zoom
- Graph filtering, focus mode, orphan detection
- Local backlink graph and analytics dashboard
**Organization**
- Sidebar file tree with drag-and-drop, folders, favorites, pinning
- Full-text search, command palette (`⌘K`), tags
- Calendar view, timeline view, kanban board
**Advanced**
- Frontmatter properties panel and database views (table/gallery/list)
- Dataview queries — inline `TABLE ... SORT ...` blocks
- Canvas whiteboard for freeform visual thinking
- Spaced repetition flashcards (SM-2 scheduling)
- Note encryption (AES-256-GCM with Argon2 key derivation)
- Git sync (commit/push/pull panel)
- Version history with inline diff viewer
**Customization**
- 5 built-in themes with live preview
- Custom CSS snippets editor
- Configurable keyboard shortcuts
- Workspace layouts (save/restore window arrangements)
**Import / Export**
- Export vault as ZIP, individual notes as HTML or PDF
- Import Markdown folders from Obsidian, Notion, etc.
## Tech Stack
| Layer | Technology |
|---|---|
| Desktop shell | [Tauri v2](https://tauri.app) |
| Frontend | React 19, TypeScript, Vite |
| Backend | Rust (filesystem, search, encryption, graph) |
| Styling | Tailwind CSS 4 + custom design tokens |
| Graph engine | [@blinksgg/canvas](https://github.com/blinksgg), d3-force, graphology |
## Development
```bash
# Install dependencies
npm install
# Run in development mode (starts both Vite dev server and Tauri)
npm run tauri dev
# Build for production
npm run tauri build
```
### Prerequisites
- [Node.js](https://nodejs.org) ≥ 18
- [Rust](https://rustup.rs) toolchain
- [Tauri v2 prerequisites](https://v2.tauri.app/start/prerequisites/)
## Project Structure
```
├── src/ # React frontend
│ ├── components/ # 33 UI components
│ ├── lib/ # Commands, frontmatter, wikilinks, note cache
│ └── index.css # Design system (tokens, component styles)
├── src-tauri/ # Rust backend
│ └── src/lib.rs # Tauri commands (notes, graph, search, encryption, git)
├── vault/ # Default vault directory (gitignored)
└── package.json
```
## License
MIT

75
package-lock.json generated
View file

@ -1,19 +1,20 @@
{ {
"name": "graph-notes", "name": "graph-notes",
"version": "0.6.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "graph-notes", "name": "graph-notes",
"version": "0.6.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@blinksgg/canvas": "file:../blinksgg/gg-antifragile/packages/canvas", "@blinksgg/canvas": "file:../space-operator/gg/packages/canvas",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-dialog": "^2",
"@tauri-apps/plugin-fs": "^2", "@tauri-apps/plugin-fs": "^2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"d3-force": "^3.0.0", "d3-force": "^3.0.0",
"dompurify": "^3.3.2",
"graphology": "^0.26.0", "graphology": "^0.26.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"jotai": "^2.18.0", "jotai": "^2.18.0",
@ -26,6 +27,7 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tauri-apps/cli": "^2", "@tauri-apps/cli": "^2",
"@types/dompurify": "^3.0.5",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0", "@vitejs/plugin-react": "^4.6.0",
@ -37,6 +39,7 @@
"../blinksgg/gg-antifragile/packages/canvas": { "../blinksgg/gg-antifragile/packages/canvas": {
"name": "@blinksgg/canvas", "name": "@blinksgg/canvas",
"version": "0.13.0", "version": "0.13.0",
"extraneous": true,
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.49.5", "@supabase/supabase-js": "^2.49.5",
"@use-gesture/react": "^10.3.1", "@use-gesture/react": "^10.3.1",
@ -83,6 +86,56 @@
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
} }
}, },
"../space-operator/gg/packages/canvas": {
"name": "@blinksgg/canvas",
"version": "0.35.0",
"dependencies": {
"@supabase/supabase-js": "^2.49.5",
"@use-gesture/react": "^10.3.1",
"debug": "^4.4.3",
"graphology": "^0.26.0",
"graphology-types": "^0.24.8",
"jotai-family": "^1.0.1"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@blocknote/core": "^0.45.0",
"@blocknote/react": "^0.45.0",
"@blocknote/shadcn": "^0.45.0",
"@tanstack/react-query": "^5.17.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/d3-force": "^3.0.10",
"@types/debug": "^4.1.12",
"@types/node": "^24.5.2",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"babel-plugin-react-compiler": "^1.0.0",
"d3-force": "^3.0.0",
"esbuild-plugin-babel": "^0.2.3",
"jotai": "^2.6.0",
"jsdom": "^26.1.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"tsup": "^8.0.0",
"typescript": "^5.3.0",
"vitest": "^3.2.1"
},
"peerDependencies": {
"@blocknote/core": "^0.45.0",
"@blocknote/react": "^0.45.0",
"@blocknote/shadcn": "^0.45.0",
"@tanstack/react-query": "^5.17.0",
"d3-force": "^3.0.0",
"jotai": "^2.6.0",
"jotai-tanstack-query": "*",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/@antfu/install-pkg": { "node_modules/@antfu/install-pkg": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
@ -379,7 +432,7 @@
} }
}, },
"node_modules/@blinksgg/canvas": { "node_modules/@blinksgg/canvas": {
"resolved": "../blinksgg/gg-antifragile/packages/canvas", "resolved": "../space-operator/gg/packages/canvas",
"link": true "link": true
}, },
"node_modules/@braintree/sanitize-url": { "node_modules/@braintree/sanitize-url": {
@ -2126,6 +2179,16 @@
"@types/d3-selection": "*" "@types/d3-selection": "*"
} }
}, },
"node_modules/@types/dompurify": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/trusted-types": "*"
}
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -2163,8 +2226,8 @@
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT", "devOptional": true,
"optional": true "license": "MIT"
}, },
"node_modules/@vitejs/plugin-react": { "node_modules/@vitejs/plugin-react": {
"version": "4.7.0", "version": "4.7.0",

View file

@ -1,7 +1,7 @@
{ {
"name": "graph-notes", "name": "graph-notes",
"private": true, "private": true,
"version": "0.9.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -10,12 +10,13 @@
"tauri": "tauri" "tauri": "tauri"
}, },
"dependencies": { "dependencies": {
"@blinksgg/canvas": "file:../blinksgg/gg-antifragile/packages/canvas", "@blinksgg/canvas": "file:../space-operator/gg/packages/canvas",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-dialog": "^2",
"@tauri-apps/plugin-fs": "^2", "@tauri-apps/plugin-fs": "^2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"d3-force": "^3.0.0", "d3-force": "^3.0.0",
"dompurify": "^3.3.2",
"graphology": "^0.26.0", "graphology": "^0.26.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"jotai": "^2.18.0", "jotai": "^2.18.0",
@ -28,6 +29,7 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tauri-apps/cli": "^2", "@tauri-apps/cli": "^2",
"@types/dompurify": "^3.0.5",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0", "@vitejs/plugin-react": "^4.6.0",
@ -35,4 +37,4 @@
"typescript": "~5.8.3", "typescript": "~5.8.3",
"vite": "^7.0.4" "vite": "^7.0.4"
} }
} }

556
src-tauri/Cargo.lock generated
View file

@ -8,6 +8,41 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.4" version = "1.1.4"
@ -47,6 +82,27 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "argon2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
"password-hash",
]
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.7.2" version = "0.7.2"
@ -225,6 +281,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -240,6 +302,15 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -319,6 +390,25 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bzip2"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
dependencies = [
"bzip2-sys",
]
[[package]]
name = "bzip2-sys"
version = "0.1.13+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
dependencies = [
"cc",
"pkg-config",
]
[[package]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.18.5" version = "0.18.5"
@ -393,6 +483,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver",
"libc",
"shlex", "shlex",
] ]
@ -443,6 +535,16 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
@ -462,6 +564,12 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@ -527,6 +635,21 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@ -558,6 +681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"rand_core 0.6.4",
"typenum", "typenum",
] ]
@ -598,6 +722,15 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.21.3" version = "0.21.3"
@ -633,6 +766,12 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "deflate64"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.5.8" version = "0.5.8"
@ -643,6 +782,17 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.20" version = "0.99.20"
@ -664,6 +814,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"crypto-common", "crypto-common",
"subtle",
] ]
[[package]] [[package]]
@ -889,17 +1040,6 @@ dependencies = [
"rustc_version", "rustc_version",
] ]
[[package]]
name = "filetime"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
dependencies = [
"cfg-if",
"libc",
"libredox",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.9"
@ -964,15 +1104,6 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "futf" name = "futf"
version = "0.1.5" version = "0.1.5"
@ -1214,9 +1345,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi 5.3.0", "r-efi 5.3.0",
"wasip2", "wasip2",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -1232,6 +1365,16 @@ dependencies = [
"wasip3", "wasip3",
] ]
[[package]]
name = "ghash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]] [[package]]
name = "gio" name = "gio"
version = "0.18.4" version = "0.18.4"
@ -1330,10 +1473,13 @@ dependencies = [
[[package]] [[package]]
name = "graph-notes" name = "graph-notes"
version = "0.1.0" version = "1.0.0"
dependencies = [ dependencies = [
"aes-gcm",
"argon2",
"base64 0.22.1",
"chrono", "chrono",
"notify", "rand 0.8.5",
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",
@ -1343,6 +1489,7 @@ dependencies = [
"tauri-plugin-fs", "tauri-plugin-fs",
"tauri-plugin-opener", "tauri-plugin-opener",
"walkdir", "walkdir",
"zip",
] ]
[[package]] [[package]]
@ -1442,6 +1589,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "html5ever" name = "html5ever"
version = "0.29.1" version = "0.29.1"
@ -1718,23 +1874,12 @@ dependencies = [
] ]
[[package]] [[package]]
name = "inotify" name = "inout"
version = "0.9.6" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [ dependencies = [
"bitflags 1.3.2", "generic-array",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
] ]
[[package]] [[package]]
@ -1823,6 +1968,16 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.91" version = "0.3.91"
@ -1866,26 +2021,6 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "kqueue"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]] [[package]]
name = "kuchikiki" name = "kuchikiki"
version = "0.8.8-speedreader" version = "0.8.8-speedreader"
@ -1950,10 +2085,7 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
dependencies = [ dependencies = [
"bitflags 2.11.0",
"libc", "libc",
"plain",
"redox_syscall 0.7.3",
] ]
[[package]] [[package]]
@ -1983,6 +2115,27 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lzma-rs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
dependencies = [
"byteorder",
"crc",
]
[[package]]
name = "lzma-sys"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "mac" name = "mac"
version = "0.1.1" version = "0.1.1"
@ -2051,18 +2204,6 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.1.1" version = "1.1.1"
@ -2137,25 +2278,6 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "notify"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.11.0",
"crossbeam-channel",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio 0.8.11",
"walkdir",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.0" version = "0.2.0"
@ -2322,6 +2444,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "open" name = "open"
version = "5.3.3" version = "5.3.3"
@ -2399,17 +2527,38 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall 0.5.18", "redox_syscall",
"smallvec", "smallvec",
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]] [[package]]
name = "pathdiff" name = "pathdiff"
version = "0.2.3" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@ -2579,12 +2728,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]] [[package]]
name = "plist" name = "plist"
version = "1.8.0" version = "1.8.0"
@ -2625,6 +2768,18 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "polyval"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.4" version = "0.1.4"
@ -2859,15 +3014,6 @@ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
] ]
[[package]]
name = "redox_syscall"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
dependencies = [
"bitflags 2.11.0",
]
[[package]] [[package]]
name = "redox_users" name = "redox_users"
version = "0.5.2" version = "0.5.2"
@ -3266,6 +3412,17 @@ dependencies = [
"stable_deref_trait", "stable_deref_trait",
] ]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.9" version = "0.10.9"
@ -3348,7 +3505,7 @@ dependencies = [
"objc2-foundation", "objc2-foundation",
"objc2-quartz-core", "objc2-quartz-core",
"raw-window-handle", "raw-window-handle",
"redox_syscall 0.5.18", "redox_syscall",
"tracing", "tracing",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
@ -3418,6 +3575,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "swift-rs" name = "swift-rs"
version = "1.0.7" version = "1.0.7"
@ -3945,7 +4108,7 @@ checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio 1.1.1", "mio",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@ -4255,6 +4418,16 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.8" version = "2.5.8"
@ -4775,15 +4948,6 @@ dependencies = [
"windows-targets 0.42.2", "windows-targets 0.42.2",
] ]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.59.0" version = "0.59.0"
@ -4826,21 +4990,6 @@ dependencies = [
"windows_x86_64_msvc 0.42.2", "windows_x86_64_msvc 0.42.2",
] ]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@ -4898,12 +5047,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
@ -4922,12 +5065,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
@ -4946,12 +5083,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
@ -4982,12 +5113,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
@ -5006,12 +5131,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
@ -5030,12 +5149,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
@ -5054,12 +5167,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
@ -5260,6 +5367,15 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "xz2"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
dependencies = [
"lzma-sys",
]
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.1" version = "0.8.1"
@ -5385,6 +5501,26 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"
version = "0.2.3" version = "0.2.3"
@ -5418,12 +5554,82 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "zip"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
dependencies = [
"aes",
"arbitrary",
"bzip2",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"deflate64",
"displaydoc",
"flate2",
"getrandom 0.3.4",
"hmac",
"indexmap 2.13.0",
"lzma-rs",
"memchr",
"pbkdf2",
"sha1",
"thiserror 2.0.18",
"time",
"xz2",
"zeroize",
"zopfli",
"zstd",
]
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.21" version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zopfli"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]
[[package]]
name = "zstd"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",
]
[[package]] [[package]]
name = "zvariant" name = "zvariant"
version = "5.10.0" version = "5.10.0"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "graph-notes" name = "graph-notes"
version = "0.9.0" version = "1.0.0"
description = "A graph-based note-taking app" description = "A graph-based note-taking app"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"

View file

@ -1,195 +0,0 @@
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use notify::{RecommendedWatcher, RecursiveMode, Watcher, Event, EventKind};
use walkdir::WalkDir;
use crate::{NoteMeta, HeadingEntry, parse_frontmatter, extract_headings, extract_wikilinks};
/* ── Cached Note ───────────────────────────────────────────── */
#[derive(Debug, Clone)]
pub struct CachedNote {
pub content: String,
pub meta: NoteMeta,
pub body: String,
pub headings: Vec<HeadingEntry>,
pub links: Vec<String>,
pub mtime: SystemTime,
pub rel_path: String,
pub name: String,
}
/* ── Vault Cache ───────────────────────────────────────────── */
pub struct VaultCache {
pub vault_path: PathBuf,
pub entries: HashMap<String, CachedNote>,
pub hits: u64,
pub misses: u64,
}
impl VaultCache {
pub fn new(vault_path: &str) -> Self {
VaultCache {
vault_path: PathBuf::from(vault_path),
entries: HashMap::new(),
hits: 0,
misses: 0,
}
}
/// Scan all .md files in the vault and populate the cache.
pub fn scan_all(&mut self) {
let vault = self.vault_path.clone();
if !vault.exists() {
return;
}
// Collect paths first to avoid borrow conflict
let paths: Vec<String> = WalkDir::new(&vault)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
e.path().extension().map_or(false, |ext| ext == "md")
&& !e.path().strip_prefix(&vault)
.map_or(false, |p| {
let s = p.to_string_lossy();
s.starts_with("_templates") || s.starts_with("attachments")
})
})
.filter_map(|entry| {
entry
.path()
.strip_prefix(&vault)
.ok()
.map(|p| p.to_string_lossy().to_string())
})
.collect();
for rel in paths {
self.load_entry(&rel);
}
}
/// Load or reload a single entry from disk.
pub fn load_entry(&mut self, rel_path: &str) -> Option<&CachedNote> {
let full = self.vault_path.join(rel_path);
let mtime = fs::metadata(&full).ok()?.modified().ok()?;
let content = fs::read_to_string(&full).ok()?;
let (meta, body) = parse_frontmatter(&content);
let headings = extract_headings(&content);
let links = extract_wikilinks(&content);
let name = Path::new(rel_path)
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string();
self.entries.insert(
rel_path.to_string(),
CachedNote {
content,
meta,
body,
headings,
links,
mtime,
rel_path: rel_path.to_string(),
name,
},
);
self.entries.get(rel_path)
}
/// Get a cached entry, reloading from disk if mtime changed.
pub fn get(&mut self, rel_path: &str) -> Option<&CachedNote> {
let full = self.vault_path.join(rel_path);
let disk_mtime = fs::metadata(&full).ok().and_then(|m| m.modified().ok());
if let Some(cached) = self.entries.get(rel_path) {
if let Some(dm) = disk_mtime {
if cached.mtime >= dm {
self.hits += 1;
return self.entries.get(rel_path);
}
}
}
self.misses += 1;
self.load_entry(rel_path)
}
/// Invalidate (remove) a single entry.
pub fn invalidate(&mut self, rel_path: &str) {
self.entries.remove(rel_path);
}
/// Invalidate and reload an entry (for file changes).
pub fn refresh(&mut self, rel_path: &str) {
self.entries.remove(rel_path);
let full = self.vault_path.join(rel_path);
if full.exists() {
self.load_entry(rel_path);
}
}
}
/* ── File Watcher ──────────────────────────────────────────── */
pub type SharedCache = Arc<Mutex<VaultCache>>;
/// Start a filesystem watcher on the vault directory.
/// Returns the watcher handle (must be kept alive) and processes events
/// by invalidating cache entries and calling the callback.
pub fn start_watcher<F>(
cache: SharedCache,
vault_path: &str,
on_change: F,
) -> Option<RecommendedWatcher>
where
F: Fn(Vec<String>) + Send + 'static,
{
let vault = PathBuf::from(vault_path);
let vault_for_closure = vault.clone();
let cache_for_closure = cache.clone();
let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
if let Ok(event) = res {
match event.kind {
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {
let mut changed: Vec<String> = Vec::new();
for path in &event.paths {
if path.extension().map_or(false, |e| e == "md") {
if let Ok(rel) = path.strip_prefix(&vault_for_closure) {
let rel_str = rel.to_string_lossy().to_string();
if let Ok(mut c) = cache_for_closure.lock() {
match event.kind {
EventKind::Remove(_) => c.invalidate(&rel_str),
_ => c.refresh(&rel_str),
}
}
changed.push(rel_str);
}
}
}
if !changed.is_empty() {
on_change(changed);
}
}
_ => {}
}
}
})
.ok()?;
watcher.watch(vault.as_path(), RecursiveMode::Recursive).ok()?;
Some(watcher)
}

View file

@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use walkdir::WalkDir; use walkdir::WalkDir;
use regex::Regex; use regex::Regex;
use chrono::Local; use chrono::Local;
@ -37,9 +38,12 @@ fn normalize_note_name(name: &str) -> String {
name.trim().to_lowercase() name.trim().to_lowercase()
} }
static WIKILINK_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]").unwrap()
});
fn extract_wikilinks(content: &str) -> Vec<String> { fn extract_wikilinks(content: &str) -> Vec<String> {
let re = Regex::new(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]").unwrap(); WIKILINK_RE.captures_iter(content)
re.captures_iter(content)
.map(|cap| cap[1].trim().to_string()) .map(|cap| cap[1].trim().to_string())
.collect() .collect()
} }
@ -94,15 +98,39 @@ fn list_notes(vault_path: String) -> Result<Vec<NoteEntry>, String> {
Ok(build_tree(vault, vault)) Ok(build_tree(vault, vault))
} }
/// Validate that a relative path stays within the vault root.
/// Returns the canonical full path, or an error if it escapes.
fn safe_vault_path(vault_path: &str, relative_path: &str) -> Result<PathBuf, String> {
let vault = Path::new(vault_path)
.canonicalize()
.map_err(|e| format!("Invalid vault path: {}", e))?;
let full = vault.join(relative_path);
// Canonicalize only works if the file exists; for new files, normalize manually
let normalized = if full.exists() {
full.canonicalize().map_err(|e| format!("Invalid path: {}", e))?
} else {
// For new files: resolve parent (must exist) + filename
let parent = full.parent()
.ok_or_else(|| "Invalid path".to_string())?;
let parent_canon = parent.canonicalize()
.unwrap_or_else(|_| parent.to_path_buf());
parent_canon.join(full.file_name().unwrap_or_default())
};
if !normalized.starts_with(&vault) {
return Err("Path escapes the vault directory".to_string());
}
Ok(normalized)
}
#[tauri::command] #[tauri::command]
fn read_note(vault_path: String, relative_path: String) -> Result<String, String> { fn read_note(vault_path: String, relative_path: String) -> Result<String, String> {
let full_path = Path::new(&vault_path).join(&relative_path); let full_path = safe_vault_path(&vault_path, &relative_path)?;
fs::read_to_string(&full_path).map_err(|e| format!("Failed to read note: {}", e)) fs::read_to_string(&full_path).map_err(|e| format!("Failed to read note: {}", e))
} }
#[tauri::command] #[tauri::command]
fn write_note(vault_path: String, relative_path: String, content: String) -> Result<(), String> { fn write_note(vault_path: String, relative_path: String, content: String) -> Result<(), String> {
let full_path = Path::new(&vault_path).join(&relative_path); let full_path = safe_vault_path(&vault_path, &relative_path)?;
// Ensure parent directory exists // Ensure parent directory exists
if let Some(parent) = full_path.parent() { if let Some(parent) = full_path.parent() {
@ -114,7 +142,7 @@ fn write_note(vault_path: String, relative_path: String, content: String) -> Res
#[tauri::command] #[tauri::command]
fn delete_note(vault_path: String, relative_path: String) -> Result<(), String> { fn delete_note(vault_path: String, relative_path: String) -> Result<(), String> {
let full_path = Path::new(&vault_path).join(&relative_path); let full_path = safe_vault_path(&vault_path, &relative_path)?;
if full_path.is_file() { if full_path.is_file() {
fs::remove_file(&full_path).map_err(|e| format!("Failed to delete note: {}", e)) fs::remove_file(&full_path).map_err(|e| format!("Failed to delete note: {}", e))
} else { } else {
@ -367,9 +395,12 @@ pub struct TagInfo {
pub notes: Vec<String>, pub notes: Vec<String>,
} }
static TAG_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?:^|[\s,;(])(#[a-zA-Z][a-zA-Z0-9_/-]*)").unwrap()
});
fn extract_tags(content: &str) -> Vec<String> { fn extract_tags(content: &str) -> Vec<String> {
let re = Regex::new(r"(?:^|[\s,;(])(#[a-zA-Z][a-zA-Z0-9_/-]*)").unwrap(); let mut tags: Vec<String> = TAG_RE
let mut tags: Vec<String> = re
.captures_iter(content) .captures_iter(content)
.map(|cap| cap[1].to_string()) .map(|cap| cap[1].to_string())
.collect(); .collect();
@ -1302,7 +1333,7 @@ fn load_fold_state(vault_path: String, note_path: String) -> Result<Vec<usize>,
#[tauri::command] #[tauri::command]
fn get_custom_css() -> Result<String, String> { fn get_custom_css() -> Result<String, String> {
let config_dir = dirs_config_path(); let config_dir = dirs_config_dir();
let css_path = config_dir.join("custom.css"); let css_path = config_dir.join("custom.css");
if css_path.exists() { if css_path.exists() {
fs::read_to_string(&css_path).map_err(|e| e.to_string()) fs::read_to_string(&css_path).map_err(|e| e.to_string())
@ -1313,15 +1344,11 @@ fn get_custom_css() -> Result<String, String> {
#[tauri::command] #[tauri::command]
fn set_custom_css(css: String) -> Result<(), String> { fn set_custom_css(css: String) -> Result<(), String> {
let config_dir = dirs_config_path(); let config_dir = dirs_config_dir();
fs::create_dir_all(&config_dir).map_err(|e| e.to_string())?; fs::create_dir_all(&config_dir).map_err(|e| e.to_string())?;
fs::write(config_dir.join("custom.css"), css).map_err(|e| e.to_string()) fs::write(config_dir.join("custom.css"), css).map_err(|e| e.to_string())
} }
fn dirs_config_path() -> PathBuf {
dirs::config_dir().unwrap_or_else(|| PathBuf::from(".")).join("graph-notes")
}
/* ── Workspace Layouts ──────────────────────────────────── */ /* ── Workspace Layouts ──────────────────────────────────── */
#[tauri::command] #[tauri::command]

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Graph Notes", "productName": "Graph Notes",
"version": "0.9.0", "version": "1.0.0",
"identifier": "com.graphnotes.app", "identifier": "com.graphnotes.app",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
@ -22,7 +22,7 @@
} }
], ],
"security": { "security": {
"csp": null "csp": "default-src 'self' tauri: https://tauri.localhost; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' asset: https://asset.localhost http://asset.localhost blob: data: tauri: https://tauri.localhost; connect-src 'self' ipc: http://ipc.localhost tauri: https://tauri.localhost"
} }
}, },
"plugins": { "plugins": {

View file

@ -13,7 +13,7 @@ import { KanbanView } from "./components/KanbanView";
import { SearchReplace } from "./components/SearchReplace"; import { SearchReplace } from "./components/SearchReplace";
import { FlashcardView } from "./components/FlashcardView"; import { FlashcardView } from "./components/FlashcardView";
import { CSSEditor, useCustomCssInit } from "./components/CSSEditor"; import { CSSEditor, useCustomCssInit } from "./components/CSSEditor";
import { TabBar, type Tab } from "./components/TabBar"; import { TabBar } from "./components/TabBar";
import { WhiteboardView } from "./components/WhiteboardView"; import { WhiteboardView } from "./components/WhiteboardView";
import { DatabaseView } from "./components/DatabaseView"; import { DatabaseView } from "./components/DatabaseView";
import { GitPanel } from "./components/GitPanel"; import { GitPanel } from "./components/GitPanel";
@ -82,8 +82,6 @@ export default function App() {
const [focusMode, setFocusMode] = useState(false); const [focusMode, setFocusMode] = useState(false);
const [searchReplaceOpen, setSearchReplaceOpen] = useState(false); const [searchReplaceOpen, setSearchReplaceOpen] = useState(false);
const [cssEditorOpen, setCssEditorOpen] = useState(false); const [cssEditorOpen, setCssEditorOpen] = useState(false);
const [tabs, setTabs] = useState<Tab[]>([]);
const [activeTab, setActiveTab] = useState(0);
const navigate = useNavigate(); const navigate = useNavigate();
// Apply saved theme + custom CSS on mount // Apply saved theme + custom CSS on mount
@ -110,8 +108,8 @@ export default function App() {
} }
if (!path) { if (!path) {
// Default to the project's vault directory // No stored vault path — use a sensible default
path = "/home/amir/code/notes/vault"; path = "./vault";
console.log("[GraphNotes] Using default vault path:", path); console.log("[GraphNotes] Using default vault path:", path);
try { try {
await setVaultPath(path); await setVaultPath(path);
@ -318,6 +316,10 @@ export default function App() {
e.preventDefault(); e.preventDefault();
setSearchReplaceOpen(v => !v); setSearchReplaceOpen(v => !v);
break; break;
case "u":
e.preventDefault();
setCssEditorOpen(v => !v);
break;
} }
}; };
window.addEventListener("keydown", handler); window.addEventListener("keydown", handler);
@ -398,27 +400,6 @@ export default function App() {
); );
} }
/* ── Note View ──────────────────────────────────────────────── */
function NoteView() {
const { path } = useParams<{ path: string }>();
const { vaultPath, setCurrentNote, noteContent, setNoteContent } = useVault();
const decodedPath = decodeURIComponent(path || "");
useEffect(() => {
if (!decodedPath || !vaultPath) return;
setCurrentNote(decodedPath);
readNote(vaultPath, decodedPath).then(setNoteContent).catch(() => setNoteContent(""));
}, [decodedPath, vaultPath, setCurrentNote, setNoteContent]);
return (
<div className="flex flex-1 overflow-hidden">
<main className="flex-1 overflow-y-auto">
<Editor />
</main>
<Backlinks />
</div>
);
}
/* ── Daily View ─────────────────────────────────────────────── */ /* ── Daily View ─────────────────────────────────────────────── */
function DailyView() { function DailyView() {

View file

@ -3,6 +3,7 @@ import { useVault } from "../App";
import { writeNote, saveAttachment, saveSnapshot, getWritingGoal, setWritingGoal as setWritingGoalCmd, isEncrypted, encryptNote, decryptNote } from "../lib/commands"; import { writeNote, saveAttachment, saveSnapshot, getWritingGoal, setWritingGoal as setWritingGoalCmd, isEncrypted, encryptNote, decryptNote } from "../lib/commands";
import { extractWikilinks } from "../lib/wikilinks"; import { extractWikilinks } from "../lib/wikilinks";
import { marked } from "marked"; import { marked } from "marked";
import DOMPurify from "dompurify";
import { PropertiesPanel } from "./PropertiesPanel"; import { PropertiesPanel } from "./PropertiesPanel";
import { TableOfContents } from "./TableOfContents"; import { TableOfContents } from "./TableOfContents";
import { SlashMenu } from "./SlashMenu"; import { SlashMenu } from "./SlashMenu";
@ -59,20 +60,35 @@ export function Editor() {
) )
: []; : [];
// Refs to avoid stale closures in debounced save
const currentNoteRef = useRef(currentNote);
const vaultPathRef = useRef(vaultPath);
currentNoteRef.current = currentNote;
vaultPathRef.current = vaultPath;
const lastSnapshotRef2 = useRef(0);
// ── Save with debounce ── // ── Save with debounce ──
const saveContent = useCallback( const saveContent = useCallback(
(value: string) => { (value: string) => {
setNoteContent(value); setNoteContent(value);
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = setTimeout(async () => { saveTimeoutRef.current = setTimeout(async () => {
if (vaultPath && currentNote) { const v = vaultPathRef.current;
const n = currentNoteRef.current;
if (v && n) {
setIsSaving(true); setIsSaving(true);
await writeNote(vaultPath, currentNote, value); await writeNote(v, n, value);
setIsSaving(false); setIsSaving(false);
// Auto-snapshot on save (max 1 per 5 min)
const now = Date.now();
if (now - lastSnapshotRef2.current > 5 * 60 * 1000 && value.length > 50) {
lastSnapshotRef2.current = now;
saveSnapshot(v, n).catch(() => { });
}
} }
}, 500); }, 500);
}, },
[vaultPath, currentNote, setNoteContent] [setNoteContent]
); );
// ── Extract raw markdown from contenteditable DOM ── // ── Extract raw markdown from contenteditable DOM ──
@ -415,10 +431,11 @@ export function Editor() {
/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
(_m, target, display) => { (_m, target, display) => {
const label = display?.trim() || target.trim(); const label = display?.trim() || target.trim();
return `<span class="wikilink" data-target="${target.trim()}">${label}</span>`; const safeTarget = target.trim().replace(/"/g, '&quot;');
return `<span class="wikilink" data-target="${safeTarget}">${DOMPurify.sanitize(label)}</span>`;
} }
); );
return html; return DOMPurify.sanitize(html, { ADD_ATTR: ['data-target'] });
})(); })();
// Mermaid post-processing (render in a separate effect) // Mermaid post-processing (render in a separate effect)
@ -492,15 +509,7 @@ export function Editor() {
}).catch(() => { }); }).catch(() => { });
}, [isPreview, renderedMarkdown]); }, [isPreview, renderedMarkdown]);
// Auto-snapshot on save (max 1 per 5 min) // Snapshot logic is now in the saveContent debounce callback above
useEffect(() => {
if (!vaultPath || !currentNote || !noteContent) return;
const now = Date.now();
if (now - lastSnapshotRef.current > 5 * 60 * 1000 && noteContent.length > 50) {
lastSnapshotRef.current = now;
saveSnapshot(vaultPath, currentNote).catch(() => { });
}
}, [vaultPath, currentNote, noteContent]);
// Load writing goal // Load writing goal
useEffect(() => { useEffect(() => {
@ -514,25 +523,16 @@ export function Editor() {
isEncrypted(vaultPath, currentNote).then(setNoteEncrypted).catch(() => setNoteEncrypted(false)); isEncrypted(vaultPath, currentNote).then(setNoteEncrypted).catch(() => setNoteEncrypted(false));
}, [vaultPath, currentNote]); }, [vaultPath, currentNote]);
// Widget rendering in preview // Widget rendering in preview (single pass to avoid clobbering DOM)
useEffect(() => { useEffect(() => {
if (!isPreview || !mermaidRef.current) return; if (!isPreview || !mermaidRef.current) return;
const container = mermaidRef.current; const container = mermaidRef.current;
// {{progress:N}} → progress bar let html = container.innerHTML;
container.innerHTML = container.innerHTML.replace( html = html
/\{\{progress:(\d+)\}\}/g, .replace(/\{\{progress:(\d+)\}\}/g, (_, n) => `<div class="widget-progress"><div class="widget-progress-fill" style="width:${Math.min(100, +n)}%"></div><span class="widget-progress-label">${n}%</span></div>`)
(_, n) => `<div class="widget-progress"><div class="widget-progress-fill" style="width:${Math.min(100, +n)}%"></div><span class="widget-progress-label">${n}%</span></div>` .replace(/\{\{counter:(\d+)\}\}/g, (_, n) => `<span class="widget-counter">${n}</span>`)
); .replace(/\{\{toggle:(on|off)\}\}/g, (_, state) => `<span class="widget-toggle ${state === 'on' ? 'on' : ''}">${state === 'on' ? '●' : '○'}</span>`);
// {{counter:N}} → counter badge if (html !== container.innerHTML) container.innerHTML = html;
container.innerHTML = container.innerHTML.replace(
/\{\{counter:(\d+)\}\}/g,
(_, n) => `<span class="widget-counter">${n}</span>`
);
// {{toggle:on/off}} → toggle indicator
container.innerHTML = container.innerHTML.replace(
/\{\{toggle:(on|off)\}\}/g,
(_, state) => `<span class="widget-toggle ${state === 'on' ? 'on' : ''}">${state === 'on' ? '●' : '○'}</span>`
);
}, [isPreview, renderedMarkdown]); }, [isPreview, renderedMarkdown]);
// Right-click for refactoring // Right-click for refactoring
@ -570,12 +570,9 @@ export function Editor() {
range.insertNode(textNode); range.insertNode(textNode);
range.collapse(false); range.collapse(false);
} }
// Trigger save // Trigger save using same extraction as regular input
const raw = ceRef.current?.innerText || ""; const raw = domToMarkdown(ceRef.current!);
setNoteContent(raw); saveContent(raw);
if (currentNote) {
await writeNote(vaultPath, currentNote, raw);
}
} catch (err) { } catch (err) {
console.error("Image paste failed:", err); console.error("Image paste failed:", err);
} }
@ -847,8 +844,7 @@ export function Editor() {
setNoteEncrypted(false); setNoteEncrypted(false);
setLockScreenOpen(false); setLockScreenOpen(false);
setLockError(null); setLockError(null);
// Save decrypted // Note: decrypted content is only held in memory, not written to disk
await writeNote(vaultPath, currentNote, content);
} catch { } catch {
setLockError("Wrong password"); setLockError("Wrong password");
} }

View file

@ -1,4 +1,4 @@
import { useEffect, useState, useCallback } from "react"; import { useEffect, useState, useCallback, useRef } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Provider as JotaiProvider } from "jotai"; import { Provider as JotaiProvider } from "jotai";
import { Canvas, CanvasStyleProvider, registerBuiltinCommands } from "@blinksgg/canvas"; import { Canvas, CanvasStyleProvider, registerBuiltinCommands } from "@blinksgg/canvas";
@ -60,8 +60,12 @@ export function GraphView() {
</div> </div>
), []); ), []);
const handleNodeClick = useCallback((nodeId: string) => { // Navigate to selected node
navigate(`/note/${encodeURIComponent(nodeId)}`); const handleSelectionChange = useCallback((selectedNodeIds: Set<string>) => {
if (selectedNodeIds.size === 1) {
const [nodeId] = selectedNodeIds;
navigate(`/note/${encodeURIComponent(nodeId)}`);
}
}, [navigate]); }, [navigate]);
return ( return (
@ -70,7 +74,7 @@ export function GraphView() {
<div className="graph-canvas-wrapper"> <div className="graph-canvas-wrapper">
<Canvas <Canvas
renderNode={renderNode} renderNode={renderNode}
onNodeClick={handleNodeClick} onSelectionChange={handleSelectionChange}
minZoom={0.1} minZoom={0.1}
maxZoom={5} maxZoom={5}
/> />

View file

@ -168,9 +168,7 @@ export function Sidebar() {
if (newPath === sourcePath) return; if (newPath === sourcePath) return;
try { try {
const oldName = sourcePath.replace(/\.md$/, "").split("/").pop() || "";
await renameNote(vaultPath, sourcePath, newPath); await renameNote(vaultPath, sourcePath, newPath);
await updateWikilinks(vaultPath, oldName, oldName);
await refreshNotes(); await refreshNotes();
if (location.pathname === `/note/${encodeURIComponent(sourcePath)}`) { if (location.pathname === `/note/${encodeURIComponent(sourcePath)}`) {
navigate(`/note/${encodeURIComponent(newPath)}`, { replace: true }); navigate(`/note/${encodeURIComponent(newPath)}`, { replace: true });

View file

@ -1,20 +1,18 @@
import { useEffect, useState, useCallback } from "react"; import { useCallback } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { Provider as JotaiProvider } from "jotai"; import { Provider as JotaiProvider } from "jotai";
import { Canvas, CanvasStyleProvider, registerBuiltinCommands, ViewportControls } from "@blinksgg/canvas"; import { Canvas, CanvasStyleProvider, registerBuiltinCommands, ViewportControls } from "@blinksgg/canvas";
import { useVault } from "../App"; import { useVault } from "../App";
import { saveCanvas, loadCanvas } from "../lib/commands"; import { saveCanvas } from "../lib/commands";
registerBuiltinCommands(); registerBuiltinCommands();
const CARD_COLORS = ["#8b5cf6", "#3b82f6", "#10b981", "#f59e0b", "#f43f5e", "#ec4899"];
/** /**
* WhiteboardView Freeform visual thinking canvas. * WhiteboardView Freeform visual thinking canvas.
*/ */
export function WhiteboardView() { export function WhiteboardView() {
const { name } = useParams<{ name: string }>(); const { name } = useParams<{ name: string }>();
const { vaultPath, navigateToNote } = useVault(); const { vaultPath } = useVault();
const renderNode = useCallback(({ node, isSelected }: any) => { const renderNode = useCallback(({ node, isSelected }: any) => {
const nodeType = node.dbData?.node_type || "card"; const nodeType = node.dbData?.node_type || "card";
@ -53,13 +51,6 @@ export function WhiteboardView() {
</div> </div>
<Canvas <Canvas
renderNode={renderNode} renderNode={renderNode}
onNodeDoubleClick={(nodeId, nodeData) => {
const label = nodeData?.label || (nodeData as any)?.dbData?.label;
if (label) navigateToNote(label);
}}
onBackgroundDoubleClick={(worldPos) => {
// Could add node creation here
}}
minZoom={0.1} minZoom={0.1}
maxZoom={5} maxZoom={5}
> >