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()}>
+
+
+
+
+
Mode PTT
+
+ Choisissez le mode de fonctionnement par défaut du bouton PTT
+
+
+
+
+
+
+
+
+
Feedback
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}