Add service worker for push notifications, create calendar layout, and implement WLAN QR code page

- Implemented a service worker (sw.js) to handle push notifications with dynamic options and notification click events.
- Created a calendar layout in test.html with a grid system for displaying events across days and times.
- Developed a visually engaging WLAN QR code page (wlan.html) with animated backgrounds, particle effects, and tips for connecting to the network.
This commit is contained in:
2026-02-22 00:50:22 +01:00
parent 6b96cd2012
commit 038910e9f0
26 changed files with 32980 additions and 5 deletions

199
public/led/v.html Normal file
View File

@@ -0,0 +1,199 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Video Frame-Stepping mit FPS-Erkennung</title>
<style>
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial;
padding: 18px;
max-width: 900px;
margin: auto;
}
.controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 12px;
}
video {
max-width: 100%;
display: block;
margin-bottom: 12px;
background: #000;
}
canvas {
border: 1px solid #ddd;
max-width: 100%;
display: block;
}
label {
font-size: 0.9rem;
}
input[type="number"] {
width: 80px;
}
button {
padding: 6px 10px;
}
.info {
margin-top: 10px;
color: #444;
font-size: 0.9rem;
}
</style>
</head>
<body>
<h1>Video: Frame für Frame mit FPS-Erkennung</h1>
<div class="controls">
<label>Wähle Datei:
<input id="file" type="file" accept="video/*">
</label>
<label>fps:
<input id="fps" type="number" step="0.01" value="25" min="1">
</label>
<button id="prev">◀️ Prev Frame</button>
<button id="next">Next Frame ▶️</button>
<button id="play">Play ▶</button>
<button id="pause">Pause ⏸</button>
<button id="export">Export Current Frame (PNG)</button>
</div>
<video id="video" controls crossorigin="anonymous" style="display:none"></video>
<canvas id="canvas"></canvas>
<div class="info" id="info">
Lade ein Video, warte auf Metadaten...
</div>
<script>
const fileInput = document.getElementById('file');
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const fpsInput = document.getElementById('fps');
const prevBtn = document.getElementById('prev');
const nextBtn = document.getElementById('next');
const playBtn = document.getElementById('play');
const pauseBtn = document.getElementById('pause');
const exportBtn = document.getElementById('export');
const info = document.getElementById('info');
let fileURL = null;
let rafId = null;
// --- Datei laden ---
fileInput.addEventListener('change', () => {
const file = fileInput.files && fileInput.files[0];
if (!file) return;
if (fileURL) URL.revokeObjectURL(fileURL);
fileURL = URL.createObjectURL(file);
video.src = fileURL;
video.style.display = 'block';
video.load();
info.textContent = 'Video geladen. Warte auf Metadaten...';
});
// --- Metadaten geladen ---
video.addEventListener('loadedmetadata', () => {
canvas.width = video.videoWidth || 640;
canvas.height = video.videoHeight || 360;
seekAndDraw(0).catch(() => { });
info.textContent = `Dauer: ${formatSeconds(video.duration)} — Bildgröße: ${canvas.width}×${canvas.height}`;
detectFPS();
});
// --- Hilfsfunktionen ---
function formatSeconds(s) {
if (!isFinite(s)) return '';
const mm = Math.floor(s / 60);
const ss = (s % 60).toFixed(2).padStart(5, '0');
return `${mm}:${ss}`;
}
function drawCurrentFrame() {
try {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
} catch (e) {
console.warn('drawImage fehlgeschlagen:', e);
}
}
function seek(videoEl, time) {
return new Promise((resolve, reject) => {
function cleanup() {
videoEl.removeEventListener('seeked', onSeeked);
videoEl.removeEventListener('error', onError);
}
function onSeeked() { cleanup(); resolve(); }
function onError(e) { cleanup(); reject(e); }
videoEl.addEventListener('seeked', onSeeked);
videoEl.addEventListener('error', onError);
videoEl.currentTime = Math.min(Math.max(time, 0), videoEl.duration || time);
});
}
async function seekAndDraw(time) {
await seek(video, time);
drawCurrentFrame();
}
// --- Buttons ---
nextBtn.addEventListener('click', async () => {
const fps = parseFloat(fpsInput.value) || 25;
const frameDur = 1 / fps;
const target = (video.currentTime || 0) + frameDur;
await seekAndDraw(Math.min(target + 0.00001, video.duration || target));
});
prevBtn.addEventListener('click', async () => {
const fps = parseFloat(fpsInput.value) || 25;
const frameDur = 1 / fps;
const target = (video.currentTime || 0) - frameDur;
await seekAndDraw(Math.max(target, 0));
});
exportBtn.addEventListener('click', () => {
drawCurrentFrame();
canvas.toBlob(blob => {
if (!blob) return;
const a = document.createElement('a');
const url = URL.createObjectURL(blob);
a.href = url;
a.download = 'frame.png';
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 2000);
}, 'image/png');
});
// --- Seeked Event ---
video.addEventListener('seeked', () => { drawCurrentFrame(); });
// --- Safety Initial Draw ---
window.addEventListener('load', () => {
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width || 640, canvas.height || 360);
});
</script>
</body>
</html>