added options to preview/export
This commit is contained in:
parent
88302fecd3
commit
5c6e8f215d
|
|
@ -143,25 +143,22 @@ def find_image_in_attachments(image_name: str, note_folder: Path, notes_dir: Pat
|
||||||
return find_media_in_attachments(image_name, note_folder, notes_dir)
|
return find_media_in_attachments(image_name, note_folder, notes_dir)
|
||||||
|
|
||||||
|
|
||||||
def generate_media_html(base64_url: str, media_type: str, alt_text: str) -> str:
|
def generate_media_placeholder(media_type: str, alt_text: str) -> str:
|
||||||
"""Generate appropriate HTML for embedded media based on type."""
|
"""Generate a placeholder for non-embeddable media (audio, video, PDF)."""
|
||||||
safe_alt = alt_text.replace('"', '"')
|
safe_alt = alt_text.replace('"', '"').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
if media_type == 'audio':
|
icons = {'audio': '🎵', 'video': '🎬', 'document': '📄'}
|
||||||
return f'''<div class="media-embed media-audio" style="margin:1.5rem 0;padding:1.5rem;background:linear-gradient(135deg,var(--bg-tertiary,#f5f5f5) 0%,var(--bg-secondary,#eee) 100%);border:1px solid var(--border-primary,#ddd);border-radius:0.5rem;display:flex;flex-direction:column;align-items:center;">
|
labels = {'audio': 'Audio file', 'video': 'Video file', 'document': 'PDF document'}
|
||||||
<audio controls preload="none" src="{base64_url}" title="{safe_alt}" style="width:100%;max-width:500px;border-radius:2rem;"></audio>
|
icon = icons.get(media_type, '📎')
|
||||||
<span style="margin-top:0.75rem;font-size:0.875rem;color:var(--text-secondary,#666);">{safe_alt}</span>
|
label = labels.get(media_type, 'Media file')
|
||||||
|
|
||||||
|
return f'''<div style="margin:1.5rem 0;padding:1.5rem;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;display:flex;align-items:center;gap:1rem;">
|
||||||
|
<span style="font-size:2rem;">{icon}</span>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600;color:var(--text-primary,#212529);">{safe_alt}</div>
|
||||||
|
<div style="font-size:0.875rem;color:var(--text-secondary,#6c757d);">{label} — not available in exported view</div>
|
||||||
|
</div>
|
||||||
</div>'''
|
</div>'''
|
||||||
elif media_type == 'video':
|
|
||||||
return f'''<div class="media-embed media-video" style="margin:1.5rem 0;background:#000;display:flex;justify-content:center;border-radius:0.5rem;overflow:hidden;">
|
|
||||||
<video controls preload="none" poster="" src="{base64_url}" title="{safe_alt}" style="width:100%;max-width:800px;max-height:450px;"></video>
|
|
||||||
</div>'''
|
|
||||||
elif media_type == 'document':
|
|
||||||
return f'''<div class="media-embed media-pdf" style="margin:1.5rem 0;border:1px solid var(--border-primary,#ddd);border-radius:0.5rem;overflow:hidden;">
|
|
||||||
<iframe src="{base64_url}" title="{safe_alt}" style="width:100%;height:600px;border:none;background:#525659;"></iframe>
|
|
||||||
</div>'''
|
|
||||||
else: # image
|
|
||||||
return f''
|
|
||||||
|
|
||||||
|
|
||||||
def embed_media_as_base64(markdown_content: str, note_folder: Path, notes_dir: Path) -> str:
|
def embed_media_as_base64(markdown_content: str, note_folder: Path, notes_dir: Path) -> str:
|
||||||
|
|
@ -182,19 +179,23 @@ def embed_media_as_base64(markdown_content: str, note_folder: Path, notes_dir: P
|
||||||
media_name = match.group(1).strip()
|
media_name = match.group(1).strip()
|
||||||
alt_text = match.group(2).strip() if match.group(2) else media_name.split('/')[-1].rsplit('.', 1)[0]
|
alt_text = match.group(2).strip() if match.group(2) else media_name.split('/')[-1].rsplit('.', 1)[0]
|
||||||
|
|
||||||
# Find the media file
|
# Check media type first
|
||||||
|
media_type = get_media_type(media_name)
|
||||||
|
|
||||||
|
# For non-image media (audio, video, PDF), show placeholder without embedding
|
||||||
|
if media_type in ('audio', 'video', 'document'):
|
||||||
|
return generate_media_placeholder(media_type, alt_text)
|
||||||
|
|
||||||
|
# For images, embed as base64
|
||||||
resolved_path = find_media_in_attachments(media_name, note_folder, notes_dir)
|
resolved_path = find_media_in_attachments(media_name, note_folder, notes_dir)
|
||||||
|
|
||||||
if resolved_path:
|
if resolved_path:
|
||||||
result = get_media_as_base64(resolved_path)
|
base64_url = get_image_as_base64(resolved_path)
|
||||||
if result:
|
if base64_url:
|
||||||
base64_url, media_type = result
|
return f''
|
||||||
return generate_media_html(base64_url, media_type, alt_text)
|
|
||||||
|
|
||||||
# Media not found, convert to placeholder
|
# Image not found
|
||||||
media_type = get_media_type(media_name)
|
return f'<span style="color:var(--text-tertiary,#999);opacity:0.7;" title="Image not found">🖼️ {alt_text}</span>'
|
||||||
icon = '🎵' if media_type == 'audio' else '🎬' if media_type == 'video' else '📄' if media_type == 'document' else '🖼️'
|
|
||||||
return f'<span style="color:var(--text-tertiary,#999);opacity:0.7;" title="Media not found">{icon} {alt_text}</span>'
|
|
||||||
|
|
||||||
markdown_content = re.sub(wikilink_pattern, replace_wikilink_media, markdown_content)
|
markdown_content = re.sub(wikilink_pattern, replace_wikilink_media, markdown_content)
|
||||||
|
|
||||||
|
|
@ -213,6 +214,15 @@ def embed_media_as_base64(markdown_content: str, note_folder: Path, notes_dir: P
|
||||||
if not media_path:
|
if not media_path:
|
||||||
return match.group(0)
|
return match.group(0)
|
||||||
|
|
||||||
|
# Check media type first
|
||||||
|
media_type = get_media_type(media_path)
|
||||||
|
display_alt = alt_text or Path(media_path).stem
|
||||||
|
|
||||||
|
# For non-image media (audio, video, PDF), show placeholder without embedding
|
||||||
|
if media_type in ('audio', 'video', 'document'):
|
||||||
|
return generate_media_placeholder(media_type, display_alt)
|
||||||
|
|
||||||
|
# For images, proceed with base64 embedding
|
||||||
# Handle /api/media/ or legacy /api/images/ paths (convert to filesystem paths)
|
# Handle /api/media/ or legacy /api/images/ paths (convert to filesystem paths)
|
||||||
if media_path.startswith('/api/media/'):
|
if media_path.startswith('/api/media/'):
|
||||||
relative_path = media_path[len('/api/media/'):]
|
relative_path = media_path[len('/api/media/'):]
|
||||||
|
|
@ -240,14 +250,12 @@ def embed_media_as_base64(markdown_content: str, note_folder: Path, notes_dir: P
|
||||||
# Path is outside notes_dir, skip
|
# Path is outside notes_dir, skip
|
||||||
return match.group(0)
|
return match.group(0)
|
||||||
|
|
||||||
# Get base64 data
|
# Get base64 data for image
|
||||||
result = get_media_as_base64(resolved_path)
|
base64_url = get_image_as_base64(resolved_path)
|
||||||
if result:
|
if base64_url:
|
||||||
base64_url, media_type = result
|
return f''
|
||||||
display_alt = alt_text or resolved_path.stem
|
|
||||||
return generate_media_html(base64_url, media_type, display_alt)
|
|
||||||
|
|
||||||
# Media not found, keep original
|
# Image not found, keep original
|
||||||
return match.group(0)
|
return match.group(0)
|
||||||
|
|
||||||
markdown_content = re.sub(img_pattern, replace_media, markdown_content)
|
markdown_content = re.sub(img_pattern, replace_media, markdown_content)
|
||||||
|
|
|
||||||
|
|
@ -2711,6 +2711,7 @@ function noteApp() {
|
||||||
this.currentNote = notePath;
|
this.currentNote = notePath;
|
||||||
this._lastRenderedContent = ''; // Clear render cache for new note
|
this._lastRenderedContent = ''; // Clear render cache for new note
|
||||||
this._cachedRenderedHTML = '';
|
this._cachedRenderedHTML = '';
|
||||||
|
this._initializedVideoSources = new Set(); // Clear video cache for new note
|
||||||
this.noteContent = data.content;
|
this.noteContent = data.content;
|
||||||
this.currentNoteName = notePath.split('/').pop().replace('.md', '');
|
this.currentNoteName = notePath.split('/').pop().replace('.md', '');
|
||||||
this.currentMedia = ''; // Clear image viewer when loading a note
|
this.currentMedia = ''; // Clear image viewer when loading a note
|
||||||
|
|
@ -4188,6 +4189,17 @@ function noteApp() {
|
||||||
this.addCopyButtonToCodeBlock(pre);
|
this.addCopyButtonToCodeBlock(pre);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enable video metadata loading (for first frame preview)
|
||||||
|
// Track by source URL to prevent duplicate requests on re-renders
|
||||||
|
if (!this._initializedVideoSources) this._initializedVideoSources = new Set();
|
||||||
|
previewEl.querySelectorAll('video[preload="none"]').forEach((video) => {
|
||||||
|
const src = video.getAttribute('src');
|
||||||
|
if (src && !this._initializedVideoSources.has(src)) {
|
||||||
|
this._initializedVideoSources.add(src);
|
||||||
|
video.preload = 'metadata';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
|
@ -4969,12 +4981,54 @@ function noteApp() {
|
||||||
// Get current rendered HTML (this already has markdown converted and will have LaTeX delimiters)
|
// Get current rendered HTML (this already has markdown converted and will have LaTeX delimiters)
|
||||||
let renderedHTML = this.renderedMarkdown;
|
let renderedHTML = this.renderedMarkdown;
|
||||||
|
|
||||||
|
// Convert non-image media (audio, video, PDF) to placeholders first
|
||||||
|
// These shouldn't be embedded as base64 (too large)
|
||||||
|
// Use CSS variables with fallbacks for theme-aware styling (matches backend export.py)
|
||||||
|
const mediaPlaceholder = (type, name) => {
|
||||||
|
const icons = { audio: '🎵', video: '🎬', document: '📄' };
|
||||||
|
const labels = { audio: 'Audio file', video: 'Video file', document: 'PDF document' };
|
||||||
|
const icon = icons[type] || '📎';
|
||||||
|
const label = labels[type] || 'Media file';
|
||||||
|
return `<div style="margin:1.5rem 0;padding:1.5rem;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;display:flex;align-items:center;gap:1rem;">
|
||||||
|
<span style="font-size:2rem;">${icon}</span>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600;color:var(--text-primary,#212529);">${name}</div>
|
||||||
|
<div style="font-size:0.875rem;color:var(--text-secondary,#6c757d);">${label} — not available in exported view</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replace audio embeds with placeholders
|
||||||
|
renderedHTML = renderedHTML.replace(
|
||||||
|
/<div class="media-embed media-audio">.*?<audio[^>]*src="[^"]*\/([^"\/]+)"[^>]*>.*?<\/div>/gs,
|
||||||
|
(match, filename) => mediaPlaceholder('audio', decodeURIComponent(filename).replace(/\.[^.]+$/, ''))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace video embeds with placeholders
|
||||||
|
renderedHTML = renderedHTML.replace(
|
||||||
|
/<div class="media-embed media-video">.*?<video[^>]*src="[^"]*\/([^"\/]+)"[^>]*>.*?<\/div>/gs,
|
||||||
|
(match, filename) => mediaPlaceholder('video', decodeURIComponent(filename).replace(/\.[^.]+$/, ''))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace PDF embeds with placeholders
|
||||||
|
renderedHTML = renderedHTML.replace(
|
||||||
|
/<div class="media-embed media-pdf">.*?<iframe[^>]*src="[^"]*\/([^"\/]+)"[^>]*>.*?<\/div>/gs,
|
||||||
|
(match, filename) => mediaPlaceholder('document', decodeURIComponent(filename).replace(/\.[^.]+$/, ''))
|
||||||
|
);
|
||||||
|
|
||||||
// Embed local images as base64 for fully self-contained HTML
|
// Embed local images as base64 for fully self-contained HTML
|
||||||
const imgRegex = /src="\/api\/images\/([^"]+)"/g;
|
// Handle both /api/media/ and legacy /api/images/ paths
|
||||||
|
const imgRegex = /src="\/api\/(?:media|images)\/([^"]+)"/g;
|
||||||
const imgMatches = [...renderedHTML.matchAll(imgRegex)];
|
const imgMatches = [...renderedHTML.matchAll(imgRegex)];
|
||||||
|
|
||||||
for (const match of imgMatches) {
|
for (const match of imgMatches) {
|
||||||
const encodedPath = match[1];
|
const encodedPath = match[1];
|
||||||
|
// Skip non-image files (already handled above)
|
||||||
|
const ext = encodedPath.split('.').pop().toLowerCase();
|
||||||
|
if (!['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch the image
|
// Fetch the image
|
||||||
const imgResponse = await fetch(`/api/media/${encodedPath}`);
|
const imgResponse = await fetch(`/api/media/${encodedPath}`);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue