Merge pull request #34 from gamosoft/features/perf-improvements
Features/perf improvements
This commit is contained in:
commit
29fafb76d4
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
> Your Self-Hosted Knowledge Base
|
> Your Self-Hosted Knowledge Base
|
||||||
|
|
||||||
|
🌐 **[Visit the official website](https://www.notediscovery.com)**
|
||||||
|
|
||||||
## What is NoteDiscovery?
|
## What is NoteDiscovery?
|
||||||
|
|
||||||
NoteDiscovery is a **lightweight, self-hosted note-taking application** that puts you in complete control of your knowledge base. Write, organize, and discover your notes with a beautiful, modern interface—all running on your own server.
|
NoteDiscovery is a **lightweight, self-hosted note-taking application** that puts you in complete control of your knowledge base. Write, organize, and discover your notes with a beautiful, modern interface—all running on your own server.
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ from .utils import (
|
||||||
save_note,
|
save_note,
|
||||||
delete_note,
|
delete_note,
|
||||||
search_notes,
|
search_notes,
|
||||||
parse_wiki_links,
|
|
||||||
create_note_metadata,
|
create_note_metadata,
|
||||||
ensure_directories,
|
ensure_directories,
|
||||||
create_folder,
|
create_folder,
|
||||||
|
|
@ -336,8 +335,8 @@ async def api_documentation():
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/api/graph",
|
"path": "/api/graph",
|
||||||
"description": "Get graph data for note visualization (wiki links)",
|
"description": "Get graph data for note visualization",
|
||||||
"response": "{ nodes: [{ id, label }], edges: [{ from, to }] }"
|
"response": "{ nodes: [{ id, label }], edges: [] }"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
|
|
@ -651,13 +650,9 @@ async def get_note(note_path: str):
|
||||||
if transformed_content is not None:
|
if transformed_content is not None:
|
||||||
content = transformed_content
|
content = transformed_content
|
||||||
|
|
||||||
# Parse wiki links
|
|
||||||
links = parse_wiki_links(content)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"path": note_path,
|
"path": note_path,
|
||||||
"content": content,
|
"content": content,
|
||||||
"links": links,
|
|
||||||
"metadata": create_note_metadata(config['storage']['notes_dir'], note_path)
|
"metadata": create_note_metadata(config['storage']['notes_dir'], note_path)
|
||||||
}
|
}
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|
@ -745,28 +740,22 @@ async def search(q: str):
|
||||||
|
|
||||||
@api_router.get("/graph")
|
@api_router.get("/graph")
|
||||||
async def get_graph():
|
async def get_graph():
|
||||||
"""Get graph data for visualization"""
|
"""Get graph data for note visualization (currently only returns nodes, link detection not implemented)"""
|
||||||
try:
|
try:
|
||||||
notes = get_all_notes(config['storage']['notes_dir'])
|
notes = get_all_notes(config['storage']['notes_dir'])
|
||||||
nodes = []
|
nodes = []
|
||||||
edges = []
|
edges = []
|
||||||
|
|
||||||
# Build graph structure
|
# Build graph structure - only nodes for now
|
||||||
for note in notes:
|
for note in notes:
|
||||||
nodes.append({
|
if note.get('type') == 'note': # Only include actual notes
|
||||||
"id": note['path'],
|
nodes.append({
|
||||||
"label": note['name']
|
"id": note['path'],
|
||||||
})
|
"label": note['name']
|
||||||
|
})
|
||||||
# Get links from this note
|
|
||||||
content = get_note_content(config['storage']['notes_dir'], note['path'])
|
# Note: Link detection between notes could be implemented here using markdown link parsing
|
||||||
if content:
|
# For now, returning empty edges
|
||||||
links = parse_wiki_links(content)
|
|
||||||
for link in links:
|
|
||||||
edges.append({
|
|
||||||
"from": note['path'],
|
|
||||||
"to": link
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"nodes": nodes, "edges": edges}
|
return {"nodes": nodes, "edges": edges}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -236,13 +236,6 @@ def delete_note(notes_dir: str, note_path: str) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def parse_wiki_links(content: str) -> List[str]:
|
|
||||||
"""Extract wiki-style links [[link]] from markdown content"""
|
|
||||||
pattern = r'\[\[([^\]]+)\]\]'
|
|
||||||
matches = re.findall(pattern, content)
|
|
||||||
return matches
|
|
||||||
|
|
||||||
|
|
||||||
def search_notes(notes_dir: str, query: str) -> List[Dict]:
|
def search_notes(notes_dir: str, query: str) -> List[Dict]:
|
||||||
"""Simple full-text search through all notes"""
|
"""Simple full-text search through all notes"""
|
||||||
results = []
|
results = []
|
||||||
|
|
|
||||||
|
|
@ -641,8 +641,8 @@
|
||||||
<div class="benefit-item">
|
<div class="benefit-item">
|
||||||
<span class="emoji">🔗</span>
|
<span class="emoji">🔗</span>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<strong>Wiki Links</strong>
|
<strong>Internal Links</strong>
|
||||||
Connect notes with internal links
|
Connect notes with markdown links
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -725,7 +725,7 @@
|
||||||
"url": "https://www.notediscovery.com",
|
"url": "https://www.notediscovery.com",
|
||||||
"applicationCategory": "ProductivityApplication",
|
"applicationCategory": "ProductivityApplication",
|
||||||
"operatingSystem": "Linux, Windows, macOS",
|
"operatingSystem": "Linux, Windows, macOS",
|
||||||
"featureList": "Markdown editor, LaTeX math equations, Mermaid diagrams, Code syntax highlighting, Dark mode, Plugin system, Internal wiki links, HTML export, Full-text search, Self-hosted, Obsidian, Evernote, Notion, Onenote, Second brain",
|
"featureList": "Markdown editor, LaTeX math equations, Mermaid diagrams, Code syntax highlighting, Dark mode, Plugin system, Internal links, HTML export, Full-text search, Self-hosted, Obsidian, Evernote, Notion, Onenote, Second brain",
|
||||||
"offers": {
|
"offers": {
|
||||||
"@type": "Offer",
|
"@type": "Offer",
|
||||||
"price": "0",
|
"price": "0",
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
## 🔗 Linking & Discovery
|
## 🔗 Linking & Discovery
|
||||||
|
|
||||||
### Internal Links
|
### Internal Links
|
||||||
- **Wiki-style links** - `[[Note Name]]` syntax
|
- **Markdown links** - `[Note Name](note.md)` syntax
|
||||||
- **Drag to link** - Drag notes or images into the editor to insert links
|
- **Drag to link** - Drag notes or images into the editor to insert links
|
||||||
- **Click to navigate** - Jump between notes seamlessly
|
- **Click to navigate** - Jump between notes seamlessly
|
||||||
- **External links** - Open in new tabs automatically
|
- **External links** - Open in new tabs automatically
|
||||||
|
|
@ -139,6 +139,8 @@ graph TD
|
||||||
|
|
||||||
## ⚡ Keyboard Shortcuts
|
## ⚡ Keyboard Shortcuts
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
| Windows/Linux | Mac | Action |
|
| Windows/Linux | Mac | Action |
|
||||||
|---------------|-----|--------|
|
|---------------|-----|--------|
|
||||||
| `Ctrl+S` | `Cmd+S` | Save note |
|
| `Ctrl+S` | `Cmd+S` | Save note |
|
||||||
|
|
@ -149,6 +151,14 @@ graph TD
|
||||||
| `F3` | `F3` | Next search match |
|
| `F3` | `F3` | Next search match |
|
||||||
| `Shift+F3` | `Shift+F3` | Previous search match |
|
| `Shift+F3` | `Shift+F3` | Previous search match |
|
||||||
|
|
||||||
|
### Markdown Formatting
|
||||||
|
|
||||||
|
| Windows/Linux | Mac | Action | Result |
|
||||||
|
|---------------|-----|--------|--------|
|
||||||
|
| `Ctrl+B` | `Cmd+B` | Bold | `**text**` |
|
||||||
|
| `Ctrl+I` | `Cmd+I` | Italic | `*text*` |
|
||||||
|
| `Ctrl+K` | `Cmd+K` | Insert link | `[text](url)` |
|
||||||
|
|
||||||
## 🚀 Performance
|
## 🚀 Performance
|
||||||
|
|
||||||
- **Instant loading** - No lag, no loading spinners
|
- **Instant loading** - No lag, no loading spinners
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ curl -X POST http://localhost:8000/api/plugins/note_stats/toggle \
|
||||||
|
|
||||||
### 🔗 Links & References
|
### 🔗 Links & References
|
||||||
- **Total Links** - All `[text](url)` links
|
- **Total Links** - All `[text](url)` links
|
||||||
- **Internal Links** - `[[WikiLinks]]` to other notes
|
- **Internal Links** - Links to other notes (`.md` files)
|
||||||
- **External Links** - HTTP/HTTPS URLs
|
- **External Links** - HTTP/HTTPS URLs
|
||||||
- **Images** - `` count
|
- **Images** - `` count
|
||||||
|
|
||||||
|
|
@ -191,7 +191,7 @@ Find orphaned notes or track reference density.
|
||||||
|
|
||||||
**Words:** Split on whitespace, excluding code blocks
|
**Words:** Split on whitespace, excluding code blocks
|
||||||
**Reading Time:** `words / 200 minutes`
|
**Reading Time:** `words / 200 minutes`
|
||||||
**Links:** Regex match `[text](url)` and `[[WikiLink]]`
|
**Links:** Regex match `[text](url)` markdown links
|
||||||
**Tasks:** Match `- [ ]` and `- [x]`
|
**Tasks:** Match `- [ ]` and `- [x]`
|
||||||
**Code Blocks:** Match ` ```language ` ` fences
|
**Code Blocks:** Match ` ```language ` ` fences
|
||||||
**Headings:** Match `#`, `##`, `###` at line start
|
**Headings:** Match `#`, `##`, `###` at line start
|
||||||
|
|
|
||||||
323
frontend/app.js
323
frontend/app.js
|
|
@ -94,89 +94,112 @@ function noteApp() {
|
||||||
// Dropdown state
|
// Dropdown state
|
||||||
showNewDropdown: false,
|
showNewDropdown: false,
|
||||||
|
|
||||||
// Homepage folder navigation
|
// Homepage state
|
||||||
selectedHomepageFolder: '',
|
selectedHomepageFolder: '',
|
||||||
|
_homepageCache: {
|
||||||
|
folderPath: null,
|
||||||
|
notes: null,
|
||||||
|
folders: null,
|
||||||
|
breadcrumb: null
|
||||||
|
},
|
||||||
|
|
||||||
// Computed-like helpers for homepage (use methods for safer evaluation)
|
// Homepage constants
|
||||||
|
HOMEPAGE_MAX_NOTES: 50,
|
||||||
|
|
||||||
|
// Computed-like helpers for homepage (cached for performance)
|
||||||
homepageNotes() {
|
homepageNotes() {
|
||||||
|
// Return cached result if folder hasn't changed
|
||||||
|
if (this._homepageCache.folderPath === this.selectedHomepageFolder && this._homepageCache.notes) {
|
||||||
|
return this._homepageCache.notes;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.folderTree || typeof this.folderTree !== 'object') {
|
if (!this.folderTree || typeof this.folderTree !== 'object') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderNode = this.getFolderNode(this.selectedHomepageFolder || '');
|
const folderNode = this.getFolderNode(this.selectedHomepageFolder || '');
|
||||||
if (!folderNode || !Array.isArray(folderNode.notes)) {
|
const result = (folderNode && Array.isArray(folderNode.notes)) ? folderNode.notes : [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return folderNode.notes;
|
// Cache the result
|
||||||
},
|
this._homepageCache.notes = result;
|
||||||
|
this._homepageCache.folderPath = this.selectedHomepageFolder;
|
||||||
// Helper: Format file size nicely
|
|
||||||
formatSize(bytes) {
|
return result;
|
||||||
if (bytes === 0 || !bytes) return '0 B';
|
|
||||||
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
||||||
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
homepageFolders() {
|
homepageFolders() {
|
||||||
|
// Return cached result if folder hasn't changed
|
||||||
|
if (this._homepageCache.folderPath === this.selectedHomepageFolder && this._homepageCache.folders) {
|
||||||
|
return this._homepageCache.folders;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.folderTree || typeof this.folderTree !== 'object') {
|
if (!this.folderTree || typeof this.folderTree !== 'object') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get child folders
|
||||||
let childFolders = [];
|
let childFolders = [];
|
||||||
if (!this.selectedHomepageFolder) {
|
if (!this.selectedHomepageFolder) {
|
||||||
|
// Root level: all top-level folders
|
||||||
childFolders = Object.entries(this.folderTree)
|
childFolders = Object.entries(this.folderTree)
|
||||||
.filter(([key]) => key !== '__root__')
|
.filter(([key]) => key !== '__root__')
|
||||||
.map(([, folder]) => folder);
|
.map(([, folder]) => folder);
|
||||||
} else {
|
} else {
|
||||||
|
// Inside a folder: get its children
|
||||||
const parentFolder = this.getFolderNode(this.selectedHomepageFolder);
|
const parentFolder = this.getFolderNode(this.selectedHomepageFolder);
|
||||||
if (!parentFolder || !parentFolder.children) {
|
if (parentFolder && parentFolder.children) {
|
||||||
return [];
|
childFolders = Object.values(parentFolder.children);
|
||||||
}
|
}
|
||||||
childFolders = Object.values(parentFolder.children);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(childFolders) || childFolders.length === 0) {
|
// Map to simplified structure (note count already cached in folder node)
|
||||||
return [];
|
const result = childFolders
|
||||||
}
|
|
||||||
|
|
||||||
return childFolders
|
|
||||||
.map(folder => ({
|
.map(folder => ({
|
||||||
name: folder.name,
|
name: folder.name,
|
||||||
path: folder.path,
|
path: folder.path,
|
||||||
noteCount: this.calculateFolderNoteCount(folder)
|
noteCount: folder.noteCount || 0 // Use pre-calculated count
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
this._homepageCache.folders = result;
|
||||||
|
this._homepageCache.folderPath = this.selectedHomepageFolder;
|
||||||
|
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
homepageBreadcrumb() {
|
homepageBreadcrumb() {
|
||||||
// Always return at least Home breadcrumb
|
// Return cached result if folder hasn't changed
|
||||||
// Safety check - ensure we always return an array
|
if (this._homepageCache.folderPath === this.selectedHomepageFolder && this._homepageCache.breadcrumb) {
|
||||||
try {
|
return this._homepageCache.breadcrumb;
|
||||||
const breadcrumb = [{ name: 'Home', path: '' }];
|
}
|
||||||
|
|
||||||
if (!this.selectedHomepageFolder) {
|
const breadcrumb = [{ name: 'Home', path: '' }];
|
||||||
return breadcrumb;
|
|
||||||
}
|
if (this.selectedHomepageFolder) {
|
||||||
|
const parts = this.selectedHomepageFolder.split('/').filter(Boolean);
|
||||||
const parts = (this.selectedHomepageFolder || '').split('/').filter(part => part.trim() !== '');
|
|
||||||
|
|
||||||
let currentPath = '';
|
let currentPath = '';
|
||||||
|
|
||||||
parts.forEach(part => {
|
parts.forEach(part => {
|
||||||
currentPath = currentPath ? currentPath + '/' + part : part;
|
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||||
breadcrumb.push({ name: part, path: currentPath });
|
breadcrumb.push({ name: part, path: currentPath });
|
||||||
});
|
});
|
||||||
|
|
||||||
return breadcrumb;
|
|
||||||
} catch (error) {
|
|
||||||
// Fallback to just Home if anything goes wrong
|
|
||||||
return [{ name: 'Home', path: '' }];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
this._homepageCache.breadcrumb = breadcrumb;
|
||||||
|
this._homepageCache.folderPath = this.selectedHomepageFolder;
|
||||||
|
|
||||||
|
return breadcrumb;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Helper: Format file size nicely
|
||||||
|
formatSize(bytes) {
|
||||||
|
if (!bytes) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||||
},
|
},
|
||||||
|
|
||||||
getFolderNode(folderPath = '') {
|
getFolderNode(folderPath = '') {
|
||||||
|
|
@ -185,10 +208,7 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!folderPath) {
|
if (!folderPath) {
|
||||||
if (this.folderTree['__root__']) {
|
return this.folderTree['__root__'] || { name: '', path: '', children: {}, notes: [], noteCount: 0 };
|
||||||
return this.folderTree['__root__'];
|
|
||||||
}
|
|
||||||
return { name: '', path: '', children: {}, notes: [] };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = folderPath.split('/').filter(Boolean);
|
const parts = folderPath.split('/').filter(Boolean);
|
||||||
|
|
@ -206,22 +226,6 @@ function noteApp() {
|
||||||
return node;
|
return node;
|
||||||
},
|
},
|
||||||
|
|
||||||
calculateFolderNoteCount(folderNode) {
|
|
||||||
if (!folderNode || typeof folderNode !== 'object') {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const directNotes = Array.isArray(folderNode.notes) ? folderNode.notes.length : 0;
|
|
||||||
if (!folderNode.children || Object.keys(folderNode.children).length === 0) {
|
|
||||||
return directNotes;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.values(folderNode.children).reduce(
|
|
||||||
(total, child) => total + this.calculateFolderNoteCount(child),
|
|
||||||
directNotes
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Check if app is empty (no notes and no folders)
|
// Check if app is empty (no notes and no folders)
|
||||||
get isAppEmpty() {
|
get isAppEmpty() {
|
||||||
const notesArray = Array.isArray(this.notes) ? this.notes : [];
|
const notesArray = Array.isArray(this.notes) ? this.notes : [];
|
||||||
|
|
@ -291,6 +295,14 @@ function noteApp() {
|
||||||
this.selectedHomepageFolder = '';
|
this.selectedHomepageFolder = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidate cache to force recalculation
|
||||||
|
this._homepageCache = {
|
||||||
|
folderPath: null,
|
||||||
|
notes: null,
|
||||||
|
folders: null,
|
||||||
|
breadcrumb: null
|
||||||
|
};
|
||||||
|
|
||||||
// Clear search
|
// Clear search
|
||||||
this.searchQuery = '';
|
this.searchQuery = '';
|
||||||
this.searchResults = [];
|
this.searchResults = [];
|
||||||
|
|
@ -373,6 +385,28 @@ function noteApp() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.previousMatch();
|
this.previousMatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only apply markdown shortcuts when editor is focused and a note is open
|
||||||
|
const isEditorFocused = document.activeElement?.id === 'note-editor';
|
||||||
|
if (isEditorFocused && this.currentNote) {
|
||||||
|
// Ctrl/Cmd + B for bold
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.wrapSelection('**', '**', 'bold text');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl/Cmd + I for italic
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'i') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.wrapSelection('*', '*', 'italic text');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl/Cmd + K for link
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.insertLink();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -590,8 +624,41 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Force Alpine reactivity by creating a new object reference
|
// Calculate and cache note counts recursively (for performance)
|
||||||
this.folderTree = { ...tree };
|
const calculateNoteCounts = (folderNode) => {
|
||||||
|
const directNotes = folderNode.notes ? folderNode.notes.length : 0;
|
||||||
|
|
||||||
|
if (!folderNode.children || Object.keys(folderNode.children).length === 0) {
|
||||||
|
folderNode.noteCount = directNotes;
|
||||||
|
return directNotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childNotesCount = Object.values(folderNode.children).reduce(
|
||||||
|
(total, child) => total + calculateNoteCounts(child),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
folderNode.noteCount = directNotes + childNotesCount;
|
||||||
|
return folderNode.noteCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate note counts for all folders
|
||||||
|
Object.values(tree).forEach(folder => {
|
||||||
|
if (folder.path !== undefined || folder === tree['__root__']) {
|
||||||
|
calculateNoteCounts(folder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Invalidate homepage cache when tree is rebuilt
|
||||||
|
this._homepageCache = {
|
||||||
|
folderPath: null,
|
||||||
|
notes: null,
|
||||||
|
folders: null,
|
||||||
|
breadcrumb: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assign new tree (Alpine will detect the change)
|
||||||
|
this.folderTree = tree;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Render folder recursively (helper for deep nesting)
|
// Render folder recursively (helper for deep nesting)
|
||||||
|
|
@ -756,8 +823,6 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
// Force Alpine reactivity by creating new Set reference
|
// Force Alpine reactivity by creating new Set reference
|
||||||
this.expandedFolders = new Set(this.expandedFolders);
|
this.expandedFolders = new Set(this.expandedFolders);
|
||||||
// Also trigger folderTree reactivity to re-render x-html
|
|
||||||
this.folderTree = { ...this.folderTree };
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check if folder is expanded
|
// Check if folder is expanded
|
||||||
|
|
@ -767,25 +832,22 @@ function noteApp() {
|
||||||
|
|
||||||
// Expand all folders
|
// Expand all folders
|
||||||
expandAllFolders() {
|
expandAllFolders() {
|
||||||
// Add all folder paths to expandedFolders
|
|
||||||
this.allFolders.forEach(folder => {
|
this.allFolders.forEach(folder => {
|
||||||
this.expandedFolders.add(folder);
|
this.expandedFolders.add(folder);
|
||||||
});
|
});
|
||||||
// Force Alpine reactivity by creating new object reference (no rebuild needed)
|
// Force Alpine reactivity
|
||||||
this.folderTree = { ...this.folderTree };
|
this.expandedFolders = new Set(this.expandedFolders);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Collapse all folders
|
// Collapse all folders
|
||||||
collapseAllFolders() {
|
collapseAllFolders() {
|
||||||
this.expandedFolders.clear();
|
this.expandedFolders.clear();
|
||||||
// Force Alpine reactivity by creating new object reference (no rebuild needed)
|
// Force Alpine reactivity
|
||||||
this.folderTree = { ...this.folderTree };
|
this.expandedFolders = new Set(this.expandedFolders);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Expand folder tree to show a specific note
|
// Expand folder tree to show a specific note
|
||||||
expandFolderForNote(notePath) {
|
expandFolderForNote(notePath) {
|
||||||
// Extract folder path from note path
|
|
||||||
// e.g., "folder1/folder2/note.md" -> ["folder1", "folder1/folder2"]
|
|
||||||
const parts = notePath.split('/');
|
const parts = notePath.split('/');
|
||||||
|
|
||||||
// If note is in root, no folders to expand
|
// If note is in root, no folders to expand
|
||||||
|
|
@ -801,13 +863,8 @@ function noteApp() {
|
||||||
this.expandedFolders.add(currentPath);
|
this.expandedFolders.add(currentPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Force Alpine reactivity by creating new object reference
|
// Force Alpine reactivity
|
||||||
// This ensures the UI updates to show the expanded folders
|
this.expandedFolders = new Set(this.expandedFolders);
|
||||||
this.folderTree = { ...this.folderTree };
|
|
||||||
|
|
||||||
// Also force a re-evaluation by modifying the Set (create new Set)
|
|
||||||
const oldFolders = this.expandedFolders;
|
|
||||||
this.expandedFolders = new Set(oldFolders);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Scroll note into view in the sidebar navigation
|
// Scroll note into view in the sidebar navigation
|
||||||
|
|
@ -1932,6 +1989,68 @@ function noteApp() {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Markdown formatting helpers
|
||||||
|
wrapSelection(before, after, placeholder) {
|
||||||
|
const editor = document.getElementById('note-editor');
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
|
const start = editor.selectionStart;
|
||||||
|
const end = editor.selectionEnd;
|
||||||
|
const selectedText = this.noteContent.substring(start, end);
|
||||||
|
const textToWrap = selectedText || placeholder;
|
||||||
|
|
||||||
|
// Build the new text
|
||||||
|
const newText = before + textToWrap + after;
|
||||||
|
|
||||||
|
// Update content
|
||||||
|
this.noteContent = this.noteContent.substring(0, start) + newText + this.noteContent.substring(end);
|
||||||
|
|
||||||
|
// Set cursor position (select the wrapped text or placeholder)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (selectedText) {
|
||||||
|
// If text was selected, keep it selected (inside the wrapper)
|
||||||
|
editor.setSelectionRange(start + before.length, start + before.length + selectedText.length);
|
||||||
|
} else {
|
||||||
|
// If no text selected, select the placeholder
|
||||||
|
editor.setSelectionRange(start + before.length, start + before.length + placeholder.length);
|
||||||
|
}
|
||||||
|
editor.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger autosave
|
||||||
|
this.autoSave();
|
||||||
|
},
|
||||||
|
|
||||||
|
insertLink() {
|
||||||
|
const editor = document.getElementById('note-editor');
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
|
const start = editor.selectionStart;
|
||||||
|
const end = editor.selectionEnd;
|
||||||
|
const selectedText = this.noteContent.substring(start, end);
|
||||||
|
|
||||||
|
// If text is selected, use it as link text; otherwise use placeholder
|
||||||
|
const linkText = selectedText || 'link text';
|
||||||
|
const linkUrl = 'url';
|
||||||
|
|
||||||
|
// Build the markdown link
|
||||||
|
const newText = `[${linkText}](${linkUrl})`;
|
||||||
|
|
||||||
|
// Update content
|
||||||
|
this.noteContent = this.noteContent.substring(0, start) + newText + this.noteContent.substring(end);
|
||||||
|
|
||||||
|
// Set cursor position to select the URL part for easy editing
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const urlStart = start + linkText.length + 3; // After "[linkText]("
|
||||||
|
const urlEnd = urlStart + linkUrl.length;
|
||||||
|
editor.setSelectionRange(urlStart, urlEnd);
|
||||||
|
editor.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger autosave
|
||||||
|
this.autoSave();
|
||||||
|
},
|
||||||
|
|
||||||
// Save current note
|
// Save current note
|
||||||
async saveNote() {
|
async saveNote() {
|
||||||
if (!this.currentNote) return;
|
if (!this.currentNote) return;
|
||||||
|
|
@ -2202,13 +2321,8 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert wiki-style links [[link]] to HTML links
|
|
||||||
let content = this.noteContent.replace(/\[\[([^\]]+)\]\]/g, (match, linkText) => {
|
|
||||||
return `<a href="#" style="color: var(--accent-primary);" onclick="return false;">[[${linkText}]]</a>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse markdown
|
// Parse markdown
|
||||||
let html = marked.parse(content);
|
let html = marked.parse(this.noteContent);
|
||||||
|
|
||||||
// Post-process: Add target="_blank" to external links and title attributes to images
|
// Post-process: Add target="_blank" to external links and title attributes to images
|
||||||
// Parse as DOM to safely manipulate
|
// Parse as DOM to safely manipulate
|
||||||
|
|
@ -2955,12 +3069,37 @@ function noteApp() {
|
||||||
|
|
||||||
// Homepage folder navigation methods
|
// Homepage folder navigation methods
|
||||||
goToHomepageFolder(folderPath) {
|
goToHomepageFolder(folderPath) {
|
||||||
// Navigate to folder (empty string = root)
|
|
||||||
this.selectedHomepageFolder = folderPath || '';
|
this.selectedHomepageFolder = folderPath || '';
|
||||||
|
|
||||||
// Update browser history
|
// Invalidate cache to force recalculation
|
||||||
const state = { homepageFolder: folderPath || '' };
|
this._homepageCache = {
|
||||||
window.history.pushState(state, '', '/');
|
folderPath: null,
|
||||||
|
notes: null,
|
||||||
|
folders: null,
|
||||||
|
breadcrumb: null
|
||||||
|
};
|
||||||
|
|
||||||
|
window.history.pushState({ homepageFolder: folderPath || '' }, '', '/');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Navigate to homepage root and clear all editor state
|
||||||
|
goHome() {
|
||||||
|
this.selectedHomepageFolder = '';
|
||||||
|
this.currentNote = '';
|
||||||
|
this.currentNoteName = '';
|
||||||
|
this.noteContent = '';
|
||||||
|
this.currentImage = '';
|
||||||
|
this.mobileSidebarOpen = false;
|
||||||
|
|
||||||
|
// Invalidate cache to force recalculation
|
||||||
|
this._homepageCache = {
|
||||||
|
folderPath: null,
|
||||||
|
notes: null,
|
||||||
|
folders: null,
|
||||||
|
breadcrumb: null
|
||||||
|
};
|
||||||
|
|
||||||
|
window.history.pushState({ homepageFolder: '' }, '', '/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -730,7 +730,7 @@
|
||||||
<div class="p-4 border-b" style="border-color: var(--border-primary);">
|
<div class="p-4 border-b" style="border-color: var(--border-primary);">
|
||||||
<button
|
<button
|
||||||
class="flex items-center gap-2 mb-3 hover:opacity-80 transition"
|
class="flex items-center gap-2 mb-3 hover:opacity-80 transition"
|
||||||
@click="goToHomepageFolder(''); currentNote = ''; currentImage = ''; currentNoteName = ''; noteContent = ''; mobileSidebarOpen = false"
|
@click="goHome()"
|
||||||
aria-label="Go to homepage"
|
aria-label="Go to homepage"
|
||||||
style="background: none; border: none; outline: none; cursor: pointer;"
|
style="background: none; border: none; outline: none; cursor: pointer;"
|
||||||
>
|
>
|
||||||
|
|
@ -858,7 +858,7 @@
|
||||||
style="background-color: var(--bg-secondary); border: 1px solid var(--border-primary);"
|
style="background-color: var(--bg-secondary); border: 1px solid var(--border-primary);"
|
||||||
>
|
>
|
||||||
<div class="py-1">
|
<div class="py-1">
|
||||||
<button
|
<button
|
||||||
@click="createNote()"
|
@click="createNote()"
|
||||||
class="w-full px-4 py-2.5 text-sm text-left flex items-center gap-3 transition-colors"
|
class="w-full px-4 py-2.5 text-sm text-left flex items-center gap-3 transition-colors"
|
||||||
style="color: var(--text-primary);"
|
style="color: var(--text-primary);"
|
||||||
|
|
@ -877,7 +877,7 @@
|
||||||
>
|
>
|
||||||
<span class="text-lg">📁</span>
|
<span class="text-lg">📁</span>
|
||||||
<span>New Folder</span>
|
<span>New Folder</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -997,6 +997,17 @@
|
||||||
<span x-text="notes.length"></span> notes
|
<span x-text="notes.length"></span> notes
|
||||||
<span class="mx-1">·</span>
|
<span class="mx-1">·</span>
|
||||||
<span x-text="'v' + appVersion"></span>
|
<span x-text="'v' + appVersion"></span>
|
||||||
|
<span class="mx-1">·</span>
|
||||||
|
<a
|
||||||
|
href="https://www.notediscovery.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="hover:underline"
|
||||||
|
style="color: var(--accent-primary);"
|
||||||
|
title="Visit NoteDiscovery website"
|
||||||
|
>
|
||||||
|
Website
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1019,21 +1030,21 @@
|
||||||
<!-- Welcome Hero - Show when app is empty -->
|
<!-- Welcome Hero - Show when app is empty -->
|
||||||
<template x-if="isAppEmpty">
|
<template x-if="isAppEmpty">
|
||||||
<div class="flex flex-col items-center justify-center h-full py-16 text-center">
|
<div class="flex flex-col items-center justify-center h-full py-16 text-center">
|
||||||
<svg class="mx-auto h-16 w-16 mb-4" style="color: var(--text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="mx-auto h-16 w-16 mb-4" style="color: var(--text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<h1 class="text-4xl font-bold mb-4" style="color: var(--text-primary);" x-text="appName"></h1>
|
<h1 class="text-4xl font-bold mb-4" style="color: var(--text-primary);" x-text="appName"></h1>
|
||||||
<p class="text-xl mb-8" style="color: var(--text-secondary);" x-text="appTagline"></p>
|
<p class="text-xl mb-8" style="color: var(--text-secondary);" x-text="appTagline"></p>
|
||||||
<button
|
<button
|
||||||
@click="createNote()"
|
@click="createNote()"
|
||||||
class="px-8 py-4 text-base font-medium text-white rounded-lg transition-colors"
|
class="px-8 py-4 text-base font-medium text-white rounded-lg transition-colors"
|
||||||
style="background-color: var(--accent-primary);"
|
style="background-color: var(--accent-primary);"
|
||||||
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
|
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
|
||||||
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
|
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
|
||||||
>
|
>
|
||||||
Create Your First Note
|
Create Your First Note
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Grid View - Show when app has content -->
|
<!-- Grid View - Show when app has content -->
|
||||||
|
|
@ -1061,8 +1072,8 @@
|
||||||
:style="crumb.path === selectedHomepageFolder ? 'color: var(--accent-primary); font-weight: 600;' : 'color: var(--text-secondary);'"
|
:style="crumb.path === selectedHomepageFolder ? 'color: var(--accent-primary); font-weight: 600;' : 'color: var(--text-secondary);'"
|
||||||
x-text="crumb.name"
|
x-text="crumb.name"
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1142,7 +1153,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<template x-for="note in homepageNotes().slice(0, 50)" :key="note.path">
|
<template x-for="note in homepageNotes().slice(0, HOMEPAGE_MAX_NOTES)" :key="note.path">
|
||||||
<div
|
<div
|
||||||
@click="note.path.match(/\.(png|jpg|jpeg|gif|webp)$/i) ? (currentImage = note.path, currentNote = '') : loadNote(note.path)"
|
@click="note.path.match(/\.(png|jpg|jpeg|gif|webp)$/i) ? (currentImage = note.path, currentNote = '') : loadNote(note.path)"
|
||||||
class="p-4 rounded-lg border-2 cursor-pointer transition-all hover:shadow-lg"
|
class="p-4 rounded-lg border-2 cursor-pointer transition-all hover:shadow-lg"
|
||||||
|
|
@ -1164,12 +1175,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Show More Indicator -->
|
<!-- Show More Indicator -->
|
||||||
<div x-show="homepageNotes().length > 50" class="mt-6 text-center">
|
<div x-show="homepageNotes().length > HOMEPAGE_MAX_NOTES" class="mt-6 text-center">
|
||||||
<p class="text-sm" style="color: var(--text-secondary);">
|
<p class="text-sm" style="color: var(--text-secondary);">
|
||||||
Showing 50 of <span x-text="homepageNotes().length"></span> notes.
|
Showing <span x-text="HOMEPAGE_MAX_NOTES"></span> of <span x-text="homepageNotes().length"></span> notes in this folder.
|
||||||
<span class="cursor-pointer underline" style="color: var(--accent-primary);" @click="goToHomepageFolder('')">
|
|
||||||
View all in sidebar
|
|
||||||
</span>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1301,6 +1309,7 @@
|
||||||
:style="viewMode === 'split' ? `width: ${editorWidth}%;` : 'width: 100%;'"
|
:style="viewMode === 'split' ? `width: ${editorWidth}%;` : 'width: 100%;'"
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
|
id="note-editor"
|
||||||
x-model="noteContent"
|
x-model="noteContent"
|
||||||
@input="autoSave()"
|
@input="autoSave()"
|
||||||
@drop="onEditorDrop($event)"
|
@drop="onEditorDrop($event)"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue