Files
camwatch/app/templates/event_detail.html
T
percoandClaude Sonnet 4.6 6aae4ef40c Toutes les frames, clip web-compatible, lightbox plein écran
- Garde toutes les frames (plus de limite TOP_FRAMES), triées score LPR desc
- Clip remuxé avec -movflags +faststart pour lecture directe navigateur + lien téléchargement
- Lightbox : double-clic sur frame → plein écran, navigation ←/→, Échap pour fermer
- Grille 8 colonnes pour voir toutes les frames

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:37:44 +02:00

200 lines
8.8 KiB
HTML
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="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CamWatch — {{ ev.time_str }}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>tailwind.config = { darkMode: 'class' }</script>
<style>
body { background: #0f172a; color: #e2e8f0; }
.card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; }
.plate { font-family: monospace; letter-spacing: 0.15em; }
.btn-ghost { background: #1e293b; border: 1px solid #475569; color: #94a3b8; border-radius: 6px; padding: 6px 14px; font-size: 0.85rem; text-decoration: none; display: inline-block; }
.btn-ghost:hover { background: #334155; color: #e2e8f0; }
.btn-dl { background: #166534; border: 1px solid #16a34a; color: #4ade80; border-radius: 6px; padding: 6px 14px; font-size: 0.85rem; text-decoration: none; display: inline-block; }
.btn-dl:hover { background: #14532d; }
.frame-thumb { cursor: zoom-in; border: 2px solid #334155; border-radius: 6px; overflow: hidden; transition: border-color 0.12s; }
.frame-thumb:hover { border-color: #60a5fa; }
.frame-thumb.active { border-color: #3b82f6; }
label { color: #64748b; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
/* Lightbox */
#lightbox { display:none; position:fixed; inset:0; z-index:50; background:rgba(0,0,0,0.92); align-items:center; justify-content:center; }
#lightbox.open { display:flex; }
#lightbox img { max-width:95vw; max-height:92vh; object-fit:contain; border-radius:4px; }
#lb-nav { position:absolute; bottom:16px; left:50%; transform:translateX(-50%); display:flex; gap:12px; align-items:center; }
#lb-close { position:absolute; top:12px; right:16px; font-size:1.8rem; color:#94a3b8; cursor:pointer; line-height:1; }
#lb-close:hover { color:#fff; }
#lb-label { color:#94a3b8; font-size:0.85rem; }
</style>
</head>
<body class="min-h-screen">
<header class="sticky top-0 z-10 px-4 py-3 flex items-center gap-3" style="background:#0f172a;border-bottom:1px solid #1e293b;">
<a href="/" class="btn-ghost">← Retour</a>
<span class="text-xl">🚗</span>
<span class="font-bold">{{ ev.time_str }}</span>
</header>
<main class="max-w-5xl mx-auto px-3 py-5 space-y-4">
<!-- Info + main image -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="card p-4 space-y-4">
<div>
<label>Plaque</label>
<div class="mt-1">
{% if ev.plate %}
<span class="plate text-xl font-bold px-4 py-2 rounded" style="background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb;">{{ ev.plate }}</span>
{% else %}
<span class="text-slate-500 italic">Non lue</span>
{% endif %}
</div>
</div>
<div>
<label>Couleur</label>
<div class="flex items-center gap-2 mt-1">
<div class="w-7 h-7 rounded-full border-2 border-slate-600" style="background:{{ ev.color_hex or '#808080' }};"></div>
<span class="font-medium">{{ ev.color_name or '—' }}</span>
<span class="text-slate-600 text-xs font-mono">{{ ev.color_hex or '' }}</span>
</div>
</div>
<div class="pt-2 border-t border-slate-700">
<label>Debug</label>
<div class="mt-1 space-y-0.5 text-xs font-mono text-slate-400">
<div>id: {{ ev.id[:8] }}…</div>
<div>frames: {{ frames|length }} (~{{ capture_total }} capturées)</div>
<div>clip: {% if ev.clip_path %}✓ {{ clip_size }}{% else %}—{% endif %}</div>
</div>
</div>
{% if has_clip %}
<a href="/{{ ev.clip_path }}" download class="btn-dl w-full text-center">⬇ Télécharger le clip</a>
{% endif %}
</div>
<div class="md:col-span-2 card overflow-hidden">
<div class="bg-black relative cursor-zoom-in" style="aspect-ratio:16/9;"
onclick="openLightbox('/{{ ev.snapshot_path }}', 'Meilleure frame ★')">
<img id="main-img"
src="{% if ev.snapshot_path %}/{{ ev.snapshot_path }}{% endif %}"
class="w-full h-full object-contain">
<span class="absolute bottom-2 right-2 text-xs text-slate-500">🔍 cliquer pour agrandir</span>
</div>
<div class="px-3 py-2 text-xs text-slate-500 text-center" id="img-label">Meilleure frame ★</div>
</div>
</div>
<!-- Video clip -->
{% if has_clip %}
<div class="card p-4">
<div class="flex items-center justify-between mb-3">
<label>Clip vidéo — {{ capture_duration }}s</label>
<a href="/{{ ev.clip_path }}" download class="btn-dl text-xs py-1 px-3">⬇ Télécharger</a>
</div>
<video controls preload="metadata" class="w-full rounded-lg" style="max-height:380px;background:#000;">
<source src="/{{ ev.clip_path }}" type="video/mp4">
<p class="text-slate-400 text-sm p-4">Lecture impossible dans le navigateur —
<a href="/{{ ev.clip_path }}" download class="text-blue-400 underline">télécharger le clip</a></p>
</video>
</div>
{% else %}
<div class="card p-4 text-slate-500 text-sm">
<label class="block mb-1">Clip vidéo</label>
{% if ev.clip_path %}Fichier introuvable{% else %}Événement antérieur à la fonctionnalité clip{% endif %}
</div>
{% endif %}
<!-- All frames -->
{% if frames %}
<div class="card p-4">
<label class="block mb-3">
Toutes les frames — {{ frames|length }} extraites à {{ capture_fps }}fps, triées meilleure plaque en premier
</label>
<div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-1.5">
{% for frame_path in frames %}
<div class="frame-thumb {% if loop.first %}active{% endif %}"
onclick="selectFrame(this, '/{{ frame_path }}', 'Frame #{{ loop.index }}{% if loop.first %} ★{% endif %}')"
ondblclick="openLightbox('/{{ frame_path }}', 'Frame #{{ loop.index }}')">
<img src="/{{ frame_path }}" class="w-full object-cover" style="aspect-ratio:16/9;" loading="lazy">
<div class="text-center text-xs text-slate-500 py-0.5 bg-slate-900 leading-none">
#{{ loop.index }}{% if loop.first %} ★{% endif %}
</div>
</div>
{% endfor %}
</div>
<p class="text-xs text-slate-600 mt-2">Clic simple → aperçu | Double-clic → plein écran</p>
</div>
{% else %}
<div class="card p-4 text-slate-500 text-sm">
<label class="block mb-1">Frames</label>
Événement antérieur à la fonctionnalité frames
</div>
{% endif %}
</main>
<!-- Lightbox -->
<div id="lightbox" onclick="closeLightboxIfOutside(event)">
<span id="lb-close" onclick="closeLightbox()">×</span>
<img id="lb-img" src="" alt="">
<div id="lb-nav">
<button onclick="lbPrev()" class="btn-ghost px-4"></button>
<span id="lb-label" class="text-slate-400 text-sm"></span>
<button onclick="lbNext()" class="btn-ghost px-4"></button>
</div>
</div>
<script>
let lbFrames = {{ frames | tojson }};
let lbIdx = 0;
function openLightbox(src, label) {
document.getElementById('lb-img').src = src;
document.getElementById('lb-label').textContent = label;
// Find index in frames array
const rel = src.startsWith('/') ? src.slice(1) : src;
lbIdx = lbFrames.indexOf(rel);
document.getElementById('lightbox').classList.add('open');
}
function closeLightbox() {
document.getElementById('lightbox').classList.remove('open');
}
function closeLightboxIfOutside(e) {
if (e.target.id === 'lightbox') closeLightbox();
}
function lbGo(idx) {
if (lbFrames.length === 0) return;
lbIdx = ((idx % lbFrames.length) + lbFrames.length) % lbFrames.length;
document.getElementById('lb-img').src = '/' + lbFrames[lbIdx];
document.getElementById('lb-label').textContent = 'Frame #' + (lbIdx + 1) + (lbIdx === 0 ? ' ★' : '');
}
function lbPrev() { lbGo(lbIdx - 1); }
function lbNext() { lbGo(lbIdx + 1); }
function selectFrame(el, src, label) {
document.getElementById('main-img').src = src;
document.getElementById('main-img').parentElement.onclick = () => openLightbox(src, label);
document.getElementById('img-label').textContent = label;
document.querySelectorAll('.frame-thumb').forEach(t => t.classList.remove('active'));
el.classList.add('active');
}
document.addEventListener('keydown', e => {
if (!document.getElementById('lightbox').classList.contains('open')) return;
if (e.key === 'Escape') closeLightbox();
if (e.key === 'ArrowLeft') lbPrev();
if (e.key === 'ArrowRight') lbNext();
});
</script>
</body>
</html>