diff --git a/TODO.md b/TODO.md index 08eab34..f5fb78c 100644 --- a/TODO.md +++ b/TODO.md @@ -156,7 +156,7 @@ Valider la faisabilité technique : 2-4 clients, PTT basique, latence < 150ms, m ### 2.2 Modes PTT avancés - [x] Mode continu : toggle ON/OFF (appui long 3s) - [x] Vibration + indicateur visuel rouge (lock actif) -- [ ] Préférences utilisateur (mode par défaut) +- [x] Préférences utilisateur (mode par défaut) ### 2.3 Interface admin - [x] Page admin web (/admin) diff --git a/client/src/App.css b/client/src/App.css index 93c0669..8cbe20b 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -125,6 +125,22 @@ font-size: 0.85rem; } +.btn-icon { + padding: var(--spacing-sm); + background: var(--color-surface-hover); + color: var(--color-text-secondary); + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; +} + +.btn-icon:hover { + background: var(--color-border); + color: var(--color-text); +} + .btn-disconnect { padding: var(--spacing-sm) var(--spacing-md); background: var(--color-surface-hover); diff --git a/client/src/App.jsx b/client/src/App.jsx index 49386c3..70f93e3 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -3,6 +3,7 @@ import useLiveKit from './hooks/useLiveKit'; import PTTButton from './components/PTTButton'; import UserList from './components/UserList'; import GroupSelector from './components/GroupSelector'; +import Settings from './components/Settings'; import './App.css'; const API_URL = import.meta.env.VITE_API_URL || '/api'; @@ -13,6 +14,7 @@ function App() { const [groups, setGroups] = useState([]); const [isConnecting, setIsConnecting] = useState(false); const [error, setError] = useState(null); + const [showSettings, setShowSettings] = useState(false); const { isConnected, @@ -222,12 +224,23 @@ function App() { {groups.find(g => g.id === groupId)?.name || groupId}

