feat: enhance media viewer with audio support and mobile gestures

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 <noreply@anthropic.com>
This commit is contained in:
wang1zhen 2025-10-02 17:49:13 +09:00
parent 5c4923caea
commit df6f1e9a88

View file

@ -1411,36 +1411,43 @@ 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 // media viewer functionality
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.tiff', '.bmp', '.heif', '.heic', '.svg', '.avif']; var 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']; var videoExtensions = ['.mp4', '.mov', '.m4v', '.mpeg', '.mpg', '.avi', '.ogg', '.webm', '.mkv', '.vob', '.gifv', '.3gp'];
let mediaItems = []; var audioExtensions = ['.mp3', '.m4a', '.aac', '.flac', '.wav', '.wma', '.midi', '.cda'];
let currentMediaIndex = 0; var mediaItems = [];
var currentMediaIndex = 0;
function isMediaFile(filename) { function isMediaFile(filename) {
const lower = filename.toLowerCase(); var lower = filename.toLowerCase();
return imageExtensions.some(ext => lower.endsWith(ext)) || return imageExtensions.some(function(ext) { return lower.endsWith(ext); }) ||
videoExtensions.some(ext => lower.endsWith(ext)); videoExtensions.some(function(ext) { return lower.endsWith(ext); }) ||
audioExtensions.some(function(ext) { return lower.endsWith(ext); });
} }
function isImageFile(filename) { function isImageFile(filename) {
const lower = filename.toLowerCase(); var lower = filename.toLowerCase();
return imageExtensions.some(ext => lower.endsWith(ext)); return imageExtensions.some(function(ext) { return lower.endsWith(ext); });
} }
function isVideoFile(filename) { function isVideoFile(filename) {
const lower = filename.toLowerCase(); var lower = filename.toLowerCase();
return videoExtensions.some(ext => lower.endsWith(ext)); 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() { function collectMediaItems() {
mediaItems = []; mediaItems = [];
// Collect from both grid and list layouts // collect from both grid and list layouts
const allLinks = document.querySelectorAll('.entry a, tr.file a'); var allLinks = document.querySelectorAll('.entry a, tr.file a');
allLinks.forEach((link, index) => { allLinks.forEach(function(link, index) {
const href = link.getAttribute('href'); var href = link.getAttribute('href');
const nameEl = link.querySelector('.name'); var nameEl = link.querySelector('.name');
const name = nameEl ? nameEl.textContent.trim() : href; var name = nameEl ? nameEl.textContent.trim() : href;
if (isMediaFile(name)) { if (isMediaFile(name)) {
mediaItems.push({ mediaItems.push({
@ -1448,7 +1455,8 @@ footer {
name: name, name: name,
element: link, element: link,
isImage: isImageFile(name), isImage: isImageFile(name),
isVideo: isVideoFile(name) isVideo: isVideoFile(name),
isAudio: isAudioFile(name)
}); });
} }
}); });
@ -1458,57 +1466,70 @@ footer {
if (index < 0 || index >= mediaItems.length) return; if (index < 0 || index >= mediaItems.length) return;
currentMediaIndex = index; currentMediaIndex = index;
const item = mediaItems[index]; var item = mediaItems[index];
const modal = document.getElementById('mediaModal'); var modal = document.getElementById('mediaModal');
const container = modal.querySelector('.modal-media-container'); var container = modal.querySelector('.modal-media-container');
const filenameEl = modal.querySelector('.modal-filename'); var filenameEl = modal.querySelector('.modal-filename');
const counterEl = modal.querySelector('.modal-counter'); var counterEl = modal.querySelector('.modal-counter');
// Clear previous content // clear previous content
container.innerHTML = ''; container.innerHTML = '';
// Create appropriate media element // create appropriate media element
if (item.isImage) { if (item.isImage) {
const img = document.createElement('img'); var img = document.createElement('img');
img.src = item.url; img.src = item.url;
img.alt = item.name; img.alt = item.name;
container.appendChild(img); container.appendChild(img);
} else if (item.isVideo) { } else if (item.isVideo) {
const video = document.createElement('video'); var video = document.createElement('video');
video.src = item.url; video.src = item.url;
video.controls = true; video.controls = true;
video.autoplay = true; video.autoplay = true;
container.appendChild(video); 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; filenameEl.textContent = item.name;
counterEl.textContent = `${index + 1} / ${mediaItems.length}`; counterEl.textContent = (index + 1) + ' / ' + mediaItems.length;
// Show modal // show modal
modal.classList.add('active'); modal.classList.add('active');
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
} }
function closeMediaViewer() { function closeMediaViewer() {
const modal = document.getElementById('mediaModal'); var modal = document.getElementById('mediaModal');
const container = modal.querySelector('.modal-media-container'); var container = modal.querySelector('.modal-media-container');
// Stop any playing video // stop any playing video or audio
const video = container.querySelector('video'); var video = container.querySelector('video');
if (video) { if (video) {
video.pause(); video.pause();
video.src = ''; video.src = '';
} }
var audio = container.querySelector('audio');
if (audio) {
audio.pause();
audio.src = '';
}
modal.classList.remove('active'); modal.classList.remove('active');
document.body.style.overflow = ''; document.body.style.overflow = '';
} }
function navigateMedia(direction) { function navigateMedia(direction) {
let newIndex = currentMediaIndex + direction; var newIndex = currentMediaIndex + direction;
// Loop around // loop around
if (newIndex < 0) { if (newIndex < 0) {
newIndex = mediaItems.length - 1; newIndex = mediaItems.length - 1;
} else if (newIndex >= mediaItems.length) { } else if (newIndex >= mediaItems.length) {
@ -1523,32 +1544,32 @@ footer {
if (mediaItems.length === 0) return; if (mediaItems.length === 0) return;
// Add click handlers to media links // add click handlers to media links
mediaItems.forEach((item, index) => { mediaItems.forEach(function(item, index) {
item.element.addEventListener('click', function(e) { item.element.addEventListener('click', function(e) {
e.preventDefault(); e.preventDefault();
openMediaViewer(index); openMediaViewer(index);
}); });
}); });
// Modal controls // modal controls
const modal = document.getElementById('mediaModal'); var modal = document.getElementById('mediaModal');
const closeBtn = modal.querySelector('.modal-close'); var closeBtn = modal.querySelector('.modal-close');
const prevBtn = modal.querySelector('.modal-prev'); var prevBtn = modal.querySelector('.modal-prev');
const nextBtn = modal.querySelector('.modal-next'); var nextBtn = modal.querySelector('.modal-next');
closeBtn.addEventListener('click', closeMediaViewer); closeBtn.addEventListener('click', closeMediaViewer);
prevBtn.addEventListener('click', () => navigateMedia(-1)); prevBtn.addEventListener('click', function() { navigateMedia(-1); });
nextBtn.addEventListener('click', () => navigateMedia(1)); nextBtn.addEventListener('click', function() { navigateMedia(1); });
// Click on background to close // click on background to close
modal.addEventListener('click', function(e) { modal.addEventListener('click', function(e) {
if (e.target === modal) { if (e.target === modal) {
closeMediaViewer(); closeMediaViewer();
} }
}); });
// Keyboard navigation // keyboard navigation
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
if (!modal.classList.contains('active')) return; if (!modal.classList.contains('active')) return;
@ -1564,9 +1585,36 @@ footer {
break; 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); window.addEventListener('load', initMediaViewer);
// @license-end // @license-end