- 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>
200 lines
8.8 KiB
HTML
200 lines
8.8 KiB
HTML
<!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>
|