diff --git a/aurora-livecam/index.php b/aurora-livecam/index.php index 78a91c6..975c3b3 100644 --- a/aurora-livecam/index.php +++ b/aurora-livecam/index.php @@ -1553,10 +1553,41 @@ nav ul li a:hover { color: #4CAF50; } } .zoom-value { font-weight: 700; - min-width: 60px; - text-align: right; + min-width: 50px; + text-align: center; color: #333; } +.zoom-btn { + width: 36px; + height: 36px; + border: none; + border-radius: 50%; + 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; } @@ -2031,9 +2062,11 @@ body.theme-neo footer {
- - - 1x + + + 1.0x + +
@@ -2260,7 +2293,7 @@ document.getElementById('qrcode')?.addEventListener('click', function() { window.zoomConfig = { enabled: true, minZoom: 1, - maxZoom: 100, + maxZoom: 4, defaultZoom: 1 }; diff --git a/aurora-livecam/indexmiau.php b/aurora-livecam/indexmiau.php new file mode 100644 index 0000000..04668ba --- /dev/null +++ b/aurora-livecam/indexmiau.php @@ -0,0 +1,721 @@ + true, 'settings' => $settingsManager->get()]); + exit; + + case 'update': + $key = $_POST['key'] ?? null; + $value = $_POST['value'] ?? null; + + if ($value === 'true') $value = true; + if ($value === 'false') $value = false; + if (is_numeric($value)) $value = intval($value); + + if ($key && $settingsManager->set($key, $value)) { + echo json_encode(['success' => true, 'message' => 'Gespeichert']); + } else { + echo json_encode(['success' => false, 'message' => 'Fehler']); + } + exit; + } +} + +if (isset($_GET['download_video'])) { + $videoDir = './videos/'; + $latestVideo = null; + $latestTime = 0; + foreach (glob($videoDir . '*.mp4') as $video) { + $mtime = filemtime($video); + if ($mtime > $latestTime) { $latestTime = $mtime; $latestVideo = $video; } + } + if ($latestVideo) { + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename="'.basename($latestVideo).'"'); + header('Content-Length: ' . filesize($latestVideo)); + readfile($latestVideo); + exit; + } + echo "Kein Video gefunden."; + exit; +} + +$oldDomains = ['www.aurora-wetter-lifecam.ch', 'www.aurora-wetter-livecam.ch']; +$newDomain = 'www.aurora-weather-livecam.com'; +if (in_array($_SERVER['HTTP_HOST'] ?? '', $oldDomains)) { + $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; + header("HTTP/1.1 301 Moved Permanently"); + header("Location: " . $protocol . '://' . $newDomain . $_SERVER['REQUEST_URI']); + exit; +} + +session_start(); +error_reporting(E_ALL); +ini_set('display_errors', 0); + +$imageDir = "./image"; +$imageFiles = glob("$imageDir/screenshot_*.jpg"); +if ($imageFiles) rsort($imageFiles); +$imageFilesJson = json_encode($imageFiles ?: []); + +class ViewerCounter { + private $file = 'active_viewers.json'; + private $timeout = 30; + + public function handleHeartbeat() { + $ip = md5($_SERVER['REMOTE_ADDR'] . ($_SERVER['HTTP_USER_AGENT'] ?? '')); + $now = time(); + $viewers = file_exists($this->file) ? json_decode(file_get_contents($this->file), true) ?? [] : []; + $viewers[$ip] = $now; + $active = []; + foreach ($viewers as $u => $t) { if ($now - $t < $this->timeout) $active[$u] = $t; } + file_put_contents($this->file, json_encode($active)); + header('Content-Type: application/json'); + echo json_encode(['count' => count($active)]); + exit; + } + + public function getInitialCount() { + if (file_exists($this->file)) { + return max(1, count(json_decode(file_get_contents($this->file), true) ?? [])); + } + return 1; + } +} + +$viewerCounter = new ViewerCounter(); + +class WebcamManager { + private $videoSrc = 'test_video.m3u8'; + + public function displayWebcam() { + return ''; + } + + public function displayStreamStats() { + return ''; + } + + public function getImageFiles() { + $f = glob("image/screenshot_*.jpg"); + if ($f) rsort($f); + return json_encode($f ?: []); + } + + public function getJavaScript() { + return " + document.addEventListener('DOMContentLoaded', function () { + var video = document.getElementById('webcam-player'); + var videoSrc = '{$this->videoSrc}'; + if(video && typeof Hls !== 'undefined' && Hls.isSupported()) { + var hls = new Hls(); + hls.loadSource(videoSrc); + hls.attachMedia(video); + hls.on(Hls.Events.MANIFEST_PARSED, function () { video.play().catch(()=>{}); }); + } else if (video) { + video.src = videoSrc; + video.play().catch(()=>{}); + } + });"; + } +} + +class VisualCalendarManager { + private $videoDir, $settingsManager; + private $months = [1=>'Jan',2=>'Feb',3=>'Mär',4=>'Apr',5=>'Mai',6=>'Jun',7=>'Jul',8=>'Aug',9=>'Sep',10=>'Okt',11=>'Nov',12=>'Dez']; + + public function __construct($videoDir = './videos/', $sm = null) { + $this->videoDir = $videoDir; + $this->settingsManager = $sm; + } + + public function hasVideosForDate($y, $m, $d) { + return count(glob($this->videoDir . sprintf("daily_video_%04d%02d%02d_*.mp4", $y, $m, $d))) > 0; + } + + public function getVideosForDate($y, $m, $d) { + $vids = []; + foreach (glob($this->videoDir . sprintf("daily_video_%04d%02d%02d_*.mp4", $y, $m, $d)) as $v) { + $vids[] = ['path' => $v, 'name' => basename($v), 'size' => filesize($v), 'time' => date('H:i', filemtime($v))]; + } + return $vids; + } + + public function displayVisualCalendar() { + $cy = isset($_GET['cal_year']) ? intval($_GET['cal_year']) : date('Y'); + $cm = isset($_GET['cal_month']) ? intval($_GET['cal_month']) : date('n'); + $sd = isset($_GET['cal_day']) ? intval($_GET['cal_day']) : null; + $pip = $this->settingsManager ? $this->settingsManager->get('video_mode.play_in_player') : true; + $dl = $this->settingsManager ? $this->settingsManager->get('video_mode.allow_download') : true; + + $o = '
'; + $o .= '
'.$this->months[$cm].' '.$cy.'
'; + $o .= '
'; + foreach(['Mo','Di','Mi','Do','Fr','Sa','So'] as $wd) $o .= '
'.$wd.'
'; + + $fd = mktime(0,0,0,$cm,1,$cy); + $dim = date('t', $fd); + $dow = date('N', $fd) - 1; + for ($i=0; $i<$dow; $i++) $o .= '
'; + + for ($d=1; $d<=$dim; $d++) { + $hv = $this->hasVideosForDate($cy,$cm,$d); + $sel = $sd==$d; + $td = ($cy==date('Y') && $cm==date('n') && $d==date('j')); + $cls = 'cal-day' . ($hv?' has-vid':'') . ($sel?' sel':'') . ($td?' today':''); + $o .= '
'.$d.''.($hv?'📹':'').'
'; + } + $o .= '
'; + + if ($sd) { + $vids = $this->getVideosForDate($cy,$cm,$sd); + $o .= '

📅 '.sprintf('%02d.%02d.%04d',$sd,$cm,$cy).'

'; + if ($vids) { + $o .= ''; + } else { + $o .= '

Keine Videos.

'; + } + $o .= '
'; + } + $o .= '
'; + return $o; + } +} + +class GuestbookManager { + private $entries = [], $file = 'guestbook.json'; + public function __construct() { if (file_exists($this->file)) $this->entries = json_decode(file_get_contents($this->file), true) ?? []; } + public function handleFormSubmission() { + if (isset($_POST['guestbook'],$_POST['guest-name'],$_POST['guest-message'])) { + $this->entries[] = ['name'=>htmlspecialchars($_POST['guest-name']),'message'=>htmlspecialchars($_POST['guest-message']),'date'=>date('Y-m-d H:i:s')]; + file_put_contents($this->file, json_encode($this->entries)); + } + } + public function deleteEntry($i) { if (isset($this->entries[$i])) { unset($this->entries[$i]); $this->entries = array_values($this->entries); file_put_contents($this->file, json_encode($this->entries)); return true; } return false; } + public function displayForm() { return '
'; } + public function displayEntries($admin=false) { + $o = '
'; + foreach ($this->entries as $i=>$e) { + $o .= '

'.$e['name'].'

'.$e['message'].'

'.$e['date'].''; + if ($admin) $o .= '
'; + $o .= '
'; + } + return $o.'
'; + } +} + +class ContactManager { + private $file = 'feedbacks.json'; + public function displayForm() { return '
'; } + public function handleSubmission($n,$e,$m) { + if (!$n||!$e||!$m) return ['success'=>false,'message'=>'Alle Felder ausfüllen']; + $fb = ['name'=>htmlspecialchars($n),'email'=>filter_var($e,FILTER_SANITIZE_EMAIL),'message'=>htmlspecialchars($m),'date'=>date('Y-m-d H:i:s'),'ip'=>$_SERVER['REMOTE_ADDR']??'']; + $all = file_exists($this->file) ? json_decode(file_get_contents($this->file),true) : []; + $all[] = $fb; + file_put_contents($this->file, json_encode($all, JSON_PRETTY_PRINT)); + return ['success'=>true,'message'=>'Nachricht gesendet!']; + } + public function deleteFeedback($i) { $all = json_decode(file_get_contents($this->file),true); if (isset($all[$i])) { unset($all[$i]); file_put_contents($this->file, json_encode(array_values($all),JSON_PRETTY_PRINT)); return true; } return false; } +} + +class AdminManager { + public function isAdmin() { return isset($_SESSION['admin']) && $_SESSION['admin'] === true; } + public function handleLogin($u,$p) { if ($u==='admin' && $p==='sonne4000$$$$Q') { $_SESSION['admin']=true; return true; } return false; } + public function displayLoginForm() { return '
'; } + public function displayAdminContent() { + global $settingsManager; + $o = '
'; + $o .= '

⚙️ Einstellungen

'; + $o .= '
get('viewer_display.enabled')?'checked':'').'>
'; + $o .= '
'; + $o .= '
get('video_mode.play_in_player')?'checked':'').'>
'; + $o .= '
get('video_mode.allow_download')?'checked':'').'>
'; + $o .= '
'; + $o .= '

