fix: refonte mode PTT continu avec activation par drag vertical
CORRECTIONS : - Fix son qui ne restait pas actif en mode lock (utilisation de ref au lieu de state) - Les event handlers utilisent isLockModeRef.current pour accès immédiat NOUVELLE ACTIVATION : - Remplacement appui long 3s par drag vertical (glisser vers le haut) - Drag de 80px vers le haut active le mode lock - Indicateur visuel avec flèche + texte "Glissez pour verrouiller" - Bouton suit le doigt pendant le drag (transform translateY) - Feedback haptique à l'activation (triple vibration) DÉSACTIVATION : - Tap/clic sur le bouton en mode lock désactive le mode - Feedback haptique simple à la désactivation UX AMÉLIORÉE : - Plus intuitif qu'un appui long - Retour visuel immédiat du drag - Compatible touch (mobile) et mouse (desktop) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl);
|
||||||
gap: var(--spacing-lg);
|
gap: var(--spacing-lg);
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ptt-button {
|
.ptt-button {
|
||||||
@@ -22,7 +23,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: var(--spacing-md);
|
gap: var(--spacing-md);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: transform 0.1s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -121,17 +122,36 @@
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Indicateur de progression pour mode lock */
|
/* Indicateur de drag vers le haut */
|
||||||
.lock-progress {
|
.drag-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
top: -60px;
|
||||||
left: 0;
|
left: 50%;
|
||||||
height: 6px;
|
transform: translateX(-50%);
|
||||||
background: linear-gradient(90deg, #fbbf24, #f59e0b);
|
display: flex;
|
||||||
border-radius: 0 0 0 120px;
|
flex-direction: column;
|
||||||
transition: width 0.05s linear;
|
align-items: center;
|
||||||
box-shadow: 0 0 20px rgba(251, 191, 36, 0.6);
|
gap: 0.5rem;
|
||||||
z-index: 1;
|
color: #fbbf24;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
animation: drag-pulse 0.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-indicator svg {
|
||||||
|
filter: drop-shadow(0 2px 8px rgba(251, 191, 36, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes drag-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(-50%) translateY(-5px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Badge mode lock */
|
/* Badge mode lock */
|
||||||
|
|||||||
+183
-103
@@ -4,15 +4,20 @@ import './PTTButton.css';
|
|||||||
/**
|
/**
|
||||||
* Bouton PTT principal
|
* Bouton PTT principal
|
||||||
* Gère touch et mouse events pour desktop et mobile
|
* Gère touch et mouse events pour desktop et mobile
|
||||||
* Modes : PTT classique (maintenir) ou mode continu (toggle)
|
* Modes :
|
||||||
* Activation mode continu : appui long 3s
|
* - PTT classique : maintenir pour parler
|
||||||
|
* - Mode continu (lock) : glisser vers le haut pendant qu'on parle
|
||||||
*/
|
*/
|
||||||
export default function PTTButton({ isTalking, onPressStart, onPressEnd }) {
|
export default function PTTButton({ isTalking, onPressStart, onPressEnd }) {
|
||||||
const buttonRef = useRef(null);
|
const buttonRef = useRef(null);
|
||||||
const isPressingRef = useRef(false);
|
const isPressingRef = useRef(false);
|
||||||
const longPressTimerRef = useRef(null);
|
|
||||||
const [isLockMode, setIsLockMode] = useState(false);
|
const [isLockMode, setIsLockMode] = useState(false);
|
||||||
const [lockProgress, setLockProgress] = useState(0);
|
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(() => {
|
useEffect(() => {
|
||||||
const button = buttonRef.current;
|
const button = buttonRef.current;
|
||||||
@@ -23,163 +28,240 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd }) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Démarrer timer pour mode lock
|
|
||||||
const startLongPressTimer = () => {
|
|
||||||
// Animation de progression (0 → 100 en 3s)
|
|
||||||
const duration = 3000;
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const updateProgress = () => {
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
const progress = Math.min(100, (elapsed / duration) * 100);
|
|
||||||
setLockProgress(progress);
|
|
||||||
|
|
||||||
if (elapsed >= duration) {
|
|
||||||
// Mode lock activé
|
|
||||||
activateLockMode();
|
|
||||||
} else {
|
|
||||||
longPressTimerRef.current = requestAnimationFrame(updateProgress);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
longPressTimerRef.current = requestAnimationFrame(updateProgress);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelLongPressTimer = () => {
|
|
||||||
if (longPressTimerRef.current) {
|
|
||||||
cancelAnimationFrame(longPressTimerRef.current);
|
|
||||||
longPressTimerRef.current = null;
|
|
||||||
}
|
|
||||||
setLockProgress(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const activateLockMode = () => {
|
|
||||||
console.log('🔒 Mode lock activé');
|
|
||||||
setIsLockMode(true);
|
|
||||||
cancelLongPressTimer();
|
|
||||||
|
|
||||||
// Vibration longue pour feedback
|
|
||||||
if (navigator.vibrate) {
|
|
||||||
navigator.vibrate([100, 50, 100]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleLockMode = () => {
|
|
||||||
if (isLockMode) {
|
|
||||||
// Désactiver mode lock
|
|
||||||
console.log('🔓 Mode lock désactivé');
|
|
||||||
setIsLockMode(false);
|
|
||||||
onPressEnd();
|
|
||||||
} else {
|
|
||||||
// En mode normal, un clic simple ne fait rien
|
|
||||||
// (il faut maintenir ou activer le mode lock)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Touch events (mobile)
|
// Touch events (mobile)
|
||||||
const handleTouchStart = (e) => {
|
const handleTouchStart = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log('🖐️ Touch start');
|
const touch = e.touches[0];
|
||||||
|
console.log('🖐️ Touch start at Y:', touch.clientY);
|
||||||
|
|
||||||
if (isLockMode) {
|
// En mode lock, un tap désactive le mode
|
||||||
// En mode lock, un tap désactive le mode
|
if (isLockModeRef.current) {
|
||||||
toggleLockMode();
|
toggleLockMode();
|
||||||
} else {
|
return;
|
||||||
// Mode PTT normal : démarrer
|
}
|
||||||
if (!isPressingRef.current) {
|
|
||||||
isPressingRef.current = true;
|
// Mode PTT normal : démarrer + init drag
|
||||||
onPressStart();
|
if (!isPressingRef.current) {
|
||||||
// Démarrer timer pour mode lock
|
isPressingRef.current = true;
|
||||||
startLongPressTimer();
|
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) => {
|
const handleTouchEnd = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log('🖐️ Touch end');
|
console.log('🖐️ Touch end, dragOffset:', dragOffset);
|
||||||
|
|
||||||
if (!isLockMode) {
|
// Réinitialiser le drag
|
||||||
// Mode PTT normal : arrêter
|
dragStartYRef.current = null;
|
||||||
if (isPressingRef.current) {
|
currentYRef.current = null;
|
||||||
isPressingRef.current = false;
|
setDragOffset(0);
|
||||||
onPressEnd();
|
|
||||||
// Annuler timer si pas encore 3s
|
// En mode lock, ne rien faire (le micro reste actif)
|
||||||
cancelLongPressTimer();
|
if (isLockModeRef.current) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode PTT normal : arrêter
|
||||||
|
if (isPressingRef.current) {
|
||||||
|
isPressingRef.current = false;
|
||||||
|
onPressEnd();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mouse events (desktop)
|
// Mouse events (desktop)
|
||||||
const handleMouseDown = (e) => {
|
const handleMouseDown = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
console.log('🖱️ Mouse down at Y:', e.clientY);
|
||||||
|
|
||||||
if (isLockMode) {
|
// En mode lock, un clic désactive le mode
|
||||||
|
if (isLockModeRef.current) {
|
||||||
toggleLockMode();
|
toggleLockMode();
|
||||||
} else {
|
return;
|
||||||
if (!isPressingRef.current) {
|
}
|
||||||
isPressingRef.current = true;
|
|
||||||
onPressStart();
|
if (!isPressingRef.current) {
|
||||||
startLongPressTimer();
|
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) => {
|
const handleMouseUp = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
console.log('🖱️ Mouse up, dragOffset:', dragOffset);
|
||||||
|
|
||||||
if (!isLockMode) {
|
// Réinitialiser le drag
|
||||||
if (isPressingRef.current) {
|
dragStartYRef.current = null;
|
||||||
isPressingRef.current = false;
|
currentYRef.current = null;
|
||||||
onPressEnd();
|
setDragOffset(0);
|
||||||
cancelLongPressTimer();
|
|
||||||
}
|
// En mode lock, ne rien faire
|
||||||
|
if (isLockModeRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPressingRef.current) {
|
||||||
|
isPressingRef.current = false;
|
||||||
|
onPressEnd();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseLeave = (e) => {
|
const handleMouseLeave = (e) => {
|
||||||
// Si on quitte le bouton en maintenant, on arrête (sauf en mode lock)
|
// Si on quitte le bouton en maintenant, on arrête (sauf en mode lock)
|
||||||
if (!isLockMode && isPressingRef.current) {
|
if (!isLockModeRef.current && isPressingRef.current) {
|
||||||
|
// Réinitialiser le drag
|
||||||
|
dragStartYRef.current = null;
|
||||||
|
currentYRef.current = null;
|
||||||
|
setDragOffset(0);
|
||||||
|
|
||||||
isPressingRef.current = false;
|
isPressingRef.current = false;
|
||||||
onPressEnd();
|
onPressEnd();
|
||||||
cancelLongPressTimer();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Attacher events
|
// Attacher events
|
||||||
button.addEventListener('touchstart', handleTouchStart, { passive: false });
|
button.addEventListener('touchstart', handleTouchStart, { passive: false });
|
||||||
|
button.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||||
button.addEventListener('touchend', handleTouchEnd, { passive: false });
|
button.addEventListener('touchend', handleTouchEnd, { passive: false });
|
||||||
button.addEventListener('touchcancel', handleTouchEnd, { passive: false });
|
button.addEventListener('touchcancel', handleTouchEnd, { passive: false });
|
||||||
button.addEventListener('mousedown', handleMouseDown);
|
button.addEventListener('mousedown', handleMouseDown);
|
||||||
|
button.addEventListener('mousemove', handleMouseMove);
|
||||||
button.addEventListener('mouseup', handleMouseUp);
|
button.addEventListener('mouseup', handleMouseUp);
|
||||||
button.addEventListener('mouseleave', handleMouseLeave);
|
button.addEventListener('mouseleave', handleMouseLeave);
|
||||||
button.addEventListener('contextmenu', preventDefault);
|
button.addEventListener('contextmenu', preventDefault);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
button.removeEventListener('touchstart', handleTouchStart);
|
button.removeEventListener('touchstart', handleTouchStart);
|
||||||
|
button.removeEventListener('touchmove', handleTouchMove);
|
||||||
button.removeEventListener('touchend', handleTouchEnd);
|
button.removeEventListener('touchend', handleTouchEnd);
|
||||||
button.removeEventListener('touchcancel', handleTouchEnd);
|
button.removeEventListener('touchcancel', handleTouchEnd);
|
||||||
button.removeEventListener('mousedown', handleMouseDown);
|
button.removeEventListener('mousedown', handleMouseDown);
|
||||||
|
button.removeEventListener('mousemove', handleMouseMove);
|
||||||
button.removeEventListener('mouseup', handleMouseUp);
|
button.removeEventListener('mouseup', handleMouseUp);
|
||||||
button.removeEventListener('mouseleave', handleMouseLeave);
|
button.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
button.removeEventListener('contextmenu', preventDefault);
|
button.removeEventListener('contextmenu', preventDefault);
|
||||||
};
|
};
|
||||||
}, [onPressStart, onPressEnd]);
|
}, [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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ptt-container">
|
<div className="ptt-container">
|
||||||
|
{/* Zone de drag vers le haut (indicateur visuel) */}
|
||||||
|
{dragOffset > 0 && !isLockMode && (
|
||||||
|
<div className="drag-indicator" style={{ opacity: dragOffset / 80 }}>
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="32" height="32">
|
||||||
|
<path d="M7 14l5-5 5 5H7z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Glissez pour verrouiller</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bouton PTT principal */}
|
||||||
<button
|
<button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
className={`ptt-button ${isTalking ? 'talking' : ''} ${isLockMode ? 'locked' : ''}`}
|
className={`ptt-button ${isTalking ? 'talking' : ''} ${isLockMode ? 'locked' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
|
style={{
|
||||||
|
transform: dragOffset > 0 && !isLockMode ? `translateY(-${dragOffset * 0.3}px)` : 'none'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Indicateur de progression pour mode lock */}
|
|
||||||
{lockProgress > 0 && !isLockMode && (
|
|
||||||
<div
|
|
||||||
className="lock-progress"
|
|
||||||
style={{ width: `${lockProgress}%` }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="ptt-icon">
|
<div className="ptt-icon">
|
||||||
{isTalking ? (
|
{isTalking ? (
|
||||||
@@ -218,10 +300,8 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd }) {
|
|||||||
{isLockMode
|
{isLockMode
|
||||||
? 'Tapez pour désactiver le mode continu'
|
? 'Tapez pour désactiver le mode continu'
|
||||||
: isTalking
|
: isTalking
|
||||||
? lockProgress > 0
|
? 'Glissez vers le haut pour verrouiller • Relâchez pour arrêter'
|
||||||
? 'Maintenez 3s pour mode continu...'
|
: 'Appuyez et maintenez pour parler • Glissez vers le haut pour verrouiller'}
|
||||||
: 'Relâchez pour arrêter'
|
|
||||||
: 'Appuyez et maintenez le bouton • Maintenez 3s pour mode continu'}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user