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 `
`;
}
@@ -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 */