Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ec8d734ee | |||
| 60dab1e9df | |||
| 191381ece4 | |||
| fabdfb121a | |||
| 4454adca59 | |||
| 9ae417cb03 | |||
| 367aa4c67b | |||
| cac3768885 |
@@ -0,0 +1,240 @@
|
||||
# Integration Guide für Aurora Livecam Erweiterungen
|
||||
|
||||
## Übersicht der neuen Dateien
|
||||
|
||||
```
|
||||
aurora-livecam/
|
||||
├── SettingsManager.php # Admin-Einstellungen Klasse
|
||||
├── settings.json # Einstellungen Datei
|
||||
├── js/
|
||||
│ ├── timelapse-controls.js # Timelapse mit Slider
|
||||
│ ├── video-player.js # Tagesvideos im Player
|
||||
│ └── admin-settings.js # Admin AJAX
|
||||
├── css/
|
||||
│ └── player-controls.css # Styles für Controls
|
||||
└── INTEGRATION.md # Diese Anleitung
|
||||
```
|
||||
|
||||
## Änderungen in index.php
|
||||
|
||||
### 1. Am Anfang der Datei (nach den requires)
|
||||
|
||||
```php
|
||||
<?php
|
||||
// ... bestehende requires ...
|
||||
|
||||
// NEU: Settings Manager einbinden
|
||||
require_once 'SettingsManager.php';
|
||||
$settingsManager = new SettingsManager();
|
||||
|
||||
// AJAX-Handler für Settings (VOR session_start!)
|
||||
$settingsManager->handleAjax();
|
||||
```
|
||||
|
||||
### 2. Im HEAD-Bereich (CSS einbinden)
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="css/player-controls.css">
|
||||
```
|
||||
|
||||
### 3. Vor </body> (JavaScript einbinden)
|
||||
|
||||
```html
|
||||
<script src="js/timelapse-controls.js"></script>
|
||||
<script src="js/video-player.js"></script>
|
||||
<?php if ($adminManager->isAdmin()): ?>
|
||||
<script src="js/admin-settings.js"></script>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### 4. Video-Container anpassen
|
||||
|
||||
Ersetze den bestehenden video-container:
|
||||
|
||||
```html
|
||||
<div class="video-container">
|
||||
<?php echo $webcamManager->displayWebcam(); ?>
|
||||
|
||||
<!-- Timelapse Overlay -->
|
||||
<div id="timelapse-viewer" style="display: none;">
|
||||
<img id="timelapse-image" src="" alt="Timelapse">
|
||||
</div>
|
||||
|
||||
<!-- NEU: Daily Video Player (wird dynamisch befüllt) -->
|
||||
</div>
|
||||
|
||||
<!-- NEU: Timelapse Controls (außerhalb des Containers) -->
|
||||
<div id="timelapse-controls"></div>
|
||||
```
|
||||
|
||||
### 5. Zuschauer-Anzeige konditionell machen
|
||||
|
||||
Ersetze die Viewer-Stat Anzeige:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$viewerCount = $viewerCounter->getInitialCount();
|
||||
$showViewers = $settingsManager->shouldShowViewers($viewerCount);
|
||||
?>
|
||||
|
||||
<?php if ($showViewers): ?>
|
||||
<div class="info-badge viewer-stat">
|
||||
<span class="live-dot"></span>
|
||||
<strong id="viewer-count-display"><?php echo $viewerCount; ?></strong>
|
||||
<span>Zuschauer</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### 6. Kalender Links anpassen
|
||||
|
||||
In der `VisualCalendarManager::displayVisualCalendar()` Methode:
|
||||
|
||||
```php
|
||||
// Für Tagesvideos
|
||||
$playInPlayer = $settingsManager->shouldPlayInPlayer();
|
||||
$allowDownload = $settingsManager->shouldAllowDownload();
|
||||
|
||||
if ($playInPlayer) {
|
||||
// Im Player abspielen
|
||||
$output .= '<a href="#" onclick="DailyVideoPlayer.playVideo(\'' . $video['path'] . '\', ' . ($allowDownload ? 'true' : 'false') . '); return false;" class="play-link">';
|
||||
$output .= '▶️ Abspielen';
|
||||
$output .= '</a>';
|
||||
}
|
||||
|
||||
if ($allowDownload) {
|
||||
// Download Link
|
||||
$output .= '<a href="?download_specific_video=..." class="download-link">⬇️ Download</a>';
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Admin-Panel erweitern
|
||||
|
||||
Füge im Admin-Bereich hinzu:
|
||||
|
||||
```php
|
||||
<?php if ($adminManager->isAdmin()): ?>
|
||||
<section id="admin" class="section">
|
||||
<div class="container">
|
||||
<h2>Admin-Bereich</h2>
|
||||
|
||||
<!-- NEU: Settings Panel -->
|
||||
<div id="admin-settings-panel">
|
||||
<h3>⚙️ Anzeige-Einstellungen</h3>
|
||||
|
||||
<div class="settings-group">
|
||||
<h4>👥 Zuschauer-Anzeige</h4>
|
||||
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">Zuschauer-Anzahl anzeigen</span>
|
||||
<div class="setting-input">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="setting-viewer-enabled"
|
||||
<?php echo $settingsManager->get('viewer_display.enabled') ? 'checked' : ''; ?>>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">Mindestanzahl für Anzeige</span>
|
||||
<div class="setting-input">
|
||||
<input type="number" id="setting-min-viewers" class="number-input"
|
||||
min="1" max="100"
|
||||
value="<?php echo $settingsManager->get('viewer_display.min_viewers'); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<h4>🎬 Video-Modus</h4>
|
||||
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">Videos im Player abspielen</span>
|
||||
<div class="setting-input">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="setting-play-in-player"
|
||||
<?php echo $settingsManager->get('video_mode.play_in_player') ? 'checked' : ''; ?>>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">Download erlauben</span>
|
||||
<div class="setting-input">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="setting-allow-download"
|
||||
<?php echo $settingsManager->get('video_mode.allow_download') ? 'checked' : ''; ?>>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bestehender Admin-Content -->
|
||||
<?php echo $adminManager->displayAdminContent(); ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### 8. Timelapse Button Event anpassen
|
||||
|
||||
Im bestehenden JavaScript:
|
||||
|
||||
```javascript
|
||||
timelapseButton.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (timelapseViewer.style.display === 'none') {
|
||||
// NEU: TimelapseController verwenden
|
||||
TimelapseController.init(imageFiles);
|
||||
TimelapseController.show();
|
||||
timelapseButton.textContent = 'Zurück zur Live-Webcam';
|
||||
} else {
|
||||
TimelapseController.backToLive();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 9. Viewer Heartbeat anpassen
|
||||
|
||||
Im JavaScript für den Viewer-Counter:
|
||||
|
||||
```javascript
|
||||
function updateViewerCount() {
|
||||
fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({action: 'viewer_heartbeat'})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const display = document.getElementById('viewer-count-display');
|
||||
const container = document.querySelector('.viewer-stat');
|
||||
|
||||
if (data.count && display) {
|
||||
display.textContent = data.count;
|
||||
|
||||
// Mindestanzahl prüfen (aus Settings)
|
||||
const minViewers = window.minViewersToShow || 1;
|
||||
if (container) {
|
||||
container.style.display = data.count >= minViewers ? 'inline-flex' : 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Fertig!
|
||||
|
||||
Nach diesen Änderungen hast du:
|
||||
- ✅ Timelapse mit Slider und 1x/10x/100x Geschwindigkeit
|
||||
- ✅ Rückwärts-Spulen im Timelapse
|
||||
- ✅ Tagesvideos im Player abspielen statt nur Download
|
||||
- ✅ "Zurück zu Live" Button
|
||||
- ✅ Admin-Einstellungen für Zuschauer-Anzeige
|
||||
- ✅ Mindestanzahl für Zuschauer-Anzeige
|
||||
- ✅ Video-Modus wählbar (Player/Download)
|
||||
- ✅ Alles ohne Seiten-Reload
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
/**
|
||||
* SettingsManager - Verwaltet Admin-Einstellungen
|
||||
* Speichert in settings.json, lädt ohne Reload
|
||||
*/
|
||||
class SettingsManager {
|
||||
private $settingsFile;
|
||||
private $settings = [];
|
||||
|
||||
public function __construct($file = null) {
|
||||
$this->settingsFile = $file ?: (__DIR__ . '/settings.json');
|
||||
$this->load();
|
||||
}
|
||||
|
||||
private function load() {
|
||||
if (file_exists($this->settingsFile)) {
|
||||
$content = file_get_contents($this->settingsFile);
|
||||
$this->settings = json_decode($content, true) ?? $this->getDefaults();
|
||||
} else {
|
||||
$this->settings = $this->getDefaults();
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function getDefaults() {
|
||||
return [
|
||||
'viewer_display' => [
|
||||
'enabled' => true,
|
||||
'min_viewers' => 1
|
||||
],
|
||||
'video_mode' => [
|
||||
'play_in_player' => true,
|
||||
'allow_download' => true
|
||||
],
|
||||
'timelapse' => [
|
||||
'default_speed' => 1,
|
||||
'available_speeds' => [1, 10, 100]
|
||||
],
|
||||
'last_updated' => null,
|
||||
'updated_by' => null
|
||||
];
|
||||
}
|
||||
|
||||
public function get($key = null) {
|
||||
if ($key === null) return $this->settings;
|
||||
$keys = explode('.', $key);
|
||||
$value = $this->settings;
|
||||
foreach ($keys as $k) {
|
||||
if (!isset($value[$k])) return null;
|
||||
$value = $value[$k];
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function set($key, $value) {
|
||||
$keys = explode('.', $key);
|
||||
$ref = &$this->settings;
|
||||
foreach ($keys as $i => $k) {
|
||||
if ($i === count($keys) - 1) {
|
||||
$ref[$k] = $value;
|
||||
} else {
|
||||
if (!isset($ref[$k])) $ref[$k] = [];
|
||||
$ref = &$ref[$k];
|
||||
}
|
||||
}
|
||||
$this->settings['last_updated'] = date('Y-m-d H:i:s');
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
private function save() {
|
||||
$payload = json_encode($this->settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
if ($payload === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return file_put_contents($this->settingsFile, $payload, LOCK_EX) !== false;
|
||||
}
|
||||
|
||||
// Für AJAX-Anfragen
|
||||
public function handleAjax() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return;
|
||||
if (!isset($_POST['settings_action'])) return;
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
switch ($_POST['settings_action']) {
|
||||
case 'get':
|
||||
echo json_encode(['success' => true, 'settings' => $this->settings]);
|
||||
exit;
|
||||
|
||||
case 'update':
|
||||
$key = $_POST['key'] ?? null;
|
||||
$value = $_POST['value'] ?? null;
|
||||
|
||||
// Boolean-Werte konvertieren
|
||||
if ($value === 'true') $value = true;
|
||||
if ($value === 'false') $value = false;
|
||||
if (is_numeric($value)) $value = intval($value);
|
||||
|
||||
if ($key && $this->set($key, $value)) {
|
||||
echo json_encode(['success' => true, 'message' => 'Einstellung gespeichert']);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Fehler beim Speichern. Bitte Dateirechte prüfen.'
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Viewer-Anzeige prüfen
|
||||
public function shouldShowViewers($currentCount) {
|
||||
if (!$this->get('viewer_display.enabled')) return false;
|
||||
return $currentCount >= $this->get('viewer_display.min_viewers');
|
||||
}
|
||||
|
||||
// Video-Modus prüfen
|
||||
public function shouldPlayInPlayer() {
|
||||
return $this->get('video_mode.play_in_player') === true;
|
||||
}
|
||||
|
||||
public function shouldAllowDownload() {
|
||||
return $this->get('video_mode.allow_download') === true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/* ========== TIMELAPSE CONTROLS ========== */
|
||||
#timelapse-controls {
|
||||
display: none;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.timelapse-control-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 12px 20px;
|
||||
border-radius: 50px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tl-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.tl-btn:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
|
||||
}
|
||||
|
||||
.tl-btn.active {
|
||||
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
|
||||
}
|
||||
|
||||
.tl-slider-container {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
#tl-slider {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: #e0e0e0;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
#tl-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
#tl-slider::-moz-range-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#tl-time-display {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #f5f5f5;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tl-speed-btn {
|
||||
width: auto !important;
|
||||
padding: 0 20px !important;
|
||||
border-radius: 22px !important;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tl-back-btn {
|
||||
width: auto !important;
|
||||
padding: 0 20px !important;
|
||||
border-radius: 22px !important;
|
||||
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%) !important;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ========== DAILY VIDEO PLAYER ========== */
|
||||
#daily-video-player {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
#daily-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.video-player-controls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
/* ========== ADMIN SETTINGS PANEL ========== */
|
||||
#admin-settings-panel {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
#admin-settings-panel h3 {
|
||||
color: #667eea;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
margin-bottom: 25px;
|
||||
padding: 20px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.settings-group h4 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.setting-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.setting-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
width: 50px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.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: 26px;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
/* Number Input */
|
||||
.number-input {
|
||||
width: 70px;
|
||||
padding: 8px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.number-input:focus {
|
||||
border-color: #667eea;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* ========== MOBILE RESPONSIVE ========== */
|
||||
@media (max-width: 600px) {
|
||||
.timelapse-control-bar {
|
||||
padding: 10px 15px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tl-btn {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tl-slider-container {
|
||||
width: 100%;
|
||||
order: 10;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#tl-time-display {
|
||||
font-size: 12px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.video-player-controls {
|
||||
flex-direction: column;
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Admin Settings Manager - AJAX ohne Reload
|
||||
*/
|
||||
const AdminSettings = {
|
||||
settings: {},
|
||||
|
||||
init: function() {
|
||||
this.loadSettings();
|
||||
this.setupEventListeners();
|
||||
},
|
||||
|
||||
loadSettings: function() {
|
||||
fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'settings_action=get'
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
this.settings = data.settings;
|
||||
this.updateUI();
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Settings load error:', err));
|
||||
},
|
||||
|
||||
updateSetting: function(key, value) {
|
||||
fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: `settings_action=update&key=${encodeURIComponent(key)}&value=${encodeURIComponent(value)}`
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
this.showNotification('✓ Einstellung gespeichert', 'success');
|
||||
// Sofort UI aktualisieren
|
||||
this.applySettingImmediately(key, value);
|
||||
} else {
|
||||
this.showNotification('✗ Fehler beim Speichern', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Settings update error:', err);
|
||||
this.showNotification('✗ Netzwerkfehler', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
applySettingImmediately: function(key, value) {
|
||||
// Sofortige Anwendung ohne Reload
|
||||
switch(key) {
|
||||
case 'viewer_display.enabled':
|
||||
const viewerEl = document.querySelector('.viewer-stat');
|
||||
if (viewerEl) {
|
||||
viewerEl.style.display = value === true || value === 'true' ? 'inline-flex' : 'none';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'viewer_display.min_viewers':
|
||||
// Wird beim nächsten Heartbeat angewendet
|
||||
window.minViewersToShow = parseInt(value);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
updateUI: function() {
|
||||
// Checkbox für Zuschauer-Anzeige
|
||||
const viewerEnabled = document.getElementById('setting-viewer-enabled');
|
||||
if (viewerEnabled) {
|
||||
viewerEnabled.checked = this.settings.viewer_display?.enabled ?? true;
|
||||
}
|
||||
|
||||
// Mindestanzahl
|
||||
const minViewers = document.getElementById('setting-min-viewers');
|
||||
if (minViewers) {
|
||||
minViewers.value = this.settings.viewer_display?.min_viewers ?? 1;
|
||||
}
|
||||
|
||||
// Video-Modus
|
||||
const playInPlayer = document.getElementById('setting-play-in-player');
|
||||
if (playInPlayer) {
|
||||
playInPlayer.checked = this.settings.video_mode?.play_in_player ?? true;
|
||||
}
|
||||
|
||||
const allowDownload = document.getElementById('setting-allow-download');
|
||||
if (allowDownload) {
|
||||
allowDownload.checked = this.settings.video_mode?.allow_download ?? true;
|
||||
}
|
||||
},
|
||||
|
||||
setupEventListeners: function() {
|
||||
// Zuschauer-Anzeige Toggle
|
||||
document.getElementById('setting-viewer-enabled')?.addEventListener('change', (e) => {
|
||||
this.updateSetting('viewer_display.enabled', e.target.checked);
|
||||
});
|
||||
|
||||
// Mindestanzahl Zuschauer
|
||||
document.getElementById('setting-min-viewers')?.addEventListener('change', (e) => {
|
||||
this.updateSetting('viewer_display.min_viewers', e.target.value);
|
||||
});
|
||||
|
||||
// Video im Player abspielen
|
||||
document.getElementById('setting-play-in-player')?.addEventListener('change', (e) => {
|
||||
this.updateSetting('video_mode.play_in_player', e.target.checked);
|
||||
});
|
||||
|
||||
// Download erlauben
|
||||
document.getElementById('setting-allow-download')?.addEventListener('change', (e) => {
|
||||
this.updateSetting('video_mode.allow_download', e.target.checked);
|
||||
});
|
||||
},
|
||||
|
||||
showNotification: function(message, type) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `admin-notification ${type}`;
|
||||
notification.textContent = message;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 25px;
|
||||
border-radius: 8px;
|
||||
background: ${type === 'success' ? '#4CAF50' : '#f44336'};
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
z-index: 10000;
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
document.body.appendChild(notification);
|
||||
setTimeout(() => notification.remove(), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialisierung nur im Admin-Bereich
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (document.getElementById('admin-settings-panel')) {
|
||||
AdminSettings.init();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Timelapse Controller mit Slider, Geschwindigkeit und Rückwärts
|
||||
*/
|
||||
const TimelapseController = {
|
||||
imageFiles: [],
|
||||
currentIndex: 0,
|
||||
isPlaying: false,
|
||||
isReverse: false,
|
||||
speed: 1,
|
||||
availableSpeeds: [1, 10, 100],
|
||||
intervalId: null,
|
||||
baseInterval: 200, // ms bei 1x
|
||||
|
||||
init: function(imageFilesArray) {
|
||||
this.imageFiles = imageFilesArray;
|
||||
this.setupControls();
|
||||
this.updateSlider();
|
||||
},
|
||||
|
||||
setupControls: function() {
|
||||
const container = document.getElementById('timelapse-controls');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="timelapse-control-bar">
|
||||
<button id="tl-play-pause" class="tl-btn" title="Play/Pause">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
<button id="tl-reverse" class="tl-btn" title="Rückwärts">
|
||||
<i class="fas fa-backward"></i>
|
||||
</button>
|
||||
<div class="tl-slider-container">
|
||||
<input type="range" id="tl-slider" min="0" max="100" value="0">
|
||||
<span id="tl-time-display">00:00:00</span>
|
||||
</div>
|
||||
<div class="tl-speed-container">
|
||||
<button id="tl-speed" class="tl-btn tl-speed-btn">1x</button>
|
||||
</div>
|
||||
<button id="tl-back-live" class="tl-btn tl-back-btn" title="Zurück zu Live">
|
||||
<i class="fas fa-video"></i> Live
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Event Listeners
|
||||
document.getElementById('tl-play-pause').onclick = () => this.togglePlay();
|
||||
document.getElementById('tl-reverse').onclick = () => this.toggleReverse();
|
||||
document.getElementById('tl-speed').onclick = () => this.cycleSpeed();
|
||||
document.getElementById('tl-back-live').onclick = () => this.backToLive();
|
||||
|
||||
const slider = document.getElementById('tl-slider');
|
||||
slider.max = this.imageFiles.length - 1;
|
||||
slider.oninput = (e) => this.seekTo(parseInt(e.target.value));
|
||||
},
|
||||
|
||||
togglePlay: function() {
|
||||
this.isPlaying = !this.isPlaying;
|
||||
const btn = document.getElementById('tl-play-pause');
|
||||
btn.innerHTML = this.isPlaying ? '<i class="fas fa-pause"></i>' : '<i class="fas fa-play"></i>';
|
||||
|
||||
if (this.isPlaying) {
|
||||
this.startPlayback();
|
||||
} else {
|
||||
this.stopPlayback();
|
||||
}
|
||||
},
|
||||
|
||||
toggleReverse: function() {
|
||||
this.isReverse = !this.isReverse;
|
||||
const btn = document.getElementById('tl-reverse');
|
||||
btn.classList.toggle('active', this.isReverse);
|
||||
btn.innerHTML = this.isReverse ?
|
||||
'<i class="fas fa-forward"></i>' :
|
||||
'<i class="fas fa-backward"></i>';
|
||||
},
|
||||
|
||||
cycleSpeed: function() {
|
||||
const idx = this.availableSpeeds.indexOf(this.speed);
|
||||
this.speed = this.availableSpeeds[(idx + 1) % this.availableSpeeds.length];
|
||||
document.getElementById('tl-speed').textContent = this.speed + 'x';
|
||||
|
||||
if (this.isPlaying) {
|
||||
this.stopPlayback();
|
||||
this.startPlayback();
|
||||
}
|
||||
},
|
||||
|
||||
startPlayback: function() {
|
||||
const interval = this.baseInterval / this.speed;
|
||||
this.intervalId = setInterval(() => this.nextFrame(), interval);
|
||||
},
|
||||
|
||||
stopPlayback: function() {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
},
|
||||
|
||||
nextFrame: function() {
|
||||
if (this.isReverse) {
|
||||
this.currentIndex--;
|
||||
if (this.currentIndex < 0) this.currentIndex = this.imageFiles.length - 1;
|
||||
} else {
|
||||
this.currentIndex++;
|
||||
if (this.currentIndex >= this.imageFiles.length) this.currentIndex = 0;
|
||||
}
|
||||
this.showFrame(this.currentIndex);
|
||||
},
|
||||
|
||||
seekTo: function(index) {
|
||||
this.currentIndex = index;
|
||||
this.showFrame(index);
|
||||
},
|
||||
|
||||
showFrame: function(index) {
|
||||
const img = document.getElementById('timelapse-image');
|
||||
if (img && this.imageFiles[index]) {
|
||||
img.src = this.imageFiles[index];
|
||||
}
|
||||
this.updateSlider();
|
||||
this.updateTimeDisplay();
|
||||
},
|
||||
|
||||
updateSlider: function() {
|
||||
const slider = document.getElementById('tl-slider');
|
||||
if (slider) slider.value = this.currentIndex;
|
||||
},
|
||||
|
||||
updateTimeDisplay: function() {
|
||||
const display = document.getElementById('tl-time-display');
|
||||
if (!display || !this.imageFiles[this.currentIndex]) return;
|
||||
|
||||
const filename = this.imageFiles[this.currentIndex];
|
||||
const match = filename.match(/(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})/);
|
||||
if (match) {
|
||||
const [_, y, m, d, h, min, s] = match;
|
||||
display.textContent = `${d}.${m}.${y} ${h}:${min}:${s}`;
|
||||
}
|
||||
},
|
||||
|
||||
backToLive: function() {
|
||||
this.stopPlayback();
|
||||
this.isPlaying = false;
|
||||
|
||||
// Live-Video wieder anzeigen
|
||||
document.getElementById('timelapse-viewer').style.display = 'none';
|
||||
document.getElementById('webcam-player').style.display = 'block';
|
||||
document.getElementById('timelapse-button').textContent = 'Wochenzeitraffer';
|
||||
|
||||
// Controls verstecken
|
||||
const controls = document.getElementById('timelapse-controls');
|
||||
if (controls) controls.style.display = 'none';
|
||||
},
|
||||
|
||||
show: function() {
|
||||
document.getElementById('timelapse-viewer').style.display = 'block';
|
||||
document.getElementById('webcam-player').style.display = 'none';
|
||||
document.getElementById('daily-video-player').style.display = 'none';
|
||||
|
||||
const controls = document.getElementById('timelapse-controls');
|
||||
if (controls) controls.style.display = 'block';
|
||||
|
||||
this.currentIndex = 0;
|
||||
this.showFrame(0);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Daily Video Player - Spielt Tagesvideos im Hauptfenster ab
|
||||
*/
|
||||
const DailyVideoPlayer = {
|
||||
currentVideo: null,
|
||||
videoElement: null,
|
||||
|
||||
init: function() {
|
||||
this.createPlayerElement();
|
||||
this.setupEventListeners();
|
||||
},
|
||||
|
||||
createPlayerElement: function() {
|
||||
// Player-Container erstellen falls nicht vorhanden
|
||||
if (document.getElementById('daily-video-player')) return;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.id = 'daily-video-player';
|
||||
container.style.display = 'none';
|
||||
container.innerHTML = `
|
||||
<video id="daily-video" controls playsinline>
|
||||
<source src="" type="video/mp4">
|
||||
</video>
|
||||
<div class="video-player-controls">
|
||||
<button id="dvp-back-live" class="tl-btn tl-back-btn">
|
||||
<i class="fas fa-video"></i> Zurück zu Live
|
||||
</button>
|
||||
<a id="dvp-download" class="button" style="display:none;">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Nach dem Webcam-Player einfügen
|
||||
const videoContainer = document.querySelector('.video-container');
|
||||
if (videoContainer) {
|
||||
videoContainer.appendChild(container);
|
||||
}
|
||||
|
||||
this.videoElement = document.getElementById('daily-video');
|
||||
},
|
||||
|
||||
setupEventListeners: function() {
|
||||
document.getElementById('dvp-back-live')?.addEventListener('click', () => this.backToLive());
|
||||
|
||||
// Video-Ende Event
|
||||
this.videoElement?.addEventListener('ended', () => {
|
||||
// Optional: Automatisch zurück zu Live
|
||||
});
|
||||
},
|
||||
|
||||
playVideo: function(videoPath, allowDownload = true) {
|
||||
this.currentVideo = videoPath;
|
||||
|
||||
// Andere Player verstecken
|
||||
document.getElementById('webcam-player').style.display = 'none';
|
||||
document.getElementById('timelapse-viewer').style.display = 'none';
|
||||
document.getElementById('timelapse-controls')?.style.display = 'none';
|
||||
|
||||
// Diesen Player anzeigen
|
||||
const player = document.getElementById('daily-video-player');
|
||||
player.style.display = 'block';
|
||||
|
||||
// Video laden
|
||||
this.videoElement.src = videoPath;
|
||||
this.videoElement.load();
|
||||
this.videoElement.play();
|
||||
|
||||
// Download-Button
|
||||
const downloadBtn = document.getElementById('dvp-download');
|
||||
if (allowDownload && downloadBtn) {
|
||||
downloadBtn.style.display = 'inline-block';
|
||||
downloadBtn.href = videoPath;
|
||||
downloadBtn.download = videoPath.split('/').pop();
|
||||
} else if (downloadBtn) {
|
||||
downloadBtn.style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
backToLive: function() {
|
||||
// Video stoppen
|
||||
if (this.videoElement) {
|
||||
this.videoElement.pause();
|
||||
this.videoElement.src = '';
|
||||
}
|
||||
|
||||
// Player verstecken
|
||||
document.getElementById('daily-video-player').style.display = 'none';
|
||||
|
||||
// Live-Stream anzeigen
|
||||
document.getElementById('webcam-player').style.display = 'block';
|
||||
},
|
||||
|
||||
// Wird vom Kalender aufgerufen
|
||||
handleCalendarClick: function(videoPath, playInPlayer, allowDownload) {
|
||||
if (playInPlayer) {
|
||||
this.playVideo(videoPath, allowDownload);
|
||||
} else {
|
||||
// Nur Download
|
||||
window.location.href = videoPath;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialisierung
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
DailyVideoPlayer.init();
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"viewer_display": {
|
||||
"enabled": true,
|
||||
"min_viewers": 1
|
||||
},
|
||||
"video_mode": {
|
||||
"play_in_player": true,
|
||||
"allow_download": true
|
||||
},
|
||||
"timelapse": {
|
||||
"default_speed": 1,
|
||||
"available_speeds": [1, 10, 100]
|
||||
},
|
||||
"last_updated": null,
|
||||
"updated_by": null
|
||||
}
|
||||
Reference in New Issue
Block a user