mirror of
https://github.com/pischule/memevizor.git
synced 2026-02-04 09:00:52 +00:00
171 lines
5.5 KiB
HTML
171 lines
5.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="icon"
|
|
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📺</text></svg>">
|
|
<title>memevizor</title>
|
|
<style>
|
|
/* Basic styling to make the media fill the container */
|
|
html, body {
|
|
height: 100%;
|
|
margin: 0;
|
|
background-color: #111;
|
|
}
|
|
#media-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100%;
|
|
width: 100%;
|
|
}
|
|
#media-container > img,
|
|
#media-container > video {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
/* QR Code styling */
|
|
:root {
|
|
--qr-size: 120px; /* Configurable QR code size */
|
|
--qr-margin: 20px; /* Configurable distance from screen edge */
|
|
}
|
|
|
|
#qr-container {
|
|
font-size: 0;
|
|
position: fixed;
|
|
bottom: var(--qr-margin);
|
|
right: var(--qr-margin);
|
|
z-index: 1000;
|
|
opacity: 0.8;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
#qr-container:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
#qr-code {
|
|
width: var(--qr-size);
|
|
height: var(--qr-size);
|
|
display: block;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<main id="media-container"></main>
|
|
|
|
<div id="qr-container">
|
|
<img id="qr-code" src="qr.svg" alt="QR Code">
|
|
</div>
|
|
|
|
<script defer>
|
|
const fileUrl = 'media';
|
|
const refreshIntervalMs = 15_000; // 15 seconds
|
|
|
|
const mediaContainer = document.getElementById("media-container");
|
|
let lastModified = null;
|
|
|
|
/**
|
|
* Creates a new media element (<img> or <video>) and replaces the current one.
|
|
* @param {Response} response The fetch response object.
|
|
*/
|
|
async function updateMediaElement(response) {
|
|
// Get the current element to revoke its blob URL later, preventing memory leaks.
|
|
const currentElement = mediaContainer.firstElementChild;
|
|
const oldBlobUrl = currentElement?.src;
|
|
|
|
const blob = await response.blob();
|
|
const newBlobUrl = URL.createObjectURL(blob);
|
|
const contentType = response.headers.get("content-type") || "";
|
|
|
|
let newElement;
|
|
if (contentType.startsWith("video/")) {
|
|
newElement = document.createElement("video");
|
|
newElement.src = newBlobUrl;
|
|
newElement.controls = true;
|
|
newElement.muted = true;
|
|
newElement.loop = true;
|
|
newElement.autoplay = true;
|
|
} else {
|
|
newElement = document.createElement("img");
|
|
newElement.src = newBlobUrl;
|
|
newElement.alt = "Dynamically loaded media content";
|
|
}
|
|
|
|
// Replace the entire content of the container with the new element.
|
|
mediaContainer.replaceChildren(newElement);
|
|
|
|
// IMPORTANT: Revoke the old blob URL to free up memory.
|
|
if (oldBlobUrl && oldBlobUrl.startsWith('blob:')) {
|
|
URL.revokeObjectURL(oldBlobUrl);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches the media file and updates it if modified.
|
|
*/
|
|
async function refreshMedia() {
|
|
try {
|
|
const headers = new Headers();
|
|
if (lastModified) {
|
|
headers.append('If-Modified-Since', lastModified);
|
|
}
|
|
|
|
// 'no-store' ensures we always check with the server.
|
|
const response = await fetch(fileUrl, { method: 'GET', headers, cache: 'no-store' });
|
|
const now = new Date().toLocaleTimeString();
|
|
|
|
if (response.status === 304) { // Not Modified
|
|
console.log(`[${now}] Status: 304 Not Modified. Media is up to date.`);
|
|
return;
|
|
}
|
|
|
|
if (response.status === 200) { // OK
|
|
const newLastModified = response.headers.get('last-modified');
|
|
if (newLastModified) {
|
|
lastModified = newLastModified;
|
|
await updateMediaElement(response);
|
|
console.log(`[${now}] Status: 200 OK. Media updated.`);
|
|
} else {
|
|
console.warn(`[${now}] Warning: No 'Last-Modified' header found. Conditional checks are disabled.`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
console.error(`[${now}] Unexpected server response: ${response.status} ${response.statusText}`);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to fetch media:', error);
|
|
}
|
|
}
|
|
|
|
function toggleQrVisibility() {
|
|
const qr = document.getElementById("qr-code");
|
|
const isVisible = qr.style.display === '';
|
|
qr.style.display = isVisible ? 'none' : '';
|
|
}
|
|
|
|
// --- Event Listeners ---
|
|
document.addEventListener('DOMContentLoaded', refreshMedia); // Initial fetch
|
|
setInterval(refreshMedia, refreshIntervalMs); // Periodic refresh
|
|
document.addEventListener('click', refreshMedia); // Manual refresh on click
|
|
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.code === 'Space') {
|
|
event.preventDefault(); // Prevent page scroll
|
|
refreshMedia();
|
|
}
|
|
|
|
if (event.key === 'q') {
|
|
toggleQrVisibility()
|
|
}
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|