📩 Nachrichten

'; + $msgs = file_exists('feedbacks.json') ? json_decode(file_get_contents('feedbacks.json'),true) : []; + foreach ($msgs as $i=>$m) { + $o .= '
'.$m['name'].' ('.$m['email'].')

'.$m['message'].'

'.$m['date'].''; + $o .= '
'; + } + if (!$msgs) $o .= '

Keine Nachrichten.

'; + $o .= '
'; + return $o; + } + public function displayGalleryImages() { + $o = ''; + } +} + +class VideoArchiveManager { + private $dir; + public function __construct($d='./videos/') { $this->dir = $d; } + public function handleSpecificVideoDownload() { + if (isset($_GET['download_specific_video'],$_GET['token'])) { + $p = $_GET['download_specific_video']; + if (!hash_equals(hash_hmac('sha256',$p,session_id()), $_GET['token'])) { echo "Invalid"; exit; } + $rp = realpath($p); + $rd = realpath($this->dir); + if ($rp && strpos($rp,$rd)===0 && file_exists($rp)) { + header('Content-Type: video/mp4'); + header('Content-Disposition: attachment; filename="'.basename($rp).'"'); + header('Content-Length: '.filesize($rp)); + readfile($rp); + exit; + } + echo "Not found"; exit; + } + } +} + +$webcamManager = new WebcamManager(); +$guestbookManager = new GuestbookManager(); +$contactManager = new ContactManager(); +$adminManager = new AdminManager(); +$videoArchiveManager = new VideoArchiveManager('./videos/'); +$videoArchiveManager->handleSpecificVideoDownload(); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (isset($_POST['action']) && $_POST['action'] === 'viewer_heartbeat') $viewerCounter->handleHeartbeat(); + if (isset($_POST['guestbook'])) { $guestbookManager->handleFormSubmission(); header("Location: ".$_SERVER['PHP_SELF']."#guestbook"); exit; } + if (isset($_POST['contact'])) { + $r = $contactManager->handleSubmission($_POST['name'],$_POST['email'],$_POST['message']); + if (isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { header('Content-Type: application/json'); echo json_encode($r); exit; } + header('Location: '.$_SERVER['PHP_SELF'].'#kontakt'); exit; + } + if (isset($_POST['admin-login'])) { $adminManager->handleLogin($_POST['username'],$_POST['password']); header('Location: '.$_SERVER['PHP_SELF'].'#admin'); exit; } + if ($adminManager->isAdmin()) { + if (isset($_POST['action']) && $_POST['action']==='delete_guestbook') { $guestbookManager->deleteEntry(intval($_POST['delete_entry'])); header("Location: ".$_SERVER['PHP_SELF']."#guestbook"); exit; } + if (isset($_POST['action']) && $_POST['action']==='delete_feedback') { $contactManager->deleteFeedback(intval($_POST['delete_index'])); header("Location: ".$_SERVER['PHP_SELF']."#admin"); exit; } + } +} + +$vc = $viewerCounter->getInitialCount(); +$sv = $settingsManager->get('viewer_display.enabled') && $vc >= $settingsManager->get('viewer_display.min_viewers'); +$mv = $settingsManager->get('viewer_display.min_viewers'); +?> + + + + +Aurora Livecam + + + + + + +
+
+ + +
+
+ +
+

