mirror of
https://github.com/caddyserver/caddy.git
synced 2025-12-08 06:09:53 +00:00
Merge df6f1e9a88 into bfdb04912d
This commit is contained in:
commit
3799f595d6
1 changed files with 352 additions and 1 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{{ $nonce := uuidv4 -}}
|
{{ $nonce := uuidv4 -}}
|
||||||
{{ $nonceAttribute := print "nonce=" (quote $nonce) -}}
|
{{ $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 -}}
|
{{/* To disable the Content-Security-Policy, set this to false */}}{{ $enableCsp := true -}}
|
||||||
{{ if $enableCsp -}}
|
{{ if $enableCsp -}}
|
||||||
{{- .RespHeader.Set "Content-Security-Policy" $csp -}}
|
{{- .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>
|
</style>
|
||||||
{{- if eq .Layout "grid"}}
|
{{- if eq .Layout "grid"}}
|
||||||
<style {{ $nonceAttribute }}>.wrapper { max-width: none; } main { margin-top: 1px; }</style>
|
<style {{ $nonceAttribute }}>.wrapper { max-width: none; } main { margin-top: 1px; }</style>
|
||||||
|
|
@ -1175,6 +1303,22 @@ footer {
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- Media Viewer Modal -->
|
||||||
|
<div id="mediaModal" class="media-modal">
|
||||||
|
<button class="modal-close" aria-label="Close">×</button>
|
||||||
|
<button class="modal-nav modal-prev" aria-label="Previous">‹</button>
|
||||||
|
<button class="modal-nav modal-next" aria-label="Next">›</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 }}>
|
<script {{ $nonceAttribute }}>
|
||||||
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0
|
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0
|
||||||
const filterEl = document.getElementById('filter');
|
const filterEl = document.getElementById('filter');
|
||||||
|
|
@ -1266,6 +1410,213 @@ footer {
|
||||||
}
|
}
|
||||||
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
|
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
|
||||||
timeList.forEach(localizeDatetime);
|
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
|
// @license-end
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue