let isEditMode = false; let editingIndex = null; let tempFileData = null; let wakeLock = null; let deferredPrompt; const DB_NAME = "SoundboardDB"; const STORE_NAME = "sounds"; const installBtn = document.getElementById('installBtn'); const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); 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); }); } 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; } 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); }); } // Lancement automatique au démarrage // On attend que la page soit prête window.addEventListener('load', () => { if (checkAuth()) { document.getElementById('loginBtn').style.display = 'none'; document.getElementById('userProfile').style.display = 'flex'; // Charger les données utilisateur depuis le serveur ici si besoin init(); } else { // Optionnel : afficher une grille vide ou un message console.log("Utilisateur non connecté."); } migrateToIndexedDB(); }); 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'; 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'; } }); }); 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(); // 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(); } }); 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); // 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}`); // Si déjà en lecture : on arrête 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 ou a été supprimé, on le crée 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; // Gestion de la progression players[i].ontimeupdate = () => { const percentage = (players[i].currentTime / players[i].duration) * 100; btnElement.style.setProperty('--progress', `${percentage}%`); }; // --- CORRECTION ICI : Gestion de la fin de lecture --- players[i].onended = () => { btnElement.style.setProperty('--progress', '0%'); // On ne retire la classe que si on n'est pas en boucle if (!data.loop) { btnElement.classList.remove('playing'); } }; 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();