import { useEffect, useRef, useState } from 'react'; import './PTTButton.css'; /** * Bouton PTT principal * Gère touch et mouse events pour desktop et mobile * Modes : * - PTT classique : maintenir pour parler * - Mode continu (lock) : glisser vers le haut pendant qu'on parle * Inclut VU-mètre intégré (anneau autour du bouton) */ export default function PTTButton({ isTalking, onPressStart, onPressEnd, audioLevel = 0 }) { const buttonRef = useRef(null); const isPressingRef = useRef(false); const [isLockMode, setIsLockMode] = useState(false); const isLockModeRef = useRef(false); // Ref pour accès immédiat dans event handlers // Drag tracking const dragStartYRef = useRef(null); const currentYRef = useRef(null); const [dragOffset, setDragOffset] = useState(0); // Offset visuel du drag (en pixels) useEffect(() => { const button = buttonRef.current; if (!button) return; // Empêcher comportements par défaut const preventDefault = (e) => { e.preventDefault(); }; // Touch events (mobile) const handleTouchStart = (e) => { e.preventDefault(); const touch = e.touches[0]; console.log('🖐️ Touch start at Y:', touch.clientY); // En mode lock, un tap désactive le mode if (isLockModeRef.current) { toggleLockMode(); return; } // Mode PTT normal : démarrer + init drag if (!isPressingRef.current) { isPressingRef.current = true; dragStartYRef.current = touch.clientY; currentYRef.current = touch.clientY; onPressStart(); } }; const handleTouchMove = (e) => { e.preventDefault(); // Pas de drag en mode lock if (isLockModeRef.current || !isPressingRef.current) { return; } const touch = e.touches[0]; currentYRef.current = touch.clientY; // Calculer le déplacement vertical (négatif = vers le haut) const deltaY = dragStartYRef.current - touch.clientY; // Limiter le drag vers le haut (max 100px) const offset = Math.max(0, Math.min(100, deltaY)); setDragOffset(offset); console.log('📏 Drag offset:', offset); // Si on a glissé de 80px vers le haut, activer le mode lock if (offset >= 80) { activateLockMode(); } }; const handleTouchEnd = (e) => { e.preventDefault(); console.log('🖐️ Touch end, dragOffset:', dragOffset); // Réinitialiser le drag dragStartYRef.current = null; currentYRef.current = null; setDragOffset(0); // En mode lock, ne rien faire (le micro reste actif) if (isLockModeRef.current) { return; } // Mode PTT normal : arrêter if (isPressingRef.current) { isPressingRef.current = false; onPressEnd(); } }; // Mouse events (desktop) const handleMouseDown = (e) => { e.preventDefault(); console.log('🖱️ Mouse down at Y:', e.clientY); // En mode lock, un clic désactive le mode if (isLockModeRef.current) { toggleLockMode(); return; } if (!isPressingRef.current) { isPressingRef.current = true; dragStartYRef.current = e.clientY; currentYRef.current = e.clientY; onPressStart(); } }; const handleMouseMove = (e) => { // Pas de drag en mode lock if (isLockModeRef.current || !isPressingRef.current) { return; } currentYRef.current = e.clientY; // Calculer le déplacement vertical (négatif = vers le haut) const deltaY = dragStartYRef.current - e.clientY; // Limiter le drag vers le haut (max 100px) const offset = Math.max(0, Math.min(100, deltaY)); setDragOffset(offset); // Si on a glissé de 80px vers le haut, activer le mode lock if (offset >= 80) { activateLockMode(); } }; const handleMouseUp = (e) => { e.preventDefault(); console.log('🖱️ Mouse up, dragOffset:', dragOffset); // Réinitialiser le drag dragStartYRef.current = null; currentYRef.current = null; setDragOffset(0); // En mode lock, ne rien faire if (isLockModeRef.current) { return; } if (isPressingRef.current) { isPressingRef.current = false; onPressEnd(); } }; const handleMouseLeave = (e) => { // Si on quitte le bouton en maintenant, on arrête (sauf en mode lock) if (!isLockModeRef.current && isPressingRef.current) { // Réinitialiser le drag dragStartYRef.current = null; currentYRef.current = null; setDragOffset(0); isPressingRef.current = false; onPressEnd(); } }; // Attacher events button.addEventListener('touchstart', handleTouchStart, { passive: false }); button.addEventListener('touchmove', handleTouchMove, { passive: false }); button.addEventListener('touchend', handleTouchEnd, { passive: false }); button.addEventListener('touchcancel', handleTouchEnd, { passive: false }); button.addEventListener('mousedown', handleMouseDown); button.addEventListener('mousemove', handleMouseMove); button.addEventListener('mouseup', handleMouseUp); button.addEventListener('mouseleave', handleMouseLeave); button.addEventListener('contextmenu', preventDefault); return () => { button.removeEventListener('touchstart', handleTouchStart); button.removeEventListener('touchmove', handleTouchMove); button.removeEventListener('touchend', handleTouchEnd); button.removeEventListener('touchcancel', handleTouchEnd); button.removeEventListener('mousedown', handleMouseDown); button.removeEventListener('mousemove', handleMouseMove); button.removeEventListener('mouseup', handleMouseUp); button.removeEventListener('mouseleave', handleMouseLeave); button.removeEventListener('contextmenu', preventDefault); }; }, [onPressStart, onPressEnd]); // Fonction pour activer le mode lock const activateLockMode = () => { console.log('🔒 Mode lock activé par drag'); setIsLockMode(true); isLockModeRef.current = true; // Réinitialiser le drag setDragOffset(0); dragStartYRef.current = null; currentYRef.current = null; // Le micro est déjà actif (onPressStart a été appelé) // Vibration pour feedback if (navigator.vibrate) { navigator.vibrate([100, 50, 100]); } }; // Fonction pour basculer le mode lock (appelée par le toggle externe) const toggleLockMode = () => { const newLockMode = !isLockModeRef.current; console.log('🔄 Toggle lock mode:', isLockModeRef.current, '→', newLockMode); setIsLockMode(newLockMode); isLockModeRef.current = newLockMode; if (newLockMode) { // Activer le mode lock : démarrer l'audio console.log('🔒 Mode lock ON'); onPressStart(); // Vibration pour feedback if (navigator.vibrate) { navigator.vibrate([100, 50, 100]); } } else { // Désactiver le mode lock : couper l'audio console.log('🔓 Mode lock OFF'); onPressEnd(); // Vibration pour feedback if (navigator.vibrate) { navigator.vibrate(50); } } }; // Calculer le niveau audio normalisé (0-100) const normalizedLevel = Math.min(100, Math.max(0, audioLevel)); // Convertir le niveau en angle pour le cercle SVG (0-360°) const levelAngle = (normalizedLevel / 100) * 360; // Calculer le dasharray pour l'arc SVG const radius = 130; // Rayon du cercle VU-mètre const circumference = 2 * Math.PI * radius; const dashOffset = circumference - (levelAngle / 360) * circumference; // Déterminer la couleur selon le niveau const getAudioColor = () => { if (normalizedLevel > 90) return '#ef4444'; // Danger (rouge) if (normalizedLevel > 75) return '#f59e0b'; // Warning (orange) if (isTalking) return '#3b82f6'; // Talking (bleu) return '#10b981'; // Normal (vert) }; return (
{/* Zone de drag vers le haut (indicateur visuel) */} {dragOffset > 0 && !isLockMode && (
Glissez pour verrouiller
)} {/* Conteneur bouton + VU-mètre */}
{/* VU-mètre circulaire (SVG) */} {/* Cercle de fond (gris) */} {/* Cercle de niveau audio (coloré) */} 0 ? `drop-shadow(0 0 8px ${getAudioColor()})` : 'none' }} /> {/* Bouton PTT principal */}

{isLockMode ? 'Tapez pour désactiver le mode continu' : isTalking ? 'Glissez vers le haut pour verrouiller • Relâchez pour arrêter' : 'Appuyez et maintenez pour parler • Glissez vers le haut pour verrouiller'}

); }