Add 4 new features: timelapse toggle, auto-screenshot, video search, email sharing
- Weekly timelapse button now toggleable via settings (zoom_timelapse.weekly_timelapse_enabled) - Auto-screenshot API for cron-based gallery capture every 10 min - Date/time video search with filter UI in archive section - Email sharing with share links and PHPMailer integration - New API endpoints: auto-screenshot.php, gallery.php, video-search.php, share.php - New settings: auto_screenshot.*, sharing.* for feature configuration
This commit is contained in:
@@ -48,7 +48,20 @@ class SettingsManager {
|
|||||||
'zoom_timelapse' => [
|
'zoom_timelapse' => [
|
||||||
'show_zoom_controls' => true,
|
'show_zoom_controls' => true,
|
||||||
'max_zoom_level' => 4.0,
|
'max_zoom_level' => 4.0,
|
||||||
'timelapse_reverse_enabled' => true
|
'timelapse_reverse_enabled' => true,
|
||||||
|
'weekly_timelapse_enabled' => true // Wochenzeitraffer Button
|
||||||
|
],
|
||||||
|
// Auto-Screenshot für Galerie
|
||||||
|
'auto_screenshot' => [
|
||||||
|
'enabled' => false,
|
||||||
|
'interval_minutes' => 10,
|
||||||
|
'max_images' => 144, // 24h bei 10min Intervall
|
||||||
|
'save_to_gallery' => true
|
||||||
|
],
|
||||||
|
// Email-Sharing
|
||||||
|
'sharing' => [
|
||||||
|
'email_enabled' => false,
|
||||||
|
'share_link_expiry_hours' => 24
|
||||||
],
|
],
|
||||||
// Punkt 5: Content Management
|
// Punkt 5: Content Management
|
||||||
'content' => [
|
'content' => [
|
||||||
@@ -268,6 +281,32 @@ class SettingsManager {
|
|||||||
return $this->get('zoom_timelapse.timelapse_reverse_enabled') === true;
|
return $this->get('zoom_timelapse.timelapse_reverse_enabled') === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isWeeklyTimelapseEnabled() {
|
||||||
|
return $this->get('zoom_timelapse.weekly_timelapse_enabled') !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-Screenshot Helper
|
||||||
|
public function isAutoScreenshotEnabled() {
|
||||||
|
return $this->get('auto_screenshot.enabled') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAutoScreenshotInterval() {
|
||||||
|
return $this->get('auto_screenshot.interval_minutes') ?? 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAutoScreenshotMaxImages() {
|
||||||
|
return $this->get('auto_screenshot.max_images') ?? 144;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sharing Helper
|
||||||
|
public function isEmailSharingEnabled() {
|
||||||
|
return $this->get('sharing.email_enabled') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShareLinkExpiryHours() {
|
||||||
|
return $this->get('sharing.share_link_expiry_hours') ?? 24;
|
||||||
|
}
|
||||||
|
|
||||||
// SEO Helper
|
// SEO Helper
|
||||||
public function getCustomTitle() {
|
public function getCustomTitle() {
|
||||||
$title = $this->get('seo.custom_title');
|
$title = $this->get('seo.custom_title');
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Auto-Screenshot API
|
||||||
|
*
|
||||||
|
* Kann als Cron-Job aufgerufen werden:
|
||||||
|
* */10 * * * * curl -s http://localhost/api/auto-screenshot.php?key=YOUR_SECRET_KEY
|
||||||
|
*
|
||||||
|
* Oder via Webhook/Timer
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
require_once dirname(__DIR__) . '/SettingsManager.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$settingsManager = new SettingsManager();
|
||||||
|
|
||||||
|
// Prüfe ob Feature aktiviert
|
||||||
|
if (!$settingsManager->isAutoScreenshotEnabled()) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Auto-Screenshot deaktiviert']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionale API-Key Validierung
|
||||||
|
$configFile = dirname(__DIR__) . '/config.php';
|
||||||
|
if (file_exists($configFile)) {
|
||||||
|
$config = require $configFile;
|
||||||
|
$apiKey = $config['auto_screenshot_key'] ?? '';
|
||||||
|
|
||||||
|
if (!empty($apiKey) && ($_GET['key'] ?? '') !== $apiKey) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Ungültiger API-Key']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Galerie-Verzeichnis erstellen
|
||||||
|
$galleryDir = dirname(__DIR__) . '/gallery/auto/';
|
||||||
|
if (!is_dir($galleryDir)) {
|
||||||
|
mkdir($galleryDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screenshot-Dateiname
|
||||||
|
$filename = 'auto_' . date('Y-m-d_H-i-s') . '.jpg';
|
||||||
|
$filepath = $galleryDir . $filename;
|
||||||
|
|
||||||
|
// Video-Stream URL
|
||||||
|
$streamUrl = 'test_video.m3u8';
|
||||||
|
$logoPath = dirname(__DIR__) . '/logo.png';
|
||||||
|
|
||||||
|
// FFmpeg-Befehl zum Erstellen des Screenshots
|
||||||
|
$command = sprintf(
|
||||||
|
'ffmpeg -i %s -vframes 1 -q:v 2 %s 2>&1',
|
||||||
|
escapeshellarg($streamUrl),
|
||||||
|
escapeshellarg($filepath)
|
||||||
|
);
|
||||||
|
|
||||||
|
exec($command, $output, $returnVar);
|
||||||
|
|
||||||
|
if ($returnVar !== 0 || !file_exists($filepath)) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Screenshot fehlgeschlagen',
|
||||||
|
'command' => $command,
|
||||||
|
'output' => implode("\n", $output)
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alte Screenshots aufräumen (max. Anzahl einhalten)
|
||||||
|
$maxImages = $settingsManager->getAutoScreenshotMaxImages();
|
||||||
|
$existingFiles = glob($galleryDir . 'auto_*.jpg');
|
||||||
|
rsort($existingFiles); // Neueste zuerst
|
||||||
|
|
||||||
|
if (count($existingFiles) > $maxImages) {
|
||||||
|
$filesToDelete = array_slice($existingFiles, $maxImages);
|
||||||
|
foreach ($filesToDelete as $file) {
|
||||||
|
@unlink($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadaten speichern
|
||||||
|
$metaFile = $galleryDir . 'metadata.json';
|
||||||
|
$metadata = [];
|
||||||
|
if (file_exists($metaFile)) {
|
||||||
|
$metadata = json_decode(file_get_contents($metaFile), true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadata[$filename] = [
|
||||||
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
|
'timestamp' => time(),
|
||||||
|
'size' => filesize($filepath)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Nur die letzten maxImages behalten
|
||||||
|
$metadata = array_slice($metadata, -$maxImages, null, true);
|
||||||
|
file_put_contents($metaFile, json_encode($metadata, JSON_PRETTY_PRINT));
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'file' => $filename,
|
||||||
|
'path' => '/gallery/auto/' . $filename,
|
||||||
|
'total_images' => count(glob($galleryDir . 'auto_*.jpg'))
|
||||||
|
]);
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Gallery API
|
||||||
|
*
|
||||||
|
* GET /api/gallery.php - Liste alle Galerie-Bilder
|
||||||
|
* GET /api/gallery.php?date=2024-01-30 - Bilder eines bestimmten Datums
|
||||||
|
* GET /api/gallery.php?from=2024-01-01&to=2024-01-31 - Bilder in einem Zeitraum
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
require_once dirname(__DIR__) . '/SettingsManager.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
|
||||||
|
$settingsManager = new SettingsManager();
|
||||||
|
|
||||||
|
$galleryDir = dirname(__DIR__) . '/gallery/auto/';
|
||||||
|
|
||||||
|
// Prüfe ob Galerie existiert
|
||||||
|
if (!is_dir($galleryDir)) {
|
||||||
|
echo json_encode(['success' => true, 'images' => [], 'total' => 0]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameter
|
||||||
|
$date = $_GET['date'] ?? null;
|
||||||
|
$from = $_GET['from'] ?? null;
|
||||||
|
$to = $_GET['to'] ?? null;
|
||||||
|
$limit = min(100, (int)($_GET['limit'] ?? 50));
|
||||||
|
$offset = max(0, (int)($_GET['offset'] ?? 0));
|
||||||
|
|
||||||
|
// Alle Bilder holen
|
||||||
|
$allFiles = glob($galleryDir . 'auto_*.jpg');
|
||||||
|
rsort($allFiles); // Neueste zuerst
|
||||||
|
|
||||||
|
$images = [];
|
||||||
|
|
||||||
|
foreach ($allFiles as $file) {
|
||||||
|
$filename = basename($file);
|
||||||
|
// Extrahiere Datum aus Dateinamen: auto_2024-01-30_14-30-00.jpg
|
||||||
|
if (preg_match('/auto_(\d{4}-\d{2}-\d{2})_(\d{2}-\d{2}-\d{2})\.jpg/', $filename, $matches)) {
|
||||||
|
$fileDate = $matches[1];
|
||||||
|
$fileTime = str_replace('-', ':', $matches[2]);
|
||||||
|
|
||||||
|
// Datumsfilter
|
||||||
|
if ($date !== null && $fileDate !== $date) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($from !== null && $fileDate < $from) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($to !== null && $fileDate > $to) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$images[] = [
|
||||||
|
'filename' => $filename,
|
||||||
|
'path' => '/gallery/auto/' . $filename,
|
||||||
|
'date' => $fileDate,
|
||||||
|
'time' => $fileTime,
|
||||||
|
'datetime' => $fileDate . ' ' . $fileTime,
|
||||||
|
'timestamp' => strtotime($fileDate . ' ' . $fileTime),
|
||||||
|
'size' => filesize($file)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = count($images);
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
$images = array_slice($images, $offset, $limit);
|
||||||
|
|
||||||
|
// Verfügbare Daten (für Kalender/Filter)
|
||||||
|
$availableDates = [];
|
||||||
|
foreach (glob($galleryDir . 'auto_*.jpg') as $file) {
|
||||||
|
if (preg_match('/auto_(\d{4}-\d{2}-\d{2})/', basename($file), $m)) {
|
||||||
|
$availableDates[$m[1]] = ($availableDates[$m[1]] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
krsort($availableDates);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'images' => $images,
|
||||||
|
'total' => $total,
|
||||||
|
'offset' => $offset,
|
||||||
|
'limit' => $limit,
|
||||||
|
'available_dates' => $availableDates,
|
||||||
|
'filters' => [
|
||||||
|
'date' => $date,
|
||||||
|
'from' => $from,
|
||||||
|
'to' => $to
|
||||||
|
]
|
||||||
|
]);
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Share API - Teilen von Bildern/Videos per E-Mail
|
||||||
|
*
|
||||||
|
* POST /api/share.php
|
||||||
|
* Body: { email: "friend@example.com", type: "video|image", path: "/videos/...", message: "Schau dir das an!" }
|
||||||
|
*/
|
||||||
|
|
||||||
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
|
use PHPMailer\PHPMailer\Exception;
|
||||||
|
|
||||||
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
require_once dirname(__DIR__) . '/SettingsManager.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||||
|
http_response_code(200);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$settingsManager = new SettingsManager();
|
||||||
|
|
||||||
|
// Prüfe ob Feature aktiviert
|
||||||
|
if (!$settingsManager->isEmailSharingEnabled()) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'E-Mail-Sharing ist deaktiviert']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config laden
|
||||||
|
$configFile = dirname(__DIR__) . '/config.php';
|
||||||
|
$config = file_exists($configFile) ? require $configFile : [];
|
||||||
|
$mailConfig = $config['mail'] ?? [];
|
||||||
|
|
||||||
|
if (empty($mailConfig['host']) || empty($mailConfig['username'])) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'E-Mail-Server nicht konfiguriert']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === GET: Share-Link generieren ===
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['generate'])) {
|
||||||
|
$path = $_GET['path'] ?? '';
|
||||||
|
$type = $_GET['type'] ?? 'video';
|
||||||
|
|
||||||
|
if (empty($path)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Kein Pfad angegeben']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token generieren
|
||||||
|
$expiryHours = $settingsManager->getShareLinkExpiryHours();
|
||||||
|
$expiry = time() + ($expiryHours * 3600);
|
||||||
|
$token = hash_hmac('sha256', $path . $expiry, session_id() . 'share_secret');
|
||||||
|
|
||||||
|
// Share-Link speichern
|
||||||
|
$shareDir = dirname(__DIR__) . '/data/shares/';
|
||||||
|
if (!is_dir($shareDir)) {
|
||||||
|
mkdir($shareDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$shareId = bin2hex(random_bytes(16));
|
||||||
|
$shareData = [
|
||||||
|
'id' => $shareId,
|
||||||
|
'path' => $path,
|
||||||
|
'type' => $type,
|
||||||
|
'token' => $token,
|
||||||
|
'expiry' => $expiry,
|
||||||
|
'created_at' => date('Y-m-d H:i:s')
|
||||||
|
];
|
||||||
|
|
||||||
|
file_put_contents($shareDir . $shareId . '.json', json_encode($shareData));
|
||||||
|
|
||||||
|
// URL generieren
|
||||||
|
$baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http')
|
||||||
|
. '://' . $_SERVER['HTTP_HOST'];
|
||||||
|
$shareUrl = $baseUrl . '/api/share.php?view=' . $shareId;
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'share_url' => $shareUrl,
|
||||||
|
'share_id' => $shareId,
|
||||||
|
'expires_at' => date('Y-m-d H:i:s', $expiry)
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === GET: Share-Link anzeigen ===
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['view'])) {
|
||||||
|
$shareId = preg_replace('/[^a-f0-9]/', '', $_GET['view']);
|
||||||
|
$shareFile = dirname(__DIR__) . '/data/shares/' . $shareId . '.json';
|
||||||
|
|
||||||
|
if (!file_exists($shareFile)) {
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
echo '<!DOCTYPE html><html><head><title>Link ungültig</title></head><body style="font-family:sans-serif;text-align:center;padding:50px;"><h1>❌ Link nicht gefunden</h1><p>Dieser Share-Link existiert nicht oder wurde bereits gelöscht.</p></body></html>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shareData = json_decode(file_get_contents($shareFile), true);
|
||||||
|
|
||||||
|
// Ablauf prüfen
|
||||||
|
if (time() > $shareData['expiry']) {
|
||||||
|
@unlink($shareFile);
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
echo '<!DOCTYPE html><html><head><title>Link abgelaufen</title></head><body style="font-family:sans-serif;text-align:center;padding:50px;"><h1>⏰ Link abgelaufen</h1><p>Dieser Share-Link ist abgelaufen. Bitte fordere einen neuen Link an.</p></body></html>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datei existiert?
|
||||||
|
$filePath = dirname(__DIR__) . $shareData['path'];
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
echo '<!DOCTYPE html><html><head><title>Datei nicht gefunden</title></head><body style="font-family:sans-serif;text-align:center;padding:50px;"><h1>📭 Datei nicht gefunden</h1><p>Die geteilte Datei existiert nicht mehr.</p></body></html>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect zur Datei oder HTML-Seite mit eingebettetem Player
|
||||||
|
$isVideo = in_array(pathinfo($filePath, PATHINFO_EXTENSION), ['mp4', 'webm', 'mov']);
|
||||||
|
$isImage = in_array(pathinfo($filePath, PATHINFO_EXTENSION), ['jpg', 'jpeg', 'png', 'gif', 'webp']);
|
||||||
|
|
||||||
|
$siteName = $config['app']['name'] ?? 'Aurora Livecam';
|
||||||
|
$baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http')
|
||||||
|
. '://' . $_SERVER['HTTP_HOST'];
|
||||||
|
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
echo '<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Geteilte ' . ($isVideo ? 'Video' : 'Bild') . ' - ' . htmlspecialchars($siteName) . '</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 30px;
|
||||||
|
max-width: 900px;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
h1 { font-size: 1.5rem; margin-bottom: 20px; color: #333; }
|
||||||
|
video, img {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 70vh;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
.download-btn {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 12px 30px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.download-btn:hover { opacity: 0.9; }
|
||||||
|
.footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.footer a { color: white; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>📤 Geteilte' . ($isVideo ? 's Video' : 's Bild') . '</h1>';
|
||||||
|
|
||||||
|
if ($isVideo) {
|
||||||
|
echo '<video controls autoplay><source src="' . htmlspecialchars($shareData['path']) . '" type="video/mp4">Ihr Browser unterstützt kein Video.</video>';
|
||||||
|
} else {
|
||||||
|
echo '<img src="' . htmlspecialchars($shareData['path']) . '" alt="Geteiltes Bild">';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<a href="' . htmlspecialchars($shareData['path']) . '" download class="download-btn">⬇️ Herunterladen</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
Geteilt von <a href="' . $baseUrl . '">' . htmlspecialchars($siteName) . '</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === POST: E-Mail senden ===
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Nur POST erlaubt']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON-Body parsen
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if (!$input) {
|
||||||
|
$input = $_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = filter_var($input['email'] ?? '', FILTER_VALIDATE_EMAIL);
|
||||||
|
$path = $input['path'] ?? '';
|
||||||
|
$type = $input['type'] ?? 'video';
|
||||||
|
$message = htmlspecialchars($input['message'] ?? '');
|
||||||
|
$senderName = htmlspecialchars($input['sender_name'] ?? 'Ein Freund');
|
||||||
|
|
||||||
|
if (!$email) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Ungültige E-Mail-Adresse']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($path)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Kein Pfad angegeben']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share-Link generieren
|
||||||
|
$expiryHours = $settingsManager->getShareLinkExpiryHours();
|
||||||
|
$expiry = time() + ($expiryHours * 3600);
|
||||||
|
|
||||||
|
$shareDir = dirname(__DIR__) . '/data/shares/';
|
||||||
|
if (!is_dir($shareDir)) {
|
||||||
|
mkdir($shareDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$shareId = bin2hex(random_bytes(16));
|
||||||
|
$shareData = [
|
||||||
|
'id' => $shareId,
|
||||||
|
'path' => $path,
|
||||||
|
'type' => $type,
|
||||||
|
'expiry' => $expiry,
|
||||||
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
|
'shared_to' => $email
|
||||||
|
];
|
||||||
|
|
||||||
|
file_put_contents($shareDir . $shareId . '.json', json_encode($shareData));
|
||||||
|
|
||||||
|
$baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http')
|
||||||
|
. '://' . $_SERVER['HTTP_HOST'];
|
||||||
|
$shareUrl = $baseUrl . '/api/share.php?view=' . $shareId;
|
||||||
|
$siteName = $config['app']['name'] ?? 'Aurora Livecam';
|
||||||
|
|
||||||
|
// E-Mail senden
|
||||||
|
try {
|
||||||
|
$mail = new PHPMailer(true);
|
||||||
|
|
||||||
|
// SMTP Konfiguration
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->Host = $mailConfig['host'];
|
||||||
|
$mail->SMTPAuth = true;
|
||||||
|
$mail->Username = $mailConfig['username'];
|
||||||
|
$mail->Password = $mailConfig['password'];
|
||||||
|
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||||
|
$mail->Port = $mailConfig['port'] ?? 587;
|
||||||
|
$mail->CharSet = 'UTF-8';
|
||||||
|
|
||||||
|
// Absender/Empfänger
|
||||||
|
$mail->setFrom($mailConfig['from_address'], $mailConfig['from_name'] ?? $siteName);
|
||||||
|
$mail->addAddress($email);
|
||||||
|
|
||||||
|
// Inhalt
|
||||||
|
$mail->isHTML(true);
|
||||||
|
$mail->Subject = $senderName . ' hat ' . ($type === 'video' ? 'ein Video' : 'ein Bild') . ' mit dir geteilt';
|
||||||
|
|
||||||
|
$mail->Body = '
|
||||||
|
<div style="font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 12px 12px 0 0; text-align: center;">
|
||||||
|
<h1 style="color: white; margin: 0; font-size: 24px;">📤 ' . htmlspecialchars($siteName) . '</h1>
|
||||||
|
</div>
|
||||||
|
<div style="background: #f7f7f7; padding: 30px; border-radius: 0 0 12px 12px;">
|
||||||
|
<p style="font-size: 18px; color: #333; margin-bottom: 20px;">
|
||||||
|
<strong>' . htmlspecialchars($senderName) . '</strong> hat ' . ($type === 'video' ? 'ein Video' : 'ein Bild') . ' mit dir geteilt!
|
||||||
|
</p>
|
||||||
|
' . (!empty($message) ? '<div style="background: white; padding: 15px; border-radius: 8px; border-left: 4px solid #667eea; margin-bottom: 20px;"><em>"' . nl2br($message) . '"</em></div>' : '') . '
|
||||||
|
<a href="' . htmlspecialchars($shareUrl) . '" style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 30px; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 16px;">
|
||||||
|
▶️ Jetzt ansehen
|
||||||
|
</a>
|
||||||
|
<p style="margin-top: 20px; color: #888; font-size: 12px;">
|
||||||
|
Dieser Link ist ' . $expiryHours . ' Stunden gültig.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>';
|
||||||
|
|
||||||
|
$mail->AltBody = $senderName . ' hat ' . ($type === 'video' ? 'ein Video' : 'ein Bild') . ' mit dir geteilt: ' . $shareUrl;
|
||||||
|
|
||||||
|
$mail->send();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'E-Mail wurde gesendet',
|
||||||
|
'share_url' => $shareUrl
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Share email error: ' . $e->getMessage());
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'E-Mail konnte nicht gesendet werden',
|
||||||
|
'share_url' => $shareUrl // URL trotzdem zurückgeben
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Video Search API
|
||||||
|
*
|
||||||
|
* Suche nach Videos nach Datum und Uhrzeit
|
||||||
|
*
|
||||||
|
* GET /api/video-search.php?date=2024-01-30
|
||||||
|
* GET /api/video-search.php?date=2024-01-30&time=14:30
|
||||||
|
* GET /api/video-search.php?from=2024-01-01&to=2024-01-31
|
||||||
|
* GET /api/video-search.php?time_from=08:00&time_to=18:00
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
require_once dirname(__DIR__) . '/SettingsManager.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
|
||||||
|
$settingsManager = new SettingsManager();
|
||||||
|
|
||||||
|
$videoDir = dirname(__DIR__) . '/videos/';
|
||||||
|
$aiDir = dirname(__DIR__) . '/ai/';
|
||||||
|
|
||||||
|
// Parameter
|
||||||
|
$date = $_GET['date'] ?? null; // Format: YYYY-MM-DD
|
||||||
|
$time = $_GET['time'] ?? null; // Format: HH:MM
|
||||||
|
$fromDate = $_GET['from'] ?? null;
|
||||||
|
$toDate = $_GET['to'] ?? null;
|
||||||
|
$timeFrom = $_GET['time_from'] ?? null;
|
||||||
|
$timeTo = $_GET['time_to'] ?? null;
|
||||||
|
$type = $_GET['type'] ?? 'all'; // all, daily, ai
|
||||||
|
$aiCategory = $_GET['ai_category'] ?? null;
|
||||||
|
$limit = min(100, (int)($_GET['limit'] ?? 50));
|
||||||
|
|
||||||
|
$results = [
|
||||||
|
'daily_videos' => [],
|
||||||
|
'ai_videos' => [],
|
||||||
|
'gallery_images' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
// AI-Kategorien
|
||||||
|
$aiCategories = ['sunny', 'rainy', 'snowy', 'planes', 'birds', 'sunset', 'sunrise', 'rainbow'];
|
||||||
|
|
||||||
|
// === TAGESVIDEOS SUCHEN ===
|
||||||
|
if ($type === 'all' || $type === 'daily') {
|
||||||
|
$pattern = $videoDir . 'daily_video_*.mp4';
|
||||||
|
$dailyVideos = glob($pattern);
|
||||||
|
|
||||||
|
foreach ($dailyVideos as $video) {
|
||||||
|
$filename = basename($video);
|
||||||
|
|
||||||
|
// Extrahiere Datum aus Dateinamen: daily_video_YYYYMMDD_HHMMSS.mp4
|
||||||
|
if (preg_match('/daily_video_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})\.mp4/', $filename, $matches)) {
|
||||||
|
$videoDate = $matches[1] . '-' . $matches[2] . '-' . $matches[3];
|
||||||
|
$videoTime = $matches[4] . ':' . $matches[5];
|
||||||
|
$videoDateTime = $videoDate . ' ' . $videoTime . ':' . $matches[6];
|
||||||
|
|
||||||
|
// Datumsfilter
|
||||||
|
if ($date !== null && $videoDate !== $date) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fromDate !== null && $videoDate < $fromDate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($toDate !== null && $videoDate > $toDate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uhrzeitfilter
|
||||||
|
if ($timeFrom !== null && $videoTime < $timeFrom) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($timeTo !== null && $videoTime > $timeTo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spezifische Uhrzeit (mit 30 Min Toleranz)
|
||||||
|
if ($time !== null) {
|
||||||
|
$searchMinutes = intval(substr($time, 0, 2)) * 60 + intval(substr($time, 3, 2));
|
||||||
|
$videoMinutes = intval($matches[4]) * 60 + intval($matches[5]);
|
||||||
|
|
||||||
|
if (abs($searchMinutes - $videoMinutes) > 30) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$results['daily_videos'][] = [
|
||||||
|
'type' => 'daily',
|
||||||
|
'filename' => $filename,
|
||||||
|
'path' => '/videos/' . $filename,
|
||||||
|
'date' => $videoDate,
|
||||||
|
'time' => $videoTime,
|
||||||
|
'datetime' => $videoDateTime,
|
||||||
|
'timestamp' => strtotime($videoDateTime),
|
||||||
|
'size' => filesize($video),
|
||||||
|
'size_mb' => round(filesize($video) / (1024 * 1024), 2)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === AI-VIDEOS SUCHEN ===
|
||||||
|
if ($type === 'all' || $type === 'ai') {
|
||||||
|
$searchCategories = $aiCategory ? [$aiCategory] : $aiCategories;
|
||||||
|
|
||||||
|
foreach ($searchCategories as $category) {
|
||||||
|
$categoryDir = $aiDir . $category . '/';
|
||||||
|
if (!is_dir($categoryDir)) continue;
|
||||||
|
|
||||||
|
$pattern = $categoryDir . $category . '_*.mp4';
|
||||||
|
$aiVideos = glob($pattern);
|
||||||
|
|
||||||
|
foreach ($aiVideos as $video) {
|
||||||
|
$filename = basename($video);
|
||||||
|
|
||||||
|
// Extrahiere Datum aus Dateinamen: category_YYYYMMDD_HHMMSS.mp4
|
||||||
|
if (preg_match('/' . $category . '_(\d{4})(\d{2})(\d{2})_?(\d{2})?(\d{2})?(\d{2})?\.mp4/', $filename, $matches)) {
|
||||||
|
$videoDate = $matches[1] . '-' . $matches[2] . '-' . $matches[3];
|
||||||
|
$videoTime = isset($matches[4]) ? ($matches[4] . ':' . ($matches[5] ?? '00')) : '00:00';
|
||||||
|
$videoDateTime = $videoDate . ' ' . $videoTime;
|
||||||
|
|
||||||
|
// Datumsfilter
|
||||||
|
if ($date !== null && $videoDate !== $date) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fromDate !== null && $videoDate < $fromDate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($toDate !== null && $videoDate > $toDate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uhrzeitfilter
|
||||||
|
if ($timeFrom !== null && $videoTime < $timeFrom) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($timeTo !== null && $videoTime > $timeTo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$results['ai_videos'][] = [
|
||||||
|
'type' => 'ai',
|
||||||
|
'category' => $category,
|
||||||
|
'filename' => $filename,
|
||||||
|
'path' => '/ai/' . $category . '/' . $filename,
|
||||||
|
'date' => $videoDate,
|
||||||
|
'time' => $videoTime,
|
||||||
|
'datetime' => $videoDateTime,
|
||||||
|
'timestamp' => strtotime($videoDateTime),
|
||||||
|
'size' => filesize($video),
|
||||||
|
'size_mb' => round(filesize($video) / (1024 * 1024), 2)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sortieren nach Datum/Zeit (neueste zuerst)
|
||||||
|
usort($results['daily_videos'], fn($a, $b) => $b['timestamp'] - $a['timestamp']);
|
||||||
|
usort($results['ai_videos'], fn($a, $b) => $b['timestamp'] - $a['timestamp']);
|
||||||
|
|
||||||
|
// Limit anwenden
|
||||||
|
$results['daily_videos'] = array_slice($results['daily_videos'], 0, $limit);
|
||||||
|
$results['ai_videos'] = array_slice($results['ai_videos'], 0, $limit);
|
||||||
|
|
||||||
|
// Statistiken
|
||||||
|
$results['stats'] = [
|
||||||
|
'total_daily' => count($results['daily_videos']),
|
||||||
|
'total_ai' => count($results['ai_videos']),
|
||||||
|
'total' => count($results['daily_videos']) + count($results['ai_videos'])
|
||||||
|
];
|
||||||
|
|
||||||
|
$results['filters'] = [
|
||||||
|
'date' => $date,
|
||||||
|
'time' => $time,
|
||||||
|
'from' => $fromDate,
|
||||||
|
'to' => $toDate,
|
||||||
|
'time_from' => $timeFrom,
|
||||||
|
'time_to' => $timeTo,
|
||||||
|
'type' => $type,
|
||||||
|
'ai_category' => $aiCategory
|
||||||
|
];
|
||||||
|
|
||||||
|
$results['success'] = true;
|
||||||
|
|
||||||
|
echo json_encode($results, JSON_PRETTY_PRINT);
|
||||||
@@ -2796,9 +2796,11 @@ body.theme-neo footer {
|
|||||||
<a href="?action=snapshot" class="button" data-en="Save Snapshot" data-de="Snapshot speichern" data-it="Salva istantanea" data-fr="Enregistrer l'instantané" data-zh="保存截图">
|
<a href="?action=snapshot" class="button" data-en="Save Snapshot" data-de="Snapshot speichern" data-it="Salva istantanea" data-fr="Enregistrer l'instantané" data-zh="保存截图">
|
||||||
Snapshot speichern
|
Snapshot speichern
|
||||||
</a>
|
</a>
|
||||||
|
<?php if ($settingsManager->isWeeklyTimelapseEnabled()): ?>
|
||||||
<a href="#" class="button" id="timelapse-button" data-en="Week Timelapse" data-de="Wochenzeitraffer" data-it="Timelapse settimanale" data-fr="Timelapse hebdomadaire" data-zh="一周延时">
|
<a href="#" class="button" id="timelapse-button" data-en="Week Timelapse" data-de="Wochenzeitraffer" data-it="Timelapse settimanale" data-fr="Timelapse hebdomadaire" data-zh="一周延时">
|
||||||
Wochenzeitraffer
|
Wochenzeitraffer
|
||||||
</a>
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
<a href="?action=sequence" class="button" data-en="Save Video Clip" data-de="Videoclip speichern" data-it="Salva clip video" data-fr="Enregistrer le clip vidéo" data-zh="保存视频片段">
|
<a href="?action=sequence" class="button" data-en="Save Video Clip" data-de="Videoclip speichern" data-it="Salva clip video" data-fr="Enregistrer le clip vidéo" data-zh="保存视频片段">
|
||||||
Videoclip speichern
|
Videoclip speichern
|
||||||
</a>
|
</a>
|
||||||
@@ -2813,6 +2815,40 @@ body.theme-neo footer {
|
|||||||
<section id="archive" class="section">
|
<section id="archive" class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 data-en="Video Archive" data-de="Videoarchiv Tagesvideos" data-it="Archivio video giornalieri" data-fr="Archive des vidéos quotidiennes" data-zh="每日视频档案">Videoarchiv Tagesvideos</h2>
|
<h2 data-en="Video Archive" data-de="Videoarchiv Tagesvideos" data-it="Archivio video giornalieri" data-fr="Archive des vidéos quotidiennes" data-zh="每日视频档案">Videoarchiv Tagesvideos</h2>
|
||||||
|
|
||||||
|
<!-- Datum/Zeit Suche -->
|
||||||
|
<div class="video-search-container" style="background: rgba(255,255,255,0.95); padding: 20px; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
|
||||||
|
<h4 style="margin: 0 0 15px 0; color: #667eea;" data-en="Search by Date/Time" data-de="Suche nach Datum/Uhrzeit" data-it="Cerca per data/ora" data-fr="Rechercher par date/heure" data-zh="按日期/时间搜索">
|
||||||
|
🔍 Suche nach Datum/Uhrzeit
|
||||||
|
</h4>
|
||||||
|
<form id="video-search-form" style="display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;">
|
||||||
|
<div style="flex: 1; min-width: 150px;">
|
||||||
|
<label style="display: block; font-size: 0.85rem; color: #666; margin-bottom: 5px;" data-en="Date" data-de="Datum" data-it="Data" data-fr="Date" data-zh="日期">Datum</label>
|
||||||
|
<input type="date" id="search-date" name="date" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 1rem;">
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; min-width: 120px;">
|
||||||
|
<label style="display: block; font-size: 0.85rem; color: #666; margin-bottom: 5px;" data-en="Time (optional)" data-de="Uhrzeit (optional)" data-it="Ora (opzionale)" data-fr="Heure (optionnel)" data-zh="时间(可选)">Uhrzeit (optional)</label>
|
||||||
|
<input type="time" id="search-time" name="time" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 1rem;">
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; min-width: 150px;">
|
||||||
|
<label style="display: block; font-size: 0.85rem; color: #666; margin-bottom: 5px;" data-en="Type" data-de="Typ" data-it="Tipo" data-fr="Type" data-zh="类型">Typ</label>
|
||||||
|
<select id="search-type" name="type" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 1rem;">
|
||||||
|
<option value="all" data-en="All Videos" data-de="Alle Videos" data-it="Tutti i video" data-fr="Toutes les vidéos" data-zh="所有视频">Alle Videos</option>
|
||||||
|
<option value="daily" data-en="Daily Videos" data-de="Tagesvideos" data-it="Video giornalieri" data-fr="Vidéos quotidiennes" data-zh="每日视频">Tagesvideos</option>
|
||||||
|
<option value="ai" data-en="AI Events" data-de="AI-Ereignisse" data-it="Eventi AI" data-fr="Événements IA" data-zh="AI事件">AI-Ereignisse</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="button" style="padding: 10px 25px;" data-en="Search" data-de="Suchen" data-it="Cerca" data-fr="Rechercher" data-zh="搜索">
|
||||||
|
🔍 Suchen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div id="search-results" style="margin-top: 20px; display: none;">
|
||||||
|
<div id="search-results-content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$visualCalendar = new VisualCalendarManager('./videos/', './ai/', $settingsManager);
|
$visualCalendar = new VisualCalendarManager('./videos/', './ai/', $settingsManager);
|
||||||
echo $visualCalendar->displayVisualCalendar();
|
echo $visualCalendar->displayVisualCalendar();
|
||||||
@@ -3375,6 +3411,234 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- VIDEO SEARCH -->
|
||||||
|
<script>
|
||||||
|
const VideoSearch = {
|
||||||
|
init: function() {
|
||||||
|
const form = document.getElementById('video-search-form');
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.search();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
search: function() {
|
||||||
|
const date = document.getElementById('search-date').value;
|
||||||
|
const time = document.getElementById('search-time').value;
|
||||||
|
const type = document.getElementById('search-type').value;
|
||||||
|
|
||||||
|
if (!date) {
|
||||||
|
alert('Bitte wählen Sie ein Datum aus.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('date', date);
|
||||||
|
if (time) params.append('time', time);
|
||||||
|
params.append('type', type);
|
||||||
|
|
||||||
|
const resultsDiv = document.getElementById('search-results');
|
||||||
|
const contentDiv = document.getElementById('search-results-content');
|
||||||
|
resultsDiv.style.display = 'block';
|
||||||
|
contentDiv.innerHTML = '<div style="text-align:center;padding:20px;"><span style="font-size:2rem;">🔄</span><br>Suche läuft...</div>';
|
||||||
|
|
||||||
|
fetch('/api/video-search.php?' + params.toString())
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.displayResults(data, contentDiv);
|
||||||
|
} else {
|
||||||
|
contentDiv.innerHTML = '<div style="color:red;padding:20px;">Fehler bei der Suche.</div>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Search error:', err);
|
||||||
|
contentDiv.innerHTML = '<div style="color:red;padding:20px;">Netzwerkfehler bei der Suche.</div>';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
displayResults: function(data, container) {
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Statistiken
|
||||||
|
html += '<div style="margin-bottom:15px;padding:10px;background:#f0f4ff;border-radius:8px;">';
|
||||||
|
html += '<strong>Gefunden:</strong> ' + data.stats.total + ' Videos ';
|
||||||
|
html += '(' + data.stats.total_daily + ' Tagesvideos, ' + data.stats.total_ai + ' AI-Ereignisse)';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
if (data.stats.total === 0) {
|
||||||
|
html += '<div style="text-align:center;padding:30px;color:#666;">';
|
||||||
|
html += '<span style="font-size:3rem;">📭</span><br>';
|
||||||
|
html += 'Keine Videos für dieses Datum/Uhrzeit gefunden.';
|
||||||
|
html += '</div>';
|
||||||
|
} else {
|
||||||
|
// Tagesvideos
|
||||||
|
if (data.daily_videos.length > 0) {
|
||||||
|
html += '<div style="margin-bottom:20px;">';
|
||||||
|
html += '<h5 style="margin:0 0 10px 0;">📹 Tagesvideos</h5>';
|
||||||
|
html += '<div style="display:grid;gap:10px;">';
|
||||||
|
data.daily_videos.forEach(video => {
|
||||||
|
html += this.renderVideoCard(video, 'daily');
|
||||||
|
});
|
||||||
|
html += '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI-Videos
|
||||||
|
if (data.ai_videos.length > 0) {
|
||||||
|
html += '<div>';
|
||||||
|
html += '<h5 style="margin:0 0 10px 0;">🤖 AI-Ereignisse</h5>';
|
||||||
|
html += '<div style="display:grid;gap:10px;">';
|
||||||
|
data.ai_videos.forEach(video => {
|
||||||
|
html += this.renderVideoCard(video, 'ai');
|
||||||
|
});
|
||||||
|
html += '</div></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderVideoCard: function(video, type) {
|
||||||
|
const categoryIcons = {
|
||||||
|
sunny: '☀️', rainy: '🌧️', snowy: '❄️', planes: '✈️',
|
||||||
|
birds: '🐦', sunset: '🌅', sunrise: '🌄', rainbow: '🌈'
|
||||||
|
};
|
||||||
|
const icon = type === 'ai' ? (categoryIcons[video.category] || '🎬') : '📹';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="background:white;border:1px solid #e0e0e0;border-radius:8px;padding:12px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;">
|
||||||
|
<div>
|
||||||
|
<span style="font-size:1.2rem;">${icon}</span>
|
||||||
|
<strong>${video.date}</strong> um <strong>${video.time}</strong>
|
||||||
|
${type === 'ai' ? '<span style="background:#e8f4ea;padding:2px 8px;border-radius:4px;margin-left:8px;font-size:0.85rem;">' + video.category + '</span>' : ''}
|
||||||
|
<span style="color:#888;font-size:0.85rem;margin-left:10px;">${video.size_mb} MB</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;">
|
||||||
|
<button onclick="DailyVideoPlayer.playVideo('${video.path}', true)" class="button" style="padding:6px 12px;font-size:0.85rem;">
|
||||||
|
▶️ Abspielen
|
||||||
|
</button>
|
||||||
|
<a href="${video.path}" download class="button" style="padding:6px 12px;font-size:0.85rem;background:#4CAF50;text-decoration:none;">
|
||||||
|
⬇️ Download
|
||||||
|
</a>
|
||||||
|
<?php if ($settingsManager->isEmailSharingEnabled()): ?>
|
||||||
|
<button onclick="ShareModal.open('${video.path}', 'video')" class="button" style="padding:6px 12px;font-size:0.85rem;background:#2196F3;">
|
||||||
|
📤 Teilen
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
VideoSearch.init();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- SHARE MODAL -->
|
||||||
|
<?php if ($settingsManager->isEmailSharingEnabled()): ?>
|
||||||
|
<div id="share-modal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:10000;align-items:center;justify-content:center;">
|
||||||
|
<div style="background:white;border-radius:16px;padding:30px;max-width:450px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.3);">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
|
||||||
|
<h3 style="margin:0;color:#667eea;">📤 Per E-Mail teilen</h3>
|
||||||
|
<button onclick="ShareModal.close()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:#888;">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="share-form">
|
||||||
|
<input type="hidden" id="share-path" name="path">
|
||||||
|
<input type="hidden" id="share-type" name="type">
|
||||||
|
<div style="margin-bottom:15px;">
|
||||||
|
<label style="display:block;font-size:0.85rem;color:#666;margin-bottom:5px;">Dein Name</label>
|
||||||
|
<input type="text" id="share-sender" name="sender_name" placeholder="Dein Name" style="width:100%;padding:12px;border:1px solid #ddd;border-radius:8px;font-size:1rem;">
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:15px;">
|
||||||
|
<label style="display:block;font-size:0.85rem;color:#666;margin-bottom:5px;">E-Mail-Adresse des Empfängers *</label>
|
||||||
|
<input type="email" id="share-email" name="email" placeholder="freund@beispiel.ch" required style="width:100%;padding:12px;border:1px solid #ddd;border-radius:8px;font-size:1rem;">
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<label style="display:block;font-size:0.85rem;color:#666;margin-bottom:5px;">Nachricht (optional)</label>
|
||||||
|
<textarea id="share-message" name="message" placeholder="Schau dir das an!" rows="3" style="width:100%;padding:12px;border:1px solid #ddd;border-radius:8px;font-size:1rem;resize:vertical;"></textarea>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;">
|
||||||
|
<button type="button" onclick="ShareModal.close()" class="button" style="flex:1;background:#e0e0e0;color:#333;">Abbrechen</button>
|
||||||
|
<button type="submit" class="button" style="flex:1;background:linear-gradient(135deg, #667eea 0%, #764ba2 100%);">📧 Senden</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div id="share-result" style="margin-top:15px;display:none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const ShareModal = {
|
||||||
|
open: function(path, type) {
|
||||||
|
document.getElementById('share-path').value = path;
|
||||||
|
document.getElementById('share-type').value = type;
|
||||||
|
document.getElementById('share-email').value = '';
|
||||||
|
document.getElementById('share-message').value = '';
|
||||||
|
document.getElementById('share-result').style.display = 'none';
|
||||||
|
document.getElementById('share-modal').style.display = 'flex';
|
||||||
|
},
|
||||||
|
|
||||||
|
close: function() {
|
||||||
|
document.getElementById('share-modal').style.display = 'none';
|
||||||
|
},
|
||||||
|
|
||||||
|
send: function() {
|
||||||
|
const form = document.getElementById('share-form');
|
||||||
|
const path = document.getElementById('share-path').value;
|
||||||
|
const type = document.getElementById('share-type').value;
|
||||||
|
const email = document.getElementById('share-email').value;
|
||||||
|
const message = document.getElementById('share-message').value;
|
||||||
|
const senderName = document.getElementById('share-sender').value || 'Ein Freund';
|
||||||
|
const resultDiv = document.getElementById('share-result');
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
resultDiv.innerHTML = '<div style="color:#f44336;padding:10px;background:#ffebee;border-radius:8px;">Bitte E-Mail-Adresse eingeben.</div>';
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultDiv.innerHTML = '<div style="color:#666;padding:10px;text-align:center;">🔄 Wird gesendet...</div>';
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
|
||||||
|
fetch('/api/share.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ path, type, email, message, sender_name: senderName })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
resultDiv.innerHTML = '<div style="color:#4CAF50;padding:10px;background:#e8f5e9;border-radius:8px;">✅ E-Mail wurde gesendet!</div>';
|
||||||
|
setTimeout(() => ShareModal.close(), 2000);
|
||||||
|
} else {
|
||||||
|
let msg = data.error || 'Fehler beim Senden';
|
||||||
|
if (data.share_url) {
|
||||||
|
msg += '<br><br>Link zum manuellen Teilen:<br><input type="text" value="' + data.share_url + '" style="width:100%;padding:5px;margin-top:5px;" onclick="this.select()" readonly>';
|
||||||
|
}
|
||||||
|
resultDiv.innerHTML = '<div style="color:#f44336;padding:10px;background:#ffebee;border-radius:8px;">' + msg + '</div>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Share error:', err);
|
||||||
|
resultDiv.innerHTML = '<div style="color:#f44336;padding:10px;background:#ffebee;border-radius:8px;">Netzwerkfehler beim Senden.</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('share-form')?.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
ShareModal.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal schliessen bei Klick ausserhalb
|
||||||
|
document.getElementById('share-modal')?.addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) ShareModal.close();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- ADMIN SETTINGS (AJAX) -->
|
<!-- ADMIN SETTINGS (AJAX) -->
|
||||||
<?php if ($adminManager->isAdmin()): ?>
|
<?php if ($adminManager->isAdmin()): ?>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in New Issue
Block a user