ux(vwb): timer capture — default 5s, label dynamique, log diagnostic

Bug terrain : le bouton 'Timer' déclenchait toujours une capture immédiate
même après sélection d'un délai dans le menu déroulant. Le retour utilisateur
'le bouton ne change pas' a confirmé qu'il n'y avait aucun feedback visuel
sur le délai sélectionné, donc impossible de diagnostiquer.

Changements :
- timerSeconds default 5s (préférence Dom) au lieu de 0 (Immediat)
- Label dynamique du bouton :
    countdown actif → '5…' '4…' etc.
    délai 0 → 'Timer' (capture immédiate)
    délai > 0 → 'Capturer dans 5s'
- Select préfixé par 'Délai :' pour clarifier
- Conversion explicite String(timerSeconds) sur value du select pour éviter
  toute ambiguïté number/string
- console.log temporaire au changement de select pour faciliter le diagnostic
  si le bug persiste (à retirer après validation)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-29 18:20:16 +02:00
parent 6261002039
commit 90c1d8036f

View File

@@ -4,7 +4,8 @@ import type { UIElement } from '../services/uiDetection';
import {
loadLibraryAsync,
saveLibrary,
compressThumbnail,
addCaptureToLibrary,
removeCaptureFromLibrary,
} from '../services/captureLibraryStorage';
/**
@@ -40,6 +41,8 @@ interface LibraryItem {
timestamp: Date;
sessionId?: string;
favorite?: boolean;
format?: 'v2';
fullImageUrl?: string;
}
export default function CapturePanel({
@@ -55,7 +58,7 @@ export default function CapturePanel({
const [showLibraryGallery, setShowLibraryGallery] = useState(false);
const [library, setLibrary] = useState<LibraryItem[]>([]);
const [currentCapture, setCurrentCapture] = useState<Capture | null>(null);
const [timerSeconds, setTimerSeconds] = useState(0);
const [timerSeconds, setTimerSeconds] = useState(5);
const [countdown, setCountdown] = useState<number | null>(null);
// Elements detectes sur l'apercu miniature
const [previewElements, setPreviewElements] = useState<UIElement[]>([]);
@@ -89,21 +92,27 @@ export default function CapturePanel({
}
}, [library, libraryLoaded]);
// Ajouter capture a la bibliotheque (thumbnail compresse JPEG 320x240)
// Ajouter capture à la bibliothèque (format v2 : PNG HD côté backend,
// thumbnail 640x360 q85 dans le JSON pour la grille).
useEffect(() => {
if (!capture) return;
setCurrentCapture(capture);
let cancelled = false;
(async () => {
const compressed = await compressThumbnail(capture.screenshot_base64);
const item = await addCaptureToLibrary(capture, { id: `cap_${Date.now()}` });
if (cancelled) return;
const newItem: LibraryItem = {
id: `cap_${Date.now()}`,
capture: { ...capture, screenshot_base64: compressed },
timestamp: new Date(),
favorite: false,
};
setLibrary(prev => [newItem, ...prev.slice(0, 19)]);
setLibrary(prev => [
{
id: item.id,
capture: item.capture,
timestamp: typeof item.timestamp === 'string' ? new Date(item.timestamp) : item.timestamp,
sessionId: item.sessionId,
favorite: item.favorite ?? false,
format: item.format,
fullImageUrl: item.fullImageUrl,
},
...prev.slice(0, 19),
]);
})();
return () => { cancelled = true; };
}, [capture]);
@@ -189,13 +198,44 @@ export default function CapturePanel({
}, 1000);
};
const handleLibrarySelect = (item: LibraryItem) => {
setCurrentCapture(item.capture);
const handleLibrarySelect = async (item: LibraryItem) => {
// Format v2 : remplacer le thumbnail par le PNG HD téléchargé du backend
// pour que la sélection d'ancre utilise une image non pixélisée.
if (item.format === 'v2' && item.fullImageUrl) {
try {
const resp = await fetch(item.fullImageUrl);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const blob = await resp.blob();
const base64 = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
// FileReader → "data:image/png;base64,..." → on retire le préfixe
const idx = result.indexOf(',');
resolve(idx >= 0 ? result.slice(idx + 1) : result);
};
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
setCurrentCapture({ ...item.capture, screenshot_base64: base64 });
} catch (e) {
console.warn('[CaptureLibrary] Échec chargement HD, fallback thumbnail', e);
setCurrentCapture(item.capture);
}
} else {
setCurrentCapture(item.capture);
}
setIsFullscreen(true);
};
const handleDeleteLibraryItem = (id: string) => {
const target = library.find(it => it.id === id);
setLibrary(prev => prev.filter(item => item.id !== id));
// v2 : supprimer aussi le PNG côté backend (le saveLibrary auto-déclenché
// par le useEffect ne nettoie que le JSON, pas les fichiers PNG orphelins).
if (target?.format === 'v2') {
void removeCaptureFromLibrary(id, true);
}
};
return (
@@ -204,17 +244,35 @@ export default function CapturePanel({
{/* Capture — auto-detection OS navigateur */}
<div className="capture-controls">
<button disabled={countdown !== null} onClick={doSmartCapture}>
<button disabled={countdown !== null} onClick={doSmartCapture} title="Capture immédiate (sans délai)">
Capturer
</button>
<select value={timerSeconds} onChange={(e) => setTimerSeconds(Number(e.target.value))}>
<option value="0">Immediat</option>
<option value="3">3 sec</option>
<option value="5">5 sec</option>
<option value="10">10 sec</option>
</select>
<button onClick={handleTimerCapture} disabled={countdown !== null}>
{countdown !== null ? countdown : 'Timer'}
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }}>
Délai :
<select
value={String(timerSeconds)}
onChange={(e) => {
const v = Number(e.target.value);
console.log('[CapturePanel] timerSeconds →', v);
setTimerSeconds(v);
}}
>
<option value="0">Immediat</option>
<option value="3">3 sec</option>
<option value="5">5 sec</option>
<option value="10">10 sec</option>
</select>
</label>
<button
onClick={handleTimerCapture}
disabled={countdown !== null}
title={`Capture après ${timerSeconds}s — utile pour préparer l'écran avant la prise`}
>
{countdown !== null
? `${countdown}`
: timerSeconds === 0
? 'Timer'
: `Capturer dans ${timerSeconds}s`}
</button>
</div>