- +
+ + +
@@ -252,6 +265,9 @@ function App() { audioLevel={audioLevel} />
+ + {/* Modal de paramètres */} + setShowSettings(false)} /> ); } diff --git a/client/src/components/PTTButton.jsx b/client/src/components/PTTButton.jsx index 2d1e5b8..f431b9d 100644 --- a/client/src/components/PTTButton.jsx +++ b/client/src/components/PTTButton.jsx @@ -1,12 +1,13 @@ import { useEffect, useRef, useState } from 'react'; import './PTTButton.css'; +import { loadSettings } from './Settings'; /** * 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 + * - Mode continu (lock) : glisser vers le haut pendant qu'on parle OU mode par défaut * Inclut VU-mètre intégré (anneau autour du bouton) */ export default function PTTButton({ isTalking, onPressStart, onPressEnd, audioLevel = 0 }) { @@ -14,12 +15,23 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd, audioLe const isPressingRef = useRef(false); const [isLockMode, setIsLockMode] = useState(false); const isLockModeRef = useRef(false); // Ref pour accès immédiat dans event handlers + const [settings, setSettings] = useState(loadSettings()); // Drag tracking const dragStartYRef = useRef(null); const currentYRef = useRef(null); const [dragOffset, setDragOffset] = useState(0); // Offset visuel du drag (en pixels) + // Initialiser le mode selon les préférences au démarrage + useEffect(() => { + const currentSettings = loadSettings(); + if (currentSettings.defaultPTTMode === 'continuous') { + setIsLockMode(true); + isLockModeRef.current = true; + console.log('Mode continu activé par défaut (préférences)'); + } + }, []); + useEffect(() => { const button = buttonRef.current; if (!button) return; @@ -207,8 +219,8 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd, audioLe // Le micro est déjà actif (onPressStart a été appelé) - // Vibration pour feedback - if (navigator.vibrate) { + // Vibration pour feedback (si activé dans les paramètres) + if (settings.vibrationEnabled && navigator.vibrate) { navigator.vibrate([100, 50, 100]); } }; @@ -226,8 +238,8 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd, audioLe console.log('🔒 Mode lock ON'); onPressStart(); - // Vibration pour feedback - if (navigator.vibrate) { + // Vibration pour feedback (si activé dans les paramètres) + if (settings.vibrationEnabled && navigator.vibrate) { navigator.vibrate([100, 50, 100]); } } else { @@ -235,8 +247,8 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd, audioLe console.log('🔓 Mode lock OFF'); onPressEnd(); - // Vibration pour feedback - if (navigator.vibrate) { + // Vibration pour feedback (si activé dans les paramètres) + if (settings.vibrationEnabled && navigator.vibrate) { navigator.vibrate(50); } } diff --git a/client/src/components/Settings.css b/client/src/components/Settings.css new file mode 100644 index 0000000..bfd9603 --- /dev/null +++ b/client/src/components/Settings.css @@ -0,0 +1,139 @@ +.settings-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: var(--spacing-md); +} + +.settings-modal { + background: var(--bg-secondary); + border-radius: 12px; + width: 100%; + max-width: 500px; + max-height: 90vh; + overflow: auto; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); +} + +.settings-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-lg); + border-bottom: 1px solid var(--border-color); +} + +.settings-header h2 { + margin: 0; + font-size: 1.5rem; +} + +.close-btn { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 0.5rem; + border-radius: 8px; + transition: all 0.2s; +} + +.close-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.settings-content { + padding: var(--spacing-lg); +} + +.setting-section { + margin-bottom: var(--spacing-xl); +} + +.setting-section:last-child { + margin-bottom: 0; +} + +.setting-section h3 { + margin: 0 0 var(--spacing-sm) 0; + font-size: 1.1rem; + color: var(--text-primary); +} + +.setting-description { + margin: 0 0 var(--spacing-md) 0; + color: var(--text-secondary); + font-size: 0.9rem; +} + +.radio-option, +.checkbox-option { + display: flex; + align-items: flex-start; + gap: var(--spacing-md); + padding: var(--spacing-md); + border: 2px solid var(--border-color); + border-radius: 8px; + margin-bottom: var(--spacing-sm); + cursor: pointer; + transition: all 0.2s; +} + +.radio-option:hover, +.checkbox-option:hover { + background: var(--bg-hover); + border-color: var(--primary-color); +} + +.radio-option:has(input:checked), +.checkbox-option:has(input:checked) { + background: rgba(59, 130, 246, 0.1); + border-color: var(--primary-color); +} + +.radio-option input[type="radio"], +.checkbox-option input[type="checkbox"] { + margin-top: 0.25rem; + cursor: pointer; + width: 18px; + height: 18px; + flex-shrink: 0; +} + +.radio-option div, +.checkbox-option div { + flex: 1; +} + +.radio-option strong, +.checkbox-option strong { + display: block; + margin-bottom: 0.25rem; + color: var(--text-primary); +} + +.radio-option p, +.checkbox-option p { + margin: 0; + color: var(--text-secondary); + font-size: 0.85rem; +} + +.settings-footer { + padding: var(--spacing-lg); + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; +} + +.settings-footer .btn-primary { + padding: var(--spacing-sm) var(--spacing-xl); +} diff --git a/client/src/components/Settings.jsx b/client/src/components/Settings.jsx new file mode 100644 index 0000000..e23cf7e --- /dev/null +++ b/client/src/components/Settings.jsx @@ -0,0 +1,141 @@ +import { useState, useEffect } from 'react'; +import './Settings.css'; + +const STORAGE_KEY = 'ptt-live-settings'; + +const defaultSettings = { + defaultPTTMode: 'normal', // 'normal' ou 'continuous' + vibrationEnabled: true, + audioFeedbackEnabled: true +}; + +/** + * Charge les paramètres depuis localStorage + */ +export function loadSettings() { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + return { ...defaultSettings, ...JSON.parse(stored) }; + } + } catch (error) { + console.error('Erreur chargement paramètres:', error); + } + return defaultSettings; +} + +/** + * Sauvegarde les paramètres dans localStorage + */ +export function saveSettings(settings) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + } catch (error) { + console.error('Erreur sauvegarde paramètres:', error); + } +} + +/** + * Composant modal de paramètres + */ +export default function Settings({ isOpen, onClose }) { + const [settings, setSettings] = useState(defaultSettings); + + useEffect(() => { + if (isOpen) { + setSettings(loadSettings()); + } + }, [isOpen]); + + const handleChange = (key, value) => { + const newSettings = { ...settings, [key]: value }; + setSettings(newSettings); + saveSettings(newSettings); + }; + + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()}> +
+

Paramètres

+ +
+ +
+
+

Mode PTT

+

+ Choisissez le mode de fonctionnement par défaut du bouton PTT +

+ + + + +
+ +
+

Feedback

+ + + + +
+
+ +
+ +
+
+
+ ); +}