Files
cookbook/app/templates/recipe_detail.html
T

1108 lines
40 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>{{ recipe.title }} – Mein Kochbuch</title>
<link rel="stylesheet" href="/static/cookbook.css?v=4.6.9">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍽️</text></svg>">
</head>
<body class="cookbook-body">
<!-- Navigation Bar -->
<header class="cb-nav">
<a href="{% if is_shared %}#{% else %}/cookbook{% endif %}" class="cb-nav-brand">
<span class="cb-brand-icon">📖</span>
<h1 class="cb-nav-title">{% if is_shared %}Mein Kochbuch{% else %}Kochbuch{% endif %}</h1>
</a>
<!-- Desktop Navigation -->
<div class="cb-nav-actions desktop-nav-actions">
<button type="button" id="cookmode-nav-btn" class="cb-btn cb-btn-outline cb-nav-cookmode-btn no-print" onclick="toggleCookingMode()" title="Kochmodus: Hält das Display beim Kochen aktiv">
<span class="cookmode-nav-icon">👨‍🍳</span>
<span>Kochmodus</span>
<span id="cookmode-nav-badge" class="cb-badge-pill">Aus</span>
</button>
{% if not is_shared %}
<a href="/shopping-list?import_recipe_id={{ recipe.id }}" class="cb-btn cb-btn-outline" title="Zutaten auf die Einkaufsliste übernehmen">
<span>🛒</span> <span>Auf Einkaufsliste</span>
</a>
<a href="/cookbook" class="cb-btn cb-btn-outline">
<span>📖</span> <span>Alle Rezepte</span>
</a>
<button type="button" onclick="openShareModal()" class="cb-btn cb-btn-outline" title="Rezept per Link teilen">
<span>🔗</span> <span>Teilen</span>
</button>
<a href="/cookbook/edit/{{ recipe.id }}" class="cb-btn cb-btn-green">
<span>✏️</span> <span>Bearbeiten</span>
</a>
{% endif %}
<button onclick="window.print()" class="cb-btn cb-btn-outline" title="Rezeptseite drucken">
<span>🖨️</span> <span>Drucken</span>
</button>
{% if not is_shared %}
<a href="/logout" class="cb-nav-btn cb-nav-btn-logout" title="Abmelden">
<span>🚪</span> <span>Abmelden</span>
</a>
{% endif %}
</div>
<!-- Mobile Kebab (3-Punkte) Menü -->
<div class="mobile-nav-wrapper">
<button type="button" class="cb-kebab-btn" onclick="toggleMobileMenu(event)" aria-label="Menü öffnen" title="Menü">
<span>⋮</span>
</button>
<div id="cb-mobile-menu" class="cb-mobile-dropdown">
<button type="button" id="cookmode-mobile-btn" class="cb-mobile-item cb-mobile-cookmode-item" onclick="toggleCookingMode()" title="Kochmodus: Hält das Display beim Kochen aktiv">
<div class="cookmode-item-left">
<span class="cookmode-icon">👨‍🍳</span>
<div class="cookmode-text-box">
<span class="cookmode-title">Kochmodus (Display bleibt an)</span>
<span class="cookmode-status-text" id="cookmode-status-mobile">Display bleibt an</span>
</div>
</div>
<span id="cookmode-mobile-badge" class="cb-badge-pill">Aus</span>
</button>
<button type="button" id="reset-all-menu-btn" onclick="resetAllChecks()" class="cb-mobile-item no-print" style="display: none;">
<span>↺</span> <span>Abgehaktes zurücksetzen</span>
</button>
<hr class="cb-mobile-divider">
{% if not is_shared %}
<a href="/shopping-list?import_recipe_id={{ recipe.id }}" class="cb-mobile-item">
<span>🛒</span> <span>Auf Einkaufsliste</span>
</a>
<button type="button" onclick="openShareModal()" class="cb-mobile-item">
<span>🔗</span> <span>Rezept teilen</span>
</button>
<a href="/cookbook" class="cb-mobile-item">
<span>📖</span> <span>Zum Kochbuch</span>
</a>
<a href="/cookbook/edit/{{ recipe.id }}" class="cb-mobile-item">
<span>✏️</span> <span>Rezept bearbeiten</span>
</a>
{% endif %}
<button type="button" onclick="window.print()" class="cb-mobile-item">
<span>🖨️</span> <span>Drucken & PDF</span>
</button>
{% if not is_shared %}
<hr class="cb-mobile-divider">
<a href="/logout" class="cb-mobile-item cb-mobile-logout">
<span>🚪</span> <span>Abmelden</span>
</a>
{% endif %}
</div>
</div>
</header>
<main class="book-wrapper">
<!-- EINHEITLICHE REZEPTSEITE -->
<article class="page recipe-page">
{% if is_shared %}
<!-- GAST FREIGABE-BANNER -->
<div class="recipe-shared-banner no-print">
<div class="recipe-shared-left">
<span class="recipe-shared-icon">📤</span>
<div class="recipe-shared-text-group">
<span class="recipe-shared-main-text">Geteiltes Rezept</span>
<span class="recipe-shared-sub-text">
{% if share.expires_at %}
⏳ {{ share.relative_status }} (Gültig bis {{ share.expires_at_formatted }} Uhr)
{% else %}
✨ Dauerhafter Link (unbegrenzt gültig)
{% endif %}
</span>
</div>
</div>
<div class="recipe-shared-right">
<span class="recipe-shared-badge">Gastansicht</span>
</div>
</div>
{% endif %}
<!-- KOCHMODUS TOGGLE BUTTON (Oben rechts auf der Rezeptseite) -->
<button type="button"
id="cookmode-recipe-btn"
class="cookmode-toggle-btn no-print"
onclick="toggleCookingMode()"
title="Kochmodus umschalten: Hält das Display beim Kochen aktiv">
<span class="cookmode-btn-icon">👨‍🍳</span>
<span class="cookmode-btn-title">Kochmodus</span>
<span id="cookmode-recipe-badge" class="cookmode-btn-badge">Aus</span>
<span class="cookmode-btn-bulb">💡</span>
</button>
<!-- Header (Ohne Kapitelangabe & ohne Quelle im Titel) -->
<header class="recipe-header">
<h2 class="recipe-title">{{ recipe.title }}</h2>
{% if recipe.subtitle and recipe.subtitle.strip() %}
<div class="recipe-subtitle">{{ recipe.subtitle.strip() }}</div>
{% endif %}
<div class="recipe-meta-row">
{% if recipe.effort %}
<div class="recipe-meta-item">
<span>⏱️</span>
<span>Aufwand:
{% if recipe.effort == 'gering' %}Gering 🟢{% elif recipe.effort == 'hoch' %}Hoch 🔴{% else %}Mittel 🟡{% endif %}
</span>
</div>
{% endif %}
{% if recipe.prep_time %}
<div class="recipe-meta-item">
<span>🔪</span>
<span>Zubereitung: {{ recipe.prep_time }}</span>
</div>
{% endif %}
{% if recipe.cook_time %}
<div class="recipe-meta-item">
<span>🍳</span>
<span>Kochen/Backen: {{ recipe.cook_time }}</span>
</div>
{% endif %}
{% if recipe.portions %}
<div class="recipe-meta-item">
<span>👥</span>
<span>{{ recipe.portions }} Portionen</span>
</div>
{% endif %}
{% if recipe.source and recipe.source.strip() %}
<div class="recipe-meta-item">
<span>📖</span>
<span>Quelle: {{ recipe.source.strip() }}</span>
</div>
{% endif %}
</div>
</header>
<!-- 2-Spalten Layout (Auf Tablets im Querformat als 2-Spalten Kochstation) -->
<div class="recipe-grid">
<!-- Linke Spalte: Foto + Zutaten -->
<aside class="recipe-side">
<div class="recipe-photo-box">
{% if recipe.image_url %}
<img src="{{ recipe.image_url }}" alt="{{ recipe.title }}">
{% else %}
<div class="recipe-photo-empty">
<span>📸</span>
<span>Kein Foto hinterlegt</span>
</div>
{% endif %}
</div>
<!-- Zutatenleiste ohne Portionsangabe (da schon in Kopfzeile) -->
<div class="ingredients-box {% if recipe.has_portion_table %}has-portion-matrix{% endif %}">
<h4 class="section-hand-title">
<span>Zutaten</span>
<button type="button" id="reset-ingredients-btn" class="cb-reset-checks-btn no-print" onclick="resetIngredientChecks()" title="Abgehakte Zutaten zurücksetzen" style="display: none;">
<span>↺</span> <span>Zurücksetzen</span>
</button>
</h4>
{% if recipe.has_portion_table and recipe.portion_matrix %}
<!-- Tabelle für 2/4/6/8 Portionen (z. B. Waffeln / Pancakes) -->
<table class="portion-matrix-table">
<thead>
<tr>
<th class="matrix-th-ing">Zutat</th>
<th class="matrix-th-p">2 P.</th>
<th class="matrix-th-p">4 P.</th>
<th class="matrix-th-p">6 P.</th>
<th class="matrix-th-p">8 P.</th>
</tr>
</thead>
<tbody>
{% for row in recipe.portion_matrix %}
{% if row.is_header %}
<tr class="matrix-header-row">
<th colspan="5">{{ row.text }}</th>
</tr>
{% else %}
<tr class="matrix-row" onclick="toggleIngredientCheck(this)" title="Tippen zum Abhaken">
<td class="matrix-td-name">{{ row.name }}</td>
<td class="matrix-td-amt">{{ row.amounts[2] }}</td>
<td class="matrix-td-amt highlight">{{ row.amounts[4] }}</td>
<td class="matrix-td-amt">{{ row.amounts[6] }}</td>
<td class="matrix-td-amt">{{ row.amounts[8] }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% else %}
<!-- Normale kompakte Zutatenliste (Tippen zum Abhaken beim Kochen) -->
<ul class="ingredients-list">
{% for line in recipe.ingredients.split('\n') %}
{% if line.strip() %}
{% set raw_line = line.strip() %}
{% if raw_line.endswith(':') or raw_line.lower().startswith('für ') %}
<li class="ing-subheading">{{ raw_line }}</li>
{% else %}
<li class="ingredient-item" onclick="toggleIngredientCheck(this)" title="Tippen zum Abhaken">{{ raw_line }}</li>
{% endif %}
{% endif %}
{% endfor %}
</ul>
{% endif %}
</div>
</aside>
<!-- Rechte Spalte: Zubereitung + Notizen + Quelle -->
<section class="recipe-main">
<div>
<h4 class="section-hand-title">
<span>Zubereitung</span>
<button type="button" id="reset-steps-btn" class="cb-reset-checks-btn no-print" onclick="resetStepChecks()" title="Erledigte Schritte zurücksetzen" style="display: none;">
<span>↺</span> <span>Zurücksetzen</span>
</button>
</h4>
<div class="instructions-steps">
{% for step in recipe.instructions.split('\n') %}
{% if step.strip() %}
<div class="step-card" onclick="toggleStepCheck(this)" title="Tippen zum Abhaken dieses Schritts">
<span class="step-badge">{{ loop.index }}</span>
<div class="step-text">{{ step.strip() | strip_step_number }}</div>
<span class="step-done-check">✓</span>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% if recipe.notes and recipe.notes.strip() %}
<div class="notes-box">
<div class="notes-box-title">💡 Tipp & Notiz</div>
<div>{{ recipe.notes.strip() }}</div>
</div>
{% else %}
<div class="notes-box notes-box-empty print-only">
<div class="notes-box-title">💡 Eigene Notizen & Tipps</div>
<div class="notes-blank-lines">
<div class="notes-blank-line"></div>
<div class="notes-blank-line"></div>
</div>
</div>
{% endif %}
</section>
</div>
</article>
{% if not is_shared %}
<!-- SHARE MODAL -->
<div id="share-modal" class="cb-modal-backdrop" onclick="closeShareModal(event)">
<div class="cb-modal-card share-modal-card" onclick="event.stopPropagation()">
<div class="cb-modal-header">
<h3 class="cb-modal-title">
<span>🔗</span> <span>Rezept teilen</span>
</h3>
<button type="button" class="cb-modal-close" onclick="closeShareModal()" aria-label="Schließen">&times;</button>
</div>
<div class="cb-modal-body">
<p style="margin-top: 0; color: #4a4a4a; line-height: 1.5; font-size: 0.96rem;">
Erstelle einen Link, um <strong>{{ recipe.title }}</strong> mit Freunden oder Familie zu teilen.
Empfänger können das Rezept direkt ansehen, kochen und ausdrucken – <em>ohne eigenes Login</em>!
</p>
<!-- Neue Freigabe konfigurieren -->
<div class="share-create-box">
<label class="share-label">Gültigkeitsdauer des Links:</label>
<div class="share-expiry-pills">
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="24h">
<span>24 Stunden</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="3d">
<span>3 Tage</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="7d" checked>
<span>7 Tage (Empfohlen)</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="30d">
<span>30 Tage</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="unlimited">
<span>Unbegrenzt</span>
</label>
</div>
<div class="share-input-row" style="margin-top: 14px;">
<input type="text" id="share-label-input" class="share-input" placeholder="Optionale Notiz (z. B. WhatsApp, Familie...)" maxlength="50">
<button type="button" id="btn-create-share" class="cb-btn cb-btn-green" onclick="submitCreateShare()">
<span>✨ Link erstellen</span>
</button>
</div>
</div>
<!-- Neuer erstellter Link (Highlight Box) -->
<div id="share-result-box" class="share-result-box" style="display: none;">
<div class="share-result-header">
<span>✓ Neuer Share-Link bereit:</span>
<span id="share-result-expiry-badge" class="share-status-pill share-badge-active"></span>
</div>
<div class="share-result-input-group">
<input type="text" id="share-result-url" class="share-url-field" readonly onclick="this.select()">
<button type="button" id="btn-copy-share" class="cb-btn cb-btn-green" onclick="copyCurrentShareUrl()">
<span id="copy-btn-icon">📋</span> <span id="copy-btn-text">Kopieren</span>
</button>
<button type="button" id="btn-native-share" class="cb-btn cb-btn-outline" style="display: none;" onclick="triggerNativeShare()">
<span>📤 Teilen</span>
</button>
</div>
<div class="share-result-hint">Jeder mit diesem Link hat für den gewählten Zeitraum vollen Lese- und Kochzugriff.</div>
</div>
<!-- Liste existierender Freigaben -->
<div class="share-existing-section">
<h4 class="share-existing-title">Aktive Freigaben für dieses Rezept</h4>
<div id="share-links-list" class="share-links-list">
<div class="share-links-loading">Lade aktive Links...</div>
</div>
</div>
</div>
<div class="cb-modal-footer" style="padding: 12px 18px; border-top: 1.5px solid #e0ded9; display: flex; justify-content: flex-end; background: #faf6f0;">
<button type="button" class="cb-btn cb-btn-outline" onclick="closeShareModal()">Schließen</button>
</div>
</div>
</div>
{% endif %}
{% if not is_shared %}
<!-- SHARE MODAL -->
<div id="share-modal" class="cb-modal-backdrop" onclick="closeShareModal(event)">
<div class="cb-modal-card share-modal-card" onclick="event.stopPropagation()">
<div class="cb-modal-header">
<h3 class="cb-modal-title">
<span>🔗</span> <span>Rezept teilen</span>
</h3>
<button type="button" class="cb-modal-close" onclick="closeShareModal()" aria-label="Schließen">&times;</button>
</div>
<div class="cb-modal-body">
<p style="margin-top: 0; color: #4a4a4a; line-height: 1.5; font-size: 0.96rem;">
Erstelle einen Link, um <strong>{{ recipe.title }}</strong> mit Freunden oder Familie zu teilen.
Empfänger können das Rezept direkt ansehen, kochen und ausdrucken – <em>ohne eigenes Login</em>!
</p>
<!-- Neue Freigabe konfigurieren -->
<div class="share-create-box">
<label class="share-label">Gültigkeitsdauer des Links:</label>
<div class="share-expiry-pills">
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="24h">
<span>24 Stunden</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="3d">
<span>3 Tage</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="7d" checked>
<span>7 Tage (Empfohlen)</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="30d">
<span>30 Tage</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="unlimited">
<span>Unbegrenzt</span>
</label>
</div>
<div class="share-input-row" style="margin-top: 14px;">
<input type="text" id="share-label-input" class="share-input" placeholder="Optionale Notiz (z. B. WhatsApp, Familie...)" maxlength="50">
<button type="button" id="btn-create-share" class="cb-btn cb-btn-green" onclick="submitCreateShare()">
<span>✨ Link erstellen</span>
</button>
</div>
</div>
<!-- Neuer erstellter Link (Highlight Box) -->
<div id="share-result-box" class="share-result-box" style="display: none;">
<div class="share-result-header">
<span>✓ Neuer Share-Link bereit:</span>
<span id="share-result-expiry-badge" class="share-status-pill share-badge-active"></span>
</div>
<div class="share-result-input-group">
<input type="text" id="share-result-url" class="share-url-field" readonly onclick="this.select()">
<button type="button" id="btn-copy-share" class="cb-btn cb-btn-green" onclick="copyCurrentShareUrl()">
<span id="copy-btn-icon">📋</span> <span id="copy-btn-text">Kopieren</span>
</button>
<button type="button" id="btn-native-share" class="cb-btn cb-btn-outline" style="display: none;" onclick="triggerNativeShare()">
<span>📤 Teilen</span>
</button>
</div>
<div class="share-result-hint">Jeder mit diesem Link hat für den gewählten Zeitraum vollen Lese- und Kochzugriff.</div>
</div>
<!-- Liste existierender Freigaben -->
<div class="share-existing-section">
<h4 class="share-existing-title">Aktive Freigaben für dieses Rezept</h4>
<div id="share-links-list" class="share-links-list">
<div class="share-links-loading">Lade aktive Links...</div>
</div>
</div>
</div>
<div class="cb-modal-footer" style="padding: 12px 18px; border-top: 1.5px solid #e0ded9; display: flex; justify-content: flex-end; background: #faf6f0;">
<button type="button" class="cb-btn cb-btn-outline" onclick="closeShareModal()">Schließen</button>
</div>
</div>
</div>
{% endif %}
<!-- Apple/iOS Safari & PWA Keep-Awake Video Element -->
<video id="cookmode-keepawake-video"
playsinline
webkit-playsinline
loop
muted
preload="auto"
aria-hidden="true"
style="position: fixed; right: 0; bottom: 0; width: 1px; height: 1px; opacity: 0.01; pointer-events: none; z-index: -9999;"
src="/static/media/cookmode_nosleep.mp4">
</video>
</main>
<script>
// ==========================================
// MOBILE NAVIGATION (3-PUNKTE-MENÜ)
// ==========================================
function toggleMobileMenu(e) {
if (e) e.stopPropagation();
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.toggle('is-open');
}
document.addEventListener('click', (e) => {
if (!e.target.closest('.mobile-nav-wrapper')) {
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.remove('is-open');
}
});
// ==========================================
// KOCHMODUS: SCREEN WAKE LOCK & iOS VIDEO KEEP-AWAKE
// ==========================================
let wakeLockSentinel = null;
let isCookModeActive = false;
// Video Keep-Awake für iOS Safari / Stromsparmodus / PWA
function getKeepAwakeVideo() {
return document.getElementById('cookmode-keepawake-video');
}
// Video-Loop Handler gegen AVPlayer Sleep-Suspension
function setupVideoLoop(v) {
if (!v || v._hasLoopListener) return;
v._hasLoopListener = true;
v.addEventListener('timeupdate', () => {
if (v.currentTime > 0.5) {
v.currentTime = 0;
}
});
}
async function acquireKeepAwake() {
let wakeLockSuccess = false;
// 1. Native Screen Wake Lock API (Safari iOS 16.4+, Chrome, Edge)
if ('wakeLock' in navigator && navigator.wakeLock && typeof navigator.wakeLock.request === 'function') {
try {
wakeLockSentinel = await navigator.wakeLock.request('screen');
wakeLockSentinel.addEventListener('release', () => {
wakeLockSentinel = null;
if (isCookModeActive && document.visibilityState === 'visible') {
acquireKeepAwake();
}
});
wakeLockSuccess = true;
} catch (err) {
console.warn('Screen WakeLock nicht verfügbar oder im Stromsparmodus:', err);
}
}
// 2. Video Keep-Awake Fallback (speziell für iOS Safari, iPadOS & Stromsparmodus)
try {
const v = getKeepAwakeVideo();
if (v) {
setupVideoLoop(v);
v.muted = true;
const playPromise = v.play();
if (playPromise !== undefined && typeof playPromise.then === 'function') {
await playPromise.catch(e => {
console.warn('Video keep-awake play Fehler:', e);
});
}
}
} catch (e) {
console.warn('Video Keep-Awake Fehler:', e);
}
return wakeLockSuccess;
}
function releaseKeepAwake() {
// 1. Release Wake Lock
if (wakeLockSentinel !== null) {
try { wakeLockSentinel.release(); } catch (e) {}
wakeLockSentinel = null;
}
// 2. Pause Video Keep-Awake
try {
const v = getKeepAwakeVideo();
if (v && !v.paused) {
v.pause();
}
} catch (e) {}
}
function updateCookModeUI(active) {
// Body-Klasse für aktiven Rahmen um das Rezept
if (active) {
document.body.classList.add('cook-mode-on');
} else {
document.body.classList.remove('cook-mode-on');
}
// 1. Toggle Button oben rechts im Rezept
const recipeBtn = document.getElementById('cookmode-recipe-btn');
const recipeBadge = document.getElementById('cookmode-recipe-badge');
if (recipeBtn) {
if (active) {
recipeBtn.classList.add('is-active');
if (recipeBadge) recipeBadge.textContent = 'An';
} else {
recipeBtn.classList.remove('is-active');
if (recipeBadge) recipeBadge.textContent = 'Aus';
}
}
// 2. Desktop Nav Button
const navBtn = document.getElementById('cookmode-nav-btn');
const navBadge = document.getElementById('cookmode-nav-badge');
if (navBtn) {
if (active) {
navBtn.classList.add('is-active');
if (navBadge) navBadge.textContent = 'An 💡';
} else {
navBtn.classList.remove('is-active');
if (navBadge) navBadge.textContent = 'Aus';
}
}
// 3. Mobile Kebab Menü Item
const mobileBtn = document.getElementById('cookmode-mobile-btn');
const mobileBadge = document.getElementById('cookmode-mobile-badge');
const mobileStatus = document.getElementById('cookmode-status-mobile');
if (mobileBtn) {
if (active) {
mobileBtn.classList.add('is-active');
if (mobileBadge) mobileBadge.textContent = 'An 💡';
if (mobileStatus) mobileStatus.textContent = 'Aktiv 💡 (Display bleibt an)';
} else {
mobileBtn.classList.remove('is-active');
if (mobileBadge) mobileBadge.textContent = 'Aus';
if (mobileStatus) mobileStatus.textContent = 'Display bleibt an';
}
}
}
async function toggleCookingMode() {
isCookModeActive = !isCookModeActive;
try {
localStorage.setItem('wtc_cookmode_active', isCookModeActive ? 'true' : 'false');
} catch (e) {}
updateCookModeUI(isCookModeActive);
if (isCookModeActive) {
await acquireKeepAwake();
showToast('✓ Kochmodus aktiv: Display bleibt an 💡');
} else {
releaseKeepAwake();
showToast('Kochmodus ausgeschaltet');
}
}
// Wenn der Nutzer den Tab wechselt und zurückkehrt
document.addEventListener('visibilitychange', async () => {
if (isCookModeActive && document.visibilityState === 'visible') {
await acquireKeepAwake();
}
});
// Gespeicherten Zustand beim Laden prüfen
window.addEventListener('DOMContentLoaded', () => {
loadChecksFromStorage();
try {
const saved = localStorage.getItem('wtc_cookmode_active') === 'true';
if (saved) {
isCookModeActive = true;
updateCookModeUI(true);
// User Gesture Listener für automatische Reaktivierung nach Reload
const unlockOnGesture = () => {
if (isCookModeActive) acquireKeepAwake();
document.removeEventListener('touchstart', unlockOnGesture);
document.removeEventListener('click', unlockOnGesture);
};
document.addEventListener('touchstart', unlockOnGesture, { passive: true });
document.addEventListener('click', unlockOnGesture, { passive: true });
}
} catch (e) {}
});
// ==========================================
// INTERAKTIVES ABHAKEN (ZUTATEN & SCHRITTE) MIT ZWISCHENSPEICHER
// ==========================================
const RECIPE_CHECK_STORAGE_KEY = 'wtc_recipe_checks_{{ recipe.id }}';
function getCheckableIngredients() {
return Array.from(document.querySelectorAll('.ingredients-list .ingredient-item, .portion-matrix-table .matrix-row'));
}
function getCheckableSteps() {
return Array.from(document.querySelectorAll('.instructions-steps .step-card'));
}
function updateResetButtonsVisibility(hasIngChecks, hasStepChecks) {
const ingResetBtn = document.getElementById('reset-ingredients-btn');
if (ingResetBtn) ingResetBtn.style.display = hasIngChecks ? 'inline-flex' : 'none';
const stepResetBtn = document.getElementById('reset-steps-btn');
if (stepResetBtn) stepResetBtn.style.display = hasStepChecks ? 'inline-flex' : 'none';
const menuResetBtn = document.getElementById('reset-all-menu-btn');
if (menuResetBtn) menuResetBtn.style.display = (hasIngChecks || hasStepChecks) ? 'flex' : 'none';
}
function saveChecksToStorage() {
try {
const ingEls = getCheckableIngredients();
const stepEls = getCheckableSteps();
const checkedIngredients = [];
ingEls.forEach((el, idx) => {
if (el.classList.contains('is-checked')) checkedIngredients.push(idx);
});
const checkedSteps = [];
stepEls.forEach((el, idx) => {
if (el.classList.contains('is-done')) checkedSteps.push(idx);
});
const data = {
recipeId: {{ recipe.id }},
ingredients: checkedIngredients,
steps: checkedSteps,
updatedAt: Date.now()
};
const json = JSON.stringify(data);
sessionStorage.setItem(RECIPE_CHECK_STORAGE_KEY, json);
localStorage.setItem(RECIPE_CHECK_STORAGE_KEY, json);
updateResetButtonsVisibility(checkedIngredients.length > 0, checkedSteps.length > 0);
} catch (e) {
console.warn('Fehler beim Speichern des Abhake-Status:', e);
}
}
function loadChecksFromStorage() {
try {
let raw = sessionStorage.getItem(RECIPE_CHECK_STORAGE_KEY);
if (!raw) {
raw = localStorage.getItem(RECIPE_CHECK_STORAGE_KEY);
}
if (!raw) {
updateResetButtonsVisibility(false, false);
return;
}
const data = JSON.parse(raw);
// Nach 8 Stunden verfällt der Zwischenspeicher automatisch
const EIGHT_HOURS = 8 * 60 * 60 * 1000;
if (data.updatedAt && (Date.now() - data.updatedAt > EIGHT_HOURS)) {
sessionStorage.removeItem(RECIPE_CHECK_STORAGE_KEY);
localStorage.removeItem(RECIPE_CHECK_STORAGE_KEY);
updateResetButtonsVisibility(false, false);
return;
}
const ingEls = getCheckableIngredients();
if (Array.isArray(data.ingredients)) {
data.ingredients.forEach(idx => {
if (ingEls[idx]) ingEls[idx].classList.add('is-checked');
});
}
const stepEls = getCheckableSteps();
if (Array.isArray(data.steps)) {
data.steps.forEach(idx => {
if (stepEls[idx]) stepEls[idx].classList.add('is-done');
});
}
const hasIngChecks = Array.isArray(data.ingredients) && data.ingredients.length > 0;
const hasStepChecks = Array.isArray(data.steps) && data.steps.length > 0;
updateResetButtonsVisibility(hasIngChecks, hasStepChecks);
} catch (e) {
console.warn('Fehler beim Laden des Abhake-Status:', e);
}
}
// Interaktive Zutaten-Checkliste (zum Abhaken beim Vorbereiten)
function toggleIngredientCheck(el) {
el.classList.toggle('is-checked');
saveChecksToStorage();
}
// Interaktiver Schritt-Tracker (zum Markieren erledigter Schritte)
function toggleStepCheck(el) {
el.classList.toggle('is-done');
saveChecksToStorage();
}
function resetIngredientChecks() {
const ingEls = getCheckableIngredients();
ingEls.forEach(el => el.classList.remove('is-checked'));
saveChecksToStorage();
showToast('Zutaten zurückgesetzt');
}
function resetStepChecks() {
const stepEls = getCheckableSteps();
stepEls.forEach(el => el.classList.remove('is-done'));
saveChecksToStorage();
showToast('Schritte zurückgesetzt');
}
function resetAllChecks() {
const ingEls = getCheckableIngredients();
ingEls.forEach(el => el.classList.remove('is-checked'));
const stepEls = getCheckableSteps();
stepEls.forEach(el => el.classList.remove('is-done'));
try {
sessionStorage.removeItem(RECIPE_CHECK_STORAGE_KEY);
localStorage.removeItem(RECIPE_CHECK_STORAGE_KEY);
} catch (e) {}
updateResetButtonsVisibility(false, false);
showToast('Alles zurückgesetzt');
}
// Toast Benachrichtigung
function showToast(msg) {
let toast = document.getElementById('cook-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'cook-toast';
toast.className = 'cb-toast';
document.body.appendChild(toast);
}
toast.textContent = msg;
toast.style.display = 'flex';
setTimeout(() => {
toast.style.display = 'none';
}, 2500);
}
// ==========================================
{% if not is_shared %}
// SHARE MODAL & FREIGABELINKS
// ==========================================
let currentShareUrl = '';
function openShareModal() {
const modal = document.getElementById('share-modal');
if (modal) {
modal.classList.add('active');
document.body.classList.add('modal-open');
loadActiveShareLinks();
}
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.remove('is-open');
}
function closeShareModal(e) {
if (e && e.target && e.target.classList && !e.target.classList.contains('cb-modal-backdrop') && !e.target.classList.contains('cb-modal-close') && e.target.tagName !== 'BUTTON') {
return;
}
const modal = document.getElementById('share-modal');
if (modal) {
modal.classList.remove('active');
document.body.classList.remove('modal-open');
}
}
async function loadActiveShareLinks() {
const listEl = document.getElementById('share-links-list');
if (!listEl) return;
try {
const resp = await fetch('/api/recipes/{{ recipe.id }}/shares');
if (!resp.ok) throw new Error('Fehler beim Laden');
const data = await resp.json();
renderShareLinks(data.shares || []);
} catch (err) {
listEl.innerHTML = '<div class="share-empty-state">Konnte aktive Links nicht laden.</div>';
}
}
function renderShareLinks(shares) {
const listEl = document.getElementById('share-links-list');
if (!listEl) return;
if (!shares || shares.length === 0) {
listEl.innerHTML = '<div class="share-empty-state">Noch keine Share-Links für dieses Rezept erstellt.</div>';
return;
}
listEl.innerHTML = shares.map(s => {
const isExp = s.is_expired;
const statusClass = isExp ? 'share-badge-expired' : 'share-badge-active';
const labelText = s.label ? `<span class="share-link-label">🏷️ ${escapeHtml(s.label)}</span>` : '';
const viewsText = `<span class="share-link-views">👁️ ${s.views_count || 0} ${s.views_count === 1 ? 'Aufruf' : 'Aufrufe'}</span>`;
const canonicalUrl = window.location.origin + '/shared/recipe/' + s.token;
return `
<div class="share-link-card ${isExp ? 'is-expired' : ''}">
<div class="share-card-main">
<div class="share-card-header">
${labelText}
<span class="share-status-pill ${statusClass}">${s.relative_status}</span>
</div>
<div class="share-card-meta">
<span>Erstellt: ${s.created_at || ''}</span>
${viewsText}
</div>
<div class="share-card-url-preview">
<input type="text" class="share-mini-url" value="${canonicalUrl}" readonly onclick="this.select()">
</div>
</div>
<div class="share-card-actions">
${!isExp ? `<button type="button" class="cb-btn cb-btn-sm cb-btn-outline" onclick="copyTextToClipboard('${canonicalUrl}', this)" title="In Zwischenablage kopieren">📋 Kopieren</button>` : ''}
<button type="button" class="cb-btn cb-btn-sm cb-btn-danger" onclick="deleteShareLink(${s.id})" title="Freigabe widerrufen">🗑️ Löschen</button>
</div>
</div>
`;
}).join('');
}
async function submitCreateShare() {
const btn = document.getElementById('btn-create-share');
const expiryRadio = document.querySelector('input[name="share_expiry"]:checked');
const labelInput = document.getElementById('share-label-input');
const expiryVal = expiryRadio ? expiryRadio.value : '7d';
const labelVal = labelInput ? labelInput.value.trim() : '';
if (btn) btn.disabled = true;
try {
const resp = await fetch('/api/recipes/{{ recipe.id }}/shares', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ expires_in: expiryVal, label: labelVal })
});
if (!resp.ok) throw new Error('Erstellen fehlgeschlagen');
const data = await resp.json();
const share = data.share;
currentShareUrl = window.location.origin + '/shared/recipe/' + share.token;
const resBox = document.getElementById('share-result-box');
const resUrl = document.getElementById('share-result-url');
const badge = document.getElementById('share-result-expiry-badge');
if (resBox && resUrl) {
resUrl.value = currentShareUrl;
if (badge) badge.textContent = share.relative_status;
resBox.style.display = 'block';
resUrl.select();
}
const nativeBtn = document.getElementById('btn-native-share');
if (nativeBtn && navigator.share) {
nativeBtn.style.display = 'inline-flex';
}
if (labelInput) labelInput.value = '';
showToast('✓ Share-Link erfolgreich erstellt!');
loadActiveShareLinks();
} catch (err) {
showToast('Fehler beim Erstellen des Links');
} finally {
if (btn) btn.disabled = false;
}
}
async function copyCurrentShareUrl() {
const input = document.getElementById('share-result-url');
if (input && input.value) {
await copyTextToClipboard(input.value);
const copyBtnText = document.getElementById('copy-btn-text');
if (copyBtnText) {
copyBtnText.textContent = 'Kopiert! ✓';
setTimeout(() => { copyBtnText.textContent = 'Kopieren'; }, 2000);
}
}
}
async function copyTextToClipboard(text, btnEl) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
} else {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
showToast('✓ In Zwischenablage kopiert!');
if (btnEl) {
const orig = btnEl.innerHTML;
btnEl.innerHTML = '✓ Kopiert!';
setTimeout(() => { btnEl.innerHTML = orig; }, 2000);
}
} catch (e) {
showToast('Konnte Link nicht automatisch kopieren');
}
}
async function triggerNativeShare() {
if (navigator.share && currentShareUrl) {
try {
await navigator.share({
title: 'Rezept: {{ recipe.title }}',
text: 'Hier ist das Rezept für {{ recipe.title }}:',
url: currentShareUrl
});
} catch (e) {}
}
}
async function deleteShareLink(shareId) {
if (!confirm('Diesen Share-Link wirklich löschen? Empfänger können das Rezept danach nicht mehr über diesen Link aufrufen.')) {
return;
}
try {
const resp = await fetch(`/api/recipes/{{ recipe.id }}/shares/${shareId}`, { method: 'DELETE' });
if (!resp.ok) throw new Error('Fehler beim Löschen');
showToast('Share-Link gelöscht');
loadActiveShareLinks();
const resBox = document.getElementById('share-result-box');
if (resBox) resBox.style.display = 'none';
} catch (err) {
showToast('Konnte Share-Link nicht löschen');
}
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
{% endif %}
</script>
</body>
</html>