diff --git a/backend/export.py b/backend/export.py index e5eb995..d924658 100644 --- a/backend/export.py +++ b/backend/export.py @@ -211,8 +211,23 @@ def process_media_for_export(markdown_content: str, note_folder: Path, notes_dir alt_text = match.group(1) media_path = match.group(2) - # Skip external URLs and already-embedded base64 - if media_path.startswith(('http://', 'https://', 'data:')): + # Handle external URLs + if media_path.startswith(('http://', 'https://')): + # Check if it's a PDF - generate styled external link + media_type = get_media_type(media_path) + if media_type == 'document': + display_name = alt_text or Path(media_path).stem + safe_name = display_name.replace('"', '"').replace('<', '<').replace('>', '>') + safe_url = media_path.replace('"', '"') + return f''' +📄 {safe_name} +Opens in new tab +''' + # Other external media: keep as-is (will show as broken image) + return match.group(0) + + # Skip already-embedded base64 + if media_path.startswith('data:'): return match.group(0) # Skip empty paths (from failed wikilink conversion) diff --git a/frontend/app.js b/frontend/app.js index 6fb92ca..c7aed86 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -2432,9 +2432,9 @@ function noteApp() { const href = link.getAttribute('href'); if (!href) return; - // Check if it's an external link - if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//') || href.startsWith('mailto:')) { - return; // Let external links work normally + // Check if it's an external link or API path (media files, etc.) + if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//') || href.startsWith('mailto:') || href.startsWith('/api/')) { + return; // Let external links and API paths work normally } // Prevent default navigation for internal links @@ -2606,6 +2606,7 @@ function noteApp() { const wasCurrentNote = this.currentNote === draggedNotePath; await this.loadNotes(); + await this.loadSharedNotePaths(); // Refresh shared paths after move if (wasCurrentNote) { this.currentNote = newPath; @@ -2668,6 +2669,7 @@ function noteApp() { } await this.loadNotes(); + await this.loadSharedNotePaths(); // Refresh shared paths after folder move // Update current note path if it was in the moved folder if (this.currentNote && this.currentNote.startsWith(oldPrefix)) { this.currentNote = this.currentNote.replace(oldPrefix, newPrefix); @@ -4021,7 +4023,8 @@ function noteApp() { case 'video': return `
`; case 'document': - return `
`; + // Local PDFs: show iframe preview + return `
`; default: // image return `${safeAlt}`; } @@ -4110,11 +4113,11 @@ function noteApp() { images.forEach(img => { let src = img.getAttribute('src'); if (src) { + const isExternal = src.startsWith('http://') || src.startsWith('https://') || src.startsWith('//'); + const isLocal = !isExternal && !src.startsWith('data:'); + // Transform relative paths to /api/media/ for serving - // Skip external URLs and already-transformed paths - if (!src.startsWith('http://') && !src.startsWith('https://') && - !src.startsWith('//') && !src.startsWith('/api/media/') && - !src.startsWith('data:')) { + if (isLocal && !src.startsWith('/api/media/')) { // URL-encode path segments to handle spaces and special characters const encodedPath = src.split('/').map(segment => { try { @@ -4132,25 +4135,42 @@ function noteApp() { const altText = img.getAttribute('alt') || src.split('/').pop().replace(/\.[^/.]+$/, ''); const safeAlt = altText.replace(/"/g, '"'); - if (mediaType === 'audio') { - const wrapper = document.createElement('div'); - wrapper.className = 'media-embed media-audio'; - wrapper.innerHTML = `${safeAlt}`; - img.replaceWith(wrapper); - return; - } else if (mediaType === 'video') { - const wrapper = document.createElement('div'); - wrapper.className = 'media-embed media-video'; - wrapper.innerHTML = ``; - img.replaceWith(wrapper); - return; - } else if (mediaType === 'document') { - const wrapper = document.createElement('div'); - wrapper.className = 'media-embed media-pdf'; - wrapper.innerHTML = ``; - img.replaceWith(wrapper); + // Only convert LOCAL media to embedded elements (security) + // External non-image media gets styled links instead + if (isLocal || src.startsWith('/api/media/')) { + if (mediaType === 'audio') { + const wrapper = document.createElement('div'); + wrapper.className = 'media-embed media-audio'; + wrapper.innerHTML = `${safeAlt}`; + img.replaceWith(wrapper); + return; + } else if (mediaType === 'video') { + const wrapper = document.createElement('div'); + wrapper.className = 'media-embed media-video'; + wrapper.innerHTML = ``; + img.replaceWith(wrapper); + return; + } else if (mediaType === 'document') { + // Local PDFs: show iframe preview + const wrapper = document.createElement('div'); + wrapper.className = 'media-embed media-pdf'; + wrapper.innerHTML = ``; + img.replaceWith(wrapper); + return; + } + } else if (isExternal && mediaType === 'document') { + // External PDFs: styled link (opens in new tab) + const link = document.createElement('a'); + link.href = src; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + link.className = 'pdf-link'; + link.title = `Open ${safeAlt}`; + link.innerHTML = `📄 ${safeAlt}Opens in new tab`; + img.replaceWith(link); return; } + // External audio/video: leave as broken image for security } // For regular images, set title attribute diff --git a/frontend/index.html b/frontend/index.html index 3995c61..1f87c21 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -484,17 +484,50 @@ border-radius: 0; } - /* PDF embeds */ + /* PDF embeds (iframe) */ .markdown-preview .media-pdf { border: 1px solid var(--border-primary); border-radius: 0.5rem; overflow: hidden; + background: var(--bg-secondary); } .markdown-preview .media-pdf iframe { width: 100%; height: 600px; border: none; - background: #525659; + display: block; + } + + /* PDF links (for external PDFs) */ + .markdown-preview .pdf-link { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 1rem 1.25rem; + margin: 1rem 0; + background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%); + border: 1px solid var(--border-primary); + border-radius: 0.5rem; + color: var(--text-primary); + text-decoration: none; + transition: all 0.15s ease; + } + .markdown-preview .pdf-link:hover { + background: var(--bg-secondary); + border-color: var(--accent-primary); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + } + .markdown-preview .pdf-link:hover .pdf-link-content { + color: var(--accent-primary); + } + .markdown-preview .pdf-link-content { + font-weight: 600; + color: var(--text-primary); + } + .markdown-preview .pdf-link-note { + font-size: 0.75rem; + color: var(--text-secondary); } /* Task lists */