Add media preview functionality for resources with image or video support
This commit is contained in:
@@ -0,0 +1,146 @@
|
|||||||
|
<div class="resource-media-wrapper" id="resource_media_wrapper_{{ resource.pk }}" data-src="{{ src|escape }}">
|
||||||
|
<noscript>
|
||||||
|
<div><a href="{{ src }}" target="_blank" rel="noopener">Open media</a></div>
|
||||||
|
</noscript>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var wrapperId = 'resource_media_wrapper_{{ resource.pk }}';
|
||||||
|
if (document.getElementById(wrapperId).dataset._inited) return;
|
||||||
|
var wrap = document.getElementById(wrapperId);
|
||||||
|
if (!wrap) return;
|
||||||
|
wrap.dataset._inited = '1';
|
||||||
|
var src = wrap.getAttribute('data-src');
|
||||||
|
if (!src) return;
|
||||||
|
var lower = src.toLowerCase();
|
||||||
|
|
||||||
|
function createImg(src) {
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = src;
|
||||||
|
img.className = 'img-fluid resource-media-element rounded';
|
||||||
|
img.style.maxHeight = '60vh';
|
||||||
|
img.style.objectFit = 'contain';
|
||||||
|
img.style.cursor = 'zoom-in';
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
function createVideo(src) {
|
||||||
|
var video = document.createElement('video');
|
||||||
|
video.className = 'resource-media-element rounded';
|
||||||
|
video.controls = true;
|
||||||
|
video.preload = 'metadata';
|
||||||
|
video.style.maxWidth = '100%';
|
||||||
|
video.style.maxHeight = '60vh';
|
||||||
|
video.style.objectFit = 'contain';
|
||||||
|
var srcEl = document.createElement('source');
|
||||||
|
srcEl.src = src;
|
||||||
|
video.appendChild(srcEl);
|
||||||
|
return video;
|
||||||
|
}
|
||||||
|
function createIframe(src, embedUrl) {
|
||||||
|
var iframe = document.createElement('iframe');
|
||||||
|
iframe.src = embedUrl || src;
|
||||||
|
iframe.setAttribute('frameborder','0');
|
||||||
|
iframe.setAttribute('allowfullscreen','');
|
||||||
|
iframe.className = 'resource-media-element rounded';
|
||||||
|
iframe.style.width = '100%';
|
||||||
|
iframe.style.height = '60vh';
|
||||||
|
return iframe;
|
||||||
|
}
|
||||||
|
|
||||||
|
var el = null;
|
||||||
|
// YouTube
|
||||||
|
if (lower.indexOf('youtube.com') !== -1 || lower.indexOf('youtu.be') !== -1) {
|
||||||
|
// build embed url
|
||||||
|
var videoId = null;
|
||||||
|
try {
|
||||||
|
var m = src.match(/(?:v=|v\/|embed\/|youtu\.be\/)([A-Za-z0-9_-]{6,})/);
|
||||||
|
if (m && m[1]) videoId = m[1];
|
||||||
|
} catch(e){}
|
||||||
|
var embed = videoId ? ('https://www.youtube.com/embed/' + videoId) : src;
|
||||||
|
el = createIframe(src, embed);
|
||||||
|
} else if (lower.indexOf('vimeo.com') !== -1) {
|
||||||
|
var m2 = src.match(/vimeo\.com\/(\d+)/);
|
||||||
|
var embed2 = m2 && m2[1] ? ('https://player.vimeo.com/video/' + m2[1]) : src;
|
||||||
|
el = createIframe(src, embed2);
|
||||||
|
} else if (lower.match(/\.(mp4|webm|ogg)(?:$|\?|#)/)) {
|
||||||
|
el = createVideo(src);
|
||||||
|
} else if (lower.match(/\.(jpe?g|png|gif|webp|svg)(?:$|\?|#)/)) {
|
||||||
|
el = createImg(src);
|
||||||
|
} else {
|
||||||
|
// unknown: try image first, fallback to link
|
||||||
|
el = createImg(src);
|
||||||
|
// if image fails to load, replace with link
|
||||||
|
el.addEventListener('error', function(){
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = src; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Open resource';
|
||||||
|
wrap.innerHTML = ''; wrap.appendChild(a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// append preview element
|
||||||
|
if (el) {
|
||||||
|
wrap.appendChild(el);
|
||||||
|
|
||||||
|
// click -> open modal/full-size
|
||||||
|
el.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
// create modal container if not present
|
||||||
|
var modalId = 'resource_media_modal_{{ resource.pk }}';
|
||||||
|
var existing = document.getElementById(modalId);
|
||||||
|
if (!existing) {
|
||||||
|
var modalDiv = document.createElement('div');
|
||||||
|
modalDiv.className = 'modal fade resource-media-modal';
|
||||||
|
modalDiv.id = modalId;
|
||||||
|
modalDiv.tabIndex = -1;
|
||||||
|
modalDiv.innerHTML = '<div class="modal-dialog modal-xl modal-dialog-centered"><div class="modal-content bg-dark"><div class="modal-body p-0 text-center"></div></div></div>';
|
||||||
|
document.body.appendChild(modalDiv);
|
||||||
|
}
|
||||||
|
var modalEl = document.getElementById(modalId);
|
||||||
|
var body = modalEl.querySelector('.modal-body');
|
||||||
|
body.innerHTML = '';
|
||||||
|
|
||||||
|
// create full-size node depending on type
|
||||||
|
if (el.tagName.toLowerCase() === 'img') {
|
||||||
|
var big = document.createElement('img');
|
||||||
|
big.src = src;
|
||||||
|
big.style.maxWidth = '95vw';
|
||||||
|
big.style.maxHeight = '95vh';
|
||||||
|
big.style.objectFit = 'contain';
|
||||||
|
big.className = 'img-fluid';
|
||||||
|
body.appendChild(big);
|
||||||
|
} else if (el.tagName.toLowerCase() === 'video') {
|
||||||
|
var bigv = document.createElement('video');
|
||||||
|
bigv.controls = true; bigv.autoplay = false; bigv.style.maxWidth = '95vw'; bigv.style.maxHeight = '95vh'; bigv.style.objectFit = 'contain'; bigv.className = 'w-100';
|
||||||
|
var s = document.createElement('source'); s.src = src; bigv.appendChild(s);
|
||||||
|
body.appendChild(bigv);
|
||||||
|
} else if (el.tagName.toLowerCase() === 'iframe') {
|
||||||
|
var frame = document.createElement('iframe');
|
||||||
|
frame.src = el.src;
|
||||||
|
frame.setAttribute('frameborder','0');
|
||||||
|
frame.setAttribute('allowfullscreen','');
|
||||||
|
frame.style.width = '95vw'; frame.style.height = '80vh';
|
||||||
|
body.appendChild(frame);
|
||||||
|
} else {
|
||||||
|
var a = document.createElement('a'); a.href = src; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Open resource'; body.appendChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// show modal using Bootstrap if available
|
||||||
|
try {
|
||||||
|
var bsModal = new bootstrap.Modal(modalEl);
|
||||||
|
bsModal.show();
|
||||||
|
} catch (e) {
|
||||||
|
// fallback: open in new tab
|
||||||
|
window.open(src, '_blank', 'noopener');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.resource-media-wrapper .resource-media-element { display: inline-block; }
|
||||||
|
.resource-media-wrapper img.resource-media-element { max-width: 100%; height: auto; }
|
||||||
|
.resource-media-wrapper video.resource-media-element { max-width: 100%; height: auto; }
|
||||||
|
.resource-media-modal .modal-content { background: rgba(0,0,0,0.95); }
|
||||||
|
</style>
|
||||||
@@ -14,6 +14,22 @@
|
|||||||
<small class="text-muted">by {{ resource.get_authors|default:"Unknown" }}</small>
|
<small class="text-muted">by {{ resource.get_authors|default:"Unknown" }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
{# Media preview: image or video autosized, clickable to open full-size modal #}
|
||||||
|
{% if resource.file or resource.url %}
|
||||||
|
<div class="resource-media-preview mb-3 text-center">
|
||||||
|
{% comment %}Prefer local file if present, otherwise try URL{% endcomment %}
|
||||||
|
{% if resource.file %}
|
||||||
|
{% with src=resource.file.url %}
|
||||||
|
{% include 'atlas/partials/resource_media.html' with src=src resource=resource %}
|
||||||
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
{% with src=resource.url %}
|
||||||
|
{% include 'atlas/partials/resource_media.html' with src=src resource=resource %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if resource.description %}
|
{% if resource.description %}
|
||||||
<p class="card-text">{{ resource.description|linebreaksbr }}</p>
|
<p class="card-text">{{ resource.description|linebreaksbr }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user