Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
353 changes: 352 additions & 1 deletion modules/caddyhttp/fileserver/browse.html
Original file line number Diff line number Diff line change
@@ -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 -}}
Expand Down Expand Up @@ -777,6 +777,134 @@
}
}

/* 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>
Expand Down Expand Up @@ -1175,6 +1303,22 @@ <h1>
</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');
Expand Down Expand Up @@ -1266,6 +1410,213 @@ <h1>
}
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>
Expand Down
Loading