Added Vault file system access/Updated README

This commit is contained in:
Elias 2025-05-09 20:19:13 +01:00
parent 2e92684a5e
commit 41ae4b0cd7
10 changed files with 237 additions and 82 deletions

204
README.md
View file

@ -20,13 +20,6 @@ Transform your Obsidian vault into a reactive computational environment. Reactiv
- Obsidian v1.4.0 or higher
- React v18.2.0
- Libraries:
- Recharts v2.10.0
- Lightweight Charts v4.1.0
- d3-force v3.0.0
- lucide-react v0.263.1
- shadcn/ui (latest components)
## 🚀 Quick Start
@ -109,46 +102,31 @@ export default MyComponent;
## 📚 Built-in Libraries & Components
## 📚 Available Libraries & Components
### React Core & Hooks
```javascript
import React, { useState, useEffect, useRef, useMemo } from react';
```
### UI Components from shadcn/ui
```javascript
import {
Card, CardHeader, CardTitle, CardContent,
Tabs, TabsList, TabsTrigger,
Button, Dialog, Form
} from '@/components/ui';
```
### Visualization Libraries
```javascript
//Include Responsive containers
// Recharts for data visualization
import {
LineChart, BarChart, PieChart,
Line, Bar, Pie,
Tooltip, Legend
} from 'recharts';
ReactiveNotes provides access to popular libraries and components directly in your notes:
### Core Libraries
```jsx
// React and hooks
import React, { useState, useEffect, useRef, useMemo } from 'react';
// Lightweight Charts for financial charts
// Data visualization
import { LineChart, BarChart, PieChart } from 'recharts';
import { createChart } from 'lightweight-charts';
// D3-force for network graphs
import {
forceSimulation,
forceLink
} from 'd3-force';
```
### Icons
```javascript
import {
Upload, Activity, AlertCircle,
TrendingUp, TrendingDown
} from 'lucide-react';
// Data processing
import Papa from 'papaparse'; // CSV parsing
import * as XLSX from 'xlsx'; // Excel files
import * as mathjs from 'mathjs'; // Mathematics
import { format, parseISO } from 'date-fns'; // Date handling
// Interface components
import { Card, Tabs, Switch } from '@/components/ui';
import { TrendingUp, Activity, Settings } from 'lucide-react'; // 70+ icons
```
See the full [component reference](./COMPONENTS.md) for a complete list of available libraries, components and utilities.
Even if needed libraries are not provided within this list CDN imports are always an option.
## 💻 Core Features
### 1. Component Systems
@ -161,21 +139,20 @@ import {
- Real-time component updates
- Error reporting
### 3. Canvas Manipulation
- Direct canvas access for custom drawing
- Real-time canvas updates
- Image processing capabilities
- Custom overlays and annotations
- Interactive drawing tools
### 2. Canvas Manipulation
- HTML Canvas API support for custom drawing
- THREE.js integration for 3D graphics
- Lightweight Charts for financial visualization
- D3 force layout integration for network graphs
- Support for both 2D and 3D rendering contexts
### 5. Performance
### 3. Performance
- Efficient re-rendering
- Memory leak prevention
- Proper cleanup of resources in useEffect hooks
- Lazy loading support
### 2. State Management
### 4. State Management
```javascript
// Local state with React
const [local, setLocal] = useState(0);
@ -189,7 +166,7 @@ const [stored, setStored] = useStorage('key', defaultValue);
- Cross-note state management
### Note-Specific Data Persistence
- Each note maintains independent state through frontmatter
- Each note maintains independent state through frontmatter storage under a key called react_data
- Component states persist across sessions
- Data automatically travels with notes when shared
- Multiple instances of same component can have different states
@ -210,19 +187,97 @@ const Counter = () => {
return <button onClick={() => setCount2(count2 + 1)}>Count: {count2}</button>;
};
```
#### Browser LocalStorage
For components that need to share state across notes or maintain state independent of specific notes, browser localStorage is available.
### 3. File System Integration
```javascript
// Read files with encoding
const text = await window.fs.readFile('data.txt', {
encoding: 'utf8'
});
// Create a localStorage hook
const useLocalStorage = (key, defaultValue) => {
const [state, setState] = useState(() => {
try {
const storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : defaultValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return defaultValue;
}
});
// Read binary files
const binary = await window.fs.readFile('data.xlsx');
useEffect(() => {
localStorage.setItem(key, JSON.stringify(state));
}, [key, state]);
return [state, setState];
};
// Use it in components
const [value, setValue] = useLocalStorage('shared-key', defaultValue);
//This key is shared across all notes
```
### 4. CDN Library Support
Key features:
- Data persists across browser sessions
- Shared state across all notes
- Independent of note content
- Approximately 5-10MB storage per domain
- Persists until browser data is cleared
### 5. File Reading Utility
The plugin provides a flexible file reading utility through the `readFile` function that helps you interactively select and read files from your vault.
```javascript
const fileData = await readFile( path,extensions);
```
### Parameters
- `path` (optional): Direct path to a specific file. If provided, skips the file selector. Default: `null`
- `extensions` (optional): Array of file extensions to filter by. Default: `['txt', 'md', 'json', 'csv']`
### Return Value
Returns a Promise that resolves to an object containing:
- `path`: Full file path
- `name`: File name without extension
- `extension`: File extension
- `content`: String content of the file
Returns `null` if the operation is canceled or fails, allowing you to handle this case gracefully.
```javascript
// Interactive file selection with file type filtering
const textFile = await readFile(null,['txt', 'md']);
if (textFile) {
console.log(`Selected: ${textFile.name}`);
console.log(`Content: ${textFile.content}`);
}
// Direct file access by path (no modal)
const jsonFile = await readFile('path/to/config.json');
if (jsonFile) {
const config = JSON.parse(jsonFile.content);
}
// Read CSV files
const csvFile = await readFile(null,['csv']);
if (csvFile) {
// Process CSV content
const lines = csvFile.content.split('\n');
const headers = lines[0].split(',');
// ...
}
// Read binary files like Excel spreadsheets
const excelFile = await readFile(null,['xlsx', 'xls']);
if (excelFile) {
// excelFile.content contains the file data
// Process with SheetJS or other libraries
}
```
This utility is particularly useful for components that need to process files from your vault, such as data visualizations, document analysis, and custom importers.
### 6. CDN Library Support
Import additional libraries from cdnjs:
```javascript
import Chart from 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js';
@ -233,24 +288,18 @@ Requirements:
- Imports at component top
- Browser-compatible libraries only
### 5. Theme Integration
### 7. Theme Integration
```javascript
const theme = getTheme(); // Returns 'dark' or 'light'
const theme = getTheme(); // Returns 'dark' or 'light' correspodning to obsidian theme
const styles = {
background: theme === 'dark' ? 'var(--background-primary)' : 'white',
color: theme === 'dark' ? 'var(--text-normal)' : 'black'
};
// Or use 'dark:' within your components
```
### File System Access
```javascript
// Read files
const csv = await window.fs.readFile('data.csv', { encoding: 'utf8' });
const binary = await window.fs.readFile('data.xlsx');
```
## 🎨 Component Examples
### Interactive Data Visualization
@ -366,6 +415,17 @@ const NetworkGraph = () => {
**Tabbed UI**
![alt text](assets/TabbedUI.gif)
**Progression Dashboard**
Knowledge progression dashboard using persistant storage and ability to make new notes/categories
![alt text](assets/ProgressionDashboard.gif)
**Integrated use case with MCP**
MCP fetches data from youtube and uses Reactive Notes to display react component for visualising fetched data.
![alt text](assets/IntegratedUsage.gif)
## 🛠Development Guide
### Component Structure
```react
@ -515,8 +575,8 @@ Best practices for optimal performance:
1. **File System**
```javascript
// File operations
await window.fs.readFile(path, options);
const text = await window.fs.readFile(path, { encoding: 'utf8' });
let file = await readFile(path, extensions);
let content=file.content;
```
2. **Theme Management**

BIN
assets/IntegratedUsage.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

BIN
assets/gamingObsidian.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

View file

@ -1,4 +1,4 @@
import { Plugin, MarkdownPostProcessorContext, MarkdownRenderChild, MarkdownRenderer, TFile, App, stringifyYaml, Notice } from 'obsidian';
import { Plugin, MarkdownPostProcessorContext, MarkdownRenderChild, MarkdownRenderer, TFile, App, stringifyYaml, Notice, SuggestModal } from 'obsidian';
import { createRoot } from 'react-dom/client';
import { transform } from '@babel/standalone';
import * as React from 'react';
@ -312,7 +312,100 @@ private getThreeJs() {
}),
//lazy load THREE if needed
...needsThree ? this.getThreeJs() : undefined,
};
readFile: async (path=null, extensions = ['txt', 'md', 'json','csv']) => {
return new Promise((resolve) => {
let isResolved = false;
let cancelTimeoutHandle: number | undefined = undefined; // To store timeout handle for cancellation
// Create a SuggestModal subclass that lists vault files
// If a specific path is provided, read that file directly without showing modal
if (path) {
const file = this.app.vault.getAbstractFileByPath(path);
if (file && file instanceof TFile) {
this.app.vault.cachedRead(file).then(content => {
isResolved = true;
resolve({
path: file.path,
name: file.name,
content: content
});
}).catch(err => {
console.error("Failed to read file:", err);
isResolved = true;
resolve(null);
});
} else {
console.error("File not found:", path);
isResolved = true;
resolve(null);
}
return;
}
class FileSuggestModal extends SuggestModal<TFile> {
getSuggestions(query:string) {
// Get all files with the right extension
const files = this.app.vault.getFiles()
.filter(file => extensions.includes(file.extension));
// Filter by query if provided
if (query) {
return files.filter(file =>
file.path.toLowerCase().includes(query.toLowerCase())
);
}
return files;
}
renderSuggestion(file:TFile, el: HTMLElement) {
el.createEl("div", { text: file.name });
el.createEl("small", { text: file.path });
}
onChooseSuggestion(file:TFile, evt: MouseEvent|KeyboardEvent) {
console.log("File content:");
if (cancelTimeoutHandle !== undefined) {
clearTimeout(cancelTimeoutHandle);
cancelTimeoutHandle = undefined;
}
this.app.vault.cachedRead(file).then(content => {isResolved = true;
resolve({
path: file.path,
name: file.basename,
extension: file.extension,
content: content
});
}).catch(err => {
console.error("Failed to read file:", err);
resolve(null);});}
}
// Show the modal
const modal = new FileSuggestModal(this.app);
modal.setPlaceholder("Select a file");
modal.onClose = () => {
// If onClose is called, and a suggestion hasn't ALREADY been chosen and processed,
// this implies a cancellation or an edge case where onClose fires very quickly.
// We use a setTimeout to yield to the event loop, giving onChooseSuggestion
// a chance to run if it was triggered by the same user action that closed the modal.
if (cancelTimeoutHandle !== undefined) { // Clear previous timeout if any (e.g. rapid open/close)
clearTimeout(cancelTimeoutHandle);
}
cancelTimeoutHandle = window.setTimeout(() => { // window.setTimeout for NodeJS.Timeout type
// After yielding, if the promise hasn't been resolved by onChooseSuggestion
// and a suggestion wasn't flagged as chosen, then resolve as null (cancellation).
if (!isResolved) {
resolve(null);
}
// If suggestionChosen IS true here, it means onChooseSuggestion ran,
// set the flag, and it's responsible for resolving (or has already).
// If promiseResolved IS true, it's already handled.
}, 0); // Yield to the event loop.
};
modal.open();
});
}
};
// Execute the code and get the component
const Component = await new Function(
@ -392,6 +485,7 @@ export default class ReactNotesPlugin extends Plugin {
);
// Register a global Markdown postprocessor for HTML
this.registerMarkdownPostProcessor((element, context) => {
// Only process elements that are within ReactiveNotes components
if (element.closest('.react-component-container')) {
this.parseMarkdownInHtml(element);
}

View file

@ -1,7 +1,7 @@
{
"id": "reactive-notes",
"name": "Reactive Notes",
"version": "1.0.1",
"version": "1.0.2",
"minAppVersion": "1.4.0",
"description": "Transform your vault into a reactive computational environment. Create dynamic React components directly in your notes.",
"author": "Elias Noesis",

2
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "reactive-notes",
"version": "1.0.1",
"version": "1.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {

View file

@ -1,6 +1,6 @@
{
"name": "reactive-notes",
"version": "1.0.1",
"version": "1.0.2",
"description": "Transform your vault into a reactive computational environment with React components.",
"main": "main.js",
"scripts": {

View file

@ -1,5 +1,4 @@
// src/config/componentRegistry.ts
//Can be modified to add more components and libraries
// src/config/componentRegistry.tsx
import React from 'react';
import {
PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend,
@ -29,8 +28,8 @@ import {
import { format, addDays, differenceInDays, parseISO } from 'date-fns';
import { parse, stringify } from 'csv/sync';
import * as mathjs from 'mathjs';
import * as XLSX from 'xlsx';
import Papa from 'papaparse';
// Core React hooks wrapper
// Type for React utilities (non-component exports)
@ -106,6 +105,8 @@ RadialBarChart, RadialBar,
dateFns: { format, addDays, differenceInDays, parseISO },
csv: { parse, stringify },
math: mathjs,
XLSX,
Papa,
} as const;

View file

@ -1,3 +1,3 @@
{
"1.0.1": "1.4.0"
"1.0.2": "1.4.0"
}