fix: remplacer prompt/confirm par modals HTML (non supportés dans Electron)

Electron bloque prompt() et confirm() dans le renderer process.
Ajout d'un showModal() générique (champs configurables, mode confirmation)
avec overlay, focus auto, Escape/Enter, et fermeture en cliquant l'overlay.
Tous les appels prompt/confirm dans editGroup, deleteGroup et addGroup
sont migrés vers showModal.
This commit is contained in:
2026-07-01 13:28:08 +02:00
parent 955bfdfe07
commit b3fbe31a2d
3 changed files with 180 additions and 11 deletions
+114 -11
View File
@@ -337,13 +337,21 @@ async function fetchGroups() {
} }
async function editGroup(id, currentName, currentBitrate) { async function editGroup(id, currentName, currentBitrate) {
const newName = prompt('Nom du groupe:', currentName); const result = await showModal({
if (newName === null || newName.trim() === '') return; 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 (!result) return;
if (newBitrateStr === null) 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) { if (isNaN(newBitrate) || newBitrate < 32 || newBitrate > 320) {
showNotification('Bitrate invalide (32-320 kbps)', 'error'); showNotification('Bitrate invalide (32-320 kbps)', 'error');
return; return;
@@ -353,7 +361,7 @@ async function editGroup(id, currentName, currentBitrate) {
const response = await fetch(`${API_BASE}/admin/groups/${id}`, { const response = await fetch(`${API_BASE}/admin/groups/${id}`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName.trim(), audioBitrate: newBitrate }) body: JSON.stringify({ name: newName, audioBitrate: newBitrate })
}); });
if (response.ok) { if (response.ok) {
@@ -370,7 +378,14 @@ async function editGroup(id, currentName, currentBitrate) {
} }
async function deleteGroup(id, name) { 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 { try {
const response = await fetch(`${API_BASE}/admin/groups/${id}`, { const response = await fetch(`${API_BASE}/admin/groups/${id}`, {
@@ -623,21 +638,33 @@ document.addEventListener('DOMContentLoaded', () => {
const btnAddGroup = document.getElementById('btn-add-group'); const btnAddGroup = document.getElementById('btn-add-group');
if (btnAddGroup) { if (btnAddGroup) {
btnAddGroup.addEventListener('click', async () => { btnAddGroup.addEventListener('click', async () => {
const name = prompt('Nom du groupe:'); const result = await showModal({
if (!name) return; 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 { try {
const response = await fetch(`${API_BASE}/admin/groups`, { const response = await fetch(`${API_BASE}/admin/groups`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, audioBitrate: 96 }) body: JSON.stringify({ name, audioBitrate })
}); });
if (response.ok) { if (response.ok) {
showNotification('Groupe créé', 'success'); showNotification('Groupe créé', 'success');
await fetchGroups(); await fetchGroups();
} else { } else {
showNotification('Erreur création groupe', 'error'); const err = await response.json().catch(() => ({}));
showNotification('Erreur: ' + (err.error || 'Création échouée'), 'error');
} }
} catch (error) { } catch (error) {
console.error('Erreur add group:', error); console.error('Erreur add group:', error);
@@ -725,6 +752,82 @@ document.addEventListener('DOMContentLoaded', () => {
// ========== Helpers ========== // ========== 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 = `<p class="modal-message">${escapeHtml(message)}</p>`;
} else {
bodyEl.innerHTML = fields.map(field => `
<div class="form-group">
<label>${escapeHtml(field.label)}</label>
<input
type="${field.type || 'text'}"
id="modal-field-${field.name}"
class="form-control"
value="${escapeHtml(String(field.default ?? ''))}"
${field.min !== undefined ? `min="${field.min}"` : ''}
${field.max !== undefined ? `max="${field.max}"` : ''}
${field.step !== undefined ? `step="${field.step}"` : ''}>
</div>
`).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) { function slugify(text) {
return text.toString().normalize('NFD').replace(/[̀-ͯ]/g, '') return text.toString().normalize('NFD').replace(/[̀-ͯ]/g, '')
.toLowerCase().trim().replace(/\s+/g, '-').replace(/[^\w-]+/g, '').replace(/--+/g, '-'); .toLowerCase().trim().replace(/\s+/g, '-').replace(/[^\w-]+/g, '').replace(/--+/g, '-');
+14
View File
@@ -196,6 +196,20 @@
</main> </main>
</div> </div>
<!-- Modal générique -->
<div id="modal-overlay" class="modal-overlay hidden">
<div class="modal">
<div class="modal-header">
<h3 id="modal-title"></h3>
</div>
<div class="modal-body" id="modal-body"></div>
<div class="modal-footer">
<button class="btn btn-secondary" id="modal-cancel">Annuler</button>
<button class="btn btn-primary" id="modal-confirm">Confirmer</button>
</div>
</div>
</div>
<!-- Le QR Code est généré côté Main Process (lib qrcode Node), pas de dépendance CDN --> <!-- Le QR Code est généré côté Main Process (lib qrcode Node), pas de dépendance CDN -->
<script src="app.js"></script> <script src="app.js"></script>
</body> </body>
+52
View File
@@ -379,6 +379,58 @@ body {
border-color: var(--accent-primary); 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 */
.config-actions { .config-actions {
display: flex; display: flex;