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)
This commit is contained in:
+52
-41
@@ -5,7 +5,13 @@ 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);
|
||||||
@@ -23,8 +29,6 @@ function Admin() {
|
|||||||
|
|
||||||
// Channel names (Phase 2.5)
|
// Channel names (Phase 2.5)
|
||||||
const [channelNames, setChannelNames] = useState({ inputs: {}, outputs: {} });
|
const [channelNames, setChannelNames] = useState({ inputs: {}, outputs: {} });
|
||||||
const editingChannelNamesRef = useRef(false);
|
|
||||||
const [, forceUpdate] = useState({});
|
|
||||||
|
|
||||||
// Gestion formulaire nouveau groupe
|
// Gestion formulaire nouveau groupe
|
||||||
const [showGroupForm, setShowGroupForm] = useState(false);
|
const [showGroupForm, setShowGroupForm] = useState(false);
|
||||||
@@ -34,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();
|
||||||
@@ -105,11 +122,7 @@ function Admin() {
|
|||||||
|
|
||||||
const device = currentData.device || { inputChannels: 8, outputChannels: 8 };
|
const device = currentData.device || { inputChannels: 8, outputChannels: 8 };
|
||||||
setCurrentDevice(device);
|
setCurrentDevice(device);
|
||||||
|
|
||||||
// Ne pas écraser les noms de canaux pendant l'édition
|
|
||||||
if (!editingChannelNamesRef.current) {
|
|
||||||
setChannelNames(channelNamesData.channelNames || { inputs: {}, outputs: {} });
|
setChannelNames(channelNamesData.channelNames || { inputs: {}, outputs: {} });
|
||||||
}
|
|
||||||
|
|
||||||
// Ne réinitialiser les sélections que lors du chargement initial (pas en train d'éditer)
|
// Ne réinitialiser les sélections que lors du chargement initial (pas en train d'éditer)
|
||||||
if (!isEditingAudioRef.current) {
|
if (!isEditingAudioRef.current) {
|
||||||
@@ -221,8 +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!');
|
||||||
editingChannelNamesRef.current = false;
|
|
||||||
forceUpdate({});
|
|
||||||
await loadAudioDevices();
|
await loadAudioDevices();
|
||||||
} else {
|
} else {
|
||||||
const error = await res.json();
|
const error = await res.json();
|
||||||
@@ -317,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>
|
||||||
@@ -444,7 +455,13 @@ 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>
|
||||||
|
|
||||||
|
<div style={{display: 'grid', gap: 'var(--spacing-lg)', marginTop: 'var(--spacing-md)'}}>
|
||||||
|
<div>
|
||||||
|
<label style={{display: 'block', marginBottom: 'var(--spacing-xs)', fontSize: '0.9rem', fontWeight: 600, color: 'var(--color-text-secondary)'}}>
|
||||||
|
Carte son d'entrée (Input)
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedInputDevice ?? ''}
|
value={selectedInputDevice ?? ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -469,8 +486,10 @@ function Admin() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="audio-section">
|
<div>
|
||||||
<h3>Carte son de sortie (Output)</h3>
|
<label style={{display: 'block', marginBottom: 'var(--spacing-xs)', fontSize: '0.9rem', fontWeight: 600, color: 'var(--color-text-secondary)'}}>
|
||||||
|
Carte son de sortie (Output)
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedOutputDevice ?? ''}
|
value={selectedOutputDevice ?? ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -495,8 +514,10 @@ function Admin() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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)'}}>
|
||||||
|
Sample Rate
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedSampleRate}
|
value={selectedSampleRate}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -510,33 +531,19 @@ function Admin() {
|
|||||||
<option value={96000}>96000 Hz (High quality)</option>
|
<option value={96000}>96000 Hz (High quality)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="audio-actions">
|
<div className="audio-actions">
|
||||||
<button onClick={handleSaveAudioDevice} className="btn-primary">
|
<button onClick={handleSaveAudioDevice} className="btn-primary">
|
||||||
Sauvegarder la configuration
|
Sauvegarder la configuration audio
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="audio-section">
|
<div className="audio-section">
|
||||||
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--spacing-md)'}}>
|
|
||||||
<h3>Nommage des canaux physiques</h3>
|
<h3>Nommage des canaux physiques</h3>
|
||||||
{!editingChannelNamesRef.current ? (
|
|
||||||
<button onClick={() => { editingChannelNamesRef.current = true; forceUpdate({}); }} className="btn-secondary">
|
|
||||||
Modifier les noms
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div style={{display: 'flex', gap: 'var(--spacing-sm)'}}>
|
|
||||||
<button onClick={handleSaveChannelNames} className="btn-primary">
|
|
||||||
Sauvegarder
|
|
||||||
</button>
|
|
||||||
<button onClick={() => { editingChannelNamesRef.current = false; forceUpdate({}); loadAudioDevices(); }} className="btn-secondary">
|
|
||||||
Annuler
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--spacing-xl)'}}>
|
<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
|
||||||
@@ -550,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={!editingChannelNamesRef.current}
|
|
||||||
style={{
|
style={{
|
||||||
padding: 'var(--spacing-sm)',
|
padding: 'var(--spacing-sm)',
|
||||||
background: editingChannelNamesRef.current ? '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)',
|
||||||
@@ -578,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={!editingChannelNamesRef.current}
|
|
||||||
style={{
|
style={{
|
||||||
padding: 'var(--spacing-sm)',
|
padding: 'var(--spacing-sm)',
|
||||||
background: editingChannelNamesRef.current ? '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)',
|
||||||
@@ -593,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,7 +194,6 @@ 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"
|
||||||
@@ -203,10 +202,6 @@ function AudioRoutingMatrix({ groups, channelNames }) {
|
|||||||
/>
|
/>
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user