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 {
|
import {
|
||||||
loadLibraryAsync,
|
loadLibraryAsync,
|
||||||
saveLibrary,
|
saveLibrary,
|
||||||
compressThumbnail,
|
addCaptureToLibrary,
|
||||||
|
removeCaptureFromLibrary,
|
||||||
} from '../services/captureLibraryStorage';
|
} from '../services/captureLibraryStorage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +41,8 @@ interface LibraryItem {
|
|||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
favorite?: boolean;
|
favorite?: boolean;
|
||||||
|
format?: 'v2';
|
||||||
|
fullImageUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CapturePanel({
|
export default function CapturePanel({
|
||||||
@@ -55,7 +58,7 @@ export default function CapturePanel({
|
|||||||
const [showLibraryGallery, setShowLibraryGallery] = useState(false);
|
const [showLibraryGallery, setShowLibraryGallery] = useState(false);
|
||||||
const [library, setLibrary] = useState<LibraryItem[]>([]);
|
const [library, setLibrary] = useState<LibraryItem[]>([]);
|
||||||
const [currentCapture, setCurrentCapture] = useState<Capture | null>(null);
|
const [currentCapture, setCurrentCapture] = useState<Capture | null>(null);
|
||||||
const [timerSeconds, setTimerSeconds] = useState(0);
|
const [timerSeconds, setTimerSeconds] = useState(5);
|
||||||
const [countdown, setCountdown] = useState<number | null>(null);
|
const [countdown, setCountdown] = useState<number | null>(null);
|
||||||
// Elements detectes sur l'apercu miniature
|
// Elements detectes sur l'apercu miniature
|
||||||
const [previewElements, setPreviewElements] = useState<UIElement[]>([]);
|
const [previewElements, setPreviewElements] = useState<UIElement[]>([]);
|
||||||
@@ -89,21 +92,27 @@ export default function CapturePanel({
|
|||||||
}
|
}
|
||||||
}, [library, libraryLoaded]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!capture) return;
|
if (!capture) return;
|
||||||
setCurrentCapture(capture);
|
setCurrentCapture(capture);
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
(async () => {
|
(async () => {
|
||||||
const compressed = await compressThumbnail(capture.screenshot_base64);
|
const item = await addCaptureToLibrary(capture, { id: `cap_${Date.now()}` });
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const newItem: LibraryItem = {
|
setLibrary(prev => [
|
||||||
id: `cap_${Date.now()}`,
|
{
|
||||||
capture: { ...capture, screenshot_base64: compressed },
|
id: item.id,
|
||||||
timestamp: new Date(),
|
capture: item.capture,
|
||||||
favorite: false,
|
timestamp: typeof item.timestamp === 'string' ? new Date(item.timestamp) : item.timestamp,
|
||||||
};
|
sessionId: item.sessionId,
|
||||||
setLibrary(prev => [newItem, ...prev.slice(0, 19)]);
|
favorite: item.favorite ?? false,
|
||||||
|
format: item.format,
|
||||||
|
fullImageUrl: item.fullImageUrl,
|
||||||
|
},
|
||||||
|
...prev.slice(0, 19),
|
||||||
|
]);
|
||||||
})();
|
})();
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [capture]);
|
}, [capture]);
|
||||||
@@ -189,13 +198,44 @@ export default function CapturePanel({
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLibrarySelect = (item: LibraryItem) => {
|
const handleLibrarySelect = async (item: LibraryItem) => {
|
||||||
setCurrentCapture(item.capture);
|
// 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);
|
setIsFullscreen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteLibraryItem = (id: string) => {
|
const handleDeleteLibraryItem = (id: string) => {
|
||||||
|
const target = library.find(it => it.id === id);
|
||||||
setLibrary(prev => prev.filter(item => item.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 (
|
return (
|
||||||
@@ -204,17 +244,35 @@ export default function CapturePanel({
|
|||||||
|
|
||||||
{/* Capture — auto-detection OS navigateur */}
|
{/* Capture — auto-detection OS navigateur */}
|
||||||
<div className="capture-controls">
|
<div className="capture-controls">
|
||||||
<button disabled={countdown !== null} onClick={doSmartCapture}>
|
<button disabled={countdown !== null} onClick={doSmartCapture} title="Capture immédiate (sans délai)">
|
||||||
Capturer
|
Capturer
|
||||||
</button>
|
</button>
|
||||||
<select value={timerSeconds} onChange={(e) => setTimerSeconds(Number(e.target.value))}>
|
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }}>
|
||||||
<option value="0">Immediat</option>
|
Délai :
|
||||||
<option value="3">3 sec</option>
|
<select
|
||||||
<option value="5">5 sec</option>
|
value={String(timerSeconds)}
|
||||||
<option value="10">10 sec</option>
|
onChange={(e) => {
|
||||||
</select>
|
const v = Number(e.target.value);
|
||||||
<button onClick={handleTimerCapture} disabled={countdown !== null}>
|
console.log('[CapturePanel] timerSeconds →', v);
|
||||||
{countdown !== null ? countdown : 'Timer'}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user