diff --git a/aurora-livecam/SettingsManager.php b/aurora-livecam/SettingsManager.php index 58c29da..6c8b7a6 100644 --- a/aurora-livecam/SettingsManager.php +++ b/aurora-livecam/SettingsManager.php @@ -4,11 +4,11 @@ * Speichert in settings.json, lädt ohne Reload */ class SettingsManager { - private $settingsFile = 'settings.json'; + private $settingsFile; private $settings = []; public function __construct($file = null) { - if ($file) $this->settingsFile = $file; + $this->settingsFile = $file ?: (__DIR__ . '/settings.json'); $this->load(); } @@ -68,10 +68,12 @@ class SettingsManager { } private function save() { - return file_put_contents( - $this->settingsFile, - json_encode($this->settings, JSON_PRETTY_PRINT) - ) !== false; + $payload = json_encode($this->settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + if ($payload === false) { + return false; + } + + return file_put_contents($this->settingsFile, $payload, LOCK_EX) !== false; } // Für AJAX-Anfragen @@ -98,7 +100,10 @@ class SettingsManager { if ($key && $this->set($key, $value)) { echo json_encode(['success' => true, 'message' => 'Einstellung gespeichert']); } else { - echo json_encode(['success' => false, 'message' => 'Fehler beim Speichern']); + echo json_encode([ + 'success' => false, + 'message' => 'Fehler beim Speichern. Bitte Dateirechte prüfen.' + ]); } exit; } diff --git a/aurora-livecam/index.php b/aurora-livecam/index.php index 8573e44..975c3b3 100644 --- a/aurora-livecam/index.php +++ b/aurora-livecam/index.php @@ -1549,11 +1549,11 @@ nav ul li a:hover { color: #4CAF50; } margin: 0; } .zoom-slider { - width: 150px; + width: 220px; } .zoom-value { font-weight: 700; - min-width: 40px; + min-width: 50px; text-align: center; color: #333; } @@ -1565,13 +1565,29 @@ nav ul li a:hover { color: #4CAF50; } background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-size: 18px; + font-weight: bold; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; + display: flex; + align-items: center; + justify-content: center; } .zoom-btn:hover { transform: scale(1.1); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); } +.zoom-btn:active { + transform: scale(0.95); +} +.video-container { + cursor: default; +} +.video-container.zoomed { + cursor: grab; +} +.video-container.zoomed:active { + cursor: grabbing; +} .tech-stat { justify-self: start; font-family: monospace; color: #555; } .bitrate-icon { color: #4CAF50; } @@ -2046,12 +2062,11 @@ body.theme-neo footer {
- - + - 1x - - + 1.0x + +
diff --git a/aurora-livecam/js/video-zoom.js b/aurora-livecam/js/video-zoom.js index 3c7ac4b..1994c98 100644 --- a/aurora-livecam/js/video-zoom.js +++ b/aurora-livecam/js/video-zoom.js @@ -1,55 +1,207 @@ /** - * Video Zoom Controller - Zoom für alle Video-Modi + * Video Zoom & Pan Controller + * - Zoom für alle Video-Modi (Live, Timelapse, Tagesvideo) + * - Pan-Funktion: Mit Maus den gezoomten Bereich verschieben */ -let currentZoom = 1; - -function applyZoom(zoomValue) { - const config = window.zoomConfig || { minZoom: 1, maxZoom: 4 }; - currentZoom = Math.max(config.minZoom, Math.min(config.maxZoom, zoomValue)); - - const valueEl = document.getElementById('zoom-value'); - const slider = document.getElementById('zoom-range'); - - if (valueEl) valueEl.textContent = currentZoom + 'x'; - if (slider) slider.value = currentZoom; - - // Alle Video-Elemente zoomen - const targets = [ - document.getElementById('webcam-player'), - document.getElementById('timelapse-image'), - document.getElementById('daily-video') - ].filter(Boolean); - - targets.forEach((el) => { - el.style.transform = `scale(${currentZoom})`; - el.style.transformOrigin = 'center center'; - el.style.transition = 'transform 0.2s ease'; - }); -} - -function adjustZoom(delta) { - applyZoom(currentZoom + delta); -} - -function resetZoom() { - applyZoom(1); -} - -// Initialisierung -document.addEventListener('DOMContentLoaded', function() { +(() => { const config = window.zoomConfig || {}; if (!config.enabled) return; + let currentZoom = 1; + let panX = 0; + let panY = 0; + let isDragging = false; + let startX = 0; + let startY = 0; + + const minZoom = Number(config.minZoom || 1); + const maxZoom = Number(config.maxZoom || 4); + const defaultZoom = Number(config.defaultZoom || 1); + const slider = document.getElementById('zoom-range'); - if (slider) { - slider.addEventListener('input', (event) => { - applyZoom(Number(event.target.value)); + const valueEl = document.getElementById('zoom-value'); + + // Finde das aktuell aktive Video-Element + function getActiveTarget() { + 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 + if (dailyPlayer && dailyPlayer.style.display !== 'none' && daily) { + return daily; + } + if (timelapseViewer && timelapseViewer.style.display !== 'none' && timelapse) { + return timelapse; + } + if (webcam) { + return webcam; + } + return null; + } + + // Wende Zoom und Pan auf das aktive Element an + function applyTransform() { + const target = getActiveTarget(); + if (!target) return; + + // Bei Zoom 1x: Kein Pan erlaubt + if (currentZoom <= 1) { + panX = 0; + panY = 0; + } + + // Begrenzen der Pan-Werte basierend auf Zoom + const maxPan = (currentZoom - 1) * 50; // Prozent + panX = Math.max(-maxPan, Math.min(maxPan, panX)); + panY = Math.max(-maxPan, Math.min(maxPan, panY)); + + target.style.transform = `scale(${currentZoom}) translate(${panX}%, ${panY}%)`; + target.style.transformOrigin = 'center center'; + target.style.transition = isDragging ? 'none' : 'transform 0.2s ease'; + + // Update UI + if (valueEl) valueEl.textContent = `${currentZoom.toFixed(1)}x`; + if (slider) slider.value = currentZoom; + } + + // Zoom setzen + function setZoom(value) { + currentZoom = Math.max(minZoom, Math.min(maxZoom, value)); + applyTransform(); + } + + // Zoom anpassen + function adjustZoom(delta) { + setZoom(currentZoom + delta); + } + + // Zoom zurücksetzen + function resetZoom() { + currentZoom = 1; + panX = 0; + panY = 0; + applyTransform(); + } + + // Mouse Events für Pan + function setupPanEvents() { + const container = document.querySelector('.video-container'); + if (!container) return; + + container.addEventListener('mousedown', (e) => { + if (currentZoom <= 1) return; + isDragging = true; + startX = e.clientX; + startY = e.clientY; + container.style.cursor = 'grabbing'; + e.preventDefault(); + }); + + document.addEventListener('mousemove', (e) => { + if (!isDragging) return; + + const dx = (e.clientX - startX) / 5; // Sensitivität anpassen + const dy = (e.clientY - startY) / 5; + + panX += dx / currentZoom; + panY += dy / currentZoom; + + startX = e.clientX; + startY = e.clientY; + + applyTransform(); + }); + + document.addEventListener('mouseup', () => { + if (isDragging) { + isDragging = false; + const container = document.querySelector('.video-container'); + if (container) container.style.cursor = currentZoom > 1 ? 'grab' : 'default'; + } + }); + + // Touch Events für Mobile + container.addEventListener('touchstart', (e) => { + if (currentZoom <= 1 || e.touches.length !== 1) return; + isDragging = true; + startX = e.touches[0].clientX; + startY = e.touches[0].clientY; + }, { passive: true }); + + container.addEventListener('touchmove', (e) => { + if (!isDragging || e.touches.length !== 1) return; + + const dx = (e.touches[0].clientX - startX) / 5; + const dy = (e.touches[0].clientY - startY) / 5; + + panX += dx / currentZoom; + panY += dy / currentZoom; + + startX = e.touches[0].clientX; + startY = e.touches[0].clientY; + + applyTransform(); + }, { passive: true }); + + container.addEventListener('touchend', () => { + isDragging = false; + }); + + // Cursor anpassen bei Zoom + container.style.cursor = 'default'; + } + + // Slider Events + function setupSlider() { + if (!slider) return; + + slider.min = minZoom; + slider.max = maxZoom; + slider.step = 0.5; + slider.value = defaultZoom; + + slider.addEventListener('input', (e) => { + setZoom(Number(e.target.value)); }); } - // Initial zoom anwenden (ohne transform bei 1x) - currentZoom = config.defaultZoom || 1; - if (currentZoom !== 1) { - applyZoom(currentZoom); - } -}); + // Globale Funktionen für Buttons + window.adjustZoom = adjustZoom; + window.resetZoom = resetZoom; + window.setZoom = setZoom; + + // Initialisierung + document.addEventListener('DOMContentLoaded', () => { + setupSlider(); + setupPanEvents(); + currentZoom = defaultZoom; + + // Warte kurz, damit Video-Elemente geladen sind + setTimeout(() => { + applyTransform(); + }, 500); + + // Update Cursor bei Zoom-Änderung + 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); + } + }); +})();