From 78e9a32e12f7ceb859efc5066adfb1e458e8d9d2 Mon Sep 17 00:00:00 2001 From: Benoit Date: Sat, 23 May 2026 09:34:30 +0200 Subject: [PATCH] feat: mode PTT continu avec activation par appui long (Phase 2.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nouveau mode PTT continu (lock) activé par appui long de 3s - Barre de progression visuelle pendant l'appui (0-100%) - Badge cadenas en haut à droite quand mode actif - Animation pulsante distinctive pour mode lock - Feedback haptique à l'activation (triple vibration) - Désactivation par simple tap quand mode lock actif - Indication textuelle claire de l'état (normal/lock) - Styles responsifs mobile + accessibilité (prefers-reduced-motion) Mode d'emploi : - Normal : Maintenir pour parler, relâcher pour arrêter - Lock : Maintenir 3s → mode continu → tap pour désactiver 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- client/src/components/PTTButton.css | 82 ++++++++++++++- client/src/components/PTTButton.jsx | 148 ++++++++++++++++++++++++---- 2 files changed, 211 insertions(+), 19 deletions(-) diff --git a/client/src/components/PTTButton.css b/client/src/components/PTTButton.css index 0a5e496..227f166 100644 --- a/client/src/components/PTTButton.css +++ b/client/src/components/PTTButton.css @@ -60,6 +60,14 @@ animation: pulse-talking 1.5s ease-in-out infinite; } +/* État: Mode lock (continu) */ +.ptt-button.locked { + background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%); + box-shadow: 0 8px 32px rgba(220, 38, 38, 0.6), + 0 0 80px rgba(220, 38, 38, 0.4); + animation: pulse-locked 2s ease-in-out infinite; +} + @keyframes pulse-talking { 0%, 100% { transform: scale(1); @@ -69,6 +77,19 @@ } } +@keyframes pulse-locked { + 0%, 100% { + transform: scale(1); + box-shadow: 0 8px 32px rgba(220, 38, 38, 0.6), + 0 0 80px rgba(220, 38, 38, 0.4); + } + 50% { + transform: scale(1.03); + box-shadow: 0 8px 40px rgba(220, 38, 38, 0.7), + 0 0 100px rgba(220, 38, 38, 0.5); + } +} + /* Icône micro */ .ptt-icon { width: 64px; @@ -96,6 +117,49 @@ color: var(--color-text-secondary); font-size: 0.9rem; text-align: center; + max-width: 90%; + line-height: 1.4; +} + +/* Indicateur de progression pour mode lock */ +.lock-progress { + position: absolute; + bottom: 0; + left: 0; + height: 6px; + background: linear-gradient(90deg, #fbbf24, #f59e0b); + border-radius: 0 0 0 120px; + transition: width 0.05s linear; + box-shadow: 0 0 20px rgba(251, 191, 36, 0.6); + z-index: 1; +} + +/* Badge mode lock */ +.lock-badge { + position: absolute; + top: 20px; + right: 20px; + width: 40px; + height: 40px; + background: rgba(255, 255, 255, 0.2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(10px); + animation: lock-badge-pulse 1s ease-in-out infinite; + z-index: 2; +} + +@keyframes lock-badge-pulse { + 0%, 100% { + transform: scale(1); + background: rgba(255, 255, 255, 0.2); + } + 50% { + transform: scale(1.1); + background: rgba(255, 255, 255, 0.3); + } } /* Responsive mobile */ @@ -140,11 +204,27 @@ } } +/* Responsive mobile - badge lock */ +@media (max-width: 640px) { + .lock-badge { + top: 15px; + right: 15px; + width: 36px; + height: 36px; + } +} + /* Accessibilité : désactiver effets réduits */ @media (prefers-reduced-motion: reduce) { .ptt-button, - .ptt-button.talking { + .ptt-button.talking, + .ptt-button.locked, + .lock-badge { animation: none; transition: none; } + + .lock-progress { + transition: none; + } } diff --git a/client/src/components/PTTButton.jsx b/client/src/components/PTTButton.jsx index 37ecc04..ed50abf 100644 --- a/client/src/components/PTTButton.jsx +++ b/client/src/components/PTTButton.jsx @@ -1,13 +1,18 @@ -import { useEffect, useRef } from 'react'; +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) ou mode continu (toggle) + * Activation mode continu : appui long 3s */ export default function PTTButton({ isTalking, onPressStart, onPressEnd }) { const buttonRef = useRef(null); const isPressingRef = useRef(false); + const longPressTimerRef = useRef(null); + const [isLockMode, setIsLockMode] = useState(false); + const [lockProgress, setLockProgress] = useState(0); useEffect(() => { const button = buttonRef.current; @@ -18,47 +23,126 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd }) { 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) const handleTouchStart = (e) => { e.preventDefault(); console.log('🖐️ Touch start'); - if (!isPressingRef.current) { - isPressingRef.current = true; - onPressStart(); + + if (isLockMode) { + // En mode lock, un tap désactive le mode + toggleLockMode(); + } else { + // Mode PTT normal : démarrer + if (!isPressingRef.current) { + isPressingRef.current = true; + onPressStart(); + // Démarrer timer pour mode lock + startLongPressTimer(); + } } }; const handleTouchEnd = (e) => { e.preventDefault(); console.log('🖐️ Touch end'); - if (isPressingRef.current) { - isPressingRef.current = false; - onPressEnd(); + + if (!isLockMode) { + // Mode PTT normal : arrêter + if (isPressingRef.current) { + isPressingRef.current = false; + onPressEnd(); + // Annuler timer si pas encore 3s + cancelLongPressTimer(); + } } }; // Mouse events (desktop) const handleMouseDown = (e) => { e.preventDefault(); - if (!isPressingRef.current) { - isPressingRef.current = true; - onPressStart(); + + if (isLockMode) { + toggleLockMode(); + } else { + if (!isPressingRef.current) { + isPressingRef.current = true; + onPressStart(); + startLongPressTimer(); + } } }; const handleMouseUp = (e) => { e.preventDefault(); - if (isPressingRef.current) { - isPressingRef.current = false; - onPressEnd(); + + if (!isLockMode) { + if (isPressingRef.current) { + isPressingRef.current = false; + onPressEnd(); + cancelLongPressTimer(); + } } }; const handleMouseLeave = (e) => { - // Si on quitte le bouton en maintenant, on arrête - if (isPressingRef.current) { + // Si on quitte le bouton en maintenant, on arrête (sauf en mode lock) + if (!isLockMode && isPressingRef.current) { isPressingRef.current = false; onPressEnd(); + cancelLongPressTimer(); } }; @@ -86,9 +170,17 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd }) {

- {isTalking ? 'Relâchez pour arrêter' : 'Appuyez et maintenez le bouton'} + {isLockMode + ? 'Tapez pour désactiver le mode continu' + : isTalking + ? lockProgress > 0 + ? 'Maintenez 3s pour mode continu...' + : 'Relâchez pour arrêter' + : 'Appuyez et maintenez le bouton • Maintenez 3s pour mode continu'}

);