express-file-index
Version:
A simple file index middleware for Express that serves static files and shows a browser for directories.
329 lines (299 loc) • 12.7 kB
JavaScript
document.addEventListener('DOMContentLoaded', async () => {
const data = JSON.parse(document.body.dataset.file);
// Define fallback HTML
const htmlFallback = /*html*/`
<div class="card column">
<div class="body">
<h3 style="text-align: center; margin: 0;">
This file can't be previewed.
</h3>
<a href="${data.pathTrue}" download class="btn" onClick="closePreview()">
<span class="icon">download</span>
Download file - ${data.sizeHuman}
</a>
</div>
</div>
`;
// Define all web browser supported file types
const maxTextSize = 1024 * 1024 * 1; // 1 MiB
const ext = data.name.split('.').pop().toLowerCase();
const types = {
image: [ 'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg' ],
video: [ 'mp4', 'webm' ],
audio: [ 'mp3', 'wav', 'ogg', 'aac', 'wav' ],
md: [ 'md', 'markdown' ],
html: [ 'html', 'htm' ]
}
// Show preview according to the file type
const elPreviewContent = document.querySelector('#content');
if (types.image.includes(ext)) {
// Show image preview
elPreviewContent.innerHTML = /*html*/`
<div class="card image">
<div class="body">
<img src="${data.pathTrue}" alt="${data.name}" />
</div>
</div>
`;
const img = elPreviewContent.querySelector('img');
img.addEventListener('error', () => {
elPreviewContent.innerHTML = htmlFallback;
});
} else if (types.video.includes(ext)) {
// Show video preview
elPreviewContent.innerHTML = /*html*/`
<div class="card video">
<div class="body" data-color-mode="dark">
<video autoplay src="${data.pathTrue}"></video>
<div class="overlay visible">
<div class="controls">
<button class="btn secondary square playPause">
<span class="icon">play_arrow</span>
</button>
<div class="progress">
<div class="current">0:00</div>
<input type="range" min="0" max="100" value="0" step="0.001" style="--value: 0;" />
<div class="duration">0:00</div>
</div>
<button class="btn secondary square fullscreen">
<span class="icon">fullscreen</span>
</button>
</div>
</div>
</div>
</div>
`;
const video = elPreviewContent.querySelector('video');
const btnPlayPause = elPreviewContent.querySelector('.playPause');
const btnFullscreen = elPreviewContent.querySelector('.fullscreen');
const elProgress = elPreviewContent.querySelector('.progress input[type="range"]');
const elTsCurrent = elPreviewContent.querySelector('.progress .current');
const elTsDuration = elPreviewContent.querySelector('.progress .duration');
const overlay = elPreviewContent.querySelector('.overlay');
video.addEventListener('error', () => {
elPreviewContent.innerHTML = htmlFallback;
});
let controlsTimeout;
const showControls = () => {
overlay.classList.add('visible');
clearTimeout(controlsTimeout);
controlsTimeout = setTimeout(() => {
hideControls();
}, 3000);
};
const hideControls = () => {
if (!video.paused) {
overlay.classList.remove('visible');
} else {
showControls();
}
};
const toggleControls = () => {
if (overlay.classList.contains('visible')) {
hideControls();
} else {
showControls();
}
};
overlay.addEventListener('mousemove', () => {
if (matchMedia('(hover: none)').matches) return;
showControls();
});
overlay.addEventListener('mouseleave', () => {
if (matchMedia('(hover: none)').matches) return;
hideControls();
});
btnPlayPause.addEventListener('click', () => {
if (video.paused) {
video.play();
} else {
video.pause();
}
});
video.addEventListener('play', () => {
btnPlayPause.querySelector('.icon').innerText = 'pause';
showControls();
});
video.addEventListener('pause', () => {
btnPlayPause.querySelector('.icon').innerText = 'play_arrow';
showControls();
});
video.addEventListener('timeupdate', () => {
const percent = (video.currentTime / video.duration) * 100;
const tsCurrent = formatSecondsToTimestamp(video.currentTime);
const tsDuration = formatSecondsToTimestamp(video.duration);
elProgress.value = percent;
elProgress.style.setProperty('--value', percent);
elTsCurrent.innerText = tsCurrent;
elTsDuration.innerText = tsDuration;
});
elProgress.addEventListener('input', () => {
const percent = elProgress.value;
elProgress.style.setProperty('--value', percent);
video.currentTime = video.duration * (percent / 100);
});
btnFullscreen.addEventListener('click', () => {
const container = elPreviewContent.querySelector('.card.video .body');
if (document.fullscreenElement) {
document.exitFullscreen();
} else if (container.requestFullscreen) {
container.requestFullscreen();
} else if (container.webkitRequestFullscreen) {
container.webkitRequestFullscreen();
} else if (container.msRequestFullscreen) {
container.msRequestFullscreen();
}
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
const canHover = !matchMedia('(hover: none)').matches;
if (canHover) {
if (video.paused) {
video.play();
} else {
video.pause();
}
} else {
toggleControls();
}
}
});
} else if (types.audio.includes(ext)) {
// Show audio preview
elPreviewContent.innerHTML = /*html*/`
<div class="card audio">
<div class="body">
<audio autoplay src="${data.pathTrue}" style="display: none"></audio>
<div class="times">
<div class="current">0:00</div>
<div class="grow"></div>
<div class="duration">0:00</div>
</div>
<div class="progress">
<input type="range" min="0" max="100" value="0" step="0.001" style="--value: 0;" />
</div>
<div class="controls">
<button class="btn secondary square backward">
<span class="icon">replay_10</span>
</button>
<button class="btn square playPause">
<span class="icon">play_arrow</span>
</button>
<button class="btn secondary square forward">
<span class="icon">forward_10</span>
</button>
</div>
</div>
</div>
`;
const audio = elPreviewContent.querySelector('audio');
const btnPlayPause = elPreviewContent.querySelector('.playPause');
const btnBackward = elPreviewContent.querySelector('.backward');
const btnForward = elPreviewContent.querySelector('.forward');
const elProgress = elPreviewContent.querySelector('.progress input[type="range"]');
const elTsCurrent = elPreviewContent.querySelector('.current');
const elTsDuration = elPreviewContent.querySelector('.duration');
audio.addEventListener('error', () => {
elPreviewContent.innerHTML = htmlFallback;
});
btnPlayPause.addEventListener('click', () => {
if (audio.paused) {
audio.play();
} else {
audio.pause();
}
});
btnBackward.addEventListener('click', () => {
audio.currentTime = Math.max(0, audio.currentTime - 10);
});
btnForward.addEventListener('click', () => {
audio.currentTime = Math.min(audio.duration, audio.currentTime + 10);
});
audio.addEventListener('play', () => {
btnPlayPause.querySelector('.icon').innerText = 'pause';
});
audio.addEventListener('pause', () => {
btnPlayPause.querySelector('.icon').innerText = 'play_arrow';
});
audio.addEventListener('timeupdate', () => {
const percent = (audio.currentTime / audio.duration) * 100;
const tsCurrent = formatSecondsToTimestamp(audio.currentTime);
const tsDuration = formatSecondsToTimestamp(audio.duration);
elProgress.value = percent;
elProgress.style.setProperty('--value', percent);
elTsCurrent.innerText = tsCurrent;
elTsDuration.innerText = tsDuration;
});
elProgress.addEventListener('input', () => {
const percent = elProgress.value;
elProgress.style.setProperty('--value', percent);
audio.currentTime = audio.duration * (percent / 100);
});
} else {
// Attempt to download file as text
let text;
if (data.size < maxTextSize) {
text = await fetchTextFile(data.pathTrue);
}
// If the file was too big or not text, prompt to download
if (!text) {
elPreviewContent.innerHTML = htmlFallback;
// Show markdown preview
} else if (types.md.includes(ext)) {
const html = markdownToSafeHTML(text);
elPreviewContent.innerHTML = /*html*/`
<div class="card html">
<div class="body">${html}</div>
</div>
`;
Prism.highlightAll(elPreviewContent);
// Show text preview
} else {
elPreviewContent.innerHTML = /*html*/`
<div class="card text">
<pre class="body"><code class="language-${ext}"></code></pre>
</div>
`;
const elCode = elPreviewContent.querySelector('code');
let htmlHighlighted = text;
try {
htmlHighlighted = Prism.highlight(text, Prism.languages[ext], ext);
} catch (error) {}
elCode.innerHTML = htmlHighlighted;
}
}
});
document.addEventListener('keydown', (e) => {
if (document.activeElement.tagName === 'INPUT') return;
const elPreviewContent = document.querySelector('#previewContent');
const media = elPreviewContent.querySelector('video') || elPreviewContent.querySelector('audio');
switch (e.key) {
case ' ':
if (!media) break;
e.preventDefault();
if (media.paused) {
media.play();
} else {
media.pause();
}
break;
case 'ArrowRight':
if (!media) break;
media.currentTime = Math.min(media.duration, media.currentTime + 10);
break;
case 'ArrowLeft':
if (!media) break;
media.currentTime = Math.max(0, media.currentTime - 10);
break;
case 'Escape':
closePreview();
break;
}
});
document.addEventListener('fullscreenchange', () => {
const btnFullscreen = document.querySelector('.fullscreen');
if (btnFullscreen) {
const isFullscreen = !!document.fullscreenElement;
btnFullscreen.querySelector('.icon').innerText = isFullscreen ? 'fullscreen_exit' : 'fullscreen';
}
});