Merge zoom & pan improvements from main-aurora
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+31
-61
@@ -160,7 +160,7 @@ class WebcamManager {
|
||||
x-webkit-airplay="allow"
|
||||
x5-video-player-type="h5"
|
||||
x5-video-player-fullscreen="true"
|
||||
style="width: 100%; height: 100%; object-fit: contain; display: block; background: #000;">
|
||||
style="width: 100%; height: 100%; object-fit: contain;">
|
||||
</video>';
|
||||
}
|
||||
|
||||
@@ -230,43 +230,22 @@ class WebcamManager {
|
||||
var isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
var isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||||
|
||||
console.log('Webcam Player Init - videoSrc:', videoSrc);
|
||||
|
||||
if(video) {
|
||||
video.controls = false;
|
||||
|
||||
// Native HLS Unterstützung prüfen (Safari, iOS)
|
||||
var canPlayHls = video.canPlayType('application/vnd.apple.mpegurl') !== '';
|
||||
|
||||
if (isIOS || canPlayHls) {
|
||||
// Native HLS (Safari, iOS)
|
||||
console.log('Using native HLS');
|
||||
if (isIOS) {
|
||||
video.src = videoSrc;
|
||||
video.setAttribute('playsinline', '');
|
||||
video.setAttribute('webkit-playsinline', '');
|
||||
video.muted = true;
|
||||
if(bitrateBadge) bitrateBadge.style.display = 'none';
|
||||
video.addEventListener('loadedmetadata', function() {
|
||||
console.log('Video metadata loaded');
|
||||
video.play().catch(function(e) { console.log('Play error:', e); });
|
||||
});
|
||||
video.addEventListener('error', function(e) {
|
||||
console.error('Video error:', e);
|
||||
});
|
||||
} else if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
||||
// HLS.js für andere Browser
|
||||
console.log('Using HLS.js');
|
||||
var hls = new Hls({
|
||||
enableWorker: !isMobile,
|
||||
lowLatencyMode: false,
|
||||
debug: false
|
||||
});
|
||||
video.addEventListener('loadedmetadata', function() { video.play().catch(console.log); });
|
||||
} else if (Hls.isSupported()) {
|
||||
var hls = new Hls({ enableWorker: !isMobile, lowLatencyMode: false });
|
||||
hls.loadSource(videoSrc);
|
||||
hls.attachMedia(video);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, function () {
|
||||
console.log('HLS manifest parsed');
|
||||
if (isMobile) video.muted = true;
|
||||
video.play().catch(function(e) { console.log('Play error:', e); });
|
||||
video.play().catch(console.log);
|
||||
if(bitrateBadge) bitrateBadge.style.display = 'inline-flex';
|
||||
});
|
||||
hls.on(Hls.Events.FRAG_LOADED, function(event, data) {
|
||||
@@ -276,18 +255,7 @@ class WebcamManager {
|
||||
if (mbs > 0) bitrateValue.textContent = mbs.toFixed(2);
|
||||
}
|
||||
});
|
||||
hls.on(Hls.Events.ERROR, function(event, data) {
|
||||
console.error('HLS error:', data.type, data.details);
|
||||
});
|
||||
} else {
|
||||
// Fallback: Versuche direkt zu laden
|
||||
console.log('No HLS support, trying direct load');
|
||||
video.src = videoSrc;
|
||||
video.muted = true;
|
||||
video.play().catch(function(e) { console.log('Play error:', e); });
|
||||
}
|
||||
} else {
|
||||
console.error('Video element not found!');
|
||||
}
|
||||
});
|
||||
";
|
||||
@@ -1527,25 +1495,13 @@ nav ul li a:hover { color: #4CAF50; }
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
#webcam-player {
|
||||
#webcam-player, #timelapse-viewer, #daily-video-player {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#timelapse-viewer, #daily-video-player {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
z-index: 5;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.video-info-bar {
|
||||
@@ -1593,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;
|
||||
}
|
||||
@@ -1609,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; }
|
||||
@@ -2090,12 +2062,11 @@ body.theme-neo footer {
|
||||
<!--
|
||||
CONTROLS -->
|
||||
<div id="zoom-controls" class="zoom-controls" aria-label="Zoom Steuerung">
|
||||
<button onclick="adjustZoom(-0.5)" class="zoom-btn">−</button>
|
||||
<label for="zoom-range">Zoom</label>
|
||||
<button type="button" onclick="adjustZoom(-0.5)" class="zoom-btn" title="Zoom out">−</button>
|
||||
<input type="range" id="zoom-range" class="zoom-slider" min="1" max="4" value="1" step="0.5">
|
||||
<span id="zoom-value" class="zoom-value">1x</span>
|
||||
<button onclick="adjustZoom(0.5)" class="zoom-btn">+</button>
|
||||
<button onclick="resetZoom()" class="zoom-btn">⟲</button>
|
||||
<span id="zoom-value" class="zoom-value">1.0x</span>
|
||||
<button type="button" onclick="adjustZoom(0.5)" class="zoom-btn" title="Zoom in">+</button>
|
||||
<button type="button" onclick="resetZoom()" class="zoom-btn" title="Reset">⟲</button>
|
||||
</div>
|
||||
|
||||
<!-- VIDEO PLAYER CONTROLS (für Tagesvideos) -->
|
||||
@@ -2320,14 +2291,13 @@ document.getElementById('qrcode')?.addEventListener('click', function() {
|
||||
|
||||
<script>
|
||||
window.zoomConfig = {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
minZoom: 1,
|
||||
maxZoom: 4,
|
||||
defaultZoom: 1
|
||||
};
|
||||
</script>
|
||||
<!-- Zoom temporär deaktiviert zum Testen -->
|
||||
<!-- <script src="js/video-zoom.js"></script> -->
|
||||
<script src="js/video-zoom.js"></script>
|
||||
|
||||
<!-- TIMELAPSE CONTROLLER -->
|
||||
<script>
|
||||
|
||||
+196
-44
@@ -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);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user