This commit is contained in:
wang1zhen 2025-10-16 01:18:44 -04:00 committed by GitHub
commit 3a7848b46e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,6 +1,6 @@
{{ $nonce := uuidv4 -}}
{{ $nonceAttribute := print "nonce=" (quote $nonce) -}}
{{ $csp := printf "default-src 'none'; img-src 'self'; object-src 'none'; base-uri 'none'; script-src 'nonce-%s'; style-src 'nonce-%s'; frame-ancestors 'self'; form-action 'self';" $nonce $nonce -}}
{{ $csp := printf "default-src 'none'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; script-src 'nonce-%s'; style-src 'nonce-%s'; frame-ancestors 'self'; form-action 'self';" $nonce $nonce -}}
{{/* To disable the Content-Security-Policy, set this to false */}}{{ $enableCsp := true -}}
{{ if $enableCsp -}}
{{- .RespHeader.Set "Content-Security-Policy" $csp -}}
@ -777,6 +777,134 @@ footer {
}
}
/* Media Viewer Modal Styles */
.media-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
z-index: 10000;
justify-content: center;
align-items: center;
}
.media-modal.active {
display: flex;
}
.modal-content {
position: relative;
max-width: 95%;
max-height: 95%;
display: flex;
flex-direction: column;
align-items: center;
}
.modal-media-container {
max-width: 100%;
max-height: calc(95vh - 60px);
display: flex;
justify-content: center;
align-items: center;
}
.modal-media-container img,
.modal-media-container video {
max-width: 100%;
max-height: calc(95vh - 60px);
object-fit: contain;
}
.modal-close {
position: absolute;
top: 20px;
right: 30px;
color: #fff;
font-size: 40px;
font-weight: bold;
background: none;
border: none;
cursor: pointer;
z-index: 10001;
padding: 0;
width: 50px;
height: 50px;
line-height: 50px;
transition: opacity 0.2s;
}
.modal-close:hover,
.modal-close:focus {
opacity: 0.7;
}
.modal-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
color: #fff;
font-size: 60px;
font-weight: bold;
background: none;
border: none;
cursor: pointer;
padding: 20px;
z-index: 10001;
transition: opacity 0.2s;
user-select: none;
}
.modal-nav:hover,
.modal-nav:focus {
opacity: 0.7;
}
.modal-prev {
left: 20px;
}
.modal-next {
right: 20px;
}
.modal-info {
color: #fff;
text-align: center;
margin-top: 20px;
display: flex;
gap: 20px;
justify-content: center;
flex-wrap: wrap;
}
.modal-filename {
font-weight: bold;
}
.modal-counter {
opacity: 0.8;
}
@media (max-width: 768px) {
.modal-nav {
font-size: 40px;
padding: 10px;
}
.modal-close {
font-size: 30px;
width: 40px;
height: 40px;
line-height: 40px;
top: 10px;
right: 10px;
}
}
</style>
{{- if eq .Layout "grid"}}
<style {{ $nonceAttribute }}>.wrapper { max-width: none; } main { margin-top: 1px; }</style>
@ -1175,6 +1303,22 @@ footer {
</a>
</footer>
<!-- Media Viewer Modal -->
<div id="mediaModal" class="media-modal">
<button class="modal-close" aria-label="Close">&times;</button>
<button class="modal-nav modal-prev" aria-label="Previous">&lsaquo;</button>
<button class="modal-nav modal-next" aria-label="Next">&rsaquo;</button>
<div class="modal-content">
<div class="modal-media-container">
<!-- Media content will be injected here -->
</div>
<div class="modal-info">
<span class="modal-filename"></span>
<span class="modal-counter"></span>
</div>
</div>
</div>
<script {{ $nonceAttribute }}>
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0
const filterEl = document.getElementById('filter');
@ -1266,6 +1410,213 @@ footer {
}
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
timeList.forEach(localizeDatetime);
// media viewer functionality
var imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.tiff', '.bmp', '.heif', '.heic', '.svg', '.avif'];
var videoExtensions = ['.mp4', '.mov', '.m4v', '.mpeg', '.mpg', '.avi', '.ogg', '.webm', '.mkv', '.vob', '.gifv', '.3gp'];
var audioExtensions = ['.mp3', '.m4a', '.aac', '.flac', '.wav', '.wma', '.midi', '.cda'];
var mediaItems = [];
var currentMediaIndex = 0;
function isMediaFile(filename) {
var lower = filename.toLowerCase();
return imageExtensions.some(function(ext) { return lower.endsWith(ext); }) ||
videoExtensions.some(function(ext) { return lower.endsWith(ext); }) ||
audioExtensions.some(function(ext) { return lower.endsWith(ext); });
}
function isImageFile(filename) {
var lower = filename.toLowerCase();
return imageExtensions.some(function(ext) { return lower.endsWith(ext); });
}
function isVideoFile(filename) {
var lower = filename.toLowerCase();
return videoExtensions.some(function(ext) { return lower.endsWith(ext); });
}
function isAudioFile(filename) {
var lower = filename.toLowerCase();
return audioExtensions.some(function(ext) { return lower.endsWith(ext); });
}
function collectMediaItems() {
mediaItems = [];
// collect from both grid and list layouts
var allLinks = document.querySelectorAll('.entry a, tr.file a');
allLinks.forEach(function(link, index) {
var href = link.getAttribute('href');
var nameEl = link.querySelector('.name');
var name = nameEl ? nameEl.textContent.trim() : href;
if (isMediaFile(name)) {
mediaItems.push({
url: href,
name: name,
element: link,
isImage: isImageFile(name),
isVideo: isVideoFile(name),
isAudio: isAudioFile(name)
});
}
});
}
function openMediaViewer(index) {
if (index < 0 || index >= mediaItems.length) return;
currentMediaIndex = index;
var item = mediaItems[index];
var modal = document.getElementById('mediaModal');
var container = modal.querySelector('.modal-media-container');
var filenameEl = modal.querySelector('.modal-filename');
var counterEl = modal.querySelector('.modal-counter');
// clear previous content
container.innerHTML = '';
// create appropriate media element
if (item.isImage) {
var img = document.createElement('img');
img.src = item.url;
img.alt = item.name;
container.appendChild(img);
} else if (item.isVideo) {
var video = document.createElement('video');
video.src = item.url;
video.controls = true;
video.autoplay = true;
container.appendChild(video);
} else if (item.isAudio) {
var audio = document.createElement('audio');
audio.src = item.url;
audio.controls = true;
audio.autoplay = true;
audio.style.width = '80vw';
container.appendChild(audio);
}
// update info
filenameEl.textContent = item.name;
counterEl.textContent = (index + 1) + ' / ' + mediaItems.length;
// show modal
modal.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeMediaViewer() {
var modal = document.getElementById('mediaModal');
var container = modal.querySelector('.modal-media-container');
// stop any playing video or audio
var video = container.querySelector('video');
if (video) {
video.pause();
video.src = '';
}
var audio = container.querySelector('audio');
if (audio) {
audio.pause();
audio.src = '';
}
modal.classList.remove('active');
document.body.style.overflow = '';
}
function navigateMedia(direction) {
var newIndex = currentMediaIndex + direction;
// loop around
if (newIndex < 0) {
newIndex = mediaItems.length - 1;
} else if (newIndex >= mediaItems.length) {
newIndex = 0;
}
openMediaViewer(newIndex);
}
function initMediaViewer() {
collectMediaItems();
if (mediaItems.length === 0) return;
// add click handlers to media links
mediaItems.forEach(function(item, index) {
item.element.addEventListener('click', function(e) {
e.preventDefault();
openMediaViewer(index);
});
});
// modal controls
var modal = document.getElementById('mediaModal');
var closeBtn = modal.querySelector('.modal-close');
var prevBtn = modal.querySelector('.modal-prev');
var nextBtn = modal.querySelector('.modal-next');
closeBtn.addEventListener('click', closeMediaViewer);
prevBtn.addEventListener('click', function() { navigateMedia(-1); });
nextBtn.addEventListener('click', function() { navigateMedia(1); });
// click on background to close
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeMediaViewer();
}
});
// keyboard navigation
document.addEventListener('keydown', function(e) {
if (!modal.classList.contains('active')) return;
switch(e.key) {
case 'Escape':
closeMediaViewer();
break;
case 'ArrowLeft':
navigateMedia(-1);
break;
case 'ArrowRight':
navigateMedia(1);
break;
}
});
// touch swipe navigation
var touchStartX = 0;
var touchEndX = 0;
modal.addEventListener('touchstart', function(e) {
touchStartX = e.changedTouches[0].screenX;
});
modal.addEventListener('touchend', function(e) {
touchEndX = e.changedTouches[0].screenX;
handleSwipe();
});
function handleSwipe() {
var swipeThreshold = 50;
var swipeDistance = touchEndX - touchStartX;
if (Math.abs(swipeDistance) > swipeThreshold) {
if (swipeDistance > 0) {
// swipe right - previous
navigateMedia(-1);
} else {
// swipe left - next
navigateMedia(1);
}
}
}
}
window.addEventListener('load', initMediaViewer);
// @license-end
</script>
</body>