performance improvements
This commit is contained in:
parent
9b46bbcde0
commit
5fa273583f
222
frontend/app.js
222
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
|
return result;
|
||||||
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];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
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) {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return childFolders
|
// Map to simplified structure (note count already cached in folder node)
|
||||||
|
const result = 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: '' }];
|
const breadcrumb = [{ name: 'Home', path: '' }];
|
||||||
|
|
||||||
if (!this.selectedHomepageFolder) {
|
if (this.selectedHomepageFolder) {
|
||||||
return breadcrumb;
|
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 });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
this._homepageCache.breadcrumb = breadcrumb;
|
||||||
|
this._homepageCache.folderPath = this.selectedHomepageFolder;
|
||||||
|
|
||||||
return breadcrumb;
|
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 = '') {
|
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 = [];
|
||||||
|
|
@ -590,8 +602,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 +801,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 +810,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 +841,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
|
||||||
|
|
@ -2955,12 +2990,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;"
|
||||||
>
|
>
|
||||||
|
|
@ -1142,7 +1142,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 +1164,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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue