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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user