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);
+ }
+ });
+})();