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)
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('"', '&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)
# Skip empty paths (from failed wikilink conversion)

View File

@ -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 `<div class="media-embed media-video"><video controls preload="none" poster="" src="${mediaSrc}" title="${safeAlt}"></video></div>`;
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
return `<img src="${mediaSrc}" alt="${safeAlt}" title="${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,6 +4135,9 @@ function noteApp() {
const altText = img.getAttribute('alt') || src.split('/').pop().replace(/\.[^/.]+$/, '');
const safeAlt = altText.replace(/"/g, '&quot;');
// 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';
@ -4145,12 +4151,26 @@ function noteApp() {
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 = `<iframe src="${src}" title="${safeAlt}" loading="lazy"></iframe>`;
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;
}
// External audio/video: leave as broken image for security
}
// For regular images, set title attribute

View File

@ -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 */