macos #1
+122
-105
@@ -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} />
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user