Merge pull request #40 from metacube2/claude/mail-finetuning-webapp-01BsRXQNeVFrCBky8aw35YHw

asdf
This commit is contained in:
2026-01-22 22:30:35 +01:00
committed by GitHub
6 changed files with 1276 additions and 26 deletions
+155 -1
View File
@@ -26,7 +26,8 @@ class SettingsManager {
return [ return [
'viewer_display' => [ 'viewer_display' => [
'enabled' => true, 'enabled' => true,
'min_viewers' => 1 'min_viewers' => 1,
'update_interval' => 5 // Sekunden
], ],
'video_mode' => [ 'video_mode' => [
'play_in_player' => true, 'play_in_player' => true,
@@ -36,6 +37,52 @@ class SettingsManager {
'default_speed' => 1, 'default_speed' => 1,
'available_speeds' => [1, 10, 100] 'available_speeds' => [1, 10, 100]
], ],
// Punkt 2: UI-Anzeige Features
'ui_display' => [
'show_recommendation_banner' => true,
'show_qr_code' => true,
'show_social_media' => true,
'show_patrouille_suisse' => true
],
// Punkt 3: Zoom & Timelapse
'zoom_timelapse' => [
'show_zoom_controls' => true,
'max_zoom_level' => 4.0,
'timelapse_reverse_enabled' => true
],
// Punkt 5: Content Management
'content' => [
'guestbook_enabled' => true,
'gallery_enabled' => true,
'ai_events_enabled' => true,
'max_guestbook_entries' => 50
],
// Punkt 6: Technische Settings
'technical' => [
'viewer_update_interval' => 5, // Sekunden
'session_timeout' => 30 // Sekunden
],
// Punkt 7: Theme & Design
'theme' => [
'default_theme' => 'theme-legacy',
'show_theme_switcher' => false
],
// Punkt 8: SEO & Meta
'seo' => [
'custom_title' => '',
'meta_description' => '',
'meta_keywords' => ''
],
// Weather Widget
'weather' => [
'enabled' => true,
'api_key' => '',
'location' => 'Oberdürnten,CH',
'lat' => '47.2833',
'lon' => '8.7167',
'update_interval' => 5, // Minuten
'units' => 'metric' // metric (Celsius) oder imperial (Fahrenheit)
],
'last_updated' => null, 'last_updated' => null,
'updated_by' => null 'updated_by' => null
]; ];
@@ -123,4 +170,111 @@ class SettingsManager {
public function shouldAllowDownload() { public function shouldAllowDownload() {
return $this->get('video_mode.allow_download') === true; return $this->get('video_mode.allow_download') === true;
} }
// UI Display Helper
public function shouldShowRecommendationBanner() {
return $this->get('ui_display.show_recommendation_banner') === true;
}
public function shouldShowQRCode() {
return $this->get('ui_display.show_qr_code') === true;
}
public function shouldShowSocialMedia() {
return $this->get('ui_display.show_social_media') === true;
}
public function shouldShowPatrouillesuisse() {
return $this->get('ui_display.show_patrouille_suisse') === true;
}
// Content Management Helper
public function isGuestbookEnabled() {
return $this->get('content.guestbook_enabled') === true;
}
public function isGalleryEnabled() {
return $this->get('content.gallery_enabled') === true;
}
public function isAIEventsEnabled() {
return $this->get('content.ai_events_enabled') === true;
}
public function getMaxGuestbookEntries() {
return $this->get('content.max_guestbook_entries') ?? 50;
}
// Theme Helper
public function getDefaultTheme() {
return $this->get('theme.default_theme') ?? 'theme-legacy';
}
public function shouldShowThemeSwitcher() {
return $this->get('theme.show_theme_switcher') === true;
}
// Technical Helper
public function getViewerUpdateInterval() {
return $this->get('technical.viewer_update_interval') ?? 5;
}
public function getSessionTimeout() {
return $this->get('technical.session_timeout') ?? 30;
}
// Zoom & Timelapse Helper
public function shouldShowZoomControls() {
return $this->get('zoom_timelapse.show_zoom_controls') === true;
}
public function getMaxZoomLevel() {
return $this->get('zoom_timelapse.max_zoom_level') ?? 4.0;
}
public function isTimelapseReverseEnabled() {
return $this->get('zoom_timelapse.timelapse_reverse_enabled') === true;
}
// SEO Helper
public function getCustomTitle() {
$title = $this->get('seo.custom_title');
return !empty($title) ? $title : null;
}
public function getMetaDescription() {
return $this->get('seo.meta_description') ?? '';
}
public function getMetaKeywords() {
return $this->get('seo.meta_keywords') ?? '';
}
// Weather Helper
public function isWeatherEnabled() {
return $this->get('weather.enabled') === true;
}
public function getWeatherApiKey() {
return $this->get('weather.api_key') ?? '';
}
public function getWeatherLocation() {
return $this->get('weather.location') ?? 'Oberdürnten,CH';
}
public function getWeatherCoords() {
return [
'lat' => $this->get('weather.lat') ?? '47.2833',
'lon' => $this->get('weather.lon') ?? '8.7167'
];
}
public function getWeatherUpdateInterval() {
return $this->get('weather.update_interval') ?? 5;
}
public function getWeatherUnits() {
return $this->get('weather.units') ?? 'metric';
}
} }
+215
View File
@@ -0,0 +1,215 @@
<?php
/**
* WeatherManager - Holt und cached Wetterdaten von Open-Meteo (kostenlos!)
* Keine API Key nötig!
*/
class WeatherManager {
private $settingsManager;
private $cacheFile = 'weather_cache.json';
private $cacheTime = 300; // 5 Minuten in Sekunden
public function __construct($settingsManager) {
$this->settingsManager = $settingsManager;
}
/**
* Holt aktuelle Wetterdaten (cached)
*/
public function getCurrentWeather() {
// Prüfe ob Weather aktiviert ist
if (!$this->settingsManager->isWeatherEnabled()) {
return null;
}
// Prüfe Cache
$cached = $this->getCache();
if ($cached !== null) {
return $cached;
}
// Hole frische Daten von API (Open-Meteo)
$coords = $this->settingsManager->getWeatherCoords();
// Open-Meteo API URL - komplett kostenlos, kein API Key!
$url = "https://api.open-meteo.com/v1/forecast?" . http_build_query([
'latitude' => $coords['lat'],
'longitude' => $coords['lon'],
'current' => 'temperature_2m,relative_humidity_2m,precipitation,weather_code,wind_speed_10m,wind_direction_10m,pressure_msl,cloud_cover',
'timezone' => 'Europe/Zurich'
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$response) {
return ['error' => 'API Fehler'];
}
$data = json_decode($response, true);
if (!$data || !isset($data['current'])) {
return ['error' => 'Ungültige API Antwort'];
}
$current = $data['current'];
// Formatiere Daten
$weather = [
'temp' => round($current['temperature_2m'], 1),
'feels_like' => round($current['temperature_2m'], 1), // Open-Meteo hat keine "feels like"
'humidity' => $current['relative_humidity_2m'],
'pressure' => round($current['pressure_msl'], 0),
'wind_speed' => round($current['wind_speed_10m'], 1), // Schon in km/h!
'wind_deg' => $current['wind_direction_10m'],
'wind_direction' => $this->getWindDirection($current['wind_direction_10m']),
'clouds' => $current['cloud_cover'] ?? 0,
'description' => $this->getWeatherDescription($current['weather_code']),
'icon' => $this->getWeatherIcon($current['weather_code']),
'rain_1h' => $current['precipitation'] ?? 0,
'snow_1h' => 0, // Open-Meteo gibt Niederschlag gesamt
'location' => $this->settingsManager->getWeatherLocation(),
'timestamp' => time()
];
// Cache speichern
$this->saveCache($weather);
return $weather;
}
/**
* Wandelt WMO Weather Code in Beschreibung um
* https://open-meteo.com/en/docs
*/
private function getWeatherDescription($code) {
$descriptions = [
0 => 'Klar',
1 => 'Überwiegend klar',
2 => 'Teilweise bewölkt',
3 => 'Bewölkt',
45 => 'Neblig',
48 => 'Nebel mit Reifablagerung',
51 => 'Leichter Nieselregen',
53 => 'Mäßiger Nieselregen',
55 => 'Dichter Nieselregen',
61 => 'Leichter Regen',
63 => 'Mäßiger Regen',
65 => 'Starker Regen',
71 => 'Leichter Schneefall',
73 => 'Mäßiger Schneefall',
75 => 'Starker Schneefall',
77 => 'Schneegraupeln',
80 => 'Leichte Regenschauer',
81 => 'Mäßige Regenschauer',
82 => 'Starke Regenschauer',
85 => 'Leichte Schneeschauer',
86 => 'Starke Schneeschauer',
95 => 'Gewitter',
96 => 'Gewitter mit leichtem Hagel',
99 => 'Gewitter mit starkem Hagel'
];
return $descriptions[$code] ?? 'Unbekannt';
}
/**
* Wandelt WMO Weather Code in Icon-Code um (OpenWeatherMap kompatibel)
*/
private function getWeatherIcon($code) {
if ($code == 0) return '01d'; // Klar
if ($code >= 1 && $code <= 2) return '02d'; // Teilweise bewölkt
if ($code == 3) return '04d'; // Bewölkt
if ($code >= 45 && $code <= 48) return '50d'; // Nebel
if ($code >= 51 && $code <= 55) return '09d'; // Nieselregen
if ($code >= 61 && $code <= 65) return '10d'; // Regen
if ($code >= 71 && $code <= 77) return '13d'; // Schnee
if ($code >= 80 && $code <= 82) return '09d'; // Regenschauer
if ($code >= 85 && $code <= 86) return '13d'; // Schneeschauer
if ($code >= 95 && $code <= 99) return '11d'; // Gewitter
return '01d'; // Default
}
/**
* Wandelt Windrichtung (Grad) in Kompassrichtung um
*/
private function getWindDirection($deg) {
$directions = ['N', 'NNO', 'NO', 'ONO', 'O', 'OSO', 'SO', 'SSO', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
$index = round($deg / 22.5) % 16;
return $directions[$index];
}
/**
* Holt Daten aus Cache (wenn noch gültig)
*/
private function getCache() {
if (!file_exists($this->cacheFile)) {
return null;
}
$content = file_get_contents($this->cacheFile);
$data = json_decode($content, true);
if (!$data || !isset($data['timestamp'])) {
return null;
}
// Update-Intervall aus Settings holen (in Minuten)
$updateInterval = $this->settingsManager->getWeatherUpdateInterval() * 60; // Minuten -> Sekunden
// Prüfe ob Cache noch gültig
if (time() - $data['timestamp'] < $updateInterval) {
return $data;
}
return null;
}
/**
* Speichert Daten im Cache
*/
private function saveCache($data) {
$json = json_encode($data, JSON_PRETTY_PRINT);
file_put_contents($this->cacheFile, $json, LOCK_EX);
}
/**
* Gibt Wetter-Icon-Emoji zurück
*/
public function getWeatherEmoji($iconCode) {
$map = [
'01d' => '☀️', '01n' => '🌙',
'02d' => '⛅', '02n' => '☁️',
'03d' => '☁️', '03n' => '☁️',
'04d' => '☁️', '04n' => '☁️',
'09d' => '🌧️', '09n' => '🌧️',
'10d' => '🌦️', '10n' => '🌧️',
'11d' => '⛈️', '11n' => '⛈️',
'13d' => '❄️', '13n' => '❄️',
'50d' => '🌫️', '50n' => '🌫️'
];
return $map[$iconCode] ?? '🌤️';
}
/**
* AJAX Handler für Wetter-Updates
*/
public function handleAjax() {
if ($_SERVER['REQUEST_METHOD'] !== 'GET') return;
if (!isset($_GET['weather_action'])) return;
header('Content-Type: application/json');
if ($_GET['weather_action'] === 'get') {
$weather = $this->getCurrentWeather();
echo json_encode(['success' => true, 'data' => $weather]);
exit;
}
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
// Clear PHP OPcache
if (function_exists('opcache_reset')) {
opcache_reset();
echo "OPcache cleared successfully!\n";
} else {
echo "OPcache not available\n";
}
// Clear realpath cache
clearstatcache(true);
echo "Realpath cache cleared!\n";
echo "\nNow reload the page with CTRL+F5 (hard refresh)\n";
?>
+813 -22
View File
@@ -4,12 +4,17 @@ use PHPMailer\PHPMailer\Exception;
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/vendor/autoload.php';
require_once 'SettingsManager.php'; require_once 'SettingsManager.php';
require_once 'WeatherManager.php';
// SettingsManager initialisieren // SettingsManager initialisieren
$settingsManager = new SettingsManager(); $settingsManager = new SettingsManager();
// AJAX-Handler für Settings (VOR anderen Ausgaben!) // WeatherManager initialisieren
$weatherManager = new WeatherManager($settingsManager);
// AJAX-Handler für Settings und Weather (VOR anderen Ausgaben!)
$settingsManager->handleAjax(); $settingsManager->handleAjax();
$weatherManager->handleAjax();
if (isset($_GET['download_video'])) { if (isset($_GET['download_video'])) {
$videoDir = './videos/'; $videoDir = './videos/';
@@ -559,7 +564,7 @@ class VisualCalendarManager {
} }
// === AI-EREIGNISSE === // === AI-EREIGNISSE ===
if (!empty($aiEvents)) { if (!empty($aiEvents) && (!$this->settingsManager || $this->settingsManager->isAIEventsEnabled())) {
$output .= '<div class="ai-events-section">'; $output .= '<div class="ai-events-section">';
$output .= '<h5>🤖 AI-erkannte Ereignisse</h5>'; $output .= '<h5>🤖 AI-erkannte Ereignisse</h5>';
$output .= '<div class="ai-events-grid">'; $output .= '<div class="ai-events-grid">';
@@ -1018,7 +1023,7 @@ class AdminManager {
} }
public function displayAdminContent() { public function displayAdminContent() {
global $settingsManager; global $settingsManager, $siteConfig;
$feedbacks = json_decode(file_get_contents('feedbacks.json') ?: '[]', true); $feedbacks = json_decode(file_get_contents('feedbacks.json') ?: '[]', true);
@@ -1073,6 +1078,251 @@ class AdminManager {
$output .= '</div>'; $output .= '</div>';
$output .= '</div>'; // settings-group $output .= '</div>'; // settings-group
// UI Display Settings (Punkt 2)
$output .= '<div class="settings-group">';
$output .= '<h4>🖼️ UI Anzeige</h4>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Empfehlungs-Banner anzeigen</span>';
$output .= '<div class="setting-input">';
$output .= '<label class="toggle-switch">';
$output .= '<input type="checkbox" id="setting-show-banner" ' . ($settingsManager->get('ui_display.show_recommendation_banner') ? 'checked' : '') . '>';
$output .= '<span class="toggle-slider"></span>';
$output .= '</label>';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">QR-Code Section anzeigen</span>';
$output .= '<div class="setting-input">';
$output .= '<label class="toggle-switch">';
$output .= '<input type="checkbox" id="setting-show-qr" ' . ($settingsManager->get('ui_display.show_qr_code') ? 'checked' : '') . '>';
$output .= '<span class="toggle-slider"></span>';
$output .= '</label>';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Social Media Links anzeigen</span>';
$output .= '<div class="setting-input">';
$output .= '<label class="toggle-switch">';
$output .= '<input type="checkbox" id="setting-show-social" ' . ($settingsManager->get('ui_display.show_social_media') ? 'checked' : '') . '>';
$output .= '<span class="toggle-slider"></span>';
$output .= '</label>';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Patrouille Suisse Section anzeigen</span>';
$output .= '<div class="setting-input">';
$output .= '<label class="toggle-switch">';
$output .= '<input type="checkbox" id="setting-show-patrouille" ' . ($settingsManager->get('ui_display.show_patrouille_suisse') ? 'checked' : '') . '>';
$output .= '<span class="toggle-slider"></span>';
$output .= '</label>';
$output .= '</div>';
$output .= '</div>';
$output .= '</div>'; // settings-group
// Zoom & Timelapse Settings (Punkt 3)
$output .= '<div class="settings-group">';
$output .= '<h4>🔍 Zoom & Timelapse</h4>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Zoom-Controls anzeigen</span>';
$output .= '<div class="setting-input">';
$output .= '<label class="toggle-switch">';
$output .= '<input type="checkbox" id="setting-show-zoom" ' . ($settingsManager->get('zoom_timelapse.show_zoom_controls') ? 'checked' : '') . '>';
$output .= '<span class="toggle-slider"></span>';
$output .= '</label>';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Max Zoom-Level</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="number" id="setting-max-zoom" class="number-input" min="1.5" max="4.0" step="0.5" value="' . $settingsManager->get('zoom_timelapse.max_zoom_level') . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Timelapse Rückwärts-Modus</span>';
$output .= '<div class="setting-input">';
$output .= '<label class="toggle-switch">';
$output .= '<input type="checkbox" id="setting-timelapse-reverse" ' . ($settingsManager->get('zoom_timelapse.timelapse_reverse_enabled') ? 'checked' : '') . '>';
$output .= '<span class="toggle-slider"></span>';
$output .= '</label>';
$output .= '</div>';
$output .= '</div>';
$output .= '</div>'; // settings-group
// Content Management Settings (Punkt 5)
$output .= '<div class="settings-group">';
$output .= '<h4>📝 Content Management</h4>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Gästebuch aktivieren</span>';
$output .= '<div class="setting-input">';
$output .= '<label class="toggle-switch">';
$output .= '<input type="checkbox" id="setting-guestbook-enabled" ' . ($settingsManager->get('content.guestbook_enabled') ? 'checked' : '') . '>';
$output .= '<span class="toggle-slider"></span>';
$output .= '</label>';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Galerie aktivieren</span>';
$output .= '<div class="setting-input">';
$output .= '<label class="toggle-switch">';
$output .= '<input type="checkbox" id="setting-gallery-enabled" ' . ($settingsManager->get('content.gallery_enabled') ? 'checked' : '') . '>';
$output .= '<span class="toggle-slider"></span>';
$output .= '</label>';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">KI-Events anzeigen</span>';
$output .= '<div class="setting-input">';
$output .= '<label class="toggle-switch">';
$output .= '<input type="checkbox" id="setting-ai-events-enabled" ' . ($settingsManager->get('content.ai_events_enabled') ? 'checked' : '') . '>';
$output .= '<span class="toggle-slider"></span>';
$output .= '</label>';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Max Gästebuch-Einträge</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="number" id="setting-max-guestbook" class="number-input" min="10" max="200" step="10" value="' . $settingsManager->get('content.max_guestbook_entries') . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '</div>'; // settings-group
// Technical Settings (Punkt 6)
$output .= '<div class="settings-group">';
$output .= '<h4>⚙️ Technische Einstellungen</h4>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Viewer Update-Intervall (Sekunden)</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="number" id="setting-viewer-interval" class="number-input" min="1" max="60" value="' . $settingsManager->get('technical.viewer_update_interval') . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Session Timeout (Sekunden)</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="number" id="setting-session-timeout" class="number-input" min="10" max="300" value="' . $settingsManager->get('technical.session_timeout') . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '</div>'; // settings-group
// Theme Settings (Punkt 7)
$output .= '<div class="settings-group">';
$output .= '<h4>🎨 Theme & Design</h4>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Standard-Theme</span>';
$output .= '<div class="setting-input">';
$output .= '<select id="setting-default-theme" class="select-input">';
$currentTheme = $settingsManager->get('theme.default_theme');
$output .= '<option value="theme-legacy" ' . ($currentTheme === 'theme-legacy' ? 'selected' : '') . '>Klassisch</option>';
$output .= '<option value="theme-alpine" ' . ($currentTheme === 'theme-alpine' ? 'selected' : '') . '>Alpin</option>';
$output .= '<option value="theme-neo" ' . ($currentTheme === 'theme-neo' ? 'selected' : '') . '>Modern</option>';
$output .= '</select>';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Theme-Switcher anzeigen</span>';
$output .= '<div class="setting-input">';
$output .= '<label class="toggle-switch">';
$output .= '<input type="checkbox" id="setting-show-theme-switcher" ' . ($settingsManager->get('theme.show_theme_switcher') ? 'checked' : '') . '>';
$output .= '<span class="toggle-slider"></span>';
$output .= '</label>';
$output .= '</div>';
$output .= '</div>';
$output .= '</div>'; // settings-group
// SEO Settings (Punkt 8)
$output .= '<div class="settings-group">';
$output .= '<h4>🔍 SEO & Meta</h4>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Custom Title (leer = Standard)</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="text" id="setting-custom-title" class="text-input" placeholder="' . $siteConfig['siteTitle'] . '" value="' . htmlspecialchars($settingsManager->get('seo.custom_title')) . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Meta Description</span>';
$output .= '<div class="setting-input">';
$output .= '<textarea id="setting-meta-description" class="textarea-input" rows="2" placeholder="SEO Beschreibung für Google...">' . htmlspecialchars($settingsManager->get('seo.meta_description')) . '</textarea>';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Meta Keywords</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="text" id="setting-meta-keywords" class="text-input" placeholder="webcam, zürich, wetter..." value="' . htmlspecialchars($settingsManager->get('seo.meta_keywords')) . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '</div>'; // settings-group
// Weather Settings
$output .= '<div class="settings-group">';
$output .= '<h4>🌤️ Wetter-Widget <span style="font-size:12px; color:#4CAF50;">(Open-Meteo - 100% kostenlos!)</span></h4>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Wetter-Widget anzeigen</span>';
$output .= '<div class="setting-input">';
$output .= '<label class="toggle-switch">';
$output .= '<input type="checkbox" id="setting-weather-enabled" ' . ($settingsManager->get('weather.enabled') ? 'checked' : '') . '>';
$output .= '<span class="toggle-slider"></span>';
$output .= '</label>';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Standort (Stadt,Land)</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="text" id="setting-weather-location" class="text-input" placeholder="Oberdürnten,CH" value="' . htmlspecialchars($settingsManager->get('weather.location')) . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Latitude (Breitengrad)</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="text" id="setting-weather-lat" class="text-input" placeholder="47.2833" value="' . htmlspecialchars($settingsManager->get('weather.lat')) . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Longitude (Längengrad)</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="text" id="setting-weather-lon" class="text-input" placeholder="8.7167" value="' . htmlspecialchars($settingsManager->get('weather.lon')) . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Update-Intervall (Minuten)</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="number" id="setting-weather-interval" class="number-input" min="5" max="60" value="' . $settingsManager->get('weather.update_interval') . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Einheit</span>';
$output .= '<div class="setting-input">';
$output .= '<select id="setting-weather-units" class="select-input">';
$currentUnits = $settingsManager->get('weather.units');
$output .= '<option value="metric" ' . ($currentUnits === 'metric' ? 'selected' : '') . '>Metrisch (°C, km/h)</option>';
$output .= '<option value="imperial" ' . ($currentUnits === 'imperial' ? 'selected' : '') . '>Imperial (°F, mph)</option>';
$output .= '</select>';
$output .= '</div>';
$output .= '</div>';
$output .= '</div>'; // settings-group
$output .= '</div>'; // admin-settings-panel $output .= '</div>'; // admin-settings-panel
// Bestehender Admin-Content // Bestehender Admin-Content
@@ -1383,11 +1633,11 @@ $minViewersToShow = $settingsManager->get('viewer_display.min_viewers');
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- SEO-optimierter Title --> <!-- SEO-optimierter Title -->
<title><?php echo $siteConfig['siteTitle']; ?></title> <title><?php echo $settingsManager->getCustomTitle() ?: $siteConfig['siteTitle']; ?></title>
<!-- SEO Meta-Tags --> <!-- SEO Meta-Tags -->
<meta name="description" content="Live Webcam Zürich Oberland mit Blick auf Zürichsee. 24/7 Livestream, Tagesvideos, AI-Wettererkennung. Patrouille Suisse Trainingsflüge jeden Montag live verfolgen. Webcam Dürnten auf 616m."> <meta name="description" content="<?php echo !empty($settingsManager->getMetaDescription()) ? htmlspecialchars($settingsManager->getMetaDescription()) : 'Live Webcam Zürich Oberland mit Blick auf Zürichsee. 24/7 Livestream, Tagesvideos, AI-Wettererkennung. Patrouille Suisse Trainingsflüge jeden Montag live verfolgen. Webcam Dürnten auf 616m.'; ?>">
<meta name="keywords" content="Webcam Zürich, Zürichsee Webcam, Zürich Oberland Webcam, Live Webcam Schweiz, Patrouille Suisse Livestream, Wetter Zürich live, Webcam Dürnten, Rapperswil Webcam, Schweizer Alpen Webcam, Wetter Zürich Oberland, <?php echo $siteConfig['siteName']; ?> Webcam, Timelapse Zürich"> <meta name="keywords" content="<?php echo !empty($settingsManager->getMetaKeywords()) ? htmlspecialchars($settingsManager->getMetaKeywords()) : 'Webcam Zürich, Zürichsee Webcam, Zürich Oberland Webcam, Live Webcam Schweiz, Patrouille Suisse Livestream, Wetter Zürich live, Webcam Dürnten, Rapperswil Webcam, Schweizer Alpen Webcam, Wetter Zürich Oberland, ' . $siteConfig['siteName'] . ' Webcam, Timelapse Zürich'; ?>">
<meta name="author" content="<?php echo $siteConfig['author']; ?>"> <meta name="author" content="<?php echo $siteConfig['author']; ?>">
<meta name="robots" content="index, follow, max-image-preview:large"> <meta name="robots" content="index, follow, max-image-preview:large">
<link rel="canonical" href="<?php echo $siteConfig['domainUrl']; ?>/"> <link rel="canonical" href="<?php echo $siteConfig['domainUrl']; ?>/">
@@ -1874,6 +2124,90 @@ button[type="submit"]:hover { background-color: #45a049; }
.delete-btn { background-color: #ff4136; color: white; border: none; padding: 5px 10px; cursor: pointer; font-size: 0.8em; margin-left: 10px; border-radius: 3px; } .delete-btn { background-color: #ff4136; color: white; border: none; padding: 5px 10px; cursor: pointer; font-size: 0.8em; margin-left: 10px; border-radius: 3px; }
/* Weather Widget */
.weather-widget {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
animation: weatherFadeIn 0.5s ease;
}
.weather-widget.weather-error {
background: linear-gradient(135deg, #f44336 0%, #e91e63 100%);
color: white;
font-weight: bold;
justify-content: center;
}
.weather-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
padding: 10px 15px;
background: rgba(255, 255, 255, 0.15);
border-radius: 8px;
backdrop-filter: blur(10px);
min-width: 120px;
transition: transform 0.3s ease, background 0.3s ease;
}
.weather-item:hover {
transform: translateY(-3px);
background: rgba(255, 255, 255, 0.25);
}
.weather-icon {
font-size: 32px;
line-height: 1;
}
.weather-value {
font-size: 18px;
font-weight: bold;
color: white;
white-space: nowrap;
}
.weather-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.9);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.weather-description {
flex: 1 1 auto;
min-width: 180px;
}
.weather-description .weather-value {
font-size: 16px;
text-align: center;
}
@keyframes weatherFadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.weather-widget {
gap: 10px;
padding: 15px 10px;
}
.weather-item {
min-width: 90px;
padding: 8px 10px;
}
.weather-icon {
font-size: 24px;
}
.weather-value {
font-size: 14px;
}
.weather-label {
font-size: 10px;
}
}
/* Guestbook */ /* Guestbook */
.guestbook-entry { background-color: #f9f9f9; border-left: 5px solid #4CAF50; margin-bottom: 20px; padding: 15px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } .guestbook-entry { background-color: #f9f9f9; border-left: 5px solid #4CAF50; margin-bottom: 20px; padding: 15px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
@@ -1896,6 +2230,119 @@ button[type="submit"]:hover { background-color: #45a049; }
.modal-next { right: 0; } .modal-next { right: 0; }
.modal-prev { left: 0; } .modal-prev { left: 0; }
/* Admin Settings Panel */
#admin-settings-panel {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px;
border-radius: 12px;
margin-bottom: 30px;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
}
#admin-settings-panel h3 {
color: white;
margin: 0 0 25px 0;
font-size: 24px;
text-align: center;
}
.settings-group {
background: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 8px;
margin-bottom: 15px;
}
.settings-group h4 {
margin: 0 0 15px 0;
color: #667eea;
font-size: 18px;
border-bottom: 2px solid #667eea;
padding-bottom: 8px;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.setting-row:last-child {
border-bottom: none;
}
.setting-label {
font-weight: 500;
color: #333;
flex: 1;
}
.setting-input {
display: flex;
align-items: center;
min-width: 200px;
}
.number-input, .text-input, .select-input {
width: 100%;
padding: 8px 12px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s;
}
.number-input:focus, .text-input:focus, .select-input:focus {
outline: none;
border-color: #667eea;
}
.textarea-input {
width: 100%;
padding: 8px 12px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 14px;
font-family: Arial, sans-serif;
resize: vertical;
transition: border-color 0.3s;
}
.textarea-input:focus {
outline: none;
border-color: #667eea;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.3s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background: linear-gradient(135deg, #667eea, #764ba2);
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(26px);
}
/* Language Switch */ /* Language Switch */
#language-switch { position: fixed; top: 10px; right: 10px; z-index: 1000; background-color: rgba(255, 255, 255, 0.8); border-radius: 5px; padding: 5px; } #language-switch { position: fixed; top: 10px; right: 10px; z-index: 1000; background-color: rgba(255, 255, 255, 0.8); border-radius: 5px; padding: 5px; }
.lang-button { background: none; border: none; cursor: pointer; padding: 5px; opacity: 0.7; transition: opacity 0.3s; margin: 0 2px; } .lang-button { background: none; border: none; cursor: pointer; padding: 5px; opacity: 0.7; transition: opacity 0.3s; margin: 0 2px; }
@@ -2105,7 +2552,7 @@ body.theme-neo footer {
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
</head> </head>
<body class="theme-legacy"> <body class="<?php echo $settingsManager->getDefaultTheme(); ?>">
<div class="sun-overlay" aria-hidden="true"></div> <div class="sun-overlay" aria-hidden="true"></div>
@@ -2147,12 +2594,12 @@ body.theme-neo footer {
<?php endif; ?> <?php endif; ?>
</ul> </ul>
</nav> </nav>
<!-- <div class="theme-switcher" aria-label="Design wechseln"> <div class="theme-switcher" aria-label="Design wechseln" style="display: <?php echo $settingsManager->shouldShowThemeSwitcher() ? 'flex' : 'none'; ?>;">
<span data-en="Design" data-de="Design" data-it="Design" data-fr="Design" data-zh="设计">Design</span> <span data-en="Design" data-de="Design" data-it="Design" data-fr="Design" data-zh="设计">Design</span>
<button class="theme-button active" data-theme="theme-legacy" type="button" data-en="Classic" data-de="Klassisch" data-it="Classico" data-fr="Classique" data-zh="经典">Klassisch</button> <button class="theme-button active" data-theme="theme-legacy" type="button" data-en="Classic" data-de="Klassisch" data-it="Classico" data-fr="Classique" data-zh="经典">Klassisch</button>
<button class="theme-button" data-theme="theme-alpine" type="button" data-en="Alpine" data-de="Alpin" data-it="Alpino" data-fr="Alpin" data-zh="高山">Alpin</button> <button class="theme-button" data-theme="theme-alpine" type="button" data-en="Alpine" data-de="Alpin" data-it="Alpino" data-fr="Alpin" data-zh="高山">Alpin</button>
<button class="theme-button" data-theme="theme-neo" type="button" data-en="Modern" data-de="Modern" data-it="Moderno" data-fr="Moderne" data-zh="现代">Modern</button> <button class="theme-button" data-theme="theme-neo" type="button" data-en="Modern" data-de="Modern" data-it="Moderno" data-fr="Moderne" data-zh="现代">Modern</button>
</div> --> </div>
</div> </div>
</header> </header>
@@ -2176,7 +2623,7 @@ body.theme-neo footer {
</div> </div>
</section> </section>
<div class="banner-container"> <div class="banner-container" style="display: <?php echo $settingsManager->shouldShowRecommendationBanner() ? 'block' : 'none'; ?>;">
<div class="recommendation-banner"> <div class="recommendation-banner">
<h2>Unsere Empfehlungen</h2> <h2>Unsere Empfehlungen</h2>
<div class="sponsor-logos"> <div class="sponsor-logos">
@@ -2194,6 +2641,57 @@ body.theme-neo footer {
<!-- WEBCAM SECTION --> <!-- WEBCAM SECTION -->
<section id="webcams" class="section"> <section id="webcams" class="section">
<div class="container"> <div class="container">
<!-- WEATHER WIDGET -->
<?php if ($settingsManager->isWeatherEnabled()): ?>
<?php
try {
$weather = $weatherManager->getCurrentWeather();
} catch (Exception $e) {
$weather = ['error' => 'Fehler: ' . $e->getMessage()];
}
if ($weather && !isset($weather['error'])):
?>
<div id="weather-widget" class="weather-widget">
<div class="weather-item weather-temp">
<span class="weather-icon">🌡️</span>
<span class="weather-value"><?php echo $weather['temp']; ?>°C</span>
<span class="weather-label">Temperatur</span>
</div>
<div class="weather-item weather-wind">
<span class="weather-icon">💨</span>
<span class="weather-value"><?php echo $weather['wind_speed']; ?> km/h <?php echo $weather['wind_direction']; ?></span>
<span class="weather-label">Wind</span>
</div>
<div class="weather-item weather-pressure">
<span class="weather-icon">🔽</span>
<span class="weather-value"><?php echo $weather['pressure']; ?> hPa</span>
<span class="weather-label">Luftdruck</span>
</div>
<div class="weather-item weather-humidity">
<span class="weather-icon">💧</span>
<span class="weather-value"><?php echo $weather['humidity']; ?>%</span>
<span class="weather-label">Luftfeuchtigkeit</span>
</div>
<div class="weather-item weather-description">
<span class="weather-icon"><?php echo $weatherManager->getWeatherEmoji($weather['icon']); ?></span>
<span class="weather-value"><?php echo $weather['description']; ?></span>
<span class="weather-label"><?php echo $weather['location']; ?></span>
</div>
<?php if ($weather['rain_1h'] > 0 || $weather['snow_1h'] > 0): ?>
<div class="weather-item weather-precipitation">
<span class="weather-icon"><?php echo $weather['rain_1h'] > 0 ? '🌧️' : '❄️'; ?></span>
<span class="weather-value"><?php echo $weather['rain_1h'] > 0 ? $weather['rain_1h'] : $weather['snow_1h']; ?> mm</span>
<span class="weather-label">Niederschlag</span>
</div>
<?php endif; ?>
</div>
<?php elseif ($weather && isset($weather['error'])): ?>
<div class="weather-widget weather-error">
<span>⚠️ Wetterdaten nicht verfügbar: <?php echo $weather['error']; ?></span>
</div>
<?php endif; ?>
<?php endif; ?>
<!-- VIDEO PLAYER --> <!-- VIDEO PLAYER -->
<div class="video-container" style="border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.15);"> <div class="video-container" style="border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.15);">
<?php echo $webcamManager->displayWebcam(); ?> <?php echo $webcamManager->displayWebcam(); ?>
@@ -2221,7 +2719,7 @@ body.theme-neo footer {
<!-- <!--
CONTROLS --> CONTROLS -->
<div id="zoom-controls" class="zoom-controls" aria-label="Zoom Steuerung"> <div id="zoom-controls" class="zoom-controls" aria-label="Zoom Steuerung" style="display: <?php echo $settingsManager->shouldShowZoomControls() ? 'flex' : 'none'; ?>;">
<button type="button" onclick="adjustZoom(-0.5)" class="zoom-btn" title="Zoom out"></button> <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"> <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">1.0x</span> <span id="zoom-value" class="zoom-value">1.0x</span>
@@ -2302,7 +2800,7 @@ body.theme-neo footer {
</section> </section>
<!-- QR CODE --> <!-- QR CODE -->
<section id="qr-code" class="section"> <section id="qr-code" class="section" style="display: <?php echo $settingsManager->shouldShowQRCode() ? 'block' : 'none'; ?>;">
<div class="container" style="text-align: center;"> <div class="container" style="text-align: center;">
<h1> <h1>
<p data-en="Follow us and share with friends" data-de="Folge uns und teile mit Freunden" data-it="Seguici e condividi con gli amici" data-fr="Suivez-nous et partagez avec vos amis" data-zh="关注我们并分享给朋友"> <p data-en="Follow us and share with friends" data-de="Folge uns und teile mit Freunden" data-it="Seguici e condividi con gli amici" data-fr="Suivez-nous et partagez avec vos amis" data-zh="关注我们并分享给朋友">
@@ -2317,7 +2815,7 @@ body.theme-neo footer {
</section> </section>
<!-- GUESTBOOK --> <!-- GUESTBOOK -->
<section id="guestbook" class="section"> <section id="guestbook" class="section" style="display: <?php echo $settingsManager->isGuestbookEnabled() ? 'block' : 'none'; ?>;">
<div class="container"> <div class="container">
<h2 data-en="Guestbook" data-de="Gästebuch" data-it="Libro degli ospiti" data-fr="Livre d'or" data-zh="留言簿">Gästebuch</h2> <h2 data-en="Guestbook" data-de="Gästebuch" data-it="Libro degli ospiti" data-fr="Livre d'or" data-zh="留言簿">Gästebuch</h2>
<?php <?php
@@ -2343,7 +2841,7 @@ body.theme-neo footer {
</section> </section>
<!-- GALLERY --> <!-- GALLERY -->
<section id="gallery" class="section"> <section id="gallery" class="section" style="display: <?php echo $settingsManager->isGalleryEnabled() ? 'block' : 'none'; ?>;">
<div class="container"> <div class="container">
<h2 data-en="Image Gallery" data-de="Bildergalerie" data-it="Galleria immagini" data-fr="Galerie d'images" data-zh="图片库">Bildergalerie</h2> <h2 data-en="Image Gallery" data-de="Bildergalerie" data-it="Galleria immagini" data-fr="Galerie d'images" data-zh="图片库">Bildergalerie</h2>
<div class="gallery-wrapper"> <div class="gallery-wrapper">
@@ -2397,7 +2895,7 @@ body.theme-neo footer {
<?php endif; ?> <?php endif; ?>
<!-- PATROUILLE SUISSE SEKTION --> <!-- PATROUILLE SUISSE SEKTION -->
<section id="patrouille-suisse" class="section" style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);"> <section id="patrouille-suisse" class="section" style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); display: <?php echo $settingsManager->shouldShowPatrouillesuisse() ? 'block' : 'none'; ?>;">
<div class="container"> <div class="container">
<h2 style="color: #fff; text-align: center;" data-en="Patrouille Suisse Live - Watch Training Flights" data-de="Patrouille Suisse Live - Trainingsflüge Beobachten" data-it="Patrouille Suisse Live - Guarda i voli di addestramento" data-fr="Patrouille Suisse en direct - Regardez les vols d'entraînement" data-zh="瑞士巡逻兵直播 - 观看训练飞行">Patrouille Suisse Live - Trainingsflüge Beobachten</h2> <h2 style="color: #fff; text-align: center;" data-en="Patrouille Suisse Live - Watch Training Flights" data-de="Patrouille Suisse Live - Trainingsflüge Beobachten" data-it="Patrouille Suisse Live - Guarda i voli di addestramento" data-fr="Patrouille Suisse en direct - Regardez les vols d'entraînement" data-zh="瑞士巡逻兵直播 - 观看训练飞行">Patrouille Suisse Live - Trainingsflüge Beobachten</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px; margin-top: 30px;"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px; margin-top: 30px;">
@@ -2525,20 +3023,20 @@ body.theme-neo footer {
<footer> <footer>
<div class="container"> <div class="container">
<!-- Social Media Icons --> <!-- Social Media Icons -->
<div class="footer-social" style="text-align: center; margin-bottom: 20px;"> <div class="footer-social social-media-container" style="text-align: center; margin-bottom: 20px; display: <?php echo $settingsManager->shouldShowSocialMedia() ? 'block' : 'none'; ?>;">
<a href="https://www.instagram.com/auroraweatherlivecam" target="_blank" rel="noopener noreferrer" title="Folge uns auf Instagram" style="display: inline-block; margin: 0 10px; color: #E1306C; font-size: 24px;"> <a href="https://www.instagram.com/auroraweatherlivecam" target="_blank" class="social-link" rel="noopener noreferrer" title="Folge uns auf Instagram" style="display: inline-block; margin: 0 10px; color: #E1306C; font-size: 24px;">
<i class="fab fa-instagram" aria-hidden="true"></i> <i class="fab fa-instagram" aria-hidden="true"></i>
<span class="sr-only">Instagram</span> <span class="sr-only">Instagram</span>
</a> </a>
<a href="https://www.facebook.com/auroraweatherlivecam" target="_blank" rel="noopener noreferrer" title="Folge uns auf Facebook" style="display: inline-block; margin: 0 10px; color: #1877F2; font-size: 24px;"> <a href="https://www.facebook.com/auroraweatherlivecam" target="_blank" class="social-link" rel="noopener noreferrer" title="Folge uns auf Facebook" style="display: inline-block; margin: 0 10px; color: #1877F2; font-size: 24px;">
<i class="fab fa-facebook" aria-hidden="true"></i> <i class="fab fa-facebook" aria-hidden="true"></i>
<span class="sr-only">Facebook</span> <span class="sr-only">Facebook</span>
</a> </a>
<a href="https://www.youtube.com/@auroraweatherlivecam" target="_blank" rel="noopener noreferrer" title="Abonniere unseren YouTube Kanal" style="display: inline-block; margin: 0 10px; color: #FF0000; font-size: 24px;"> <a href="https://www.youtube.com/@auroraweatherlivecam" target="_blank" class="social-link" rel="noopener noreferrer" title="Abonniere unseren YouTube Kanal" style="display: inline-block; margin: 0 10px; color: #FF0000; font-size: 24px;">
<i class="fab fa-youtube" aria-hidden="true"></i> <i class="fab fa-youtube" aria-hidden="true"></i>
<span class="sr-only">YouTube</span> <span class="sr-only">YouTube</span>
</a> </a>
<a href="https://www.tiktok.com/@auroraweatherlivecam" target="_blank" rel="noopener noreferrer" title="Folge uns auf TikTok" style="display: inline-block; margin: 0 10px; color: #000000; font-size: 24px;"> <a href="https://www.tiktok.com/@auroraweatherlivecam" target="_blank" class="social-link" rel="noopener noreferrer" title="Folge uns auf TikTok" style="display: inline-block; margin: 0 10px; color: #000000; font-size: 24px;">
<i class="fab fa-tiktok" aria-hidden="true"></i> <i class="fab fa-tiktok" aria-hidden="true"></i>
<span class="sr-only">TikTok</span> <span class="sr-only">TikTok</span>
</a> </a>
@@ -2617,6 +3115,7 @@ const TimelapseController = {
availableSpeeds: [1, 10, 100], availableSpeeds: [1, 10, 100],
intervalId: null, intervalId: null,
baseInterval: 200, baseInterval: 200,
reverseEnabled: <?php echo $settingsManager->isTimelapseReverseEnabled() ? 'true' : 'false'; ?>,
init: function() { init: function() {
this.setupControls(); this.setupControls();
@@ -2632,7 +3131,7 @@ const TimelapseController = {
<button id="tl-play-pause" class="tl-btn" title="Play/Pause"> <button id="tl-play-pause" class="tl-btn" title="Play/Pause">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
</button> </button>
<button id="tl-reverse" class="tl-btn" title="Rückwärts"> <button id="tl-reverse" class="tl-btn" title="Rückwärts" style="display: ${this.reverseEnabled ? 'inline-block' : 'none'};">
<i class="fas fa-backward"></i> <i class="fas fa-backward"></i>
</button> </button>
<div class="tl-slider-container"> <div class="tl-slider-container">
@@ -2867,16 +3366,115 @@ const AdminSettings = {
}, },
applySettingImmediately: function(key, value) { applySettingImmediately: function(key, value) {
const boolValue = (value === true || value === 'true');
switch(key) { switch(key) {
case 'viewer_display.enabled': case 'viewer_display.enabled':
const viewerEl = document.getElementById('viewer-stat-container'); const viewerEl = document.getElementById('viewer-stat-container');
if (viewerEl) { if (viewerEl) {
viewerEl.style.display = (value === true || value === 'true') ? 'inline-flex' : 'none'; viewerEl.style.display = boolValue ? 'inline-flex' : 'none';
} }
break; break;
case 'viewer_display.min_viewers': case 'viewer_display.min_viewers':
window.minViewersToShow = parseInt(value); window.minViewersToShow = parseInt(value);
break; break;
// UI Display (Punkt 2)
case 'ui_display.show_recommendation_banner':
const bannerEl = document.querySelector('.banner-container');
if (bannerEl) bannerEl.style.display = boolValue ? 'block' : 'none';
break;
case 'ui_display.show_qr_code':
const qrSection = document.getElementById('qr-code');
if (qrSection) qrSection.style.display = boolValue ? 'block' : 'none';
break;
case 'ui_display.show_social_media':
const socialEls = document.querySelectorAll('.social-link, .social-media-container');
socialEls.forEach(el => el.style.display = boolValue ? '' : 'none');
break;
case 'ui_display.show_patrouille_suisse':
const patrouilleSection = document.getElementById('patrouille-suisse');
if (patrouilleSection) patrouilleSection.style.display = boolValue ? 'block' : 'none';
break;
// Zoom & Timelapse (Punkt 3)
case 'zoom_timelapse.show_zoom_controls':
const zoomControls = document.getElementById('zoom-controls');
if (zoomControls) zoomControls.style.display = boolValue ? 'flex' : 'none';
break;
case 'zoom_timelapse.max_zoom_level':
window.maxZoomLevel = parseFloat(value);
this.showNotification('Max Zoom: ' + value + 'x (Reload empfohlen)', 'success');
break;
case 'zoom_timelapse.timelapse_reverse_enabled':
const reverseBtn = document.getElementById('tl-reverse');
if (reverseBtn) reverseBtn.style.display = boolValue ? 'inline-block' : 'none';
break;
// Content Management (Punkt 5)
case 'content.guestbook_enabled':
const guestbookSection = document.getElementById('guestbook');
if (guestbookSection) guestbookSection.style.display = boolValue ? 'block' : 'none';
break;
case 'content.gallery_enabled':
const gallerySection = document.getElementById('gallery');
if (gallerySection) gallerySection.style.display = boolValue ? 'block' : 'none';
break;
case 'content.ai_events_enabled':
const aiSections = document.querySelectorAll('.ai-events-section');
aiSections.forEach(section => section.style.display = boolValue ? 'block' : 'none');
this.showNotification('AI-Events ' + (boolValue ? 'aktiviert' : 'deaktiviert') + ' (Reload empfohlen)', 'success');
break;
case 'content.max_guestbook_entries':
this.showNotification('Max Einträge: ' + value + ' (Reload empfohlen)', 'success');
break;
// Technical (Punkt 6)
case 'technical.viewer_update_interval':
this.showNotification('Update-Intervall: ' + value + 's (Reload empfohlen)', 'success');
break;
case 'technical.session_timeout':
this.showNotification('Session Timeout: ' + value + 's (Reload empfohlen)', 'success');
break;
// Theme (Punkt 7)
case 'theme.default_theme':
document.body.className = value;
this.showNotification('Theme geändert: ' + value, 'success');
break;
case 'theme.show_theme_switcher':
const themeSwitcher = document.querySelector('.theme-switcher');
if (themeSwitcher) themeSwitcher.style.display = boolValue ? 'flex' : 'none';
break;
// SEO (Punkt 8)
case 'seo.custom_title':
document.title = value || document.title;
this.showNotification('Title aktualisiert (Meta bei Reload)', 'success');
break;
case 'seo.meta_description':
case 'seo.meta_keywords':
this.showNotification('SEO Meta gespeichert (wirksam bei Reload)', 'success');
break;
// Weather Settings
case 'weather.enabled':
const weatherWidget = document.getElementById('weather-widget');
if (weatherWidget) {
weatherWidget.style.display = boolValue ? 'flex' : 'none';
}
this.showNotification('Wetter-Widget ' + (boolValue ? 'aktiviert' : 'deaktiviert'), 'success');
break;
case 'weather.api_key':
case 'weather.location':
case 'weather.lat':
case 'weather.lon':
case 'weather.units':
this.showNotification('Wetter-Einstellung gespeichert (Reload empfohlen)', 'success');
break;
case 'weather.update_interval':
this.showNotification('Update-Intervall: ' + value + ' Minuten (Reload empfohlen)', 'success');
break;
} }
}, },
@@ -2896,6 +3494,109 @@ const AdminSettings = {
document.getElementById('setting-allow-download')?.addEventListener('change', (e) => { document.getElementById('setting-allow-download')?.addEventListener('change', (e) => {
this.updateSetting('video_mode.allow_download', e.target.checked); this.updateSetting('video_mode.allow_download', e.target.checked);
}); });
// UI Display Settings (Punkt 2)
document.getElementById('setting-show-banner')?.addEventListener('change', (e) => {
this.updateSetting('ui_display.show_recommendation_banner', e.target.checked);
});
document.getElementById('setting-show-qr')?.addEventListener('change', (e) => {
this.updateSetting('ui_display.show_qr_code', e.target.checked);
});
document.getElementById('setting-show-social')?.addEventListener('change', (e) => {
this.updateSetting('ui_display.show_social_media', e.target.checked);
});
document.getElementById('setting-show-patrouille')?.addEventListener('change', (e) => {
this.updateSetting('ui_display.show_patrouille_suisse', e.target.checked);
});
// Zoom & Timelapse Settings (Punkt 3)
document.getElementById('setting-show-zoom')?.addEventListener('change', (e) => {
this.updateSetting('zoom_timelapse.show_zoom_controls', e.target.checked);
});
document.getElementById('setting-max-zoom')?.addEventListener('change', (e) => {
this.updateSetting('zoom_timelapse.max_zoom_level', parseFloat(e.target.value));
});
document.getElementById('setting-timelapse-reverse')?.addEventListener('change', (e) => {
this.updateSetting('zoom_timelapse.timelapse_reverse_enabled', e.target.checked);
});
// Content Management Settings (Punkt 5)
document.getElementById('setting-guestbook-enabled')?.addEventListener('change', (e) => {
this.updateSetting('content.guestbook_enabled', e.target.checked);
});
document.getElementById('setting-gallery-enabled')?.addEventListener('change', (e) => {
this.updateSetting('content.gallery_enabled', e.target.checked);
});
document.getElementById('setting-ai-events-enabled')?.addEventListener('change', (e) => {
this.updateSetting('content.ai_events_enabled', e.target.checked);
});
document.getElementById('setting-max-guestbook')?.addEventListener('change', (e) => {
this.updateSetting('content.max_guestbook_entries', parseInt(e.target.value));
});
// Technical Settings (Punkt 6)
document.getElementById('setting-viewer-interval')?.addEventListener('change', (e) => {
this.updateSetting('technical.viewer_update_interval', parseInt(e.target.value));
});
document.getElementById('setting-session-timeout')?.addEventListener('change', (e) => {
this.updateSetting('technical.session_timeout', parseInt(e.target.value));
});
// Theme Settings (Punkt 7)
document.getElementById('setting-default-theme')?.addEventListener('change', (e) => {
this.updateSetting('theme.default_theme', e.target.value);
});
document.getElementById('setting-show-theme-switcher')?.addEventListener('change', (e) => {
this.updateSetting('theme.show_theme_switcher', e.target.checked);
});
// SEO Settings (Punkt 8)
document.getElementById('setting-custom-title')?.addEventListener('change', (e) => {
this.updateSetting('seo.custom_title', e.target.value);
});
document.getElementById('setting-meta-description')?.addEventListener('change', (e) => {
this.updateSetting('seo.meta_description', e.target.value);
});
document.getElementById('setting-meta-keywords')?.addEventListener('change', (e) => {
this.updateSetting('seo.meta_keywords', e.target.value);
});
// Weather Settings
document.getElementById('setting-weather-enabled')?.addEventListener('change', (e) => {
this.updateSetting('weather.enabled', e.target.checked);
});
document.getElementById('setting-weather-location')?.addEventListener('change', (e) => {
this.updateSetting('weather.location', e.target.value);
});
document.getElementById('setting-weather-lat')?.addEventListener('change', (e) => {
this.updateSetting('weather.lat', e.target.value);
});
document.getElementById('setting-weather-lon')?.addEventListener('change', (e) => {
this.updateSetting('weather.lon', e.target.value);
});
document.getElementById('setting-weather-interval')?.addEventListener('change', (e) => {
this.updateSetting('weather.update_interval', parseInt(e.target.value));
});
document.getElementById('setting-weather-units')?.addEventListener('change', (e) => {
this.updateSetting('weather.units', e.target.value);
});
}, },
showNotification: function(message, type) { showNotification: function(message, type) {
@@ -3135,6 +3836,96 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
</script> </script>
<!-- WEATHER AUTO-UPDATE -->
<script>
const WeatherUpdater = {
updateInterval: <?php echo $settingsManager->getWeatherUpdateInterval() * 60 * 1000; ?>, // Minuten -> Millisekunden
init: function() {
if (!document.getElementById('weather-widget')) return;
// Update alle X Minuten
setInterval(() => this.updateWeather(), this.updateInterval);
},
updateWeather: function() {
fetch(window.location.href + '?weather_action=get')
.then(r => r.json())
.then(data => {
if (data.success && data.data && !data.data.error) {
this.renderWeather(data.data);
}
})
.catch(err => console.error('Weather update error:', err));
},
renderWeather: function(weather) {
const widget = document.getElementById('weather-widget');
if (!widget) return;
const rainSnow = weather.rain_1h > 0 || weather.snow_1h > 0;
const precipIcon = weather.rain_1h > 0 ? '🌧️' : '❄️';
const precipValue = weather.rain_1h > 0 ? weather.rain_1h : weather.snow_1h;
widget.innerHTML = `
<div class="weather-item weather-temp">
<span class="weather-icon">🌡️</span>
<span class="weather-value">${weather.temp}°C</span>
<span class="weather-label">Temperatur</span>
</div>
<div class="weather-item weather-wind">
<span class="weather-icon">💨</span>
<span class="weather-value">${weather.wind_speed} km/h ${weather.wind_direction}</span>
<span class="weather-label">Wind</span>
</div>
<div class="weather-item weather-pressure">
<span class="weather-icon">🔽</span>
<span class="weather-value">${weather.pressure} hPa</span>
<span class="weather-label">Luftdruck</span>
</div>
<div class="weather-item weather-humidity">
<span class="weather-icon">💧</span>
<span class="weather-value">${weather.humidity}%</span>
<span class="weather-label">Luftfeuchtigkeit</span>
</div>
<div class="weather-item weather-description">
<span class="weather-icon">${this.getWeatherEmoji(weather.icon)}</span>
<span class="weather-value">${weather.description}</span>
<span class="weather-label">${weather.location}</span>
</div>
${rainSnow ? `
<div class="weather-item weather-precipitation">
<span class="weather-icon">${precipIcon}</span>
<span class="weather-value">${precipValue} mm</span>
<span class="weather-label">Niederschlag</span>
</div>
` : ''}
`;
// Fade-in Animation
widget.style.animation = 'weatherFadeIn 0.5s ease';
},
getWeatherEmoji: function(iconCode) {
const map = {
'01d': '☀️', '01n': '🌙',
'02d': '⛅', '02n': '☁️',
'03d': '☁️', '03n': '☁️',
'04d': '☁️', '04n': '☁️',
'09d': '🌧️', '09n': '🌧️',
'10d': '🌦️', '10n': '🌧️',
'11d': '⛈️', '11n': '⛈️',
'13d': '❄️', '13n': '❄️',
'50d': '🌫️', '50n': '🌫️'
};
return map[iconCode] || '🌤️';
}
};
document.addEventListener('DOMContentLoaded', function() {
WeatherUpdater.init();
});
</script>
</body> </body>
</html> </html>
+46 -2
View File
@@ -1,7 +1,8 @@
{ {
"viewer_display": { "viewer_display": {
"enabled": true, "enabled": true,
"min_viewers": 1 "min_viewers": 1,
"update_interval": 5
}, },
"video_mode": { "video_mode": {
"play_in_player": true, "play_in_player": true,
@@ -9,7 +10,50 @@
}, },
"timelapse": { "timelapse": {
"default_speed": 1, "default_speed": 1,
"available_speeds": [1, 10, 100] "available_speeds": [
1,
10,
100
]
},
"ui_display": {
"show_recommendation_banner": true,
"show_qr_code": true,
"show_social_media": true,
"show_patrouille_suisse": true
},
"zoom_timelapse": {
"show_zoom_controls": true,
"max_zoom_level": 4.0,
"timelapse_reverse_enabled": true
},
"content": {
"guestbook_enabled": true,
"gallery_enabled": true,
"ai_events_enabled": true,
"max_guestbook_entries": 50
},
"technical": {
"viewer_update_interval": 5,
"session_timeout": 30
},
"theme": {
"default_theme": "theme-legacy",
"show_theme_switcher": false
},
"seo": {
"custom_title": "",
"meta_description": "",
"meta_keywords": ""
},
"weather": {
"enabled": true,
"api_key": "",
"location": "Oberdürnten,CH",
"lat": "47.2833",
"lon": "8.7167",
"update_interval": 5,
"units": "metric"
}, },
"last_updated": null, "last_updated": null,
"updated_by": null "updated_by": null
+31
View File
@@ -0,0 +1,31 @@
<?php
// Fehler anzeigen
error_reporting(E_ALL);
ini_set('display_errors', 1);
echo "Test 1: Settings Manager laden...<br>";
require_once 'SettingsManager.php';
echo "✓ SettingsManager.php geladen<br>";
echo "Test 2: Weather Manager laden...<br>";
require_once 'WeatherManager.php';
echo "✓ WeatherManager.php geladen<br>";
echo "Test 3: SettingsManager initialisieren...<br>";
$settingsManager = new SettingsManager();
echo "✓ SettingsManager initialisiert<br>";
echo "Test 4: WeatherManager initialisieren...<br>";
$weatherManager = new WeatherManager($settingsManager);
echo "✓ WeatherManager initialisiert<br>";
echo "Test 5: Wetter abrufen...<br>";
$weather = $weatherManager->getCurrentWeather();
echo "✓ Wetter abgerufen<br>";
echo "<pre>";
print_r($weather);
echo "</pre>";
echo "<br><br>✅ ALLE TESTS ERFOLGREICH!";
?>