Merge pull request #27 from metacube2/claude/add-video-download-sppLI

Claude/add video download spp li
This commit is contained in:
2026-01-17 18:18:37 +01:00
committed by GitHub
3 changed files with 3209 additions and 98 deletions
+38 -15
View File
@@ -152,16 +152,18 @@ class WebcamManager {
// Zeigt NUR das Video ohne Schnickschnack // Zeigt NUR das Video ohne Schnickschnack
public function displayWebcam() { public function displayWebcam() {
return ' return '
<video id="webcam-player" <div id="live-video-wrapper" class="video-zoom-wrapper">
autoplay <video id="webcam-player"
muted autoplay
playsinline muted
webkit-playsinline playsinline
x-webkit-airplay="allow" webkit-playsinline
x5-video-player-type="h5" x-webkit-airplay="allow"
x5-video-player-fullscreen="true" x5-video-player-type="h5"
style="width: 100%; height: 100%; object-fit: contain;"> x5-video-player-fullscreen="true"
</video>'; style="width: 100%; height: 100%; object-fit: contain;">
</video>
</div>';
} }
// Das ist die neue Anzeige für unten links // Das ist die neue Anzeige für unten links
@@ -1495,13 +1497,30 @@ nav ul li a:hover { color: #4CAF50; }
z-index: 30; z-index: 30;
} }
#webcam-player, #timelapse-viewer, #daily-video-player { #live-video-wrapper, #timelapse-viewer, #daily-video-player {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 8px; border-radius: 8px;
overflow: hidden;
}
.video-zoom-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
transform-origin: center center;
}
#webcam-player, #timelapse-image, #daily-video {
width: 100%;
height: 100%;
object-fit: contain;
} }
.video-info-bar { .video-info-bar {
@@ -2044,15 +2063,19 @@ body.theme-neo footer {
<!-- Timelapse Overlay --> <!-- Timelapse Overlay -->
<div id="timelapse-viewer" style="display: none;"> <div id="timelapse-viewer" style="display: none;">
<img id="timelapse-image" src="" alt="Timelapse Image" style="width: 100%; height: 100%; object-fit: cover;"> <div id="timelapse-wrapper" class="video-zoom-wrapper">
<img id="timelapse-image" src="" alt="Timelapse Image" style="width: 100%; height: 100%; object-fit: cover;">
</div>
<div id="timelapse-time-overlay"></div> <div id="timelapse-time-overlay"></div>
</div> </div>
<!-- Daily Video Player (für Tagesvideos) --> <!-- Daily Video Player (für Tagesvideos) -->
<div id="daily-video-player" style="display: none;"> <div id="daily-video-player" style="display: none;">
<video id="daily-video" controls playsinline style="width: 100%; height: 100%; object-fit: contain;"> <div id="daily-video-wrapper" class="video-zoom-wrapper">
<source src="" type="video/mp4"> <video id="daily-video" controls playsinline style="width: 100%; height: 100%; object-fit: contain;">
</video> <source src="" type="video/mp4">
</video>
</div>
</div> </div>
</div> </div>
File diff suppressed because it is too large Load Diff
+91 -83
View File
@@ -1,7 +1,6 @@
/** /**
* Video Zoom & Pan Controller * Video Zoom & Pan Controller
* - Zoom für alle Video-Modi (Live, Timelapse, Tagesvideo) * Zoomt auf Wrapper-Layer statt direkt auf Video-Elemente
* - Pan-Funktion: Mit Maus den gezoomten Bereich verschieben
*/ */
(() => { (() => {
const config = window.zoomConfig || {}; const config = window.zoomConfig || {};
@@ -11,60 +10,75 @@
let panX = 0; let panX = 0;
let panY = 0; let panY = 0;
let isDragging = false; let isDragging = false;
let startX = 0; let lastX = 0;
let startY = 0; let lastY = 0;
const minZoom = Number(config.minZoom || 1); const minZoom = Number(config.minZoom || 1);
const maxZoom = Number(config.maxZoom || 4); const maxZoom = Number(config.maxZoom || 4);
const defaultZoom = Number(config.defaultZoom || 1);
const slider = document.getElementById('zoom-range'); const slider = document.getElementById('zoom-range');
const valueEl = document.getElementById('zoom-value'); const valueEl = document.getElementById('zoom-value');
// Finde das aktuell aktive Video-Element // Wrapper-IDs für jeden Modus
function getActiveTarget() { const wrapperIds = ['live-video-wrapper', 'timelapse-wrapper', 'daily-video-wrapper'];
const webcam = document.getElementById('webcam-player');
const timelapse = document.getElementById('timelapse-image');
const daily = document.getElementById('daily-video');
const timelapseViewer = document.getElementById('timelapse-viewer');
const dailyPlayer = document.getElementById('daily-video-player');
// Prüfe welches Element sichtbar ist // Finde den aktuell sichtbaren Wrapper
if (dailyPlayer && dailyPlayer.style.display !== 'none' && daily) { function getActiveWrapper() {
return daily; // Prüfe daily-video-player
const dailyPlayer = document.getElementById('daily-video-player');
if (dailyPlayer && dailyPlayer.style.display !== 'none') {
return document.getElementById('daily-video-wrapper');
} }
if (timelapseViewer && timelapseViewer.style.display !== 'none' && timelapse) {
return timelapse; // Prüfe timelapse-viewer
const timelapseViewer = document.getElementById('timelapse-viewer');
if (timelapseViewer && timelapseViewer.style.display !== 'none') {
return document.getElementById('timelapse-wrapper');
} }
if (webcam) {
return webcam; // Fallback: Live-Video
} return document.getElementById('live-video-wrapper');
return null;
} }
// Wende Zoom und Pan auf das aktive Element an // Wende Transform auf ALLE Wrapper an (damit beim Wechsel der Zoom erhalten bleibt)
function applyTransform() { function applyTransform() {
const target = getActiveTarget(); // Bei Zoom 1x: Kein Pan
if (!target) return;
// Bei Zoom 1x: Kein Pan erlaubt
if (currentZoom <= 1) { if (currentZoom <= 1) {
panX = 0; panX = 0;
panY = 0; panY = 0;
} }
// Begrenzen der Pan-Werte basierend auf Zoom // Pan begrenzen basierend auf Zoom
const maxPan = (currentZoom - 1) * 50; // Prozent const maxPan = (currentZoom - 1) * 50;
panX = Math.max(-maxPan, Math.min(maxPan, panX)); panX = Math.max(-maxPan, Math.min(maxPan, panX));
panY = Math.max(-maxPan, Math.min(maxPan, panY)); panY = Math.max(-maxPan, Math.min(maxPan, panY));
target.style.transform = `scale(${currentZoom}) translate(${panX}%, ${panY}%)`; // Transform auf alle Wrapper anwenden
target.style.transformOrigin = 'center center'; wrapperIds.forEach(id => {
target.style.transition = isDragging ? 'none' : 'transform 0.2s ease'; const wrapper = document.getElementById(id);
if (wrapper) {
wrapper.style.transform = `scale(${currentZoom}) translate(${panX}%, ${panY}%)`;
wrapper.style.transition = isDragging ? 'none' : 'transform 0.15s ease-out';
}
});
// Update UI // UI Update
if (valueEl) valueEl.textContent = `${currentZoom.toFixed(1)}x`; if (valueEl) valueEl.textContent = `${currentZoom.toFixed(1)}x`;
if (slider) slider.value = currentZoom; if (slider) slider.value = currentZoom;
// Cursor Update
updateCursor();
}
function updateCursor() {
const container = document.querySelector('.video-container');
if (container) {
if (currentZoom > 1) {
container.classList.add('zoomed');
} else {
container.classList.remove('zoomed');
}
}
} }
// Zoom setzen // Zoom setzen
@@ -91,57 +105,68 @@
const container = document.querySelector('.video-container'); const container = document.querySelector('.video-container');
if (!container) return; if (!container) return;
// Mousedown - Start dragging
container.addEventListener('mousedown', (e) => { container.addEventListener('mousedown', (e) => {
if (currentZoom <= 1) return; if (currentZoom <= 1) return;
// Ignoriere Klicks auf Controls
if (e.target.closest('.zoom-controls, button, a')) return;
isDragging = true; isDragging = true;
startX = e.clientX; lastX = e.clientX;
startY = e.clientY; lastY = e.clientY;
container.style.cursor = 'grabbing';
e.preventDefault(); e.preventDefault();
}); });
// Mousemove - Dragging
document.addEventListener('mousemove', (e) => { document.addEventListener('mousemove', (e) => {
if (!isDragging) return; if (!isDragging) return;
const dx = (e.clientX - startX) / 5; // Sensitivität anpassen const deltaX = e.clientX - lastX;
const dy = (e.clientY - startY) / 5; const deltaY = e.clientY - lastY;
panX += dx / currentZoom; // Sensitivität basierend auf Zoom
panY += dy / currentZoom; const sensitivity = 0.15 / currentZoom;
panX += deltaX * sensitivity;
panY += deltaY * sensitivity;
startX = e.clientX; lastX = e.clientX;
startY = e.clientY; lastY = e.clientY;
applyTransform(); applyTransform();
}); });
// Mouseup - Stop dragging
document.addEventListener('mouseup', () => { document.addEventListener('mouseup', () => {
if (isDragging) { isDragging = false;
isDragging = false; });
const container = document.querySelector('.video-container');
if (container) container.style.cursor = currentZoom > 1 ? 'grab' : 'default'; // Mouse leave
} document.addEventListener('mouseleave', () => {
isDragging = false;
}); });
// Touch Events für Mobile // Touch Events für Mobile
container.addEventListener('touchstart', (e) => { container.addEventListener('touchstart', (e) => {
if (currentZoom <= 1 || e.touches.length !== 1) return; if (currentZoom <= 1 || e.touches.length !== 1) return;
if (e.target.closest('.zoom-controls, button, a')) return;
isDragging = true; isDragging = true;
startX = e.touches[0].clientX; lastX = e.touches[0].clientX;
startY = e.touches[0].clientY; lastY = e.touches[0].clientY;
}, { passive: true }); }, { passive: true });
container.addEventListener('touchmove', (e) => { container.addEventListener('touchmove', (e) => {
if (!isDragging || e.touches.length !== 1) return; if (!isDragging || e.touches.length !== 1) return;
const dx = (e.touches[0].clientX - startX) / 5; const deltaX = e.touches[0].clientX - lastX;
const dy = (e.touches[0].clientY - startY) / 5; const deltaY = e.touches[0].clientY - lastY;
panX += dx / currentZoom; const sensitivity = 0.15 / currentZoom;
panY += dy / currentZoom; panX += deltaX * sensitivity;
panY += deltaY * sensitivity;
startX = e.touches[0].clientX; lastX = e.touches[0].clientX;
startY = e.touches[0].clientY; lastY = e.touches[0].clientY;
applyTransform(); applyTransform();
}, { passive: true }); }, { passive: true });
@@ -150,25 +175,28 @@
isDragging = false; isDragging = false;
}); });
// Cursor anpassen bei Zoom // Doppelklick zum Zurücksetzen
container.style.cursor = 'default'; container.addEventListener('dblclick', (e) => {
if (e.target.closest('.zoom-controls, button, a')) return;
resetZoom();
});
} }
// Slider Events // Slider Setup
function setupSlider() { function setupSlider() {
if (!slider) return; if (!slider) return;
slider.min = minZoom; slider.min = minZoom;
slider.max = maxZoom; slider.max = maxZoom;
slider.step = 0.5; slider.step = 0.5;
slider.value = defaultZoom; slider.value = 1;
slider.addEventListener('input', (e) => { slider.addEventListener('input', (e) => {
setZoom(Number(e.target.value)); setZoom(Number(e.target.value));
}); });
} }
// Globale Funktionen für Buttons // Globale Funktionen
window.adjustZoom = adjustZoom; window.adjustZoom = adjustZoom;
window.resetZoom = resetZoom; window.resetZoom = resetZoom;
window.setZoom = setZoom; window.setZoom = setZoom;
@@ -177,31 +205,11 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
setupSlider(); setupSlider();
setupPanEvents(); setupPanEvents();
currentZoom = defaultZoom;
// Warte kurz, damit Video-Elemente geladen sind // Initial State
setTimeout(() => { currentZoom = 1;
applyTransform(); applyTransform();
}, 500);
// Update Cursor bei Zoom-Änderung console.log('Video Zoom & Pan initialized');
const container = document.querySelector('.video-container');
if (container) {
const observer = new MutationObserver(() => {
container.style.cursor = currentZoom > 1 ? 'grab' : 'default';
});
}
});
// Bei Moduswechsel Pan zurücksetzen
window.addEventListener('click', (e) => {
if (e.target.id === 'timelapse-button' ||
e.target.closest('#timelapse-button') ||
e.target.id === 'dvp-back-live' ||
e.target.closest('.play-link')) {
panX = 0;
panY = 0;
setTimeout(applyTransform, 100);
}
}); });
})(); })();