From 5c4923caea97cba854bdd8f953d43bbbb7cb8369 Mon Sep 17 00:00:00 2001 From: wang1zhen <31798841+wang1zhen@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:09:47 +0900 Subject: [PATCH 1/2] feat: add media viewer with lightbox for file browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add interactive media viewer (lightbox) for images and videos in the file browser template: - Images and videos now open in a modal overlay instead of navigating to a new page - Navigation controls: left/right arrow buttons and keyboard shortcuts (←/→) - Keyboard support: ESC to close, arrow keys to navigate - Auto-play support for videos with playback controls - Display file information and current position (e.g., "3 / 10") - Responsive design for both desktop and mobile devices Supported formats: - Images: jpg, jpeg, png, gif, webp, tiff, bmp, heif, heic, svg, avif - Videos: mp4, mov, m4v, mpeg, mpg, avi, ogg, webm, mkv, vob, gifv, 3gp 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- modules/caddyhttp/fileserver/browse.html | 305 ++++++++++++++++++++++- 1 file changed, 304 insertions(+), 1 deletion(-) diff --git a/modules/caddyhttp/fileserver/browse.html b/modules/caddyhttp/fileserver/browse.html index 5d9dc7dbe57..4b78776ceaa 100644 --- a/modules/caddyhttp/fileserver/browse.html +++ b/modules/caddyhttp/fileserver/browse.html @@ -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 @@ } } +/* 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; + } +} + {{- if eq .Layout "grid"}} @@ -1175,6 +1303,22 @@

+ +
+ + + + +
+ From df6f1e9a884a01f0c2a12b693774c3e02890a665 Mon Sep 17 00:00:00 2001 From: wang1zhen Date: Thu, 2 Oct 2025 17:49:13 +0900 Subject: [PATCH 2/2] feat: enhance media viewer with audio support and mobile gestures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance the file browser media viewer with additional features: - Add audio file support (mp3, m4a, aac, flac, wav, wma, midi, cda) - Add touch swipe navigation for mobile devices (swipe left/right to navigate) - Refactor code style to match original template conventions (use var instead of const/let) - Add CSP media-src policy for video/audio playback support - Audio player width set to 80vw for better visibility All media types (images, videos, audio) now support: - Modal overlay viewing with navigation controls - Keyboard shortcuts (ESC, ←, →) - Touch gestures on mobile devices - File info display with position counter 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- modules/caddyhttp/fileserver/browse.html | 146 +++++++++++++++-------- 1 file changed, 97 insertions(+), 49 deletions(-) diff --git a/modules/caddyhttp/fileserver/browse.html b/modules/caddyhttp/fileserver/browse.html index 4b78776ceaa..5d60b45e65c 100644 --- a/modules/caddyhttp/fileserver/browse.html +++ b/modules/caddyhttp/fileserver/browse.html @@ -1411,36 +1411,43 @@

var timeList = Array.prototype.slice.call(document.getElementsByTagName("time")); timeList.forEach(localizeDatetime); - // Media Viewer Functionality - const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.tiff', '.bmp', '.heif', '.heic', '.svg', '.avif']; - const videoExtensions = ['.mp4', '.mov', '.m4v', '.mpeg', '.mpg', '.avi', '.ogg', '.webm', '.mkv', '.vob', '.gifv', '.3gp']; - let mediaItems = []; - let currentMediaIndex = 0; + // 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) { - const lower = filename.toLowerCase(); - return imageExtensions.some(ext => lower.endsWith(ext)) || - videoExtensions.some(ext => lower.endsWith(ext)); + 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) { - const lower = filename.toLowerCase(); - return imageExtensions.some(ext => lower.endsWith(ext)); + var lower = filename.toLowerCase(); + return imageExtensions.some(function(ext) { return lower.endsWith(ext); }); } function isVideoFile(filename) { - const lower = filename.toLowerCase(); - return videoExtensions.some(ext => lower.endsWith(ext)); + 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 - const allLinks = document.querySelectorAll('.entry a, tr.file a'); - allLinks.forEach((link, index) => { - const href = link.getAttribute('href'); - const nameEl = link.querySelector('.name'); - const name = nameEl ? nameEl.textContent.trim() : href; + // 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({ @@ -1448,7 +1455,8 @@

name: name, element: link, isImage: isImageFile(name), - isVideo: isVideoFile(name) + isVideo: isVideoFile(name), + isAudio: isAudioFile(name) }); } }); @@ -1458,57 +1466,70 @@

if (index < 0 || index >= mediaItems.length) return; currentMediaIndex = index; - const item = mediaItems[index]; - const modal = document.getElementById('mediaModal'); - const container = modal.querySelector('.modal-media-container'); - const filenameEl = modal.querySelector('.modal-filename'); - const counterEl = modal.querySelector('.modal-counter'); + 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 + // clear previous content container.innerHTML = ''; - // Create appropriate media element + // create appropriate media element if (item.isImage) { - const img = document.createElement('img'); + var img = document.createElement('img'); img.src = item.url; img.alt = item.name; container.appendChild(img); } else if (item.isVideo) { - const video = document.createElement('video'); + 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 + // update info filenameEl.textContent = item.name; - counterEl.textContent = `${index + 1} / ${mediaItems.length}`; + counterEl.textContent = (index + 1) + ' / ' + mediaItems.length; - // Show modal + // show modal modal.classList.add('active'); document.body.style.overflow = 'hidden'; } function closeMediaViewer() { - const modal = document.getElementById('mediaModal'); - const container = modal.querySelector('.modal-media-container'); + var modal = document.getElementById('mediaModal'); + var container = modal.querySelector('.modal-media-container'); - // Stop any playing video - const video = container.querySelector('video'); + // 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) { - let newIndex = currentMediaIndex + direction; + var newIndex = currentMediaIndex + direction; - // Loop around + // loop around if (newIndex < 0) { newIndex = mediaItems.length - 1; } else if (newIndex >= mediaItems.length) { @@ -1523,32 +1544,32 @@

if (mediaItems.length === 0) return; - // Add click handlers to media links - mediaItems.forEach((item, index) => { + // add click handlers to media links + mediaItems.forEach(function(item, index) { item.element.addEventListener('click', function(e) { e.preventDefault(); openMediaViewer(index); }); }); - // Modal controls - const modal = document.getElementById('mediaModal'); - const closeBtn = modal.querySelector('.modal-close'); - const prevBtn = modal.querySelector('.modal-prev'); - const nextBtn = modal.querySelector('.modal-next'); + // 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', () => navigateMedia(-1)); - nextBtn.addEventListener('click', () => navigateMedia(1)); + prevBtn.addEventListener('click', function() { navigateMedia(-1); }); + nextBtn.addEventListener('click', function() { navigateMedia(1); }); - // Click on background to close + // click on background to close modal.addEventListener('click', function(e) { if (e.target === modal) { closeMediaViewer(); } }); - // Keyboard navigation + // keyboard navigation document.addEventListener('keydown', function(e) { if (!modal.classList.contains('active')) return; @@ -1564,9 +1585,36 @@

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); + } + } + } } - // Initialize media viewer after page load window.addEventListener('load', initMediaViewer); // @license-end