unify templating and refactoring

This commit is contained in:
Gamosoft 2026-01-19 10:14:37 +01:00
parent 4d0b744f55
commit 60f145daa2
2 changed files with 71 additions and 78 deletions

View File

@ -466,7 +466,7 @@ function noteApp() {
this.loadSyntaxHighlightSetting(); this.loadSyntaxHighlightSetting();
// Parse URL and load specific note if provided // Parse URL and load specific note if provided
this.loadNoteFromURL(); this.loadItemFromURL();
// Set initial homepage state ONLY if we're actually on the homepage // Set initial homepage state ONLY if we're actually on the homepage
if (window.location.pathname === '/') { if (window.location.pathname === '/') {
@ -490,6 +490,9 @@ function noteApp() {
this.searchResults = []; this.searchResults = [];
this.clearSearchHighlights(); this.clearSearchHighlights();
} }
} else if (e.state && e.state.mediaPath) {
// Navigating to a media file
this.viewMedia(e.state.mediaPath, null, false);
} else { } else {
// Navigating back to homepage // Navigating back to homepage
this.currentNote = ''; this.currentNote = '';
@ -1886,47 +1889,7 @@ function noteApp() {
// Then, render notes and images in this folder (after subfolders) // Then, render notes and images in this folder (after subfolders)
if (folder.notes && folder.notes.length > 0) { if (folder.notes && folder.notes.length > 0) {
folder.notes.forEach(note => { folder.notes.forEach(note => {
// Check if it's a media file or a note html += this.renderNoteItem(note);
const isMediaFile = note.type !== 'note';
const isCurrentNote = this.currentNote === note.path;
const isCurrentMedia = this.currentMedia === note.path;
const isCurrent = isMediaFile ? isCurrentMedia : isCurrentNote;
// Icon based on media type, share icon for shared notes
const isShared = !isMediaFile && this.isNoteShared(note.path);
const shareIcon = isShared ? '<svg title="Shared" style="display: inline-block; width: 12px; height: 12px; vertical-align: middle; margin-right: 2px; opacity: 0.7;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg>' : '';
const icon = this.getMediaIcon(note.type);
html += `
<div
data-path="${esc(note.path)}"
data-name="${esc(note.name)}"
data-type="${note.type}"
draggable="true"
ondragstart="window.$root.onItemDragStart(this.dataset.path, this.dataset.type || 'note', event)"
ondragend="window.$root.onItemDragEnd()"
onclick="window.$root.handleItemClick(this)"
class="note-item px-2 py-1 text-sm relative"
style="${isCurrent ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'} ${isMediaFile ? 'opacity: 0.85;' : ''} cursor: pointer;"
onmouseover="window.$root.handleItemHover(this, true)"
onmouseout="window.$root.handleItemHover(this, false)"
>
<span class="truncate" style="display: block; padding-right: 30px;" title="${esc(note.name)}">${shareIcon}${icon}${icon ? ' ' : ''}${esc(note.name)}</span>
<button
data-path="${esc(note.path)}"
data-name="${esc(note.name)}"
data-type="${note.type}"
onclick="window.$root.handleDeleteItemClick(this, event)"
class="note-delete-btn absolute right-2 top-1/2 transform -translate-y-1/2 px-1 py-0.5 text-xs rounded hover:brightness-110 transition-opacity"
style="opacity: 0; color: var(--error);"
title="${isMediaFile ? 'Delete file' : 'Delete note'}"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
`;
}); });
} }
@ -1937,6 +1900,60 @@ function noteApp() {
return html; return html;
}, },
// Render a single note/media item (used by both folders and root level)
renderNoteItem(note) {
const esc = (s) => this.escapeHtmlAttr(s);
const isMediaFile = note.type !== 'note';
const isCurrentNote = this.currentNote === note.path;
const isCurrentMedia = this.currentMedia === note.path;
const isCurrent = isMediaFile ? isCurrentMedia : isCurrentNote;
// Share icon for shared notes
const isShared = !isMediaFile && this.isNoteShared(note.path);
const shareIcon = isShared ? '<svg title="Shared" style="display: inline-block; width: 12px; height: 12px; vertical-align: middle; margin-right: 2px; opacity: 0.7;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg>' : '';
const icon = this.getMediaIcon(note.type);
return `
<div
data-path="${esc(note.path)}"
data-name="${esc(note.name)}"
data-type="${note.type}"
draggable="true"
ondragstart="window.$root.onItemDragStart(this.dataset.path, this.dataset.type || 'note', event)"
ondragend="window.$root.onItemDragEnd()"
onclick="window.$root.handleItemClick(this)"
class="note-item px-2 py-1 text-sm relative"
style="${isCurrent ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'} ${isMediaFile ? 'opacity: 0.85;' : ''} cursor: pointer;"
onmouseover="window.$root.handleItemHover(this, true)"
onmouseout="window.$root.handleItemHover(this, false)"
>
<span class="truncate" style="display: block; padding-right: 30px;" title="${esc(note.name)}">${shareIcon}${icon}${icon ? ' ' : ''}${esc(note.name)}</span>
<button
data-path="${esc(note.path)}"
data-name="${esc(note.name)}"
data-type="${note.type}"
onclick="window.$root.handleDeleteItemClick(this, event)"
class="note-delete-btn absolute right-2 top-1/2 transform -translate-y-1/2 px-1 py-0.5 text-xs rounded hover:brightness-110 transition-opacity"
style="opacity: 0; color: var(--error);"
title="${isMediaFile ? 'Delete file' : 'Delete note'}"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
`;
},
// Render root-level items (notes and media not in any folder)
renderRootItems() {
const root = this.folderTree['__root__'];
if (!root || !root.notes || root.notes.length === 0) {
return '';
}
return root.notes.map(note => this.renderNoteItem(note)).join('');
},
// Toggle folder expansion // Toggle folder expansion
toggleFolder(folderPath) { toggleFolder(folderPath) {
if (this.expandedFolders.has(folderPath)) { if (this.expandedFolders.has(folderPath)) {
@ -2362,6 +2379,9 @@ function noteApp() {
const fileName = mediaPath.split('/').pop(); const fileName = mediaPath.split('/').pop();
document.title = `${fileName} - ${this.appName}`; document.title = `${fileName} - ${this.appName}`;
// Expand folder tree to show the media file
this.expandFolderForNote(mediaPath);
// Update browser URL // Update browser URL
if (updateHistory) { if (updateHistory) {
// Encode each path segment to handle special characters // Encode each path segment to handle special characters
@ -2752,9 +2772,9 @@ function noteApp() {
} }
}, },
// Load note from URL path // Load item (note or media) from URL path
loadNoteFromURL() { loadItemFromURL() {
// Get path from URL (e.g., /folder/note or /note) // Get path from URL (e.g., /folder/note or /folder/image.png)
let path = window.location.pathname; let path = window.location.pathname;
// Strip .md extension if present (for MKdocs/Zensical integration) // Strip .md extension if present (for MKdocs/Zensical integration)
@ -2772,12 +2792,12 @@ function noteApp() {
// Remove leading slash and decode URL encoding (e.g., %20 -> space) // Remove leading slash and decode URL encoding (e.g., %20 -> space)
const decodedPath = decodeURIComponent(path.substring(1)); const decodedPath = decodeURIComponent(path.substring(1));
// Check if this is an image path (check if it exists in notes list as an image) // Check if this is a media file (image, audio, video, PDF)
const matchedItem = this.notes.find(n => n.path === decodedPath); const matchedItem = this.notes.find(n => n.path === decodedPath);
if (matchedItem && matchedItem.type === 'image') { if (matchedItem && matchedItem.type !== 'note') {
// It's an image, view it // It's a media file, view it
this.viewImage(decodedPath, false); // false = don't update history (we're already at this URL) this.viewMedia(decodedPath, matchedItem.type, false); // false = don't update history
} else { } else {
// It's a note, add .md extension and load it // It's a note, add .md extension and load it
const notePath = decodedPath + '.md'; const notePath = decodedPath + '.md';

View File

@ -1529,35 +1529,8 @@
<div x-html="renderFolderRecursive(folder, 0, true)"></div> <div x-html="renderFolderRecursive(folder, 0, true)"></div>
</template> </template>
<!-- Root notes and images (no folder) - shown after folders --> <!-- Root notes and images (no folder) - rendered via JS for consistency -->
<template x-for="note in (folderTree.__root__ && folderTree.__root__.notes ? folderTree.__root__.notes : [])" :key="note.path"> <div x-html="renderRootItems()"></div>
<div
draggable="true"
@dragstart="onItemDragStart(note.path, note.type, $event)"
@dragend="onItemDragEnd()"
@click="openItem(note.path, note.type)"
class="note-item px-2 py-1 text-sm relative"
:style="((note.type !== 'note' ? currentMedia === note.path : currentNote === note.path) ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);') + (note.type !== 'note' ? ' opacity: 0.85;' : '') + (draggedItem ? ' cursor: grabbing;' : ' cursor: pointer;')"
@mouseover="if(currentNote !== note.path && currentMedia !== note.path) $el.style.backgroundColor='var(--bg-hover)'"
@mouseout="if(currentNote !== note.path && currentMedia !== note.path) $el.style.backgroundColor='transparent'"
>
<span class="truncate" style="display: block; padding-right: 30px;" :title="note.name">
<span x-show="note.type === 'note' && isNoteShared(note.path)" style="opacity: 0.6; margin-right: 2px;" title="Shared">🔗</span>
<span x-text="getMediaIcon(note.type)"></span>
<span x-text="note.name"></span>
</span>
<button
@click.stop="note.type !== 'note' ? deleteMedia(note.path) : deleteNote(note.path, note.name)"
class="note-delete-btn absolute right-2 top-1/2 transform -translate-y-1/2 px-1 py-0.5 text-xs rounded hover:brightness-110 transition-opacity"
style="opacity: 0; color: var(--error);"
:title="note.type !== 'note' ? t('toolbar.delete_image') : t('toolbar.delete_note')"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</template>
<!-- Empty state --> <!-- Empty state -->
<template x-if="notes.length === 0 && allFolders.length === 0"> <template x-if="notes.length === 0 && allFolders.length === 0">