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
|
||||
|
||||
🌐 **[Visit the official website](https://www.notediscovery.com)**
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ from .utils import (
|
|||
save_note,
|
||||
delete_note,
|
||||
search_notes,
|
||||
parse_wiki_links,
|
||||
create_note_metadata,
|
||||
ensure_directories,
|
||||
create_folder,
|
||||
|
|
@ -336,8 +335,8 @@ async def api_documentation():
|
|||
{
|
||||
"method": "GET",
|
||||
"path": "/api/graph",
|
||||
"description": "Get graph data for note visualization (wiki links)",
|
||||
"response": "{ nodes: [{ id, label }], edges: [{ from, to }] }"
|
||||
"description": "Get graph data for note visualization",
|
||||
"response": "{ nodes: [{ id, label }], edges: [] }"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
|
|
@ -651,13 +650,9 @@ async def get_note(note_path: str):
|
|||
if transformed_content is not None:
|
||||
content = transformed_content
|
||||
|
||||
# Parse wiki links
|
||||
links = parse_wiki_links(content)
|
||||
|
||||
return {
|
||||
"path": note_path,
|
||||
"content": content,
|
||||
"links": links,
|
||||
"metadata": create_note_metadata(config['storage']['notes_dir'], note_path)
|
||||
}
|
||||
except HTTPException:
|
||||
|
|
@ -745,28 +740,22 @@ async def search(q: str):
|
|||
|
||||
@api_router.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:
|
||||
notes = get_all_notes(config['storage']['notes_dir'])
|
||||
nodes = []
|
||||
edges = []
|
||||
|
||||
# Build graph structure
|
||||
# Build graph structure - only nodes for now
|
||||
for note in notes:
|
||||
if note.get('type') == 'note': # Only include actual notes
|
||||
nodes.append({
|
||||
"id": note['path'],
|
||||
"label": note['name']
|
||||
})
|
||||
|
||||
# Get links from this note
|
||||
content = get_note_content(config['storage']['notes_dir'], note['path'])
|
||||
if content:
|
||||
links = parse_wiki_links(content)
|
||||
for link in links:
|
||||
edges.append({
|
||||
"from": note['path'],
|
||||
"to": link
|
||||
})
|
||||
# Note: Link detection between notes could be implemented here using markdown link parsing
|
||||
# For now, returning empty edges
|
||||
|
||||
return {"nodes": nodes, "edges": edges}
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -236,13 +236,6 @@ def delete_note(notes_dir: str, note_path: str) -> bool:
|
|||
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]:
|
||||
"""Simple full-text search through all notes"""
|
||||
results = []
|
||||
|
|
|
|||
|
|
@ -641,8 +641,8 @@
|
|||
<div class="benefit-item">
|
||||
<span class="emoji">🔗</span>
|
||||
<div class="text">
|
||||
<strong>Wiki Links</strong>
|
||||
Connect notes with internal links
|
||||
<strong>Internal Links</strong>
|
||||
Connect notes with markdown links
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -725,7 +725,7 @@
|
|||
"url": "https://www.notediscovery.com",
|
||||
"applicationCategory": "ProductivityApplication",
|
||||
"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": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
## 🔗 Linking & Discovery
|
||||
|
||||
### 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
|
||||
- **Click to navigate** - Jump between notes seamlessly
|
||||
- **External links** - Open in new tabs automatically
|
||||
|
|
@ -139,6 +139,8 @@ graph TD
|
|||
|
||||
## ⚡ Keyboard Shortcuts
|
||||
|
||||
### General
|
||||
|
||||
| Windows/Linux | Mac | Action |
|
||||
|---------------|-----|--------|
|
||||
| `Ctrl+S` | `Cmd+S` | Save note |
|
||||
|
|
@ -149,6 +151,14 @@ graph TD
|
|||
| `F3` | `F3` | Next 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
|
||||
|
||||
- **Instant loading** - No lag, no loading spinners
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ curl -X POST http://localhost:8000/api/plugins/note_stats/toggle \
|
|||
|
||||
### 🔗 Links & References
|
||||
- **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
|
||||
- **Images** - `` count
|
||||
|
||||
|
|
@ -191,7 +191,7 @@ Find orphaned notes or track reference density.
|
|||
|
||||
**Words:** Split on whitespace, excluding code blocks
|
||||
**Reading Time:** `words / 200 minutes`
|
||||
**Links:** Regex match `[text](url)` and `[[WikiLink]]`
|
||||
**Links:** Regex match `[text](url)` markdown links
|
||||
**Tasks:** Match `- [ ]` and `- [x]`
|
||||
**Code Blocks:** Match ` ```language ` ` fences
|
||||
**Headings:** Match `#`, `##`, `###` at line start
|
||||
|
|
|
|||
313
frontend/app.js
313
frontend/app.js
|
|
@ -94,89 +94,112 @@ function noteApp() {
|
|||
// Dropdown state
|
||||
showNewDropdown: false,
|
||||
|
||||
// Homepage folder navigation
|
||||
// Homepage state
|
||||
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() {
|
||||
// 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') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const folderNode = this.getFolderNode(this.selectedHomepageFolder || '');
|
||||
if (!folderNode || !Array.isArray(folderNode.notes)) {
|
||||
return [];
|
||||
}
|
||||
const result = (folderNode && Array.isArray(folderNode.notes)) ? folderNode.notes : [];
|
||||
|
||||
return folderNode.notes;
|
||||
},
|
||||
// Cache the result
|
||||
this._homepageCache.notes = result;
|
||||
this._homepageCache.folderPath = this.selectedHomepageFolder;
|
||||
|
||||
// Helper: Format file size nicely
|
||||
formatSize(bytes) {
|
||||
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];
|
||||
return result;
|
||||
},
|
||||
|
||||
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') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get child folders
|
||||
let childFolders = [];
|
||||
if (!this.selectedHomepageFolder) {
|
||||
// Root level: all top-level folders
|
||||
childFolders = Object.entries(this.folderTree)
|
||||
.filter(([key]) => key !== '__root__')
|
||||
.map(([, folder]) => folder);
|
||||
} else {
|
||||
// Inside a folder: get its children
|
||||
const parentFolder = this.getFolderNode(this.selectedHomepageFolder);
|
||||
if (!parentFolder || !parentFolder.children) {
|
||||
return [];
|
||||
}
|
||||
if (parentFolder && parentFolder.children) {
|
||||
childFolders = Object.values(parentFolder.children);
|
||||
}
|
||||
|
||||
if (!Array.isArray(childFolders) || childFolders.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return childFolders
|
||||
// Map to simplified structure (note count already cached in folder node)
|
||||
const result = childFolders
|
||||
.map(folder => ({
|
||||
name: folder.name,
|
||||
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()));
|
||||
|
||||
// Cache the result
|
||||
this._homepageCache.folders = result;
|
||||
this._homepageCache.folderPath = this.selectedHomepageFolder;
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
homepageBreadcrumb() {
|
||||
// Always return at least Home breadcrumb
|
||||
// Safety check - ensure we always return an array
|
||||
try {
|
||||
// Return cached result if folder hasn't changed
|
||||
if (this._homepageCache.folderPath === this.selectedHomepageFolder && this._homepageCache.breadcrumb) {
|
||||
return this._homepageCache.breadcrumb;
|
||||
}
|
||||
|
||||
const breadcrumb = [{ name: 'Home', path: '' }];
|
||||
|
||||
if (!this.selectedHomepageFolder) {
|
||||
return breadcrumb;
|
||||
}
|
||||
|
||||
const parts = (this.selectedHomepageFolder || '').split('/').filter(part => part.trim() !== '');
|
||||
|
||||
if (this.selectedHomepageFolder) {
|
||||
const parts = this.selectedHomepageFolder.split('/').filter(Boolean);
|
||||
let currentPath = '';
|
||||
|
||||
parts.forEach(part => {
|
||||
currentPath = currentPath ? currentPath + '/' + part : part;
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||
breadcrumb.push({ name: part, path: currentPath });
|
||||
});
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
this._homepageCache.breadcrumb = breadcrumb;
|
||||
this._homepageCache.folderPath = this.selectedHomepageFolder;
|
||||
|
||||
return breadcrumb;
|
||||
} catch (error) {
|
||||
// Fallback to just Home if anything goes wrong
|
||||
return [{ name: 'Home', path: '' }];
|
||||
}
|
||||
},
|
||||
|
||||
// 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 = '') {
|
||||
|
|
@ -185,10 +208,7 @@ function noteApp() {
|
|||
}
|
||||
|
||||
if (!folderPath) {
|
||||
if (this.folderTree['__root__']) {
|
||||
return this.folderTree['__root__'];
|
||||
}
|
||||
return { name: '', path: '', children: {}, notes: [] };
|
||||
return this.folderTree['__root__'] || { name: '', path: '', children: {}, notes: [], noteCount: 0 };
|
||||
}
|
||||
|
||||
const parts = folderPath.split('/').filter(Boolean);
|
||||
|
|
@ -206,22 +226,6 @@ function noteApp() {
|
|||
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)
|
||||
get isAppEmpty() {
|
||||
const notesArray = Array.isArray(this.notes) ? this.notes : [];
|
||||
|
|
@ -291,6 +295,14 @@ function noteApp() {
|
|||
this.selectedHomepageFolder = '';
|
||||
}
|
||||
|
||||
// Invalidate cache to force recalculation
|
||||
this._homepageCache = {
|
||||
folderPath: null,
|
||||
notes: null,
|
||||
folders: null,
|
||||
breadcrumb: null
|
||||
};
|
||||
|
||||
// Clear search
|
||||
this.searchQuery = '';
|
||||
this.searchResults = [];
|
||||
|
|
@ -373,6 +385,28 @@ function noteApp() {
|
|||
e.preventDefault();
|
||||
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
|
||||
this.folderTree = { ...tree };
|
||||
// Calculate and cache note counts recursively (for performance)
|
||||
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)
|
||||
|
|
@ -756,8 +823,6 @@ function noteApp() {
|
|||
}
|
||||
// Force Alpine reactivity by creating new Set reference
|
||||
this.expandedFolders = new Set(this.expandedFolders);
|
||||
// Also trigger folderTree reactivity to re-render x-html
|
||||
this.folderTree = { ...this.folderTree };
|
||||
},
|
||||
|
||||
// Check if folder is expanded
|
||||
|
|
@ -767,25 +832,22 @@ function noteApp() {
|
|||
|
||||
// Expand all folders
|
||||
expandAllFolders() {
|
||||
// Add all folder paths to expandedFolders
|
||||
this.allFolders.forEach(folder => {
|
||||
this.expandedFolders.add(folder);
|
||||
});
|
||||
// Force Alpine reactivity by creating new object reference (no rebuild needed)
|
||||
this.folderTree = { ...this.folderTree };
|
||||
// Force Alpine reactivity
|
||||
this.expandedFolders = new Set(this.expandedFolders);
|
||||
},
|
||||
|
||||
// Collapse all folders
|
||||
collapseAllFolders() {
|
||||
this.expandedFolders.clear();
|
||||
// Force Alpine reactivity by creating new object reference (no rebuild needed)
|
||||
this.folderTree = { ...this.folderTree };
|
||||
// Force Alpine reactivity
|
||||
this.expandedFolders = new Set(this.expandedFolders);
|
||||
},
|
||||
|
||||
// Expand folder tree to show a specific note
|
||||
expandFolderForNote(notePath) {
|
||||
// Extract folder path from note path
|
||||
// e.g., "folder1/folder2/note.md" -> ["folder1", "folder1/folder2"]
|
||||
const parts = notePath.split('/');
|
||||
|
||||
// If note is in root, no folders to expand
|
||||
|
|
@ -801,13 +863,8 @@ function noteApp() {
|
|||
this.expandedFolders.add(currentPath);
|
||||
});
|
||||
|
||||
// Force Alpine reactivity by creating new object reference
|
||||
// This ensures the UI updates to show the expanded folders
|
||||
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);
|
||||
// Force Alpine reactivity
|
||||
this.expandedFolders = new Set(this.expandedFolders);
|
||||
},
|
||||
|
||||
// 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
|
||||
async saveNote() {
|
||||
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
|
||||
let html = marked.parse(content);
|
||||
let html = marked.parse(this.noteContent);
|
||||
|
||||
// Post-process: Add target="_blank" to external links and title attributes to images
|
||||
// Parse as DOM to safely manipulate
|
||||
|
|
@ -2955,12 +3069,37 @@ function noteApp() {
|
|||
|
||||
// Homepage folder navigation methods
|
||||
goToHomepageFolder(folderPath) {
|
||||
// Navigate to folder (empty string = root)
|
||||
this.selectedHomepageFolder = folderPath || '';
|
||||
|
||||
// Update browser history
|
||||
const state = { homepageFolder: folderPath || '' };
|
||||
window.history.pushState(state, '', '/');
|
||||
// Invalidate cache to force recalculation
|
||||
this._homepageCache = {
|
||||
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);">
|
||||
<button
|
||||
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"
|
||||
style="background: none; border: none; outline: none; cursor: pointer;"
|
||||
>
|
||||
|
|
@ -997,6 +997,17 @@
|
|||
<span x-text="notes.length"></span> notes
|
||||
<span class="mx-1">·</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>
|
||||
|
|
@ -1142,7 +1153,7 @@
|
|||
</template>
|
||||
|
||||
<!-- 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
|
||||
@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"
|
||||
|
|
@ -1164,12 +1175,9 @@
|
|||
</template>
|
||||
|
||||
<!-- 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);">
|
||||
Showing 50 of <span x-text="homepageNotes().length"></span> notes.
|
||||
<span class="cursor-pointer underline" style="color: var(--accent-primary);" @click="goToHomepageFolder('')">
|
||||
View all in sidebar
|
||||
</span>
|
||||
Showing <span x-text="HOMEPAGE_MAX_NOTES"></span> of <span x-text="homepageNotes().length"></span> notes in this folder.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1301,6 +1309,7 @@
|
|||
:style="viewMode === 'split' ? `width: ${editorWidth}%;` : 'width: 100%;'"
|
||||
>
|
||||
<textarea
|
||||
id="note-editor"
|
||||
x-model="noteContent"
|
||||
@input="autoSave()"
|
||||
@drop="onEditorDrop($event)"
|
||||
|
|
|
|||
Loading…
Reference in New Issue