1179 lines
No EOL
51 KiB
PHP
1179 lines
No EOL
51 KiB
PHP
<?php
|
|
/*
|
|
* 2026-01-08 20:45:00
|
|
* FEATURES: Robustere Fehlerbehandlung, Safety-Check für PromptFeedback, Konfigurierbare API-Modelle
|
|
*/
|
|
|
|
// 1. Projekt identifizieren und schützen
|
|
$current_project = 'dinos';
|
|
require_once '../auth.php'; // Falls du deine globale Auth hast, sonst auskommentieren
|
|
|
|
// 2. Nutzerdaten abrufen
|
|
$loggedInUser = isset($_SESSION['user_email']) ? $_SESSION['user_email'] : 'Gast';
|
|
|
|
// --- KONFIGURATION ---
|
|
$dailyLimitEco = 100; // Eco (Flash) etwa 3-4 Cent pro Bild
|
|
$dailyLimitPro = 10; // Pro (Imagen 4) etwa 3-4 Cent pro Bild
|
|
$dailyLimitUltra = 10; // Ultra (Gemini 3) etwa 12 Cent pro Bild
|
|
|
|
// API-Key sicher aus .env laden
|
|
$apiKey = '';
|
|
$envFiles = [__DIR__ . '/.env', __DIR__ . '/../home/.env'];
|
|
foreach ($envFiles as $envFile) {
|
|
if (file_exists($envFile)) {
|
|
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
|
$line = trim($line);
|
|
if ($line === '' || $line[0] === '#') continue;
|
|
if (strpos($line, '=') !== false) {
|
|
list($k, $v) = explode('=', $line, 2);
|
|
if (trim($k) === 'GEMINI_API_KEY') {
|
|
$apiKey = trim(trim($v), '"\'');
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// API Modelle (hier zentral ändern, wenn Versionen veralten) siehe apis.php
|
|
$modelText = "gemini-2.5-flash"; // Für Fakten und Zufallsgenerator
|
|
$modelImageEco = "gemini-2.5-flash-image"; // ECO Modus
|
|
$modelImagePro = "imagen-4.0-generate-001"; // PRO Modus (Imagen)
|
|
$modelImageUltra = "gemini-3-pro-image-preview"; // ULTRA Modus
|
|
|
|
// --- LOGGING SYSTEM ---
|
|
$logFile = 'usage_stats.json';
|
|
|
|
// Funktion zum Lesen der Stats
|
|
function getStats($file)
|
|
{
|
|
if (!file_exists($file)) {
|
|
return ['total_eco' => 0, 'total_pro' => 0, 'total_ultra' => 0, 'dates' => []];
|
|
}
|
|
$content = file_get_contents($file);
|
|
$data = $content ? json_decode($content, true) : [];
|
|
|
|
// Fallback/Initialisierung
|
|
if (!isset($data['total_eco'])) $data['total_eco'] = 0;
|
|
if (!isset($data['total_pro'])) $data['total_pro'] = 0;
|
|
if (!isset($data['total_ultra'])) $data['total_ultra'] = 0;
|
|
if (!isset($data['dates'])) $data['dates'] = [];
|
|
|
|
return $data;
|
|
}
|
|
|
|
// AJAX Handler für Stats Logging
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'log_success') {
|
|
$stats = getStats($logFile);
|
|
$today = date('Y-m-d');
|
|
$type = isset($_POST['type']) ? $_POST['type'] : 'eco'; // eco, pro, ultra
|
|
|
|
// Gesamt hochzählen
|
|
if ($type === 'ultra') $stats['total_ultra']++;
|
|
elseif ($type === 'pro') $stats['total_pro']++;
|
|
else $stats['total_eco']++;
|
|
|
|
// Heute initialisieren (falls noch nicht da)
|
|
if (!isset($stats['dates'][$today])) {
|
|
$stats['dates'][$today] = ['eco' => 0, 'pro' => 0, 'ultra' => 0];
|
|
}
|
|
// Sicherstellen dass alle Keys existieren
|
|
if (!isset($stats['dates'][$today]['eco'])) $stats['dates'][$today]['eco'] = 0;
|
|
if (!isset($stats['dates'][$today]['pro'])) $stats['dates'][$today]['pro'] = 0;
|
|
if (!isset($stats['dates'][$today]['ultra'])) $stats['dates'][$today]['ultra'] = 0;
|
|
|
|
$stats['dates'][$today][$type]++;
|
|
|
|
// REIHENFOLGE ERZWINGEN (SORTIERUNG)
|
|
foreach ($stats['dates'] as $dateKey => $dayStats) {
|
|
$stats['dates'][$dateKey] = [
|
|
'eco' => isset($dayStats['eco']) ? $dayStats['eco'] : 0,
|
|
'pro' => isset($dayStats['pro']) ? $dayStats['pro'] : 0,
|
|
'ultra' => isset($dayStats['ultra']) ? $dayStats['ultra'] : 0
|
|
];
|
|
}
|
|
|
|
// Hauptstruktur sortieren
|
|
$orderedStats = [
|
|
'total_eco' => $stats['total_eco'],
|
|
'total_pro' => $stats['total_pro'],
|
|
'total_ultra' => $stats['total_ultra'],
|
|
'dates' => $stats['dates']
|
|
];
|
|
|
|
// Speichern
|
|
file_put_contents($logFile, json_encode($orderedStats, JSON_PRETTY_PRINT));
|
|
|
|
header('Content-Type: application/json');
|
|
echo json_encode([
|
|
'status' => 'logged',
|
|
'today_eco' => $stats['dates'][$today]['eco'],
|
|
'today_pro' => $stats['dates'][$today]['pro'],
|
|
'today_ultra' => $stats['dates'][$today]['ultra'],
|
|
'total_eco' => $stats['total_eco'],
|
|
'total_pro' => $stats['total_pro'],
|
|
'total_ultra' => $stats['total_ultra']
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
// Stats laden für Initial-Anzeige
|
|
$currentStats = getStats($logFile);
|
|
$todayDate = date('Y-m-d');
|
|
$todayEco = isset($currentStats['dates'][$todayDate]['eco']) ? $currentStats['dates'][$todayDate]['eco'] : 0;
|
|
$todayPro = isset($currentStats['dates'][$todayDate]['pro']) ? $currentStats['dates'][$todayDate]['pro'] : 0;
|
|
$todayUltra = isset($currentStats['dates'][$todayDate]['ultra']) ? $currentStats['dates'][$todayDate]['ultra'] : 0;
|
|
|
|
$totalEco = $currentStats['total_eco'];
|
|
$totalPro = $currentStats['total_pro'];
|
|
$totalUltra = $currentStats['total_ultra'];
|
|
?>
|
|
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Dino Wissenskarten Generator</title>
|
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<script src="prompts.js?v=<?php echo time(); ?>"></script>
|
|
|
|
<style>
|
|
.loader {
|
|
border: 3px solid #0f172a;
|
|
border-radius: 50%;
|
|
border-top: 3px solid transparent;
|
|
width: 24px;
|
|
height: 24px;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
#renderCanvas {
|
|
display: none;
|
|
}
|
|
|
|
/* Custom Radio Buttons Look */
|
|
.model-option input:checked+div {
|
|
background-color: #f59e0b;
|
|
/* Amber 500 */
|
|
border-color: #f59e0b;
|
|
color: #0f172a;
|
|
/* Slate 900 */
|
|
}
|
|
|
|
.model-option:hover div {
|
|
border-color: #f59e0b;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="min-h-screen bg-slate-900 font-sans text-slate-100 flex flex-col">
|
|
|
|
<div class="max-w-6xl mx-auto p-4 md:p-8 flex-grow w-full">
|
|
|
|
<!-- Header -->
|
|
<div class="text-center mb-8 relative">
|
|
<div class="absolute top-0 right-0 text-xs text-slate-600 hidden md:block">
|
|
User: <?php echo htmlspecialchars($loggedInUser); ?>
|
|
</div>
|
|
<h1 class="text-4xl font-bold mb-2 flex items-center justify-center gap-3">
|
|
<span class="text-4xl">🦕</span>
|
|
<span class="text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-orange-500">
|
|
Dino Wissenskarten
|
|
</span>
|
|
</h1>
|
|
<p class="text-slate-400">Generiere fotorealistische Bilder und entdecke spannende Fakten.</p>
|
|
</div>
|
|
|
|
<!-- Input Section -->
|
|
<div class="bg-slate-800 rounded-2xl shadow-2xl p-6 mb-8 border border-slate-700">
|
|
<div class="flex flex-col md:flex-row gap-4 items-stretch">
|
|
|
|
<!-- 1. Zufallsgenerator Button -->
|
|
<button id="randomBtn" class="w-full md:w-auto aspect-auto md:aspect-square px-4 md:p-0 rounded-xl hover:bg-slate-600 text-amber-400 transition-colors flex md:flex-col items-center justify-center gap-1 border border-slate-600 h-14 md:h-auto min-w-[70px]" title="Zufälliges Urzeit-Tier vorschlagen">
|
|
<i id="diceIcon" data-lucide="dices" width="24" height="24"></i>
|
|
<span class="text-[10px] font-normal opacity-70 uppercase tracking-wide text-slate-300">Zufall</span>
|
|
</button>
|
|
|
|
<!-- 2. Eingabefeld -->
|
|
<div class="relative flex-1 group">
|
|
<input type="text" id="searchTerm" placeholder="Welcher Dino? (z.B. Triceratops)" class="w-full text-lg p-4 pb-8 bg-slate-900 border border-slate-700 text-white rounded-xl focus:ring-2 focus:ring-amber-500 outline-none transition-all placeholder:text-slate-600 h-full" />
|
|
<div id="randomTagline" class="absolute left-4 bottom-2 text-xs text-slate-500 italic pointer-events-none hidden transition-opacity"></div>
|
|
<button id="clearBtn" class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-500 hover:text-white transition-colors p-1 hidden" title="Eingabe löschen"><i data-lucide="x" width="20"></i></button>
|
|
</div>
|
|
|
|
<!-- 3. Generieren & Optionen -->
|
|
<div class="flex flex-col gap-3 min-w-[280px]">
|
|
<button id="generateBtn" class="px-8 py-3 rounded-xl text-lg font-bold text-slate-900 shadow-lg transition-all flex items-center gap-2 justify-center bg-gradient-to-r from-amber-400 to-orange-500 hover:scale-105 hover:shadow-amber-500/20 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 flex-1">
|
|
<span id="btnIcon"><i data-lucide="camera" width="24"></i></span>
|
|
<span id="btnText">Erstellen</span>
|
|
<div id="btnLoader" class="loader hidden"></div>
|
|
</button>
|
|
|
|
<!-- Modell Auswahl (3-Way) - SORTIERT: ECO > PRO > ULTRA -->
|
|
<div class="flex gap-2 justify-between">
|
|
<label class="model-option cursor-pointer flex-1">
|
|
<input type="radio" name="model" value="eco" class="hidden" checked>
|
|
<div class="border border-slate-600 rounded-lg p-2 text-center text-xs font-bold text-slate-400 transition-all">
|
|
ECO<br><span class="text-[10px] font-normal opacity-70">Flash</span>
|
|
</div>
|
|
</label>
|
|
<label class="model-option cursor-pointer flex-1">
|
|
<input type="radio" name="model" value="pro" class="hidden">
|
|
<div class="border border-slate-600 rounded-lg p-2 text-center text-xs font-bold text-slate-400 transition-all">
|
|
PRO<br><span class="text-[10px] font-normal opacity-70">Imagen 4</span>
|
|
</div>
|
|
</label>
|
|
<label class="model-option cursor-pointer flex-1">
|
|
<input type="radio" name="model" value="ultra" class="hidden">
|
|
<div class="border border-slate-600 rounded-lg p-2 text-center text-xs font-bold text-slate-400 transition-all">
|
|
ULTRA<br><span class="text-[10px] font-normal opacity-70">Gemini 3</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Status Meldung -->
|
|
<div id="statusMessage" class="mt-4 text-center text-amber-400 text-sm animate-pulse hidden"></div>
|
|
</div>
|
|
|
|
<!-- Fehler Anzeige -->
|
|
<div id="errorBox" class="bg-red-900/30 text-red-400 p-4 rounded-xl mb-8 border border-red-900/50 flex items-center gap-2 hidden">
|
|
<i data-lucide="info" width="20"></i> <span id="errorText"></span>
|
|
</div>
|
|
|
|
<!-- Preview Section -->
|
|
<div id="previewSection" class="hidden animate-in fade-in slide-in-from-bottom-4 duration-700">
|
|
|
|
<div class="rounded-2xl overflow-hidden shadow-2xl border border-slate-700 relative group">
|
|
<img id="previewImage" src="" alt="Vorschau" class="w-full h-auto" />
|
|
<!-- Qualitäts-Badge auf dem Bild -->
|
|
<div id="qualityBadge" class="absolute top-4 right-4 bg-black/60 backdrop-blur text-white text-xs px-2 py-1 rounded border border-white/20 hidden">
|
|
MODE
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end items-center mt-4">
|
|
|
|
<div class="flex gap-3">
|
|
|
|
|
|
|
|
<button id="shareBtn" class="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg font-bold transition-colors flex items-center gap-2 shadow-lg hidden" title="Direkt teilen">
|
|
<i data-lucide="share-2" width="20"></i> <span class="hidden sm:inline">Teilen</span>
|
|
</button>
|
|
<!-- NEU: Cloud Upload Button mit Wrapper für Icon -->
|
|
<button id="uploadCloudBtn" class="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded-lg font-bold transition-colors flex items-center gap-2 shadow-lg shadow-blue-600/20">
|
|
<span id="uploadIconWrapper">
|
|
<i data-lucide="cloud-upload" width="20"></i>
|
|
</span>
|
|
<span id="uploadText">Auf Nest Hub</span>
|
|
<div id="uploadLoader" class="loader border-white hidden" style="width:16px; height:16px;"></div>
|
|
</button>
|
|
<button id="downloadBtn" class="bg-amber-500 hover:bg-amber-400 text-slate-900 px-6 py-2 rounded-lg font-bold transition-colors flex items-center gap-2 shadow-lg shadow-amber-500/10">
|
|
<i data-lucide="download" width="20"></i> Speichern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div id="emptyState" class="text-center py-20 opacity-50">
|
|
<div class="w-32 h-32 bg-slate-800 rounded-full mx-auto flex items-center justify-center mb-6 border-4 border-slate-700">
|
|
<span class="text-6xl">🦖</span>
|
|
</div>
|
|
<p class="text-slate-400 text-lg">Tippe einen Dino-Namen ein und schau zu, was passiert.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer mit Nutzungsstatistik -->
|
|
<footer class="text-center p-4 border-t border-slate-800 text-slate-600 text-xs font-mono">
|
|
<span class="text-amber-500/50">Statistik - Heute:</span>
|
|
<span id="statTodayEco" class="text-slate-300"><?php echo $todayEco; ?></span> Eco |
|
|
<span id="statTodayPro" class="text-amber-400"><?php echo $todayPro; ?></span> Pro |
|
|
<span id="statTodayUltra" class="text-pink-400"><?php echo $todayUltra; ?></span> Ultra
|
|
<br class="sm:hidden">
|
|
<span class="text-slate-700 mx-2 hidden sm:inline">|</span>
|
|
<span class="text-amber-500/50">Gesamt:</span>
|
|
<span id="statTotalEco" class="text-slate-300"><?php echo $totalEco; ?></span> Eco |
|
|
<span id="statTotalPro" class="text-amber-400"><?php echo $totalPro; ?></span> Pro |
|
|
<span id="statTotalUltra" class="text-pink-400"><?php echo $totalUltra; ?></span> Ultra
|
|
</footer>
|
|
|
|
<canvas id="renderCanvas"></canvas>
|
|
|
|
<script>
|
|
// API Konfiguration ans Frontend übergeben
|
|
const apiKey = "<?php echo $apiKey; ?>";
|
|
const apiModels = {
|
|
text: "<?php echo $modelText; ?>",
|
|
imageEco: "<?php echo $modelImageEco; ?>",
|
|
imagePro: "<?php echo $modelImagePro; ?>",
|
|
imageUltra: "<?php echo $modelImageUltra; ?>"
|
|
};
|
|
|
|
// Limits
|
|
const limits = {
|
|
eco: <?php echo $dailyLimitEco; ?>,
|
|
pro: <?php echo $dailyLimitPro; ?>,
|
|
ultra: <?php echo $dailyLimitUltra; ?>
|
|
};
|
|
// Aktuelle Zähler
|
|
const currentCounts = {
|
|
eco: <?php echo $todayEco; ?>,
|
|
pro: <?php echo $todayPro; ?>,
|
|
ultra: <?php echo $todayUltra; ?>
|
|
};
|
|
|
|
const searchTermInput = document.getElementById('searchTerm');
|
|
const clearBtn = document.getElementById('clearBtn');
|
|
const randomBtn = document.getElementById('randomBtn');
|
|
const generateBtn = document.getElementById('generateBtn');
|
|
const btnText = document.getElementById('btnText');
|
|
const btnIcon = document.getElementById('btnIcon');
|
|
const btnLoader = document.getElementById('btnLoader');
|
|
|
|
const modelRadios = document.getElementsByName('model');
|
|
const qualityBadge = document.getElementById('qualityBadge');
|
|
|
|
const statusMessage = document.getElementById('statusMessage');
|
|
const randomTagline = document.getElementById('randomTagline');
|
|
const errorBox = document.getElementById('errorBox');
|
|
const errorText = document.getElementById('errorText');
|
|
const previewSection = document.getElementById('previewSection');
|
|
const previewImage = document.getElementById('previewImage');
|
|
const emptyState = document.getElementById('emptyState');
|
|
const downloadBtn = document.getElementById('downloadBtn');
|
|
const shareBtn = document.getElementById('shareBtn');
|
|
const canvas = document.getElementById('renderCanvas');
|
|
|
|
// Statistik Elemente
|
|
const statElems = {
|
|
eco: document.getElementById('statTodayEco'),
|
|
pro: document.getElementById('statTodayPro'),
|
|
ultra: document.getElementById('statTodayUltra'),
|
|
totalEco: document.getElementById('statTotalEco'),
|
|
totalPro: document.getElementById('statTotalPro'),
|
|
totalUltra: document.getElementById('statTotalUltra')
|
|
};
|
|
|
|
let currentDinoData = null;
|
|
let isResultVisible = false;
|
|
let activeModelType = 'eco'; // eco, pro, ultra
|
|
|
|
lucide.createIcons();
|
|
|
|
if (navigator.share && navigator.canShare) {
|
|
shareBtn.classList.remove('hidden');
|
|
}
|
|
|
|
searchTermInput.addEventListener('input', () => {
|
|
if (searchTermInput.value.length > 0) clearBtn.classList.remove('hidden');
|
|
else clearBtn.classList.add('hidden');
|
|
randomTagline.classList.add('hidden');
|
|
updateButtonState();
|
|
});
|
|
|
|
searchTermInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') handleGenerate();
|
|
});
|
|
|
|
clearBtn.addEventListener('click', () => {
|
|
searchTermInput.value = '';
|
|
clearBtn.classList.add('hidden');
|
|
randomTagline.classList.add('hidden');
|
|
resetUI();
|
|
updateButtonState();
|
|
});
|
|
|
|
generateBtn.addEventListener('click', handleGenerate);
|
|
randomBtn.addEventListener('click', handleRandomDino);
|
|
|
|
function getSelectedModel() {
|
|
for (const radio of modelRadios) {
|
|
if (radio.checked) return radio.value;
|
|
}
|
|
return 'eco';
|
|
}
|
|
|
|
function getFilename() {
|
|
if (!currentDinoData) return 'dino.png';
|
|
const cleanName = currentDinoData.name.replace(/\s+/g, '-');
|
|
let suffix = '';
|
|
if (activeModelType === 'pro') suffix = '-Pro';
|
|
if (activeModelType === 'ultra') suffix = '-Ultra';
|
|
return `${cleanName}${suffix}.png`;
|
|
}
|
|
|
|
downloadBtn.addEventListener('click', () => {
|
|
if (!currentDinoData || !previewImage.src) return;
|
|
const link = document.createElement('a');
|
|
link.download = getFilename();
|
|
link.href = previewImage.src;
|
|
link.click();
|
|
});
|
|
|
|
shareBtn.addEventListener('click', async () => {
|
|
if (!currentDinoData || !previewImage.src) return;
|
|
try {
|
|
const response = await fetch(previewImage.src);
|
|
const blob = await response.blob();
|
|
const fileName = getFilename();
|
|
const file = new File([blob], fileName, {
|
|
type: 'image/png'
|
|
});
|
|
const shareData = {
|
|
files: [file],
|
|
title: `Wissenskarte: ${currentDinoData.name}`,
|
|
text: `Hier ist eine Wissenskarte für ${currentDinoData.name}.`
|
|
};
|
|
if (navigator.canShare(shareData)) await navigator.share(shareData);
|
|
else alert("Teilen nicht unterstützt.");
|
|
} catch (err) {
|
|
if (err.name !== 'AbortError') {
|
|
console.error(err);
|
|
alert("Fehler: " + err.message);
|
|
}
|
|
}
|
|
});
|
|
|
|
function updateButtonState() {
|
|
if (isResultVisible) {
|
|
btnText.innerText = "Neue Variante";
|
|
btnIcon.innerHTML = '<i data-lucide="refresh-cw" width="24"></i>';
|
|
lucide.createIcons();
|
|
generateBtn.disabled = false;
|
|
} else {
|
|
btnText.innerText = "Erstellen";
|
|
btnIcon.innerHTML = '<i data-lucide="camera" width="24"></i>';
|
|
lucide.createIcons();
|
|
generateBtn.disabled = searchTermInput.value.trim() === '';
|
|
}
|
|
}
|
|
|
|
function resetUI() {
|
|
currentDinoData = null;
|
|
isResultVisible = false;
|
|
previewSection.classList.add('hidden');
|
|
emptyState.classList.remove('hidden');
|
|
errorBox.classList.add('hidden');
|
|
statusMessage.classList.add('hidden');
|
|
updateButtonState();
|
|
}
|
|
|
|
// --- ZUFALLSGENERATOR LOGIK ---
|
|
async function handleRandomDino() {
|
|
if (!apiKey) {
|
|
showError("API Key fehlt.");
|
|
return;
|
|
}
|
|
|
|
let diceIconElem = document.getElementById('diceIcon');
|
|
if (!diceIconElem) diceIconElem = randomBtn.querySelector('svg');
|
|
|
|
if (diceIconElem) diceIconElem.classList.add('animate-spin');
|
|
|
|
try {
|
|
const prompt = DinoPrompts.getRandom();
|
|
|
|
const response = await fetch(
|
|
`https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent?key=${apiKey}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
contents: [{
|
|
parts: [{
|
|
text: prompt
|
|
}]
|
|
}]
|
|
})
|
|
}
|
|
);
|
|
|
|
if (!response.ok) throw new Error("Netzwerkfehler");
|
|
const result = await response.json();
|
|
const textContent = result.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
if (!textContent) throw new Error("Keine Daten");
|
|
|
|
// --- FIX 1: Robustes JSON Parsing ---
|
|
// Sucht einfach nach dem ersten { und dem letzten } im Text
|
|
const firstBrace = textContent.indexOf('{');
|
|
const lastBrace = textContent.lastIndexOf('}');
|
|
|
|
if (firstBrace === -1 || lastBrace === -1) {
|
|
throw new Error("Ungültiges JSON-Format von KI erhalten.");
|
|
}
|
|
|
|
// Alles davor und danach (z.B. "Hier ist dein JSON:" oder ```json) wird ignoriert
|
|
const jsonStr = textContent.substring(firstBrace, lastBrace + 1);
|
|
const data = JSON.parse(jsonStr);
|
|
|
|
searchTermInput.value = data.name;
|
|
randomTagline.innerText = data.tagline || "";
|
|
randomTagline.classList.remove('hidden');
|
|
clearBtn.classList.remove('hidden');
|
|
|
|
resetUI();
|
|
updateButtonState();
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
showError("Zufallsgenerator Fehler: " + err.message);
|
|
} finally {
|
|
let currentDiceIcon = document.getElementById('diceIcon');
|
|
if (!currentDiceIcon) currentDiceIcon = randomBtn.querySelector('svg');
|
|
if (currentDiceIcon) currentDiceIcon.classList.remove('animate-spin');
|
|
}
|
|
}
|
|
|
|
async function handleGenerate() {
|
|
const selectedType = getSelectedModel();
|
|
randomTagline.classList.add('hidden');
|
|
|
|
if (currentCounts[selectedType] >= limits[selectedType]) {
|
|
showError(`Tageslimit für ${selectedType.toUpperCase()}-Bilder erreicht (${limits[selectedType]}). Wähle einen anderen Modus.`);
|
|
return;
|
|
}
|
|
|
|
const searchTerm = searchTermInput.value.trim();
|
|
if (!searchTerm) return;
|
|
if (!apiKey) {
|
|
showError("API Key fehlt.");
|
|
return;
|
|
}
|
|
if (typeof DinoPrompts === 'undefined') {
|
|
showError("Fehler: prompts.js nicht geladen.");
|
|
return;
|
|
}
|
|
|
|
activeModelType = selectedType;
|
|
|
|
setLoading(true);
|
|
errorBox.classList.add('hidden');
|
|
statusMessage.classList.remove('hidden');
|
|
previewSection.classList.add('hidden');
|
|
emptyState.classList.add('hidden');
|
|
|
|
try {
|
|
setStatus(`Sammle Fakten über ${searchTerm}`);
|
|
const dinoData = await fetchDinoFacts(searchTerm);
|
|
currentDinoData = dinoData;
|
|
|
|
// STATUS UPDATES
|
|
if (activeModelType === 'ultra') setStatus('Generiere ULTRA-Bild (Gemini 3, Beste Qualität)');
|
|
else if (activeModelType === 'pro') setStatus('Generiere PRO-Bild (Imagen 4, Gute Qualität)');
|
|
else setStatus('Generiere Entwurf (Flash, Schnell)');
|
|
|
|
// VERSUCH + RETRY (standardmäßig 1 Versuch + 1 Retry)
|
|
const base64Image = await fetchDinoImage(dinoData, activeModelType, 1);
|
|
|
|
setStatus('Erstelle finale Wissenskarte');
|
|
const finalImageUrl = await drawCanvas(dinoData, base64Image);
|
|
|
|
previewImage.src = finalImageUrl;
|
|
previewSection.classList.remove('hidden');
|
|
|
|
// BADGE UPDATE
|
|
qualityBadge.classList.remove('hidden');
|
|
if (activeModelType === 'ultra') {
|
|
// Ultra = Gemini 3
|
|
qualityBadge.innerText = "ULTRA (GEMINI 3)";
|
|
qualityBadge.className = "absolute top-4 right-4 bg-pink-600/80 backdrop-blur text-white text-xs px-2 py-1 rounded border border-white/20";
|
|
} else if (activeModelType === 'pro') {
|
|
// Pro = Imagen 4
|
|
qualityBadge.innerText = "PRO (IMAGEN 4)";
|
|
qualityBadge.className = "absolute top-4 right-4 bg-amber-600/80 backdrop-blur text-white text-xs px-2 py-1 rounded border border-white/20";
|
|
} else {
|
|
qualityBadge.innerText = "ECO (FLASH)";
|
|
qualityBadge.className = "absolute top-4 right-4 bg-slate-600/80 backdrop-blur text-white text-xs px-2 py-1 rounded border border-white/20";
|
|
}
|
|
|
|
isResultVisible = true;
|
|
logUsage(activeModelType);
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
const msg = err.message || err.toString();
|
|
showError('Hoppla: ' + msg);
|
|
emptyState.classList.remove('hidden');
|
|
} finally {
|
|
setLoading(false);
|
|
statusMessage.classList.add('hidden');
|
|
updateButtonState();
|
|
}
|
|
}
|
|
|
|
function logUsage(type) {
|
|
fetch('index.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: 'action=log_success&type=' + type
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.status === 'logged') {
|
|
statElems.eco.innerText = data.today_eco;
|
|
statElems.pro.innerText = data.today_pro;
|
|
statElems.ultra.innerText = data.today_ultra;
|
|
|
|
statElems.totalEco.innerText = data.total_eco;
|
|
statElems.totalPro.innerText = data.total_pro;
|
|
statElems.totalUltra.innerText = data.total_ultra;
|
|
|
|
currentCounts.eco = parseInt(data.today_eco);
|
|
currentCounts.pro = parseInt(data.today_pro);
|
|
currentCounts.ultra = parseInt(data.today_ultra);
|
|
}
|
|
})
|
|
.catch(err => console.error("Logging failed", err));
|
|
}
|
|
|
|
function setLoading(isLoading) {
|
|
generateBtn.disabled = isLoading;
|
|
if (isLoading) {
|
|
btnIcon.classList.add('hidden');
|
|
btnLoader.classList.remove('hidden');
|
|
btnText.innerText = "Lade...";
|
|
} else {
|
|
btnLoader.classList.add('hidden');
|
|
btnIcon.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
function setStatus(msg) {
|
|
statusMessage.innerText = msg;
|
|
}
|
|
|
|
function showError(msg) {
|
|
errorText.innerText = msg;
|
|
errorBox.classList.remove('hidden');
|
|
}
|
|
|
|
async function fetchDinoFacts(term) {
|
|
const factsPrompt = DinoPrompts.getFacts(term);
|
|
const response = await fetch(
|
|
`https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent?key=${apiKey}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
contents: [{
|
|
parts: [{
|
|
text: factsPrompt
|
|
}]
|
|
}]
|
|
})
|
|
}
|
|
);
|
|
if (!response.ok) {
|
|
const errText = await response.text();
|
|
if (response.status === 429 || errText.includes('RESOURCE_EXHAUSTED')) {
|
|
throw new Error("API Quota Limit für Text (Facts) erreicht.");
|
|
}
|
|
throw new Error(`Fakten-Fehler (${response.status})`);
|
|
}
|
|
const result = await response.json();
|
|
const textContent = result.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
if (!textContent) throw new Error("Keine Fakten erhalten");
|
|
|
|
// Auch hier: Robustes JSON Parsing verwenden
|
|
const firstBrace = textContent.indexOf('{');
|
|
const lastBrace = textContent.lastIndexOf('}');
|
|
if (firstBrace !== -1 && lastBrace !== -1) {
|
|
const jsonStr = textContent.substring(firstBrace, lastBrace + 1);
|
|
return JSON.parse(jsonStr);
|
|
}
|
|
// Fallback (falls die KI wirklich nur nacktes JSON schickt)
|
|
return JSON.parse(textContent);
|
|
}
|
|
|
|
// --- UPDATED: JETZT MIT RETRY PARAMETER ---
|
|
async function fetchDinoImage(dinoData, type, attempt = 1) {
|
|
const imagePrompt = DinoPrompts.getImage(dinoData);
|
|
|
|
let url = '';
|
|
let payload = {};
|
|
let isImagen = false;
|
|
|
|
if (type === 'ultra') {
|
|
url = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imageUltra}:generateContent?key=${apiKey}`;
|
|
payload = {
|
|
contents: [{
|
|
parts: [{
|
|
text: imagePrompt
|
|
}]
|
|
}],
|
|
generationConfig: {
|
|
responseModalities: ["IMAGE"]
|
|
}
|
|
};
|
|
} else if (type === 'pro') {
|
|
url = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imagePro}:predict?key=${apiKey}`;
|
|
payload = {
|
|
instances: [{
|
|
prompt: imagePrompt
|
|
}],
|
|
parameters: {
|
|
sampleCount: 1
|
|
}
|
|
};
|
|
isImagen = true;
|
|
} else {
|
|
url = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imageEco}:generateContent?key=${apiKey}`;
|
|
payload = {
|
|
contents: [{
|
|
parts: [{
|
|
text: imagePrompt
|
|
}]
|
|
}],
|
|
generationConfig: {
|
|
responseModalities: ["IMAGE"]
|
|
}
|
|
};
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errText = await response.text();
|
|
if (response.status === 429 || errText.includes('RESOURCE_EXHAUSTED')) {
|
|
throw new Error("Google API Limit erreicht (Quota).");
|
|
}
|
|
throw new Error(`Bild-Fehler (${type}): ${errText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
// DEBUGGING
|
|
console.log(`API Response (Versuch ${attempt}):`, result);
|
|
|
|
// CHECK GLOBAL BLOCK (Oft bei Prompt-Safety)
|
|
if (result.promptFeedback && result.promptFeedback.blockReason) {
|
|
console.warn(`Blockiert: ${result.promptFeedback.blockReason}`);
|
|
// Bei Blockaden hilft Retry oft nicht, aber wir versuchen es trotzdem 1x
|
|
}
|
|
|
|
if (isImagen) {
|
|
if (result.predictions && result.predictions.length > 0) {
|
|
return result.predictions[0].bytesBase64Encoded;
|
|
}
|
|
} else {
|
|
const candidate = result.candidates?.[0];
|
|
if (candidate) {
|
|
// Check Safety / Finish Reason
|
|
if (candidate.finishReason && candidate.finishReason !== "STOP") {
|
|
console.warn("Safety Filter triggered:", candidate.safetyRatings);
|
|
// Wir werfen hier Fehler, damit der catch-Block unten den Retry auslösen kann
|
|
throw new Error(`Bild blockiert durch Filter (${candidate.finishReason}).`);
|
|
}
|
|
|
|
const parts = candidate.content?.parts;
|
|
const imagePart = parts?.find(p => p.inlineData);
|
|
if (imagePart && imagePart.inlineData) return imagePart.inlineData.data;
|
|
}
|
|
}
|
|
|
|
throw new Error('Keine Bilddaten in API-Antwort gefunden.');
|
|
|
|
} catch (err) {
|
|
// RETRY LOGIK
|
|
console.error(`Fehler in fetchDinoImage (Versuch ${attempt}):`, err);
|
|
|
|
// Wenn wir noch Versuche haben (max 2 Versuche total = attempt 1 < 2)
|
|
if (attempt < 2) {
|
|
setStatus(`Hoppla, kleiner Schluckauf. Versuche es sofort nochmal... (${attempt}/2)`);
|
|
await new Promise(r => setTimeout(r, 1500)); // 1.5s warten
|
|
return fetchDinoImage(dinoData, type, attempt + 1);
|
|
}
|
|
|
|
// Wenn alle Versuche fehlgeschlagen sind, werfen wir den Fehler weiter
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
function drawCanvas(dinoData, base64Image) {
|
|
return new Promise((resolve) => {
|
|
const ctx = canvas.getContext('2d');
|
|
canvas.width = 1280;
|
|
canvas.height = 800;
|
|
|
|
const img = new Image();
|
|
img.crossOrigin = "anonymous";
|
|
img.onload = () => {
|
|
// 1. Hintergrundbild
|
|
const scale = Math.max(canvas.width / img.width, canvas.height / img.height);
|
|
const x = (canvas.width / 2) - (img.width / 2) * scale;
|
|
const y = (canvas.height / 2) - (img.height / 2) * scale;
|
|
ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
|
|
|
|
// 2. Gradients (Nest Hub Safe)
|
|
const topGradient = ctx.createLinearGradient(0, 0, 0, 350);
|
|
topGradient.addColorStop(0, "rgba(0,0,0,0.85)");
|
|
topGradient.addColorStop(1, "rgba(0,0,0,0)");
|
|
ctx.fillStyle = topGradient;
|
|
ctx.fillRect(0, 0, canvas.width, 350);
|
|
|
|
const bottomGradient = ctx.createLinearGradient(0, canvas.height - 250, 0, canvas.height);
|
|
bottomGradient.addColorStop(0, "rgba(0,0,0,0)");
|
|
bottomGradient.addColorStop(1, "rgba(0,0,0,0.95)");
|
|
ctx.fillStyle = bottomGradient;
|
|
ctx.fillRect(0, canvas.height - 250, canvas.width, 250);
|
|
|
|
// NEST HUB SAFE ZONE PADDING
|
|
const padding = 80;
|
|
|
|
// Vorab-Berechnung für Layout-Grenzen (Stats Box ist rechts)
|
|
const boxWidth = 340;
|
|
const boxX = canvas.width - boxWidth - padding;
|
|
|
|
// 3. Name & Fun Fact (Oben Links)
|
|
const nameY = 110;
|
|
|
|
ctx.textAlign = 'left';
|
|
ctx.shadowColor = "rgba(0, 0, 0, 0.9)";
|
|
ctx.shadowBlur = 20;
|
|
ctx.fillStyle = '#ffffff';
|
|
|
|
// --- FIX: Dynamische Schriftgröße ---
|
|
let fontSize = 70;
|
|
ctx.font = `bold ${fontSize}px sans-serif`;
|
|
|
|
// Maximaler Platz bis zur Stats-Box (mit 20px Puffer)
|
|
const maxNameWidth = boxX - padding - 20;
|
|
|
|
// Solange verkleinern, bis es passt oder Minimum erreicht ist
|
|
while (ctx.measureText(dinoData.name).width > maxNameWidth && fontSize > 30) {
|
|
fontSize--;
|
|
ctx.font = `bold ${fontSize}px sans-serif`;
|
|
}
|
|
|
|
ctx.fillText(dinoData.name, padding, nameY);
|
|
|
|
// Fun Fact (darunter) - Hier nehmen wir wieder eine feste Größe, aber wrappen früher
|
|
const maxFactWidth = boxX - padding - 60;
|
|
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.font = 'italic 28px serif'; // Reset font
|
|
wrapText(ctx, `"${dinoData.fun_fact}"`, padding + 4, nameY + 45, maxFactWidth, 35);
|
|
ctx.shadowBlur = 0;
|
|
|
|
// 4. STATS BOX (OBEN RECHTS)
|
|
const boxHeight = 140;
|
|
const boxY = 60; // Oben (Safe Zone)
|
|
|
|
ctx.save();
|
|
ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
|
|
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)";
|
|
ctx.lineWidth = 1;
|
|
roundRect(ctx, boxX, boxY, boxWidth, boxHeight, 16);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
|
|
// Grid für 2 Zeilen, 2 Spalten
|
|
const col1X = boxX + 25;
|
|
const col2X = boxX + (boxWidth / 2) + 15;
|
|
const row1Y = boxY + 45;
|
|
const row2Y = boxY + 105;
|
|
const colWidth = (boxWidth / 2) - 20;
|
|
|
|
drawBoxStat(ctx, "Länge", `${dinoData.length} m`, col1X, row1Y, colWidth);
|
|
drawBoxStat(ctx, "Höhe", `${dinoData.height} m`, col2X, row1Y, colWidth);
|
|
drawBoxStat(ctx, "Gewicht", `${dinoData.weight} kg`, col1X, row2Y, colWidth);
|
|
drawBoxStat(ctx, "Futter", dinoData.diet, col2X, row2Y, colWidth);
|
|
|
|
|
|
// 5. ZEITACHSE
|
|
const timelineY = canvas.height - 100; // Unten (Safe Zone)
|
|
const timelineExtraPadding = 40;
|
|
const axisWidth = canvas.width - (padding * 2) - (timelineExtraPadding * 2);
|
|
const startX = padding + timelineExtraPadding;
|
|
|
|
// Definiere Zeitfenster
|
|
const START_TIME = 252;
|
|
const END_TIME = 66;
|
|
const TOTAL_YEARS = START_TIME - END_TIME;
|
|
|
|
// Helper: Zeit zu X-Koordinate
|
|
const getX = (mya) => {
|
|
const offset = START_TIME - mya;
|
|
return startX + (offset / TOTAL_YEARS) * axisWidth;
|
|
};
|
|
|
|
// Hintergrund-Achse
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)";
|
|
ctx.lineWidth = 2;
|
|
ctx.moveTo(startX, timelineY);
|
|
ctx.lineTo(startX + axisWidth, timelineY);
|
|
ctx.stroke();
|
|
|
|
// Epochen
|
|
drawEpochLabel(ctx, "TRIAS", getX(226), timelineY);
|
|
drawEpochSeparator(ctx, getX(201), timelineY);
|
|
drawEpochLabel(ctx, "JURA", getX(173), timelineY);
|
|
drawEpochSeparator(ctx, getX(145), timelineY);
|
|
drawEpochLabel(ctx, "KREIDE", getX(105), timelineY);
|
|
|
|
// ECHTER ZEITRAUM DES DINOS
|
|
const myaStart = dinoData.mya_start || 100;
|
|
const myaEnd = dinoData.mya_end || 90;
|
|
|
|
if (myaEnd > START_TIME) {
|
|
// Zu alt (Pfeil Links)
|
|
ctx.fillStyle = '#f59e0b';
|
|
ctx.textAlign = 'left';
|
|
ctx.font = 'bold 14px sans-serif';
|
|
ctx.beginPath();
|
|
ctx.moveTo(startX, timelineY);
|
|
ctx.lineTo(startX + 15, timelineY - 8);
|
|
ctx.lineTo(startX + 15, timelineY + 8);
|
|
ctx.fill();
|
|
ctx.fillText("LEBTE DAVOR", startX + 25, timelineY + 5);
|
|
ctx.fillStyle = '#f59e0b';
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(`${dinoData.years}`, startX, timelineY + 35);
|
|
|
|
} else if (myaStart < END_TIME) {
|
|
// Zu jung (Pfeil Rechts)
|
|
ctx.fillStyle = '#f59e0b';
|
|
ctx.textAlign = 'right';
|
|
ctx.font = 'bold 14px sans-serif';
|
|
const endX = startX + axisWidth;
|
|
ctx.beginPath();
|
|
ctx.moveTo(endX, timelineY);
|
|
ctx.lineTo(endX - 15, timelineY - 8);
|
|
ctx.lineTo(endX - 15, timelineY + 8);
|
|
ctx.fill();
|
|
ctx.fillText("LEBTE DANACH", endX - 25, timelineY + 5);
|
|
ctx.fillStyle = '#f59e0b';
|
|
ctx.textAlign = 'right';
|
|
ctx.fillText(`${dinoData.years}`, endX, timelineY + 35);
|
|
|
|
} else {
|
|
// Standard Balken
|
|
const clampedStart = Math.min(Math.max(myaStart, END_TIME), START_TIME);
|
|
const clampedEnd = Math.min(Math.max(myaEnd, END_TIME), START_TIME);
|
|
const dinoStartX = getX(clampedStart);
|
|
const dinoEndX = getX(clampedEnd);
|
|
let barWidth = Math.max(dinoEndX - dinoStartX, 4);
|
|
|
|
ctx.shadowColor = "#f59e0b";
|
|
ctx.shadowBlur = 15;
|
|
ctx.fillStyle = "#f59e0b";
|
|
ctx.beginPath();
|
|
ctx.roundRect(dinoStartX, timelineY - 6, barWidth, 12, 4);
|
|
ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
|
|
ctx.textAlign = 'center';
|
|
ctx.font = 'bold 16px sans-serif';
|
|
ctx.fillStyle = '#f59e0b';
|
|
ctx.fillText(`${dinoData.years}`, dinoStartX + barWidth / 2, timelineY + 35);
|
|
}
|
|
|
|
// FANTASIE CHECK
|
|
if (dinoData.is_fictional) {
|
|
const badgeText = "FANTASIE - NICHT ECHT";
|
|
ctx.font = 'bold 14px sans-serif';
|
|
const badgeWidth = ctx.measureText(badgeText).width + 30;
|
|
const badgeHeight = 28;
|
|
const badgeX = (canvas.width - badgeWidth) / 2;
|
|
const badgeY = timelineY - 60;
|
|
|
|
ctx.fillStyle = '#ec4899';
|
|
ctx.shadowColor = "rgba(0,0,0,0.5)";
|
|
ctx.shadowBlur = 10;
|
|
ctx.beginPath();
|
|
ctx.roundRect(badgeX, badgeY, badgeWidth, badgeHeight, 14);
|
|
ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(badgeText, canvas.width / 2, badgeY + 19);
|
|
}
|
|
|
|
resolve(canvas.toDataURL('image/png'));
|
|
};
|
|
img.src = `data:image/png;base64,${base64Image}`;
|
|
});
|
|
}
|
|
|
|
// --- Helper ---
|
|
|
|
function roundRect(ctx, x, y, width, height, radius) {
|
|
ctx.beginPath();
|
|
ctx.roundRect(x, y, width, height, radius);
|
|
ctx.closePath();
|
|
}
|
|
|
|
function drawBoxStat(ctx, label, value, x, y, maxWidth) {
|
|
ctx.textAlign = 'left';
|
|
|
|
// Label
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
|
|
ctx.font = 'bold 11px sans-serif';
|
|
ctx.fillText(label, x, y - 10);
|
|
|
|
// Value
|
|
ctx.fillStyle = '#ffffff';
|
|
// Prüfen ob Text zu lang
|
|
let fontSize = 24;
|
|
ctx.font = `bold ${fontSize}px sans-serif`;
|
|
if (ctx.measureText(value).width > maxWidth) fontSize = 18;
|
|
ctx.font = `bold ${fontSize}px sans-serif`;
|
|
|
|
ctx.fillText(value, x, y + 15);
|
|
}
|
|
|
|
function drawEpochSeparator(ctx, x, y) {
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)";
|
|
ctx.lineWidth = 1;
|
|
ctx.moveTo(x, y - 10);
|
|
ctx.lineTo(x, y + 10);
|
|
ctx.stroke();
|
|
}
|
|
|
|
function drawEpochLabel(ctx, text, x, y) {
|
|
ctx.textAlign = 'center';
|
|
ctx.fillStyle = "rgba(255, 255, 255, 0.4)";
|
|
ctx.font = 'bold 12px sans-serif';
|
|
ctx.fillText(text, x, y - 15);
|
|
}
|
|
|
|
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
|
|
const words = text.split(' ');
|
|
let line = '';
|
|
for (let n = 0; n < words.length; n++) {
|
|
const testLine = line + words[n] + ' ';
|
|
const metrics = ctx.measureText(testLine);
|
|
const testWidth = metrics.width;
|
|
if (testWidth > maxWidth && n > 0) {
|
|
ctx.fillText(line, x, y);
|
|
line = words[n] + ' ';
|
|
y += lineHeight;
|
|
} else {
|
|
line = testLine;
|
|
}
|
|
}
|
|
ctx.fillText(line, x, y);
|
|
}
|
|
|
|
// --- GOOGLE UPLOAD LOGIK ---
|
|
const uploadCloudBtn = document.getElementById('uploadCloudBtn');
|
|
const uploadText = document.getElementById('uploadText');
|
|
const uploadLoader = document.getElementById('uploadLoader');
|
|
const uploadIconWrapper = document.getElementById('uploadIconWrapper');
|
|
|
|
if (uploadCloudBtn) {
|
|
uploadCloudBtn.addEventListener('click', async () => {
|
|
if (!currentDinoData || !previewImage.src) return;
|
|
|
|
// UI auf "Laden" stellen
|
|
if (uploadCloudBtn) uploadCloudBtn.disabled = true;
|
|
if (uploadText) uploadText.innerText = "Sende...";
|
|
|
|
// Icon Wrapper ausblenden (nicht das <i> selbst, das ist jetzt weg!)
|
|
if (uploadIconWrapper) uploadIconWrapper.classList.add('hidden');
|
|
if (uploadLoader) uploadLoader.classList.remove('hidden');
|
|
|
|
try {
|
|
const response = await fetch('upload_handler.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
image: previewImage.src, // Das ist der Base64 String
|
|
name: currentDinoData.name,
|
|
fun_fact: currentDinoData.fun_fact,
|
|
years: currentDinoData.years
|
|
})
|
|
});
|
|
|
|
// Falls PHP schon beim Parsen scheitert (z.B. Syntax Fehler)
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP Fehler ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
// FIX 2: Check success OR if google details are present (toleranter für PHP Typo)
|
|
if (result.success || (result.details && result.details.newMediaItemResults)) {
|
|
if (uploadText) uploadText.innerText = "Gesendet!";
|
|
if (uploadLoader) uploadLoader.classList.add('hidden');
|
|
if (uploadIconWrapper) {
|
|
uploadIconWrapper.classList.remove('hidden');
|
|
|
|
// Icon zu Checkmark wechseln und neu rendern
|
|
uploadIconWrapper.innerHTML = '<i data-lucide="check" width="20"></i>';
|
|
lucide.createIcons();
|
|
}
|
|
|
|
// Nach 3 Sekunden zurücksetzen
|
|
setTimeout(() => {
|
|
if (uploadText) uploadText.innerText = "Auf Nest Hub";
|
|
if (uploadCloudBtn) uploadCloudBtn.disabled = false;
|
|
|
|
if (uploadIconWrapper) {
|
|
// Icon zurück zu Cloud wechseln
|
|
uploadIconWrapper.innerHTML = '<i data-lucide="cloud-upload" width="20"></i>';
|
|
lucide.createIcons();
|
|
}
|
|
}, 3000);
|
|
} else {
|
|
// Detailliertes Logging für dich als Entwickler
|
|
console.error("Google Upload Error Details:", result);
|
|
|
|
// Versuch, eine verständliche Fehlermeldung zu bauen
|
|
let detailMsg = "";
|
|
if (result.details && result.details.error && result.details.error.message) {
|
|
detailMsg = " (" + result.details.error.message + ")";
|
|
} else if (result.details && typeof result.details === 'string') {
|
|
detailMsg = " (" + result.details + ")";
|
|
}
|
|
|
|
throw new Error((result.error || "Unbekannter Fehler") + detailMsg);
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("Upload Fehler: " + err.message);
|
|
|
|
if (uploadText) uploadText.innerText = "Fehler";
|
|
if (uploadCloudBtn) uploadCloudBtn.disabled = false;
|
|
if (uploadLoader) uploadLoader.classList.add('hidden');
|
|
if (uploadIconWrapper) uploadIconWrapper.classList.remove('hidden');
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|