diff --git a/electron/ui/app.js b/electron/ui/app.js index 07cd6d4..2d24cc4 100644 --- a/electron/ui/app.js +++ b/electron/ui/app.js @@ -337,13 +337,21 @@ async function fetchGroups() { } async function editGroup(id, currentName, currentBitrate) { - const newName = prompt('Nom du groupe:', currentName); - if (newName === null || newName.trim() === '') return; + const result = await showModal({ + title: 'Modifier le groupe', + fields: [ + { name: 'name', label: 'Nom', default: currentName }, + { name: 'bitrate', label: 'Bitrate (kbps)', type: 'number', default: currentBitrate, min: 32, max: 320, step: 1 } + ], + confirmLabel: 'Modifier' + }); - const newBitrateStr = prompt('Bitrate (kbps, 32-320):', String(currentBitrate)); - if (newBitrateStr === null) return; + if (!result) return; - const newBitrate = parseInt(newBitrateStr); + const newName = result.name.trim(); + const newBitrate = parseInt(result.bitrate); + + if (!newName) { showNotification('Nom requis', 'error'); return; } if (isNaN(newBitrate) || newBitrate < 32 || newBitrate > 320) { showNotification('Bitrate invalide (32-320 kbps)', 'error'); return; @@ -353,7 +361,7 @@ async function editGroup(id, currentName, currentBitrate) { const response = await fetch(`${API_BASE}/admin/groups/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: newName.trim(), audioBitrate: newBitrate }) + body: JSON.stringify({ name: newName, audioBitrate: newBitrate }) }); if (response.ok) { @@ -370,7 +378,14 @@ async function editGroup(id, currentName, currentBitrate) { } async function deleteGroup(id, name) { - if (!confirm(`Supprimer le groupe "${name}" ?`)) return; + const confirmed = await showModal({ + title: 'Supprimer le groupe', + message: `Supprimer le groupe "${name}" ? Cette action est irréversible.`, + confirmLabel: 'Supprimer', + confirmClass: 'btn-danger' + }); + + if (!confirmed) return; try { const response = await fetch(`${API_BASE}/admin/groups/${id}`, { @@ -623,21 +638,33 @@ document.addEventListener('DOMContentLoaded', () => { const btnAddGroup = document.getElementById('btn-add-group'); if (btnAddGroup) { btnAddGroup.addEventListener('click', async () => { - const name = prompt('Nom du groupe:'); - if (!name) return; + const result = await showModal({ + title: 'Nouveau groupe', + fields: [ + { name: 'name', label: 'Nom du groupe' }, + { name: 'bitrate', label: 'Bitrate (kbps)', type: 'number', default: 96, min: 32, max: 320, step: 1 } + ], + confirmLabel: 'Créer' + }); + + if (!result || !result.name.trim()) return; + + const name = result.name.trim(); + const audioBitrate = parseInt(result.bitrate) || 96; try { const response = await fetch(`${API_BASE}/admin/groups`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, audioBitrate: 96 }) + body: JSON.stringify({ name, audioBitrate }) }); if (response.ok) { showNotification('Groupe créé', 'success'); await fetchGroups(); } else { - showNotification('Erreur création groupe', 'error'); + const err = await response.json().catch(() => ({})); + showNotification('Erreur: ' + (err.error || 'Création échouée'), 'error'); } } catch (error) { console.error('Erreur add group:', error); @@ -725,6 +752,82 @@ document.addEventListener('DOMContentLoaded', () => { // ========== Helpers ========== +/** + * Modal générique (remplace prompt/confirm, non supportés dans Electron). + * - fields[] → formulaire ; message → confirmation simple + * Retourne : objet {champ: valeur} | true (confirm) | null (annulé) + */ +function showModal({ title, fields = [], confirmLabel = 'Confirmer', confirmClass = 'btn-primary', message = null }) { + return new Promise((resolve) => { + const overlay = document.getElementById('modal-overlay'); + const titleEl = document.getElementById('modal-title'); + const bodyEl = document.getElementById('modal-body'); + const cancelBtn = document.getElementById('modal-cancel'); + const confirmBtn = document.getElementById('modal-confirm'); + + titleEl.textContent = title; + confirmBtn.textContent = confirmLabel; + confirmBtn.className = `btn ${confirmClass}`; + + if (message) { + bodyEl.innerHTML = ``; + } else { + bodyEl.innerHTML = fields.map(field => ` +
+ + +
+ `).join(''); + } + + overlay.classList.remove('hidden'); + + const firstInput = bodyEl.querySelector('input'); + if (firstInput) { firstInput.focus(); firstInput.select(); } + + function cleanup() { + overlay.classList.add('hidden'); + cancelBtn.removeEventListener('click', onCancel); + confirmBtn.removeEventListener('click', onConfirm); + overlay.removeEventListener('click', onOverlayClick); + document.removeEventListener('keydown', onKeydown); + } + + function onCancel() { cleanup(); resolve(null); } + + function onConfirm() { + if (message) { + cleanup(); resolve(true); + } else { + const result = {}; + fields.forEach(f => { + const input = document.getElementById(`modal-field-${f.name}`); + result[f.name] = input ? input.value : ''; + }); + cleanup(); resolve(result); + } + } + + function onOverlayClick(e) { if (e.target === overlay) onCancel(); } + function onKeydown(e) { + if (e.key === 'Escape') onCancel(); + if (e.key === 'Enter' && document.activeElement?.tagName !== 'BUTTON') onConfirm(); + } + + cancelBtn.addEventListener('click', onCancel); + confirmBtn.addEventListener('click', onConfirm); + overlay.addEventListener('click', onOverlayClick); + document.addEventListener('keydown', onKeydown); + }); +} + function slugify(text) { return text.toString().normalize('NFD').replace(/[̀-ͯ]/g, '') .toLowerCase().trim().replace(/\s+/g, '-').replace(/[^\w-]+/g, '').replace(/--+/g, '-'); diff --git a/electron/ui/index.html b/electron/ui/index.html index 2e4335d..83050fd 100644 --- a/electron/ui/index.html +++ b/electron/ui/index.html @@ -196,6 +196,20 @@ + + + diff --git a/electron/ui/styles.css b/electron/ui/styles.css index 4a72ff4..2698b57 100644 --- a/electron/ui/styles.css +++ b/electron/ui/styles.css @@ -379,6 +379,58 @@ body { border-color: var(--accent-primary); } +/* Modal */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; +} + +.modal-overlay.hidden { + display: none; +} + +.modal { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 10px; + width: 420px; + max-width: 90vw; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); +} + +.modal-header { + padding: 1.25rem 1.5rem; + border-bottom: 1px solid var(--border-color); +} + +.modal-header h3 { + font-size: 1.125rem; + margin: 0; +} + +.modal-body { + padding: 1.5rem; +} + +.modal-message { + color: var(--text-primary); + line-height: 1.5; + margin: 0; +} + +.modal-footer { + padding: 1rem 1.5rem; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; + gap: 0.75rem; +} + /* Config actions */ .config-actions { display: flex;