Compare commits

..

2 Commits

Author SHA1 Message Date
benoit 77bc36b765 feat: amélioration UX interface admin audio
- Admin : regroupement des 3 dropdowns cartes son dans une seule section
- Admin : suppression du mode édition pour noms de canaux (directement éditables)
- Admin : unification des boutons de sauvegarde en bas de chaque section
- Admin : routing par hash URL pour persistance des onglets (#groups, #audio, etc.)
- AudioRoutingMatrix : bouton sauvegarde déplacé en bas de la matrice
- AudioRoutingMatrix : dropdowns de gain en nuance de bleu (cohérence visuelle)
2026-06-01 23:04:57 +02:00
benoit 58bc91b966 fix: UX interface admin et client
- Settings : suppression paramètres inutiles (mode PTT continu, feedback audio non implémenté)
- Settings : conservation uniquement du paramètre vibrations (fonctionnel)
- PTTButton : suppression init mode continu par défaut (redondant avec geste verrouillage)
- PWAInstallPrompt : ajout fond semi-transparent et couleurs hardcodées pour lisibilité
- Admin : fix dropdowns audio qui se réinitialisaient (useRef au lieu de useState pour édition)
2026-06-01 22:30:51 +02:00
6 changed files with 181 additions and 196 deletions
+122 -105
View File
@@ -1,11 +1,17 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import './Admin.css'; import './Admin.css';
import AudioRoutingMatrix from './components/AudioRoutingMatrix'; import AudioRoutingMatrix from './components/AudioRoutingMatrix';
const API_URL = import.meta.env.VITE_API_URL || '/api'; const API_URL = import.meta.env.VITE_API_URL || '/api';
function Admin() { function Admin() {
const [activeTab, setActiveTab] = useState('groups'); // Lire l'onglet depuis l'URL hash (ex: #audio) ou utiliser 'groups' par défaut
const getInitialTab = () => {
const hash = window.location.hash.slice(1); // Enlever le #
return ['groups', 'audio', 'users', 'stats', 'logs'].includes(hash) ? hash : 'groups';
};
const [activeTab, setActiveTab] = useState(getInitialTab());
const [groups, setGroups] = useState([]); const [groups, setGroups] = useState([]);
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const [stats, setStats] = useState(null); const [stats, setStats] = useState(null);
@@ -19,11 +25,10 @@ function Admin() {
const [selectedInputDevice, setSelectedInputDevice] = useState(null); const [selectedInputDevice, setSelectedInputDevice] = useState(null);
const [selectedOutputDevice, setSelectedOutputDevice] = useState(null); const [selectedOutputDevice, setSelectedOutputDevice] = useState(null);
const [selectedSampleRate, setSelectedSampleRate] = useState(48000); const [selectedSampleRate, setSelectedSampleRate] = useState(48000);
const [isEditingAudio, setIsEditingAudio] = useState(false); const isEditingAudioRef = useRef(false);
// Channel names (Phase 2.5) // Channel names (Phase 2.5)
const [channelNames, setChannelNames] = useState({ inputs: {}, outputs: {} }); const [channelNames, setChannelNames] = useState({ inputs: {}, outputs: {} });
const [editingChannelNames, setEditingChannelNames] = useState(false);
// Gestion formulaire nouveau groupe // Gestion formulaire nouveau groupe
const [showGroupForm, setShowGroupForm] = useState(false); const [showGroupForm, setShowGroupForm] = useState(false);
@@ -33,6 +38,19 @@ function Admin() {
audioBitrate: 96 audioBitrate: 96
}); });
// Synchroniser l'onglet avec l'URL hash
useEffect(() => {
const handleHashChange = () => {
const hash = window.location.hash.slice(1);
if (['groups', 'audio', 'users', 'stats', 'logs'].includes(hash)) {
setActiveTab(hash);
}
};
window.addEventListener('hashchange', handleHashChange);
return () => window.removeEventListener('hashchange', handleHashChange);
}, []);
// Rafraîchissement automatique // Rafraîchissement automatique
useEffect(() => { useEffect(() => {
loadData(); loadData();
@@ -106,8 +124,8 @@ function Admin() {
setCurrentDevice(device); setCurrentDevice(device);
setChannelNames(channelNamesData.channelNames || { inputs: {}, outputs: {} }); setChannelNames(channelNamesData.channelNames || { inputs: {}, outputs: {} });
// Ne réinitialiser les sélections que si l'utilisateur n'est pas en train d'éditer // Ne réinitialiser les sélections que lors du chargement initial (pas en train d'éditer)
if (!isEditingAudio) { if (!isEditingAudioRef.current) {
setSelectedInputDevice(device.inputDeviceId ?? null); setSelectedInputDevice(device.inputDeviceId ?? null);
setSelectedOutputDevice(device.outputDeviceId ?? null); setSelectedOutputDevice(device.outputDeviceId ?? null);
setSelectedSampleRate(device.sampleRate || 48000); setSelectedSampleRate(device.sampleRate || 48000);
@@ -216,7 +234,6 @@ function Admin() {
if (res.ok) { if (res.ok) {
alert('Noms de canaux sauvegardés avec succès!'); alert('Noms de canaux sauvegardés avec succès!');
setEditingChannelNames(false);
await loadAudioDevices(); await loadAudioDevices();
} else { } else {
const error = await res.json(); const error = await res.json();
@@ -251,7 +268,7 @@ function Admin() {
}); });
if (res.ok) { if (res.ok) {
setIsEditingAudio(false); // Désactiver le mode édition isEditingAudioRef.current = false; // Désactiver le mode édition
alert('Configuration audio sauvegardée avec succès!'); alert('Configuration audio sauvegardée avec succès!');
await loadAudioDevices(); await loadAudioDevices();
} else { } else {
@@ -311,31 +328,31 @@ function Admin() {
<nav className="admin-tabs"> <nav className="admin-tabs">
<button <button
className={activeTab === 'groups' ? 'active' : ''} className={activeTab === 'groups' ? 'active' : ''}
onClick={() => setActiveTab('groups')} onClick={() => { window.location.hash = 'groups'; setActiveTab('groups'); }}
> >
Groupes Groupes
</button> </button>
<button <button
className={activeTab === 'audio' ? 'active' : ''} className={activeTab === 'audio' ? 'active' : ''}
onClick={() => setActiveTab('audio')} onClick={() => { window.location.hash = 'audio'; setActiveTab('audio'); }}
> >
Audio Audio
</button> </button>
<button <button
className={activeTab === 'users' ? 'active' : ''} className={activeTab === 'users' ? 'active' : ''}
onClick={() => setActiveTab('users')} onClick={() => { window.location.hash = 'users'; setActiveTab('users'); }}
> >
Utilisateurs ({users.length}) Utilisateurs ({users.length})
</button> </button>
<button <button
className={activeTab === 'stats' ? 'active' : ''} className={activeTab === 'stats' ? 'active' : ''}
onClick={() => setActiveTab('stats')} onClick={() => { window.location.hash = 'stats'; setActiveTab('stats'); }}
> >
Statistiques Statistiques
</button> </button>
<button <button
className={activeTab === 'logs' ? 'active' : ''} className={activeTab === 'logs' ? 'active' : ''}
onClick={() => setActiveTab('logs')} onClick={() => { window.location.hash = 'logs'; setActiveTab('logs'); }}
> >
Logs Logs
</button> </button>
@@ -438,99 +455,95 @@ function Admin() {
<div className="audio-config-container"> <div className="audio-config-container">
<div className="audio-section"> <div className="audio-section">
<h3>Carte son d'entrée (Input)</h3> <h3>Configuration des cartes son</h3>
<select
value={selectedInputDevice ?? ''}
onChange={(e) => {
setIsEditingAudio(true);
setSelectedInputDevice(e.target.value === '' ? null : e.target.value);
}}
className="device-select"
>
<option value="">-- Sélectionner une carte --</option>
{audioDevices
.filter(d => d.maxInputChannels > 0)
.map((device, index) => (
<option key={`input-${device.id}-${index}`} value={device.id}>
{device.name} - {device.maxInputChannels} canaux - {device.defaultSampleRate}Hz
</option>
))}
</select>
{selectedInputDevice !== null && selectedInputDevice !== '' && (
<p style={{marginTop: 'var(--spacing-sm)', color: 'var(--color-text-secondary)', fontSize: '0.85rem', wordBreak: 'break-all'}}>
Device ID: {selectedInputDevice}
</p>
)}
</div>
<div className="audio-section"> <div style={{display: 'grid', gap: 'var(--spacing-lg)', marginTop: 'var(--spacing-md)'}}>
<h3>Carte son de sortie (Output)</h3> <div>
<select <label style={{display: 'block', marginBottom: 'var(--spacing-xs)', fontSize: '0.9rem', fontWeight: 600, color: 'var(--color-text-secondary)'}}>
value={selectedOutputDevice ?? ''} Carte son d'entrée (Input)
onChange={(e) => { </label>
setIsEditingAudio(true); <select
setSelectedOutputDevice(e.target.value === '' ? null : e.target.value); value={selectedInputDevice ?? ''}
}} onChange={(e) => {
className="device-select" isEditingAudioRef.current = true;
> setSelectedInputDevice(e.target.value === '' ? null : e.target.value);
<option value="">-- Sélectionner une carte --</option> }}
{audioDevices className="device-select"
.filter(d => d.maxOutputChannels > 0) >
.map((device, index) => ( <option value="">-- Sélectionner une carte --</option>
<option key={`output-${device.id}-${index}`} value={device.id}> {audioDevices
{device.name} - {device.maxOutputChannels} canaux - {device.defaultSampleRate}Hz .filter(d => d.maxInputChannels > 0)
</option> .map((device, index) => (
))} <option key={`input-${device.id}-${index}`} value={device.id}>
</select> {device.name} - {device.maxInputChannels} canaux - {device.defaultSampleRate}Hz
{selectedOutputDevice !== null && selectedOutputDevice !== '' && ( </option>
<p style={{marginTop: 'var(--spacing-sm)', color: 'var(--color-text-secondary)', fontSize: '0.85rem', wordBreak: 'break-all'}}> ))}
Device ID: {selectedOutputDevice} </select>
</p> {selectedInputDevice !== null && selectedInputDevice !== '' && (
)} <p style={{marginTop: 'var(--spacing-sm)', color: 'var(--color-text-secondary)', fontSize: '0.85rem', wordBreak: 'break-all'}}>
</div> Device ID: {selectedInputDevice}
</p>
)}
</div>
<div className="audio-section"> <div>
<h3>Sample Rate</h3> <label style={{display: 'block', marginBottom: 'var(--spacing-xs)', fontSize: '0.9rem', fontWeight: 600, color: 'var(--color-text-secondary)'}}>
<select Carte son de sortie (Output)
value={selectedSampleRate} </label>
onChange={(e) => { <select
setIsEditingAudio(true); value={selectedOutputDevice ?? ''}
setSelectedSampleRate(parseInt(e.target.value)); onChange={(e) => {
}} isEditingAudioRef.current = true;
className="device-select" setSelectedOutputDevice(e.target.value === '' ? null : e.target.value);
> }}
<option value={44100}>44100 Hz (CD quality)</option> className="device-select"
<option value={48000}>48000 Hz (Recommended)</option> >
<option value={96000}>96000 Hz (High quality)</option> <option value="">-- Sélectionner une carte --</option>
</select> {audioDevices
</div> .filter(d => d.maxOutputChannels > 0)
.map((device, index) => (
<option key={`output-${device.id}-${index}`} value={device.id}>
{device.name} - {device.maxOutputChannels} canaux - {device.defaultSampleRate}Hz
</option>
))}
</select>
{selectedOutputDevice !== null && selectedOutputDevice !== '' && (
<p style={{marginTop: 'var(--spacing-sm)', color: 'var(--color-text-secondary)', fontSize: '0.85rem', wordBreak: 'break-all'}}>
Device ID: {selectedOutputDevice}
</p>
)}
</div>
<div className="audio-actions"> <div>
<button onClick={handleSaveAudioDevice} className="btn-primary"> <label style={{display: 'block', marginBottom: 'var(--spacing-xs)', fontSize: '0.9rem', fontWeight: 600, color: 'var(--color-text-secondary)'}}>
Sauvegarder la configuration Sample Rate
</button> </label>
</div> <select
value={selectedSampleRate}
<div className="audio-section"> onChange={(e) => {
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--spacing-md)'}}> isEditingAudioRef.current = true;
<h3>Nommage des canaux physiques</h3> setSelectedSampleRate(parseInt(e.target.value));
{!editingChannelNames ? ( }}
<button onClick={() => setEditingChannelNames(true)} className="btn-secondary"> className="device-select"
Modifier les noms >
</button> <option value={44100}>44100 Hz (CD quality)</option>
) : ( <option value={48000}>48000 Hz (Recommended)</option>
<div style={{display: 'flex', gap: 'var(--spacing-sm)'}}> <option value={96000}>96000 Hz (High quality)</option>
<button onClick={handleSaveChannelNames} className="btn-primary"> </select>
Sauvegarder </div>
</button>
<button onClick={() => { setEditingChannelNames(false); loadAudioDevices(); }} className="btn-secondary">
Annuler
</button>
</div>
)}
</div> </div>
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--spacing-xl)'}}> <div className="audio-actions">
<button onClick={handleSaveAudioDevice} className="btn-primary">
Sauvegarder la configuration audio
</button>
</div>
</div>
<div className="audio-section">
<h3>Nommage des canaux physiques</h3>
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--spacing-xl)', marginTop: 'var(--spacing-md)'}}>
<div> <div>
<h4 style={{marginBottom: 'var(--spacing-md)', color: 'var(--color-text-secondary)'}}> <h4 style={{marginBottom: 'var(--spacing-md)', color: 'var(--color-text-secondary)'}}>
Entrées (Inputs) - {currentDevice.inputChannels || 0} canaux disponibles Entrées (Inputs) - {currentDevice.inputChannels || 0} canaux disponibles
@@ -544,10 +557,9 @@ function Admin() {
value={channelNames.inputs?.[i] || ''} value={channelNames.inputs?.[i] || ''}
onChange={(e) => updateChannelName('inputs', i, e.target.value)} onChange={(e) => updateChannelName('inputs', i, e.target.value)}
placeholder={`Input ${i}`} placeholder={`Input ${i}`}
disabled={!editingChannelNames}
style={{ style={{
padding: 'var(--spacing-sm)', padding: 'var(--spacing-sm)',
background: editingChannelNames ? 'var(--color-bg)' : 'var(--color-surface-hover)', background: 'var(--color-bg)',
border: '1px solid var(--color-border)', border: '1px solid var(--color-border)',
borderRadius: '6px', borderRadius: '6px',
color: 'var(--color-text)', color: 'var(--color-text)',
@@ -572,10 +584,9 @@ function Admin() {
value={channelNames.outputs?.[i] || ''} value={channelNames.outputs?.[i] || ''}
onChange={(e) => updateChannelName('outputs', i, e.target.value)} onChange={(e) => updateChannelName('outputs', i, e.target.value)}
placeholder={`Output ${i}`} placeholder={`Output ${i}`}
disabled={!editingChannelNames}
style={{ style={{
padding: 'var(--spacing-sm)', padding: 'var(--spacing-sm)',
background: editingChannelNames ? 'var(--color-bg)' : 'var(--color-surface-hover)', background: 'var(--color-bg)',
border: '1px solid var(--color-border)', border: '1px solid var(--color-border)',
borderRadius: '6px', borderRadius: '6px',
color: 'var(--color-text)', color: 'var(--color-text)',
@@ -587,6 +598,12 @@ function Admin() {
</div> </div>
</div> </div>
</div> </div>
<div className="audio-actions">
<button onClick={handleSaveChannelNames} className="btn-primary">
Sauvegarder les noms des canaux
</button>
</div>
</div> </div>
<AudioRoutingMatrix groups={groups} channelNames={channelNames} /> <AudioRoutingMatrix groups={groups} channelNames={channelNames} />
+13 -5
View File
@@ -6,6 +6,13 @@
margin-top: var(--spacing-lg); margin-top: var(--spacing-lg);
} }
.routing-actions {
margin-top: var(--spacing-xl);
display: flex;
justify-content: flex-start;
gap: var(--spacing-sm);
}
.routing-matrix-header { .routing-matrix-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -169,9 +176,9 @@
width: 100%; width: 100%;
padding: 4px 8px; padding: 4px 8px;
font-size: 0.75rem; font-size: 0.75rem;
background: rgba(255, 255, 255, 0.9); background: rgba(59, 130, 246, 0.2);
color: var(--color-text); color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 1);
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
@@ -180,8 +187,9 @@
.gain-select:focus { .gain-select:focus {
outline: none; outline: none;
border-color: rgba(255, 255, 255, 0.6); background: rgba(59, 130, 246, 0.3);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 1);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
+14 -13
View File
@@ -194,19 +194,14 @@ function AudioRoutingMatrix({ groups, channelNames }) {
{wsConnected ? '● Live' : '○ Offline'} {wsConnected ? '● Live' : '○ Offline'}
</span> </span>
</div> </div>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}> <label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}> <input
<input type="checkbox"
type="checkbox" checked={showOnlyNamedChannels}
checked={showOnlyNamedChannels} onChange={(e) => setShowOnlyNamedChannels(e.target.checked)}
onChange={(e) => setShowOnlyNamedChannels(e.target.checked)} />
/> <span>Afficher uniquement les canaux nommés</span>
<span>Afficher uniquement les canaux nommés</span> </label>
</label>
<button onClick={saveRouting} className="btn-primary">
Sauvegarder le routing
</button>
</div>
</div> </div>
<div className="routing-section"> <div className="routing-section">
@@ -341,6 +336,12 @@ function AudioRoutingMatrix({ groups, channelNames }) {
))} ))}
</div> </div>
</div> </div>
<div className="routing-actions">
<button onClick={saveRouting} className="btn-primary">
Sauvegarder le routing audio
</button>
</div>
</div> </div>
); );
} }
-10
View File
@@ -22,16 +22,6 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd, audioLe
const currentYRef = useRef(null); const currentYRef = useRef(null);
const [dragOffset, setDragOffset] = useState(0); // Offset visuel du drag (en pixels) 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(() => { useEffect(() => {
const button = buttonRef.current; const button = buttonRef.current;
if (!button) return; if (!button) return;
+30 -14
View File
@@ -3,8 +3,22 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
top: 0;
z-index: 1001; z-index: 1001;
animation: slideUp 0.3s ease; background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
display: flex;
align-items: flex-end;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
} }
@keyframes slideUp { @keyframes slideUp {
@@ -17,12 +31,14 @@
} }
.pwa-prompt { .pwa-prompt {
background: var(--bg-secondary); width: 100%;
background: #1a1a1a;
border-top-left-radius: 16px; border-top-left-radius: 16px;
border-top-right-radius: 16px; border-top-right-radius: 16px;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3); box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
max-height: 80vh; max-height: 80vh;
overflow-y: auto; overflow-y: auto;
animation: slideUp 0.3s ease;
} }
.pwa-prompt-header { .pwa-prompt-header {
@@ -36,13 +52,13 @@
.pwa-prompt-header h3 { .pwa-prompt-header h3 {
margin: 0; margin: 0;
font-size: 1.2rem; font-size: 1.2rem;
color: var(--text-primary); color: #ffffff;
} }
.pwa-prompt-close { .pwa-prompt-close {
background: none; background: none;
border: none; border: none;
color: var(--text-secondary); color: #9ca3af;
cursor: pointer; cursor: pointer;
padding: 0.5rem; padding: 0.5rem;
border-radius: 8px; border-radius: 8px;
@@ -50,8 +66,8 @@
} }
.pwa-prompt-close:hover { .pwa-prompt-close:hover {
background: var(--bg-hover); background: rgba(255, 255, 255, 0.1);
color: var(--text-primary); color: #ffffff;
} }
.pwa-prompt-content { .pwa-prompt-content {
@@ -59,8 +75,8 @@
} }
.pwa-prompt-content > p { .pwa-prompt-content > p {
margin: 0 0 var(--spacing-lg) 0; margin: 0 0 1.5rem 0;
color: var(--text-secondary); color: #d1d5db;
line-height: 1.6; line-height: 1.6;
} }
@@ -73,9 +89,9 @@
.pwa-prompt-step { .pwa-prompt-step {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-md); gap: 1rem;
padding: var(--spacing-md); padding: 1rem;
background: var(--bg-hover); background: rgba(255, 255, 255, 0.05);
border-radius: 8px; border-radius: 8px;
} }
@@ -85,7 +101,7 @@
justify-content: center; justify-content: center;
width: 32px; width: 32px;
height: 32px; height: 32px;
background: var(--primary-color); background: #3b82f6;
color: white; color: white;
border-radius: 50%; border-radius: 50%;
font-weight: 600; font-weight: 600;
@@ -95,13 +111,13 @@
.pwa-prompt-step p { .pwa-prompt-step p {
flex: 1; flex: 1;
margin: 0; margin: 0;
color: var(--text-primary); color: #ffffff;
font-size: 0.95rem; font-size: 0.95rem;
} }
.pwa-prompt-step svg { .pwa-prompt-step svg {
flex-shrink: 0; flex-shrink: 0;
color: var(--primary-color); color: #3b82f6;
} }
.pwa-prompt-footer { .pwa-prompt-footer {
+2 -49
View File
@@ -4,9 +4,7 @@ import './Settings.css';
const STORAGE_KEY = 'ptt-live-settings'; const STORAGE_KEY = 'ptt-live-settings';
const defaultSettings = { const defaultSettings = {
defaultPTTMode: 'normal', // 'normal' ou 'continuous' vibrationEnabled: true
vibrationEnabled: true,
audioFeedbackEnabled: true
}; };
/** /**
@@ -68,39 +66,6 @@ export default function Settings({ isOpen, onClose }) {
</div> </div>
<div className="settings-content"> <div className="settings-content">
<div className="setting-section">
<h3>Mode PTT</h3>
<p className="setting-description">
Choisissez le mode de fonctionnement par défaut du bouton PTT
</p>
<label className="radio-option">
<input
type="radio"
name="pttMode"
checked={settings.defaultPTTMode === 'normal'}
onChange={() => handleChange('defaultPTTMode', 'normal')}
/>
<div>
<strong>Mode normal (Push-To-Talk)</strong>
<p>Maintenir le bouton pour parler, relâcher pour arrêter</p>
</div>
</label>
<label className="radio-option">
<input
type="radio"
name="pttMode"
checked={settings.defaultPTTMode === 'continuous'}
onChange={() => handleChange('defaultPTTMode', 'continuous')}
/>
<div>
<strong>Mode continu (verrouillé)</strong>
<p>Un appui active le micro en continu, un second appui le désactive</p>
</div>
</label>
</div>
<div className="setting-section"> <div className="setting-section">
<h3>Feedback</h3> <h3>Feedback</h3>
@@ -112,19 +77,7 @@ export default function Settings({ isOpen, onClose }) {
/> />
<div> <div>
<strong>Vibrations</strong> <strong>Vibrations</strong>
<p>Activer le retour haptique (si disponible)</p> <p>Activer le retour haptique lors du verrouillage PTT</p>
</div>
</label>
<label className="checkbox-option">
<input
type="checkbox"
checked={settings.audioFeedbackEnabled}
onChange={(e) => handleChange('audioFeedbackEnabled', e.target.checked)}
/>
<div>
<strong>Feedback audio</strong>
<p>Sons de confirmation pour les actions</p>
</div> </div>
</label> </label>
</div> </div>