Aurora Wetter Livecam

+

Faszinierende Ausblicke aus dem Zürcher Oberland

+
+ +
+
+
+
+displayWebcam(); ?> +
+
+
+ + + +
+
+
+ +
+
+ + + +--:--:-- + + +
+
+ + + +
+displayStreamStats(); ?> +
Zuschauer
+
+ +
+📷 Snapshot + +⬇️ Tagesvideo +
+
+
+ +
+
+

📅 Videoarchiv

+displayVisualCalendar(); ?> +
+
+ +
+
+

Gästebuch

+displayForm(); echo $guestbookManager->displayEntries($adminManager->isAdmin()); ?> +
+
+ +
+
+

Kontakt

+displayForm(); ?> +
+
+ + + +isAdmin()): ?> +
+
+

⚙️ Admin

+displayAdminContent(); ?> +
+
+ +
+
+

Admin Login

+displayLoginForm(); ?> +
+
+ + + + + + + + + diff --git a/aurora-livecam/js/video-zoom.js b/aurora-livecam/js/video-zoom.js index e641760..1994c98 100644 --- a/aurora-livecam/js/video-zoom.js +++ b/aurora-livecam/js/video-zoom.js @@ -1,37 +1,207 @@ +/** + * Video Zoom & Pan Controller + * - Zoom für alle Video-Modi (Live, Timelapse, Tagesvideo) + * - Pan-Funktion: Mit Maus den gezoomten Bereich verschieben + */ (() => { const config = window.zoomConfig || {}; if (!config.enabled) return; - const slider = document.getElementById('zoom-range'); - const valueEl = document.getElementById('zoom-value'); - if (!slider || !valueEl) 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 || 100); + const maxZoom = Number(config.maxZoom || 4); const defaultZoom = Number(config.defaultZoom || 1); - slider.min = minZoom; - slider.max = maxZoom; - slider.value = defaultZoom; + const slider = document.getElementById('zoom-range'); + const valueEl = document.getElementById('zoom-value'); - const targets = [ - document.getElementById('webcam-player'), - document.getElementById('timelapse-image'), - document.getElementById('daily-video') - ].filter(Boolean); + // 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'); - const applyZoom = (zoomValue) => { - const zoom = Math.max(minZoom, Math.min(maxZoom, zoomValue)); - valueEl.textContent = `${zoom}x`; - targets.forEach((el) => { - el.style.transform = `scale(${zoom})`; - el.style.transformOrigin = 'center center'; + // 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(); }); - }; - applyZoom(defaultZoom); + document.addEventListener('mousemove', (e) => { + if (!isDragging) return; - slider.addEventListener('input', (event) => { - applyZoom(Number(event.target.value)); + 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)); + }); + } + + // 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); + } }); })();