diff --git a/public/index.html b/public/index.html index 13c55dc..4d6dcd4 100644 --- a/public/index.html +++ b/public/index.html @@ -16,7 +16,7 @@ đ STOP ALL - v1.208 + v1.212 đ„ INSTALLER diff --git a/public/readme.md b/public/readme.md index 02ca466..ebbed1d 100644 --- a/public/readme.md +++ b/public/readme.md @@ -3,17 +3,18 @@ ## FonctionnalitĂ©s -* Edition de 64 boutons poru diffuser des sons simultanĂ© +* Edition de 64 boutons pour diffuser des sons de maniĂšre simultanĂ©e +* Renommage automatique des libellĂ© au chargement d'une piste +* switch entre deux boutons * Stockage du son en interne du navigateur * ArrĂȘt de toutes les pistes avec un bouton STOP ALL -* Progressif Web App - +* Progressif Web App : fonctionnement offline +* Utilisation d'IndexedDB pour stocker les son ## FonctionnalitĂ©s Ă venir * Authentification pour stocker les sons sur le serveur * Authentification pour rĂ©cuperer les sons depuis le serveur -* Utilisation d'IndexedDB pour stocker les son * Partager des sons pour que d'autres puissent les dĂ©ployer dans leur soundboard * Rechercher des sons dans sa mĂ©diathĂšque ou des sons partagĂ©s pour une diffusion One Shot * Ajouter des sons partagĂ©s par d'autres utilisateurs diff --git a/public/script.js b/public/script.js index 13fe8a4..e6aafb9 100644 --- a/public/script.js +++ b/public/script.js @@ -1,252 +1,361 @@ - let isEditMode = false; - let editingIndex = null; - let tempFileData = null; - let wakeLock = null; +let isEditMode = false; +let editingIndex = null; +let tempFileData = null; +let wakeLock = null; +let deferredPrompt; - let deferredPrompt; - const installBtn = document.getElementById('installBtn'); - const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); +const DB_NAME = "SoundboardDB"; +const STORE_NAME = "sounds"; - window.addEventListener('beforeinstallprompt', (e) => { - // EmpĂȘche Chrome d'afficher sa propre mini-barre - e.preventDefault(); - // Stocke l'Ă©vĂ©nement pour l'utiliser plus tard - deferredPrompt = e; - // Affiche votre bouton personnalisĂ© - installBtn.style.display = 'block'; +const installBtn = document.getElementById('installBtn'); +const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); - installBtn.addEventListener('click', async () => { - if (deferredPrompt) { - // Montre la fenĂȘtre d'installation native - deferredPrompt.prompt(); - // Attend la rĂ©ponse de l'utilisateur - const { outcome } = await deferredPrompt.userChoice; - console.log(`L'utilisateur a rĂ©pondu : ${outcome}`); - // On nettoie - deferredPrompt = null; - installBtn.style.display = 'none'; +function openDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, 1); + request.onupgradeneeded = (e) => { + const db = e.target.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); } - }); + }; + request.onsuccess = (e) => resolve(e.target.result); + request.onerror = (e) => reject(e.target.error); }); +} - if (isFirefox && !window.matchMedia('(display-mode: standalone)').matches) { - const infoZone = document.getElementById('app-version'); - if (infoZone) { - // Message plus pro-actif pour l'utilisateur - infoZone.innerHTML += " (Utilisez 'Installer' dans le menu Firefox)"; - infoZone.style.color = "#ff8800"; - } - } +async function saveSound(id, data) { + const db = await openDB(); + const tx = db.transaction(STORE_NAME, "readwrite"); + tx.objectStore(STORE_NAME).put(data, id); + return tx.complete; +} - // Cache le bouton si l'app est dĂ©jĂ installĂ©e - window.addEventListener('appinstalled', () => { - installBtn.style.display = 'none'; - deferredPrompt = null; - console.log('PWA installĂ©e avec succĂšs !'); +async function getSound(id) { + const db = await openDB(); + return new Promise((resolve) => { + const tx = db.transaction(STORE_NAME, "readonly"); + const request = tx.objectStore(STORE_NAME).get(id); + request.onsuccess = () => resolve(request.result); }); +} - // DonnĂ©es pour 64 boutons (Grille 8x8) - let btnData = JSON.parse(localStorage.getItem('sb_studio_data')) || Array(64).fill(null).map((_, i) => ({ - id: i, name: "", file: null, loop: false, volume: 1 - })); +// Lancement automatique au dĂ©marrage +// On attend que la page soit prĂȘte +window.addEventListener('load', () => { + migrateToIndexedDB(); +}); - const players = {}; +window.addEventListener('beforeinstallprompt', (e) => { + // EmpĂȘche Chrome d'afficher sa propre mini-barre + e.preventDefault(); + // Stocke l'Ă©vĂ©nement pour l'utiliser plus tard + deferredPrompt = e; + // Affiche votre bouton personnalisĂ© + installBtn.style.display = 'block'; - let draggedIndex = null; - - function handleDragStart(e, index) { - draggedIndex = index; - e.target.classList.add('dragging'); - // On dĂ©finit un transfert de donnĂ©es pour Firefox - e.dataTransfer.setData('text/plain', index); - } - - function handleDragOver(e) { - e.preventDefault(); // NĂ©cessaire pour permettre le drop - return false; - } - - function handleDrop(e, targetIndex) { - e.preventDefault(); - const targetElement = document.getElementById(`btn-${targetIndex}`); - targetElement.classList.remove('drag-over'); - - if (draggedIndex === targetIndex) return; - - // --- LOGIQUE D'INVERSION --- - // On inverse les donnĂ©es dans le tableau btnData - const temp = btnData[draggedIndex]; - btnData[draggedIndex] = btnData[targetIndex]; - btnData[targetIndex] = temp; - - // On s'assure que les IDs restent corrects (optionnel selon ton usage) - btnData[draggedIndex].id = draggedIndex; - btnData[targetIndex].id = targetIndex; - - // Si des sons Ă©taient en cours, on les arrĂȘte pour Ă©viter les bugs - stopAll(); - - // On rĂ©initialise les lecteurs audio pour ces deux boutons - delete players[draggedIndex]; - delete players[targetIndex]; - - // Sauvegarde et mise Ă jour de l'affichage - localStorage.setItem('sb_studio_data', JSON.stringify(btnData)); - init(); - } - - // Fonction pour demander le verrouillage de l'Ă©cran - async function requestWakeLock() { - try { - if ('wakeLock' in navigator) { - wakeLock = await navigator.wakeLock.request('screen'); - console.log("L'Ă©cran est verrouillĂ© (ne s'Ă©teindra pas)"); - - // Si le verrouillage est libĂ©rĂ© (ex: perte de focus), on le rĂ©initialise - wakeLock.addEventListener('release', () => { - console.log("Le verrouillage de l'Ă©cran a Ă©tĂ© libĂ©rĂ©"); - }); - } - } catch (err) { - console.error(`${err.name}, ${err.message}`); - } - } - - // RĂ©activer le verrouillage quand l'app revient au premier plan - document.addEventListener('visibilitychange', async () => { - if (wakeLock !== null && document.visibilityState === 'visible') { - await requestWakeLock(); + installBtn.addEventListener('click', async () => { + if (deferredPrompt) { + // Montre la fenĂȘtre d'installation native + deferredPrompt.prompt(); + // Attend la rĂ©ponse de l'utilisateur + const { outcome } = await deferredPrompt.userChoice; + console.log(`L'utilisateur a rĂ©pondu : ${outcome}`); + // On nettoie + deferredPrompt = null; + installBtn.style.display = 'none'; } }); +}); - function init() { - const board = document.getElementById('board'); - board.innerHTML = ""; - btnData.forEach((data, i) => { - const div = document.createElement('div'); - div.className = `btn ${data.file ? 'active' : ''} ${data.loop ? 'has-loop' : ''}`; - div.id = `btn-${i}`; - - // Le bouton n'est "draggable" que si le mode Ă©dition est actif - div.setAttribute('draggable', isEditMode ? 'true' : 'false'); - - div.innerHTML = `${i+1} - ${data.name || "-"} - â`; - - div.onclick = () => handleBtnClick(i); +if (isFirefox && !window.matchMedia('(display-mode: standalone)').matches) { + const infoZone = document.getElementById('app-version'); + if (infoZone) { + // Message plus pro-actif pour l'utilisateur + infoZone.innerHTML += " (Utilisez 'Installer' dans le menu Firefox)"; + infoZone.style.color = "#ff8800"; + } +} + +// Cache le bouton si l'app est dĂ©jĂ installĂ©e +window.addEventListener('appinstalled', () => { +installBtn.style.display = 'none'; +deferredPrompt = null; +console.log('PWA installĂ©e avec succĂšs !'); +}); + +// DonnĂ©es pour 64 boutons (Grille 8x8) +let btnData = JSON.parse(localStorage.getItem('sb_studio_data')) || Array(64).fill(null).map((_, i) => ({ + id: i, name: "", file: null, loop: false, volume: 1 +})); + +const players = {}; + +let draggedIndex = null; + +function handleDragStart(e, index) { + draggedIndex = index; + e.target.classList.add('dragging'); + // On dĂ©finit un transfert de donnĂ©es pour Firefox + e.dataTransfer.setData('text/plain', index); +} + +function handleDragOver(e) { + e.preventDefault(); // NĂ©cessaire pour permettre le drop + return false; +} + +function handleDrop(e, targetIndex) { + e.preventDefault(); + const targetElement = document.getElementById(`btn-${targetIndex}`); + targetElement.classList.remove('drag-over'); + + if (draggedIndex === targetIndex) return; + + // --- LOGIQUE D'INVERSION --- + // On inverse les donnĂ©es dans le tableau btnData + const temp = btnData[draggedIndex]; + btnData[draggedIndex] = btnData[targetIndex]; + btnData[targetIndex] = temp; + + // On s'assure que les IDs restent corrects (optionnel selon ton usage) + btnData[draggedIndex].id = draggedIndex; + btnData[targetIndex].id = targetIndex; + + // Si des sons Ă©taient en cours, on les arrĂȘte pour Ă©viter les bugs + stopAll(); - // Les Ă©vĂ©nements de Drag & Drop (ils ne se dĂ©clencheront que si draggable est true) - div.ondragstart = (e) => handleDragStart(e, i); - div.ondragover = (e) => handleDragOver(e); - div.ondragenter = (e) => isEditMode && div.classList.add('drag-over'); - div.ondragleave = (e) => div.classList.remove('drag-over'); - div.ondrop = (e) => handleDrop(e, i); - div.ondragend = (e) => div.classList.remove('dragging'); - - board.appendChild(div); - }); - } + // On rĂ©initialise les lecteurs audio pour ces deux boutons + delete players[draggedIndex]; + delete players[targetIndex]; - function toggleEditMode() { - isEditMode = !isEditMode; - document.body.classList.toggle('edit-mode', isEditMode); - - // Mise Ă jour visuelle du bouton - const toggleBtn = document.getElementById('toggleBtn'); - toggleBtn.innerText = isEditMode ? "QUITTER L'ĂDITION" : "MODE ĂDITION"; - toggleBtn.style.background = isEditMode ? "var(--accent)" : "#444"; - toggleBtn.style.color = isEditMode ? "black" : "white"; - - // On rĂ©initialise la grille pour appliquer le changement de 'draggable' - init(); - } - - function handleBtnClick(i) { - if (isEditMode) openEdit(i); - else playSound(i); - } - - function playSound(i) { - const data = btnData[i]; - if (!data.file) return; - - if (players[i] && !players[i].paused) { - players[i].pause(); - players[i].currentTime = 0; - document.getElementById(`btn-${i}`).classList.remove('playing'); - } else { - if (!players[i]) players[i] = new Audio(data.file); - players[i].loop = data.loop; - players[i].volume = data.volume || 1; - players[i].play(); - document.getElementById(`btn-${i}`).classList.add('playing'); - players[i].onended = () => { if(!data.loop) document.getElementById(`btn-${i}`).classList.remove('playing'); }; - } - } - - function stopAll() { - Object.keys(players).forEach(key => { - players[key].pause(); players[key].currentTime = 0; - document.getElementById(`btn-${key}`).classList.remove('playing'); - }); - } - - function openEdit(i) { - editingIndex = i; - const data = btnData[i]; - document.getElementById('editTitle').innerText = "Bouton " + (i + 1); - document.getElementById('inputName').value = data.name; - document.getElementById('inputLoop').checked = data.loop; - document.getElementById('inputVolume').value = data.volume || 1; - document.getElementById('fileNameLabel').innerText = data.file ? "Audio prĂ©sent" : "Audio vide"; - tempFileData = data.file; - document.getElementById('editOverlay').style.display = 'flex'; - } - - document.getElementById('fileInput').onchange = (e) => { - const file = e.target.files[0]; - if (file) { - // Extraction du nom sans l'extension - // .replace(/\.[^/.]+$/, "") retire tout ce qui suit le dernier point - const fileNameWithoutExtension = file.name.replace(/\.[^/.]+$/, ""); - - // On met Ă jour le champ texte du nom dans la modale - document.getElementById('inputName').value = fileNameWithoutExtension; - - const reader = new FileReader(); - reader.onload = (ev) => { - tempFileData = ev.target.result; - document.getElementById('fileNameLabel').innerText = "Fichier : " + file.name; - }; - reader.readAsDataURL(file); - } - }; - - function saveSettings() { - btnData[editingIndex].name = document.getElementById('inputName').value; - btnData[editingIndex].loop = document.getElementById('inputLoop').checked; - btnData[editingIndex].volume = parseFloat(document.getElementById('inputVolume').value); - btnData[editingIndex].file = tempFileData; - localStorage.setItem('sb_studio_data', JSON.stringify(btnData)); - if (players[editingIndex]) delete players[editingIndex]; - closeEdit(); - init(); - } - - // DĂ©sactive le menu contextuel (clic droit) sur toute la page - window.oncontextmenu = function(event) { - event.preventDefault(); - event.stopPropagation(); - return false; - }; - - function closeEdit() { document.getElementById('editOverlay').style.display = 'none'; } + // Sauvegarde et mise Ă jour de l'affichage + localStorage.setItem('sb_studio_data', JSON.stringify(btnData)); init(); +} - if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('sw.js') - .then(() => console.log("Service Worker EnregistrĂ©")); +// Fonction pour demander le verrouillage de l'Ă©cran +async function requestWakeLock() { + try { + if ('wakeLock' in navigator) { + wakeLock = await navigator.wakeLock.request('screen'); + console.log("L'Ă©cran est verrouillĂ© (ne s'Ă©teindra pas)"); + + // Si le verrouillage est libĂ©rĂ© (ex: perte de focus), on le rĂ©initialise + wakeLock.addEventListener('release', () => { + console.log("Le verrouillage de l'Ă©cran a Ă©tĂ© libĂ©rĂ©"); + }); + } + } catch (err) { + console.error(`${err.name}, ${err.message}`); } +} + +// RĂ©activer le verrouillage quand l'app revient au premier plan +document.addEventListener('visibilitychange', async () => { + if (wakeLock !== null && document.visibilityState === 'visible') { + await requestWakeLock(); + } +}); + +function init() { + const board = document.getElementById('board'); + board.innerHTML = ""; + btnData.forEach((data, i) => { + const div = document.createElement('div'); + div.className = `btn ${data.file ? 'active' : ''} ${data.loop ? 'has-loop' : ''}`; + div.id = `btn-${i}`; + + // Le bouton n'est "draggable" que si le mode Ă©dition est actif + div.setAttribute('draggable', isEditMode ? 'true' : 'false'); + + div.innerHTML = `${i+1} + ${data.name || "-"} + â`; + + div.onclick = () => handleBtnClick(i); + + // Les Ă©vĂ©nements de Drag & Drop (ils ne se dĂ©clencheront que si draggable est true) + div.ondragstart = (e) => handleDragStart(e, i); + div.ondragover = (e) => handleDragOver(e); + div.ondragenter = (e) => isEditMode && div.classList.add('drag-over'); + div.ondragleave = (e) => div.classList.remove('drag-over'); + div.ondrop = (e) => handleDrop(e, i); + div.ondragend = (e) => div.classList.remove('dragging'); + + board.appendChild(div); + }); +} + +function toggleEditMode() { + isEditMode = !isEditMode; + document.body.classList.toggle('edit-mode', isEditMode); - requestWakeLock(); \ No newline at end of file + // Mise Ă jour visuelle du bouton + const toggleBtn = document.getElementById('toggleBtn'); + toggleBtn.innerText = isEditMode ? "QUITTER L'ĂDITION" : "MODE ĂDITION"; + toggleBtn.style.background = isEditMode ? "var(--accent)" : "#444"; + toggleBtn.style.color = isEditMode ? "black" : "white"; + + // On rĂ©initialise la grille pour appliquer le changement de 'draggable' + init(); +} + +function handleBtnClick(i) { + if (isEditMode) openEdit(i); + else playSound(i); +} + +async function migrateToIndexedDB() { + // 1. VĂ©rifie si des donnĂ©es existent dans le localStorage + const rawData = localStorage.getItem('sb_studio_data'); + if (!rawData) return; // Rien Ă migrer + + const data = JSON.parse(rawData); + let migrationEffectuĂ©e = false; + + console.log("Migration vers IndexedDB en cours..."); + + for (let i = 0; i < data.length; i++) { + // 2. Si un bouton contient un fichier en base64 (ancien format) + if (data[i].file && data[i].file.startsWith('data:audio')) { + try { + // Sauvegarde le son dans IndexedDB + await saveSound(`sound-${i}`, data[i].file); + + // Supprime le fichier lourd de l'objet de donnĂ©es + delete data[i].file; + + // Marque le bouton comme ayant un fichier dans IndexedDB + data[i].hasFile = true; + + migrationEffectuĂ©e = true; + console.log(`Bouton ${i+1} migrĂ© avec succĂšs.`); + } catch (err) { + console.error(`Erreur migration bouton ${i}:`, err); + } + } + } + + if (migrationEffectuĂ©e) { + // 3. Met Ă jour le localStorage avec les donnĂ©es allĂ©gĂ©es + localStorage.setItem('sb_studio_data', JSON.stringify(data)); + + // 4. RafraĂźchit les donnĂ©es globales et l'interface + btnData = data; + init(); + alert("Mise Ă jour du stockage rĂ©ussie ! Vos sons sont maintenant sĂ©curisĂ©s dans IndexedDB."); + } +} + + + +async function playSound(i) { + const data = btnData[i]; + if (!data.hasFile) return; + + const btnElement = document.getElementById(`btn-${i}`); + + if (players[i] && !players[i].paused) { + players[i].pause(); + players[i].currentTime = 0; + btnElement.classList.remove('playing'); + btnElement.style.setProperty('--progress', '0%'); + } else { + // Si le player n'existe pas, on rĂ©cupĂšre le fichier dans IndexedDB + if (!players[i]) { + const fileData = await getSound(`sound-${i}`); + if (!fileData) return; + players[i] = new Audio(fileData); + } + + players[i].loop = data.loop; + players[i].volume = data.volume || 1; + + // ... (votre logique de ontimeupdate et play reste la mĂȘme) + players[i].play(); + btnElement.classList.add('playing'); + } +} + +function stopAll() { + Object.keys(players).forEach(key => { + players[key].pause(); players[key].currentTime = 0; + document.getElementById(`btn-${key}`).classList.remove('playing'); + }); +} + +function openEdit(i) { + editingIndex = i; + const data = btnData[i]; + document.getElementById('editTitle').innerText = "Bouton " + (i + 1); + document.getElementById('inputName').value = data.name; + document.getElementById('inputLoop').checked = data.loop; + document.getElementById('inputVolume').value = data.volume || 1; + document.getElementById('fileNameLabel').innerText = data.file ? "Audio prĂ©sent" : "Audio vide"; + tempFileData = data.file; + document.getElementById('editOverlay').style.display = 'flex'; +} + +document.getElementById('fileInput').onchange = (e) => { + const file = e.target.files[0]; + if (file) { + // Extraction du nom sans l'extension + // .replace(/\.[^/.]+$/, "") retire tout ce qui suit le dernier point + const fileNameWithoutExtension = file.name.replace(/\.[^/.]+$/, ""); + + // On met Ă jour le champ texte du nom dans la modale + document.getElementById('inputName').value = fileNameWithoutExtension; + + const reader = new FileReader(); + reader.onload = (ev) => { + tempFileData = ev.target.result; + document.getElementById('fileNameLabel').innerText = "Fichier : " + file.name; + }; + reader.readAsDataURL(file); + } +}; + +async function saveSettings() { + const name = document.getElementById('inputName').value; + const loop = document.getElementById('inputLoop').checked; + const volume = parseFloat(document.getElementById('inputVolume').value); + + // Mise Ă jour des mĂ©tadonnĂ©es + btnData[editingIndex].name = name; + btnData[editingIndex].loop = loop; + btnData[editingIndex].volume = volume; + + // Sauvegarde du fichier lourd dans IndexedDB si prĂ©sent + if (tempFileData) { + await saveSound(`sound-${editingIndex}`, tempFileData); + btnData[editingIndex].hasFile = true; // Flag pour savoir qu'un son existe + } + + // Sauvegarde des rĂ©glages lĂ©gers dans localStorage + localStorage.setItem('sb_studio_data', JSON.stringify(btnData)); + + // Nettoyage du lecteur existant + if (players[editingIndex]) delete players[editingIndex]; + + tempFileData = null; + closeEdit(); + init(); +} + +// DĂ©sactive le menu contextuel (clic droit) sur toute la page +window.oncontextmenu = function(event) { + event.preventDefault(); + event.stopPropagation(); + return false; +}; + +function closeEdit() { document.getElementById('editOverlay').style.display = 'none'; } +init(); + +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('sw.js') + .then(() => console.log("Service Worker EnregistrĂ©")); +} + +requestWakeLock(); \ No newline at end of file diff --git a/public/sw.js b/public/sw.js index 8340df5..0cb5b5f 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'sb-v1.208'; // Change ce numĂ©ro pour forcer une mise Ă jour globale +const CACHE_NAME = 'sb-v1.212'; // Change ce numĂ©ro pour forcer une mise Ă jour globale const ASSETS = [ './', // Racine './index.html',