open local PDFs in iframes, externals as links...

This commit is contained in:
Gamosoft 2026-01-18 19:04:05 +01:00
parent 1751e84dd3
commit 37d7c0eba9
3 changed files with 97 additions and 29 deletions

View File

@ -211,8 +211,23 @@ def process_media_for_export(markdown_content: str, note_folder: Path, notes_dir
alt_text = match.group(1) alt_text = match.group(1)
media_path = match.group(2) media_path = match.group(2)
# Skip external URLs and already-embedded base64 # Handle external URLs
if media_path.startswith(('http://', 'https://', 'data:')): 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('"', '&quot;').replace('<', '&lt;').replace('>', '&gt;')
safe_url = media_path.replace('"', '&quot;')
return f'''<a href="{safe_url}" target="_blank" rel="noopener noreferrer" style="display:flex;flex-direction:column;gap:0.25rem;padding:1rem 1.25rem;margin:1rem 0;background:linear-gradient(135deg,var(--bg-tertiary,#f8f9fa) 0%,var(--bg-secondary,#e9ecef) 100%);border:1px solid var(--border-primary,#dee2e6);border-radius:0.5rem;color:var(--text-primary,#212529);text-decoration:none;">
<span style="font-weight:600;">📄 {safe_name}</span>
<span style="font-size:0.75rem;color:var(--text-secondary,#6c757d);">Opens in new tab</span>
</a>'''
# 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) return match.group(0)
# Skip empty paths (from failed wikilink conversion) # Skip empty paths (from failed wikilink conversion)

View File

@ -2432,9 +2432,9 @@ function noteApp() {
const href = link.getAttribute('href'); const href = link.getAttribute('href');
if (!href) return; if (!href) return;
// Check if it's an external link // 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:')) { if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//') || href.startsWith('mailto:') || href.startsWith('/api/')) {
return; // Let external links work normally return; // Let external links and API paths work normally
} }
// Prevent default navigation for internal links // Prevent default navigation for internal links
@ -2606,6 +2606,7 @@ function noteApp() {
const wasCurrentNote = this.currentNote === draggedNotePath; const wasCurrentNote = this.currentNote === draggedNotePath;
await this.loadNotes(); await this.loadNotes();
await this.loadSharedNotePaths(); // Refresh shared paths after move
if (wasCurrentNote) { if (wasCurrentNote) {
this.currentNote = newPath; this.currentNote = newPath;
@ -2668,6 +2669,7 @@ function noteApp() {
} }
await this.loadNotes(); await this.loadNotes();
await this.loadSharedNotePaths(); // Refresh shared paths after folder move
// Update current note path if it was in the moved folder // Update current note path if it was in the moved folder
if (this.currentNote && this.currentNote.startsWith(oldPrefix)) { if (this.currentNote && this.currentNote.startsWith(oldPrefix)) {
this.currentNote = this.currentNote.replace(oldPrefix, newPrefix); this.currentNote = this.currentNote.replace(oldPrefix, newPrefix);
@ -4021,7 +4023,8 @@ function noteApp() {
case 'video': case 'video':
return `<div class="media-embed media-video"><video controls preload="none" poster="" src="${mediaSrc}" title="${safeAlt}"></video></div>`; return `<div class="media-embed media-video"><video controls preload="none" poster="" src="${mediaSrc}" title="${safeAlt}"></video></div>`;
case 'document': case 'document':
return `<div class="media-embed media-pdf"><iframe src="${mediaSrc}" title="${safeAlt}" loading="lazy"></iframe></div>`; // Local PDFs: show iframe preview
return `<div class="media-embed media-pdf"><iframe src="${mediaSrc}" title="${safeAlt}"></iframe></div>`;
default: // image default: // image
return `<img src="${mediaSrc}" alt="${safeAlt}" title="${safeAlt}">`; return `<img src="${mediaSrc}" alt="${safeAlt}" title="${safeAlt}">`;
} }
@ -4110,11 +4113,11 @@ function noteApp() {
images.forEach(img => { images.forEach(img => {
let src = img.getAttribute('src'); let src = img.getAttribute('src');
if (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 // Transform relative paths to /api/media/ for serving
// Skip external URLs and already-transformed paths if (isLocal && !src.startsWith('/api/media/')) {
if (!src.startsWith('http://') && !src.startsWith('https://') &&
!src.startsWith('//') && !src.startsWith('/api/media/') &&
!src.startsWith('data:')) {
// URL-encode path segments to handle spaces and special characters // URL-encode path segments to handle spaces and special characters
const encodedPath = src.split('/').map(segment => { const encodedPath = src.split('/').map(segment => {
try { try {
@ -4132,25 +4135,42 @@ function noteApp() {
const altText = img.getAttribute('alt') || src.split('/').pop().replace(/\.[^/.]+$/, ''); const altText = img.getAttribute('alt') || src.split('/').pop().replace(/\.[^/.]+$/, '');
const safeAlt = altText.replace(/"/g, '&quot;'); const safeAlt = altText.replace(/"/g, '&quot;');
if (mediaType === 'audio') { // Only convert LOCAL media to embedded elements (security)
const wrapper = document.createElement('div'); // External non-image media gets styled links instead
wrapper.className = 'media-embed media-audio'; if (isLocal || src.startsWith('/api/media/')) {
wrapper.innerHTML = `<audio controls preload="none" src="${src}" title="${safeAlt}"></audio><span class="media-caption">${safeAlt}</span>`; if (mediaType === 'audio') {
img.replaceWith(wrapper); const wrapper = document.createElement('div');
return; wrapper.className = 'media-embed media-audio';
} else if (mediaType === 'video') { wrapper.innerHTML = `<audio controls preload="none" src="${src}" title="${safeAlt}"></audio><span class="media-caption">${safeAlt}</span>`;
const wrapper = document.createElement('div'); img.replaceWith(wrapper);
wrapper.className = 'media-embed media-video'; return;
wrapper.innerHTML = `<video controls preload="none" poster="" src="${src}" title="${safeAlt}"></video>`; } else if (mediaType === 'video') {
img.replaceWith(wrapper); const wrapper = document.createElement('div');
return; wrapper.className = 'media-embed media-video';
} else if (mediaType === 'document') { wrapper.innerHTML = `<video controls preload="none" poster="" src="${src}" title="${safeAlt}"></video>`;
const wrapper = document.createElement('div'); img.replaceWith(wrapper);
wrapper.className = 'media-embed media-pdf'; return;
wrapper.innerHTML = `<iframe src="${src}" title="${safeAlt}" loading="lazy"></iframe>`; } else if (mediaType === 'document') {
img.replaceWith(wrapper); // Local PDFs: show iframe preview
const wrapper = document.createElement('div');
wrapper.className = 'media-embed media-pdf';
wrapper.innerHTML = `<iframe src="${src}" title="${safeAlt}"></iframe>`;
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 = `<span class="pdf-link-content">📄 ${safeAlt}</span><span class="pdf-link-note">Opens in new tab</span>`;
img.replaceWith(link);
return; return;
} }
// External audio/video: leave as broken image for security
} }
// For regular images, set title attribute // For regular images, set title attribute

View File

@ -484,17 +484,50 @@
border-radius: 0; border-radius: 0;
} }
/* PDF embeds */ /* PDF embeds (iframe) */
.markdown-preview .media-pdf { .markdown-preview .media-pdf {
border: 1px solid var(--border-primary); border: 1px solid var(--border-primary);
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
background: var(--bg-secondary);
} }
.markdown-preview .media-pdf iframe { .markdown-preview .media-pdf iframe {
width: 100%; width: 100%;
height: 600px; height: 600px;
border: none; 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 */ /* Task lists */