diff --git a/aurora-livecam/index.php b/aurora-livecam/index.php
index 975c3b3..c901964 100644
--- a/aurora-livecam/index.php
+++ b/aurora-livecam/index.php
@@ -152,16 +152,18 @@ class WebcamManager {
// Zeigt NUR das Video ohne Schnickschnack
public function displayWebcam() {
return '
-
- ';
+
';
+
+ // Navigation
+ $output .= '
';
+ $output .= '◀ ';
+ $output .= '
' . $this->monthNames[$currentMonth]['de'] . ' ' . $currentYear . ' ';
+ $output .= '▶ ';
+ $output .= '';
+
+ // AI-Legende
+ $output .= '
';
+ $output .= '🤖 AI-Erkennung: ';
+ foreach ($this->aiCategories as $cat => $info) {
+ $output .= '' . $info['icon'] . ' ';
+ }
+ $output .= '
';
+
+ // Kalender-Grid
+ $output .= '
';
+
+ // Wochentage Header
+ $weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
+ foreach ($weekdays as $day) {
+ $output .= '
' . $day . '
';
+ }
+
+ // Tage des Monats
+ $firstDay = mktime(0, 0, 0, $currentMonth, 1, $currentYear);
+ $daysInMonth = date('t', $firstDay);
+ $dayOfWeek = date('N', $firstDay) - 1;
+
+ // Leere Zellen vor dem ersten Tag
+ for ($i = 0; $i < $dayOfWeek; $i++) {
+ $output .= '
';
+ }
+
+ // Tage mit Videos/AI-Events markieren
+ for ($day = 1; $day <= $daysInMonth; $day++) {
+ $hasVideos = $this->hasVideosForDate($currentYear, $currentMonth, $day);
+ $hasAiEvents = $this->hasAiEventsForDate($currentYear, $currentMonth, $day);
+ $aiIcons = $this->getAiIconsForDate($currentYear, $currentMonth, $day);
+ $isSelected = ($selectedDay == $day);
+ $isToday = ($currentYear == date('Y') && $currentMonth == date('n') && $day == date('j'));
+
+ $classes = 'calendar-day';
+ if ($hasVideos) $classes .= ' has-video';
+ if ($hasAiEvents) $classes .= ' has-ai-events';
+ if ($isSelected) $classes .= ' selected';
+ if ($isToday) $classes .= ' today';
+
+ $output .= '
';
+ $output .= '
' . $day . ' ';
+
+ // Video-Indikator
+ if ($hasVideos) {
+ $output .= '
📹 ';
+ }
+
+ // AI-Event-Icons (max 3 anzeigen)
+ if (!empty($aiIcons)) {
+ $output .= '
';
+ $displayIcons = array_slice($aiIcons, 0, 3);
+ foreach ($displayIcons as $icon) {
+ $output .= '' . $icon . ' ';
+ }
+ if (count($aiIcons) > 3) {
+ $output .= '+' . (count($aiIcons) - 3) . ' ';
+ }
+ $output .= '
';
+ }
+
+ $output .= '
';
+ }
+
+ $output .= '
'; // calendar-grid
+
+ // Video-Liste + AI-Events für ausgewählten Tag
+ if ($selectedDay) {
+ $videos = $this->getVideosForDate($currentYear, $currentMonth, $selectedDay);
+ $aiEvents = $this->getAiEventsForDate($currentYear, $currentMonth, $selectedDay);
+
+ $output .= '
';
+ $output .= '
📅 ' . sprintf('%02d.%02d.%04d', $selectedDay, $currentMonth, $currentYear) . ' ';
+
+ // === TAGESVIDEOS ===
+ if (!empty($videos)) {
+ $output .= '
';
+ $output .= '
📹 Tagesvideos ';
+ $output .= '
';
+
+ foreach ($videos as $video) {
+ $sizeInMb = round($video['filesize'] / (1024 * 1024), 2);
+ $token = hash_hmac('sha256', $video['path'], session_id());
+ $videoUrl = '?download_specific_video=' . urlencode($video['path']) . '&token=' . $token;
+
+ $output .= '';
+ $output .= '🕐 ' . $video['time'] . ' Uhr ';
+ $output .= '' . $sizeInMb . ' MB ';
+ $output .= '';
+ $output .= ' ';
+ }
+
+ $output .= ' ';
+ $output .= '
';
+ }
+
+ // === AI-EREIGNISSE ===
+ if (!empty($aiEvents)) {
+ $output .= '
';
+ $output .= '
🤖 AI-erkannte Ereignisse ';
+ $output .= '
';
+
+ foreach ($aiEvents as $category => $event) {
+ $output .= '
';
+ $output .= '';
+
+ // Videos für dieses Event
+ $output .= '
';
+
+ $output .= '
'; // ai-event-card
+ }
+
+ $output .= '
'; // ai-events-grid
+ $output .= '
'; // ai-events-section
+ }
+
+ // Keine Inhalte
+ if (empty($videos) && empty($aiEvents)) {
+ $output .= '
';
+ $output .= '
📭 Keine Videos oder AI-Ereignisse für diesen Tag verfügbar.
';
+ $output .= '
';
+ }
+
+ $output .= '
'; // day-details
+ }
+
+ $output .= '
'; // visual-calendar-container
+
+ return $output;
+ }
+
+ /**
+ * Handler für AI-Video Downloads
+ */
+ public function handleAiVideoDownload() {
+ if (isset($_GET['download_ai_video']) && isset($_GET['token'])) {
+ $videoPath = $_GET['download_ai_video'];
+ $token = $_GET['token'];
+
+ // Token-Validierung
+ $expectedToken = hash_hmac('sha256', $videoPath, session_id());
+ if (!hash_equals($expectedToken, $token)) {
+ echo "Ungültiger Token. Zugriff verweigert.";
+ exit;
+ }
+
+ // Sicherheitsüberprüfung
+ $aiDir = realpath($this->aiDir);
+ $requestedPath = realpath($videoPath);
+
+ if ($requestedPath && strpos($requestedPath, $aiDir) === 0 && file_exists($requestedPath)) {
+ $extension = pathinfo($requestedPath, PATHINFO_EXTENSION);
+ if (strtolower($extension) !== 'mp4') {
+ echo "Nur MP4-Dateien können heruntergeladen werden.";
+ exit;
+ }
+
+ header('Content-Description: File Transfer');
+ header('Content-Type: video/mp4');
+ header('Content-Disposition: attachment; filename="'.basename($requestedPath).'"');
+ header('Expires: 0');
+ header('Cache-Control: must-revalidate');
+ header('Pragma: public');
+ header('Content-Length: ' . filesize($requestedPath));
+ readfile($requestedPath);
+ exit;
+ } else {
+ echo "Datei nicht gefunden oder ungültiger Dateipfad.";
+ exit;
+ }
+ }
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+class GuestbookManager {
+ private $entries = [];
+ private $dbFile = 'guestbook.json';
+
+ public function __construct() {
+ if (file_exists($this->dbFile)) {
+ $this->entries = json_decode(file_get_contents($this->dbFile), true);
+ }
+ }
+
+ public function handleFormSubmission() {
+ if (isset($_POST['guestbook'], $_POST['guest-name'], $_POST['guest-message'])) {
+ $this->addEntry($_POST['guest-name'], $_POST['guest-message']);
+ $this->saveEntries();
+ }
+ }
+
+ private function addEntry($name, $message) {
+ $this->entries[] = [
+ 'name' => $name,
+ 'message' => $message,
+ 'date' => date('Y-m-d H:i:s')
+ ];
+ }
+
+ public function deleteEntry($index) {
+ if (isset($this->entries[$index])) {
+ unset($this->entries[$index]);
+ $this->entries = array_values($this->entries); // Re-indizieren des Arrays
+ $this->saveEntries();
+ return true;
+ }
+ return false;
+ }
+
+
+
+ private function saveEntries() {
+ file_put_contents($this->dbFile, json_encode($this->entries));
+ }
+
+ public function displayForm() {
+ return '
+ ';
+ foreach ($this->entries as $index => $entry) {
+ $output .= "
+
+
{$entry['name']}
+
{$entry['message']}
+
{$entry['date']}";
+ if ($isAdmin) {
+ $output .= "
";
+ }
+
+ }
+ $output .= '
';
+ return $output;
+}
+
+
+}
+
+
+class ContactManager {
+ private $adminEmail = 'metacube@gmail.com';
+ private $feedbackFile = 'feedbacks.json';
+ private $gmailUser = 'metacube@gmail.com';
+ private $gmailAppPassword = 'qggk hsxz fdkq jgxa';
+
+ public function displayForm() {
+ return '
+
+
';
+ }
+
+ public function handleSubmission($name, $email, $message) {
+ if (empty($name) || empty($email) || empty($message)) {
+ return ['success' => false, 'message' => 'Alle Felder sind erforderlich'];
+ }
+
+ if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ return ['success' => false, 'message' => 'Ungültige E-Mail-Adresse'];
+ }
+
+ if (strlen($message) < 10) {
+ return ['success' => false, 'message' => 'Nachricht zu kurz (mindestens 10 Zeichen)'];
+ }
+
+ $name = htmlspecialchars(trim($name), ENT_QUOTES, 'UTF-8');
+ $email = filter_var(trim($email), FILTER_SANITIZE_EMAIL);
+ $message = htmlspecialchars(trim($message), ENT_QUOTES, 'UTF-8');
+
+ $feedback = [
+ 'name' => $name,
+ 'email' => $email,
+ 'message' => $message,
+ 'date' => date('Y-m-d H:i:s'),
+ 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
+ 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
+ ];
+
+ $feedbacks = file_exists($this->feedbackFile)
+ ? json_decode(file_get_contents($this->feedbackFile), true)
+ : [];
+
+ if (!is_array($feedbacks)) $feedbacks = [];
+
+ $feedbacks[] = $feedback;
+ file_put_contents($this->feedbackFile, json_encode($feedbacks, JSON_PRETTY_PRINT));
+
+ $mailSent = $this->sendEmailViaGmail($name, $email, $message, $feedback['date'], $feedback['ip']);
+
+ if ($mailSent) {
+ return ['success' => true, 'message' => 'Vielen Dank! Ihre Nachricht wurde gesendet.'];
+ } else {
+ error_log("Mail-Fehler: Nachricht von {$email} konnte nicht gesendet werden");
+ return ['success' => false, 'message' => 'Nachricht wurde gespeichert, aber E-Mail konnte nicht gesendet werden.'];
+ }
+ }
+
+ private function sendEmailViaGmail($name, $email, $message, $date, $ip) {
+ $mail = new PHPMailer(true);
+
+ try {
+ $mail->SMTPDebug = 2;
+ $mail->Debugoutput = function($str, $level) {
+ error_log("PHPMailer Debug: $str");
+ };
+
+ $mail->isSMTP();
+ $mail->Host = 'smtp.gmail.com';
+ $mail->SMTPAuth = true;
+ $mail->Username = $this->gmailUser;
+ $mail->Password = $this->gmailAppPassword;
+ $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
+ $mail->Port = 587;
+
+ $mail->setFrom($this->gmailUser, 'Aurora Livecam');
+ $mail->addAddress($this->adminEmail);
+ $mail->addReplyTo($email, $name);
+
+ $mail->isHTML(true);
+ $mail->CharSet = 'UTF-8';
+ $mail->Subject = '🔔 Neue Kontaktanfrage von ' . $name;
+ $mail->Body = $this->getEmailTemplate($name, $email, $message, $date, $ip);
+
+ $mail->send();
+ return true;
+ } catch (Exception $e) {
+ error_log("PHPMailer Error: {$mail->ErrorInfo}");
+ return false;
+ }
+ }
+
+ public function deleteFeedback($index) {
+ if (!file_exists($this->feedbackFile)) return false;
+
+ $feedbacks = json_decode(file_get_contents($this->feedbackFile), true);
+ if (isset($feedbacks[$index])) {
+ unset($feedbacks[$index]);
+ $feedbacks = array_values($feedbacks);
+ file_put_contents($this->feedbackFile, json_encode($feedbacks, JSON_PRETTY_PRINT));
+ return true;
+ }
+ return false;
+ }
+
+ private function getEmailTemplate($name, $email, $message, $date, $ip) {
+ return "
+
+
+
+
+
+
+
+
+
+
+
👤 Name: {$name}
+
+
💬 Nachricht:
+
" . nl2br($message) . "
+
+
+
+
+
+ ";
+ }
+}
+
+
+class AdminManager {
+ public function isAdmin() {
+ return isset($_SESSION['admin']) && $_SESSION['admin'] === true;
+ }
+
+ public function handleLogin($username, $password) {
+ if ($username === 'admin' && $password === 'sonne4000$$$$Q') {
+ $_SESSION['admin'] = true;
+ return true;
+ }
+ return false;
+ }
+
+ public function handleImageUpload($file) {
+ if (!$this->isAdmin()) return false;
+ if (!isset($file['tmp_name']) || empty($file['tmp_name'])) { echo "Keine Datei."; return false; }
+
+ $target_dir = "uploads/";
+ if (!file_exists($target_dir)) mkdir($target_dir, 0777, true);
+
+ $target_file = $target_dir . basename($file["name"]);
+ $imageFileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION));
+
+ $check = @getimagesize($file["tmp_name"]);
+ if($check === false) { echo "Kein Bild."; return false; }
+ if ($file["size"] > 5000000) { echo "Zu groß (>5MB)."; return false; }
+ if(!in_array($imageFileType, ['jpg','png','jpeg','gif'])) { echo "Falsches Format."; return false; }
+
+ if (move_uploaded_file($file["tmp_name"], $target_file)) {
+ echo "
✅ Bild erfolgreich hochgeladen.
";
+ return true;
+ } else {
+ echo "Upload Fehler.";
+ return false;
+ }
+ }
+
+ public function displayLoginForm() {
+ return '
+
+
+ Benutzername:
+ Passwort:
+ Einloggen
+ ';
+ }
+
+ public function displayAdminContent() {
+ global $settingsManager;
+
+ $feedbacks = json_decode(file_get_contents('feedbacks.json') ?: '[]', true);
+
+ // NEUES SETTINGS PANEL
+ $output = '
';
+ $output .= '
⚙️ Anzeige-Einstellungen ';
+
+ // Zuschauer-Anzeige Settings
+ $output .= '
';
+ $output .= '
👥 Zuschauer-Anzeige ';
+
+ $output .= '
';
+ $output .= '
Zuschauer-Anzahl anzeigen ';
+ $output .= '
';
+ $output .= '';
+ $output .= ' get('viewer_display.enabled') ? 'checked' : '') . '>';
+ $output .= ' ';
+ $output .= ' ';
+ $output .= '
';
+ $output .= '
';
+
+ $output .= '
';
+ $output .= '
Mindestanzahl für Anzeige ';
+ $output .= '
';
+ $output .= ' ';
+ $output .= '
';
+ $output .= '
';
+ $output .= '
'; // settings-group
+
+ // Video-Modus Settings
+ $output .= '
';
+ $output .= '
🎬 Video-Modus ';
+
+ $output .= '
';
+ $output .= '
Videos im Player abspielen ';
+ $output .= '
';
+ $output .= '';
+ $output .= ' get('video_mode.play_in_player') ? 'checked' : '') . '>';
+ $output .= ' ';
+ $output .= ' ';
+ $output .= '
';
+ $output .= '
';
+
+ $output .= '
';
+ $output .= '
Download erlauben ';
+ $output .= '
';
+ $output .= '';
+ $output .= ' get('video_mode.allow_download') ? 'checked' : '') . '>';
+ $output .= ' ';
+ $output .= ' ';
+ $output .= '
';
+ $output .= '
';
+ $output .= '
'; // settings-group
+
+ $output .= '
'; // admin-settings-panel
+
+ // Bestehender Admin-Content
+ $output .= '
';
+ $output .= '
📩 Posteingang (Kontaktformular) ';
+
+ if (empty($feedbacks)) {
+ $output .= '
Keine Nachrichten vorhanden.
';
+ } else {
+ $output .= '
';
+ foreach ($feedbacks as $index => $feedback) {
+ $ip = isset($feedback['ip']) ? $feedback['ip'] : 'Unbekannt';
+
+ $output .= "
";
+ $output .= "
";
+ $output .= "
";
+
+ $output .= "
+
+
+ 🗑️ Löschen
+ ";
+ $output .= "
";
+
+ $output .= "
{$feedback['message']}
";
+ $output .= "
📅 {$feedback['date']} | IP: {$ip} ";
+ $output .= "
";
+ }
+ $output .= '
';
+ }
+ $output .= '
';
+
+ $output .= '
';
+ $output .= '
🖼️ Bildergalerie verwalten ';
+
+ $output .= $this->displayGalleryImages(true);
+
+ $output .= '⬆️ Neues Bild hochladen
+
+
+
+ ';
+ $output .= '';
+
+ $output .= '
';
+ $output .= '
📲 Social Media Links
+
+
+
+ Facebook
+ Instagram
+ TikTok
+
+
+ Link aktualisieren
+ ';
+ $output .= '';
+
+ return $output;
+ }
+
+ public function displayGalleryImages($isAdmin = false) {
+ $output = '
';
+ $files = glob("uploads/*.{jpg,jpeg,png,gif}", GLOB_BRACE);
+
+ if ($files) {
+ foreach($files as $file) {
+ $filename = basename($file);
+ $output .= '
';
+ $output .= '
';
+
+ if ($isAdmin) {
+ $output .= '
+
+
+
+ X
+ ';
+ }
+
+ $output .= '
';
+ }
+ } else {
+ $output .= '
Keine Bilder in der Galerie.
';
+ }
+ $output .= '
';
+ return $output;
+ }
+
+ public function handleSocialMediaUpdate($platform, $url) {
+ $socialLinks = json_decode(file_get_contents('social_links.json') ?: '{}', true);
+ $socialLinks[$platform] = $url;
+ file_put_contents('social_links.json', json_encode($socialLinks));
+ }
+}
+
+
+class VideoArchiveManager {
+ private $videoDir;
+ private $monthNames;
+
+ public function __construct($videoDir = './videos/') {
+ $this->videoDir = $videoDir;
+ $this->monthNames = [
+ '01' => 'Januar', '02' => 'Februar', '03' => 'März', '04' => 'April',
+ '05' => 'Mai', '06' => 'Juni', '07' => 'Juli', '08' => 'August',
+ '09' => 'September', '10' => 'Oktober', '11' => 'November', '12' => 'Dezember'
+ ];
+ }
+
+ public function getVideosGroupedByDate() {
+ $videos = [];
+
+ foreach (glob($this->videoDir . 'daily_video_*.mp4') as $video) {
+ if (preg_match('/daily_video_(\d{8})_\d{6}\.mp4/', basename($video), $matches)) {
+ $dateStr = $matches[1];
+ $year = substr($dateStr, 0, 4);
+ $month = substr($dateStr, 4, 2);
+ $day = substr($dateStr, 6, 2);
+
+ $videos[$year][$month][] = [
+ 'path' => $video,
+ 'filename' => basename($video),
+ 'day' => $day,
+ 'filesize' => filesize($video),
+ 'modified' => filemtime($video)
+ ];
+ }
+ }
+
+ foreach ($videos as $year => $months) {
+ foreach ($months as $month => $days) {
+ usort($videos[$year][$month], function($a, $b) {
+ return $b['day'] - $a['day'];
+ });
+ }
+ }
+
+ return $videos;
+ }
+
+ public function handleSpecificVideoDownload() {
+ if (isset($_GET['download_specific_video']) && isset($_GET['token'])) {
+ $videoPath = $_GET['download_specific_video'];
+ $token = $_GET['token'];
+
+ $expectedToken = hash_hmac('sha256', $videoPath, session_id());
+ if (!hash_equals($expectedToken, $token)) {
+ echo "Ungültiger Token. Zugriff verweigert.";
+ exit;
+ }
+
+ $videoDir = realpath($this->videoDir);
+ $requestedPath = realpath($videoPath);
+
+ if ($requestedPath && strpos($requestedPath, $videoDir) === 0 && file_exists($requestedPath)) {
+ $extension = pathinfo($requestedPath, PATHINFO_EXTENSION);
+ if (strtolower($extension) !== 'mp4') {
+ echo "Nur MP4-Dateien können heruntergeladen werden.";
+ exit;
+ }
+
+ header('Content-Description: File Transfer');
+ header('Content-Type: video/mp4');
+ header('Content-Disposition: attachment; filename="'.basename($requestedPath).'"');
+ header('Expires: 0');
+ header('Cache-Control: must-revalidate');
+ header('Pragma: public');
+ header('Content-Length: ' . filesize($requestedPath));
+ readfile($requestedPath);
+ exit;
+ } else {
+ echo "Datei nicht gefunden oder ungültiger Dateipfad.";
+ exit;
+ }
+ }
+ }
+}
+
+
+$webcamManager = new WebcamManager();
+$imageFilesJson = $webcamManager->getImageFiles();
+$guestbookManager = new GuestbookManager();
+$contactManager = new ContactManager();
+$adminManager = new AdminManager();
+
+$videoArchiveManager = new VideoArchiveManager('./videos/');
+$videoArchiveManager->handleSpecificVideoDownload();
+
+// AI-Video Download Handler
+if (isset($_GET['download_ai_video']) && isset($_GET['token'])) {
+ $videoPath = $_GET['download_ai_video'];
+ $token = $_GET['token'];
+
+ $expectedToken = hash_hmac('sha256', $videoPath, session_id());
+ if (hash_equals($expectedToken, $token)) {
+ $aiDir = realpath('./ai/');
+ $requestedPath = realpath($videoPath);
+
+ if ($requestedPath && strpos($requestedPath, $aiDir) === 0 && file_exists($requestedPath)) {
+ header('Content-Description: File Transfer');
+ header('Content-Type: video/mp4');
+ header('Content-Disposition: attachment; filename="'.basename($requestedPath).'"');
+ header('Expires: 0');
+ header('Cache-Control: must-revalidate');
+ header('Pragma: public');
+ header('Content-Length: ' . filesize($requestedPath));
+ readfile($requestedPath);
+ exit;
+ }
+ }
+ echo "Download fehlgeschlagen.";
+ exit;
+}
+
+if (isset($_GET['action'])) {
+ switch ($_GET['action']) {
+ case 'snapshot':
+ $webcamManager->captureSnapshot();
+ break;
+ case 'sequence':
+ $webcamManager->captureVideoSequence();
+ break;
+ }
+}
+
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ // Viewer Heartbeat
+ if (isset($_POST['action']) && $_POST['action'] === 'viewer_heartbeat') {
+ $viewerCounter->handleHeartbeat();
+ }
+
+ // GÄSTEBUCH
+ if (isset($_POST['guestbook'])) {
+ $guestbookManager->handleFormSubmission();
+ header("Location: " . $_SERVER['PHP_SELF'] . "#guestbook");
+ exit;
+ }
+
+ // KONTAKTFORMULAR
+ elseif (isset($_POST['contact'])) {
+ $result = $contactManager->handleSubmission($_POST['name'], $_POST['email'], $_POST['message']);
+
+ if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
+ header('Content-Type: application/json');
+ echo json_encode($result);
+ exit;
+ }
+ $_SESSION['contact_result'] = $result;
+ header('Location: ' . $_SERVER['PHP_SELF'] . '#kontakt');
+ exit;
+ }
+
+ // ADMIN LOGIN
+ elseif (isset($_POST['admin-login'])) {
+ $adminManager->handleLogin($_POST['username'], $_POST['password']);
+ header('Location: ' . $_SERVER['PHP_SELF'] . '#admin');
+ exit;
+ }
+
+ // ADMIN AKTIONEN
+ elseif ($adminManager->isAdmin()) {
+ if (isset($_POST['action']) && $_POST['action'] === 'delete_guestbook' && isset($_POST['delete_entry'])) {
+ $guestbookManager->deleteEntry(intval($_POST['delete_entry']));
+ header("Location: " . $_SERVER['PHP_SELF'] . "#guestbook");
+ exit;
+ }
+
+ if (isset($_POST['action']) && $_POST['action'] === 'delete_feedback' && isset($_POST['delete_index'])) {
+ $contactManager->deleteFeedback(intval($_POST['delete_index']));
+ header("Location: " . $_SERVER['PHP_SELF'] . "#admin");
+ exit;
+ }
+
+ if (isset($_POST['action']) && $_POST['action'] === 'delete_image' && isset($_POST['image_name'])) {
+ $filename = basename($_POST['image_name']);
+ $filepath = "uploads/" . $filename;
+ if (file_exists($filepath)) { unlink($filepath); }
+ header("Location: " . $_SERVER['PHP_SELF'] . "#admin");
+ exit;
+ }
+
+ if (isset($_POST['update-social-media'])) {
+ $adminManager->handleSocialMediaUpdate($_POST['social-platform'], $_POST['social-url']);
+ header("Location: " . $_SERVER['PHP_SELF'] . "#admin");
+ exit;
+ }
+
+ if (isset($_FILES["fileToUpload"])) {
+ $adminManager->handleImageUpload($_FILES["fileToUpload"]);
+ }
+ }
+}
+
+// Viewer-Anzeige Einstellungen für JavaScript
+$viewerCount = $viewerCounter->getInitialCount();
+$showViewers = $settingsManager->shouldShowViewers($viewerCount);
+$minViewersToShow = $settingsManager->get('viewer_display.min_viewers');
+?>
+
+
+
+
+
+
+
+
+
+
+
Zürich Oberland Webcam Live - Zürichsee & Patrouille Suisse | Aurora Livecam 24/7
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Willkommen bei Aurora Wetter Livecam
+
+
+
+
+ Erleben Sie faszinierende Ausblicke der Züricher Region - in Echtzeit!
+
+
+
+
+
+
+
Unsere Empfehlungen
+
+
+
+
+
+
+
+
+
+
+ displayWebcam(); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ −
+
+ 1.0x
+ +
+ ⟲
+
+
+
+
+
+
+
+ displayStreamStats(); ?>
+
+
+
+
+
+ Zuschauer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Videoarchiv Tagesvideos
+ displayVisualCalendar();
+ ?>
+
+
+
+
+
+
+
Kamera-Blickrichtung
+
+
+
+
+
+
📍 Ungefährer Standort
+
⛰️ Höhe: ca. 616 m ü.M.
+
🧭 Blickrichtung: Südwest (SW)
+
📍 Region: Zürich Oberland
+
+
+
+
+
+
+
+
+
+
+ Folge uns und kopiere den Code und sende es deinen Freunden
+
+
+
+
+ Klicke auf den QR-Code, um die URL zu kopieren
+
+
+
+
+
+
+
+
Gästebuch
+ displayForm();
+ echo $guestbookManager->displayEntries($adminManager->isAdmin());
+ ?>
+
+
+
+
+
+
+
Kontakt
+
+ Haben Sie Fragen, Anregungen oder möchten uns unterstützen? Wir freuen uns auf Ihre Nachricht!
+
+ displayForm(); ?>
+
+
+
+
+
+
+
Bildergalerie
+
+
+ displayGalleryImages(); ?>
+
+
+
+
+
+
+
+
+
Über unser Projekt
+
+
+
+ Aurora Wetter Livecam ist ein Herzensprojekt von Wetterbegeisterten. Wir möchten Ihnen die Schönheit der Natur und Faszination des Wetters näher bringen.
+
+
Dazu betreiben wir seit 2010 rund um die Uhr hochauflösende Webcams. Besonders stolz sind wir auf einzigartige Einblicke, wie z.B. die Trainingsflüge der Patrouille Suisse jeden Montagmorgen.
+
+
+
+
+
+
+isAdmin()): ?>
+
+
+
Admin-Bereich
+ displayAdminContent(); ?>
+
+
+
+
+
+
Admin Login
+ displayLoginForm(); ?>
+
+
+
+
+
+
+
+
Patrouille Suisse Live - Trainingsflüge Beobachten
+
+
+
Jeden Montag Live!
+
+ Die Patrouille Suisse, das offizielle Kunstflugteam der Schweizer Luftwaffe, trainiert jeden Montagmorgen in der Region Zürich Oberland. Unsere Webcam bietet einen einzigartigen Blick auf die spektakulären Flugmanöver der sechs F-5E Tiger II Jets.
+
+
+ Trainingszeit: ca. 09:00 - 11:00 Uhr
+ Bei gutem Wetter sichtbar
+ Einzigartige Perspektive aus dem Zürcher Oberland
+
+
+
+
Geschichte der Patrouille Suisse
+
+ Gegründet 1964, ist die Patrouille Suisse eines der renommiertesten Kunstflugteams Europas. Das Team fliegt seit 1995 die Northrop F-5E Tiger II und begeistert bei Shows in der ganzen Schweiz und international.
+
+
+ Heimatbasis: Payerne (VD)
+ Flugzeuge: 6x F-5E Tiger II
+ Teamgrösse: 6 Piloten + Crew
+
+
+
+
Beste Beobachtungstipps
+
+ Für die beste Sicht auf die Trainingsflüge empfehlen wir:
+
+
+ Nutzen Sie die Zoom-Funktion unserer Webcam
+ Timelapse-Modus für beschleunigte Ansicht
+ Tagesvideos zum Nachschauen
+ AI-Erkennung markiert Flugzeug-Sichtungen
+
+
+ Hinweis: Bei schlechtem Wetter können Trainings abgesagt werden.
+
+
+
+
+
+
+
+
+
+
Aurora Wetter Blog
+
Aktuelle Wetter-News, Webcam-Updates und Naturbeobachtungen aus dem Zürcher Oberland
+
+
+
+
+
+ 🌅
+
+
+
Sonnenaufgänge über dem Zürichsee
+
Januar 2024
+
+ Die Wintermonate bieten spektakuläre Sonnenaufgänge über dem Zürichsee. Unsere AI-Erkennung identifiziert automatisch die schönsten Momente und speichert sie in der Galerie.
+
+
+ Besonders bei Hochnebel entstehen eindrucksvolle Lichtstimmungen, wenn die Sonne durch die Wolkendecke bricht.
+
+
+
+
+
+
+
+ 🏔️
+
+
+
Alpenpanorama im Winter
+
Dezember 2023
+
+ An klaren Wintertagen reicht die Sicht von unserer Webcam auf 616m Höhe bis zu den schneebedeckten Gipfeln der Glarner Alpen. Säntis, Glärnisch und weitere Bergspitzen sind sichtbar.
+
+
+ Nutzen Sie die Zoom-Funktion für detaillierte Ansichten der Berglandschaft.
+
+
+
+
+
+
+
+ ✈️
+
+
+
Patrouille Suisse Saison 2024
+
März 2024
+
+ Die neue Flugsaison der Patrouille Suisse hat begonnen! Jeden Montag trainiert das Kunstflugteam über dem Zürcher Oberland - unsere Webcam fängt die Flugmanöver live ein.
+
+
+ Die AI-Erkennung markiert Flugzeug-Sichtungen automatisch in unserer Galerie.
+
+
+
+
+
+
+
+ Weitere Wetter-Updates und Beobachtungen finden Sie auf unseren Social Media Kanälen.
+
+
+
+
+
+
+
+
+
Impressum
+
Aurora Wetter Livecam
+
M. Kessler
+
Dürnten, Schweiz
+
Anfragen per Kontaktformular
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+isAdmin()): ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aurora-livecam/js/video-zoom.js b/aurora-livecam/js/video-zoom.js
index 1994c98..61be429 100644
--- a/aurora-livecam/js/video-zoom.js
+++ b/aurora-livecam/js/video-zoom.js
@@ -1,7 +1,6 @@
/**
* Video Zoom & Pan Controller
- * - Zoom für alle Video-Modi (Live, Timelapse, Tagesvideo)
- * - Pan-Funktion: Mit Maus den gezoomten Bereich verschieben
+ * Zoomt auf Wrapper-Layer statt direkt auf Video-Elemente
*/
(() => {
const config = window.zoomConfig || {};
@@ -11,60 +10,75 @@
let panX = 0;
let panY = 0;
let isDragging = false;
- let startX = 0;
- let startY = 0;
+ let lastX = 0;
+ let lastY = 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');
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');
+ // Wrapper-IDs für jeden Modus
+ const wrapperIds = ['live-video-wrapper', 'timelapse-wrapper', 'daily-video-wrapper'];
- // Prüfe welches Element sichtbar ist
- if (dailyPlayer && dailyPlayer.style.display !== 'none' && daily) {
- return daily;
+ // Finde den aktuell sichtbaren Wrapper
+ function getActiveWrapper() {
+ // 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;
- }
- return null;
+
+ // Fallback: Live-Video
+ return document.getElementById('live-video-wrapper');
}
- // Wende Zoom und Pan auf das aktive Element an
+ // Wende Transform auf ALLE Wrapper an (damit beim Wechsel der Zoom erhalten bleibt)
function applyTransform() {
- const target = getActiveTarget();
- if (!target) return;
-
- // Bei Zoom 1x: Kein Pan erlaubt
+ // Bei Zoom 1x: Kein Pan
if (currentZoom <= 1) {
panX = 0;
panY = 0;
}
- // Begrenzen der Pan-Werte basierend auf Zoom
- const maxPan = (currentZoom - 1) * 50; // Prozent
+ // Pan begrenzen basierend auf Zoom
+ const maxPan = (currentZoom - 1) * 50;
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';
+ // Transform auf alle Wrapper anwenden
+ wrapperIds.forEach(id => {
+ 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 (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
@@ -91,57 +105,68 @@
const container = document.querySelector('.video-container');
if (!container) return;
+ // Mousedown - Start dragging
container.addEventListener('mousedown', (e) => {
if (currentZoom <= 1) return;
+ // Ignoriere Klicks auf Controls
+ if (e.target.closest('.zoom-controls, button, a')) return;
+
isDragging = true;
- startX = e.clientX;
- startY = e.clientY;
- container.style.cursor = 'grabbing';
+ lastX = e.clientX;
+ lastY = e.clientY;
e.preventDefault();
});
+ // Mousemove - Dragging
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
- const dx = (e.clientX - startX) / 5; // Sensitivität anpassen
- const dy = (e.clientY - startY) / 5;
+ const deltaX = e.clientX - lastX;
+ const deltaY = e.clientY - lastY;
- panX += dx / currentZoom;
- panY += dy / currentZoom;
+ // Sensitivität basierend auf Zoom
+ const sensitivity = 0.15 / currentZoom;
+ panX += deltaX * sensitivity;
+ panY += deltaY * sensitivity;
- startX = e.clientX;
- startY = e.clientY;
+ lastX = e.clientX;
+ lastY = e.clientY;
applyTransform();
});
+ // Mouseup - Stop dragging
document.addEventListener('mouseup', () => {
- if (isDragging) {
- isDragging = false;
- const container = document.querySelector('.video-container');
- if (container) container.style.cursor = currentZoom > 1 ? 'grab' : 'default';
- }
+ isDragging = false;
+ });
+
+ // Mouse leave
+ document.addEventListener('mouseleave', () => {
+ isDragging = false;
});
// Touch Events für Mobile
container.addEventListener('touchstart', (e) => {
if (currentZoom <= 1 || e.touches.length !== 1) return;
+ if (e.target.closest('.zoom-controls, button, a')) return;
+
isDragging = true;
- startX = e.touches[0].clientX;
- startY = e.touches[0].clientY;
+ lastX = e.touches[0].clientX;
+ lastY = 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;
+ const deltaX = e.touches[0].clientX - lastX;
+ const deltaY = e.touches[0].clientY - lastY;
- panX += dx / currentZoom;
- panY += dy / currentZoom;
+ const sensitivity = 0.15 / currentZoom;
+ panX += deltaX * sensitivity;
+ panY += deltaY * sensitivity;
- startX = e.touches[0].clientX;
- startY = e.touches[0].clientY;
+ lastX = e.touches[0].clientX;
+ lastY = e.touches[0].clientY;
applyTransform();
}, { passive: true });
@@ -150,25 +175,28 @@
isDragging = false;
});
- // Cursor anpassen bei Zoom
- container.style.cursor = 'default';
+ // Doppelklick zum Zurücksetzen
+ container.addEventListener('dblclick', (e) => {
+ if (e.target.closest('.zoom-controls, button, a')) return;
+ resetZoom();
+ });
}
- // Slider Events
+ // Slider Setup
function setupSlider() {
if (!slider) return;
slider.min = minZoom;
slider.max = maxZoom;
slider.step = 0.5;
- slider.value = defaultZoom;
+ slider.value = 1;
slider.addEventListener('input', (e) => {
setZoom(Number(e.target.value));
});
}
- // Globale Funktionen für Buttons
+ // Globale Funktionen
window.adjustZoom = adjustZoom;
window.resetZoom = resetZoom;
window.setZoom = setZoom;
@@ -177,31 +205,11 @@
document.addEventListener('DOMContentLoaded', () => {
setupSlider();
setupPanEvents();
- currentZoom = defaultZoom;
- // Warte kurz, damit Video-Elemente geladen sind
- setTimeout(() => {
- applyTransform();
- }, 500);
+ // Initial State
+ currentZoom = 1;
+ applyTransform();
- // 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);
- }
+ console.log('Video Zoom & Pan initialized');
});
})();