let isEditMode = false; let editingIndex = null; let tempFileData = null; let wakeLock = null; let deferredPrompt; const DB_NAME = "SoundboardDB"; const STORE_NAME = "sounds"; const players = {}; let draggedIndex = null; let btnData = JSON.parse(localStorage.getItem('sb_studio_data')) || Array(64).fill(null).map((_, i) => ({ id: i, name: "", file: null, loop: false, volume: 1, hasFile: false })); const installBtn = document.getElementById('installBtn'); const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); /* --- BASE DE DONNÉES INDEXEDDB --- */ let dbInstance = null; /** * Ouvre ou récupère l'instance de la base de données (Singleton). */ async function getDB() { if (dbInstance) return dbInstance; 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) => { dbInstance = e.target.result; resolve(dbInstance); }; request.onerror = (e) => { console.error("Erreur IndexedDB:", e.target.error); reject(e.target.error); }; }); } /** * Enregistre un son dans la base de données. */ async function saveSound(id, data) { try { const db = await getDB(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, "readwrite"); const store = tx.objectStore(STORE_NAME); store.put(data, id); tx.oncomplete = () => resolve(); tx.onerror = (e) => reject(e.target.error); }); } catch (err) { console.error(`Erreur sauvegarde son ${id}:`, err); } } /** * Récupère un son depuis la base de données. */ async function getSound(id) { try { const db = await getDB(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, "readonly"); const store = tx.objectStore(STORE_NAME); const request = store.get(id); request.onsuccess = () => resolve(request.result); request.onerror = (e) => reject(e.target.error); }); } catch (err) { console.error(`Erreur récupération son ${id}:`, err); return null; } } /* --- NAVIGATION ET GRILLE --- */ function init() { const board = document.getElementById('board'); if (!board) return; board.innerHTML = ""; btnData.forEach((data, i) => { const div = document.createElement('div'); // On utilise data.hasFile pour la classe active div.className = `btn ${data.hasFile ? 'active' : ''} ${data.loop ? 'has-loop' : ''}`; div.id = `btn-${i}`; div.setAttribute('draggable', isEditMode ? 'true' : 'false'); div.innerHTML = `${i+1} ${data.name || "-"} `; div.onclick = () => handleBtnClick(i); div.ondragstart = (e) => handleDragStart(e, i); div.ondragover = (e) => handleDragOver(e); div.ondrop = (e) => handleDrop(e, i); board.appendChild(div); }); } function handleBtnClick(i) { if (isEditMode) openEdit(i); else playSound(i); } function toggleEditMode() { isEditMode = !isEditMode; document.body.classList.toggle('edit-mode', isEditMode); const toggleBtn = document.getElementById('toggleBtn'); toggleBtn.innerText = isEditMode ? "QUITTER L'ÉDITION" : "MODE ÉDITION"; init(); } /* --- LECTURE AUDIO --- */ /** * Gère la lecture, la pause et la progression d'un échantillon sonore. * @param {number} i - Index du bouton. */ async function playSound(i) { const data = btnData[i]; if (!data.hasFile) return; const btnElement = document.getElementById(`btn-${i}`); if (!btnElement) return; // 1. Si déjà en lecture : on arrête proprement if (players[i] && !players[i].paused) { players[i].pause(); players[i].currentTime = 0; resetButtonUI(btnElement); return; } try { // 2. Initialisation du lecteur si inexistant if (!players[i]) { const fileData = await getSound(`sound-${i}`); if (!fileData) throw new Error("Données audio introuvables"); players[i] = new Audio(fileData); } const audio = players[i]; audio.loop = data.loop || false; audio.volume = data.volume ?? 1; // 3. Événements de mise à jour UI audio.ontimeupdate = () => { if (audio.duration) { const percentage = (audio.currentTime / audio.duration) * 100; btnElement.style.setProperty('--progress', `${percentage.toFixed(2)}%`); } }; audio.onended = () => { if (!audio.loop) { resetButtonUI(btnElement); } }; // 4. Lancement de la lecture (gère la promesse pour éviter les erreurs de navigateur) await audio.play(); btnElement.classList.add('playing'); } catch (err) { console.error(`Erreur de lecture (Bouton ${i}):`, err); // En cas d'erreur (ex: format non supporté), on nettoie l'UI resetButtonUI(btnElement); } } /** * Arrête tous les sons en cours et réinitialise l'interface de tous les boutons. */ function stopAll() { Object.keys(players).forEach(key => { const audio = players[key]; if (audio) { audio.pause(); audio.currentTime = 0; } const btn = document.getElementById(`btn-${key}`); if (btn) resetButtonUI(btn); }); } /** * Utilitaire interne pour remettre un bouton à son état initial. * @param {HTMLElement} btn */ function resetButtonUI(btn) { btn.classList.remove('playing'); btn.style.setProperty('--progress', '0%'); } /* --- CYCLE DE VIE (LOAD / INSTALL) --- */ /** * Initialisation principale au chargement de la page. */ window.addEventListener('load', async () => { // 2. Initialisation de la grille init(); // 3. Traitements asynchrones (Migration & Verrouillage écran) try { await migrateToIndexedDB(); await requestWakeLock(); } catch (err) { console.warn("Échec des services secondaires :", err); } }); /** * Gestion de l'installation PWA (Prompt). */ window.addEventListener('beforeinstallprompt', (e) => { // Empêche l'affichage automatique de la bannière système e.preventDefault(); deferredPrompt = e; // Affiche notre bouton d'installation personnalisé if (installBtn) { installBtn.style.display = 'block'; } }); // Écouteur sur le bouton d'installation if (installBtn) { installBtn.addEventListener('click', async () => { if (!deferredPrompt) return; // Déclenche le prompt natif deferredPrompt.prompt(); // Attend la réponse de l'utilisateur const { outcome } = await deferredPrompt.userChoice; console.log(`Résultat installation : ${outcome}`); // Nettoyage deferredPrompt = null; installBtn.style.display = 'none'; }); } 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."); } } /* --- MODALE D'ÉDITION --- */ /** * Ouvre la modale et pré-remplit les champs avec les données du bouton. * @param {number} i - Index du bouton dans btnData. */ function openEdit(i) { editingIndex = i; const data = btnData[i]; // Mise à jour des éléments texte document.getElementById('editTitle').textContent = `Configuration Bouton ${i + 1}`; document.getElementById('fileNameLabel').textContent = data.hasFile ? "✅ Son présent" : "❌ Aucun son"; // Remplissage des champs de saisie document.getElementById('inputName').value = data.name || ""; document.getElementById('inputLoop').checked = data.loop || false; document.getElementById('inputVolume').value = data.volume ?? 1; // Reset du cache temporaire de fichier tempFileData = null; // Affichage de l'overlay const overlay = document.getElementById('editOverlay'); if (overlay) overlay.style.display = 'flex'; } /** * Gère la sélection d'un nouveau fichier audio. */ document.getElementById('fileInput').addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; // Extraction du nom (sans extension) pour pré-remplir le champ nom const fileNameWithoutExtension = file.name.replace(/\.[^/.]+$/, ""); document.getElementById('inputName').value = fileNameWithoutExtension; const label = document.getElementById('fileNameLabel'); label.textContent = "⌛ Chargement..."; const reader = new FileReader(); reader.onload = (ev) => { tempFileData = ev.target.result; label.textContent = `Fichier prêt : ${file.name}`; }; reader.onerror = () => { label.textContent = "❌ Erreur lors de la lecture du fichier"; }; reader.readAsDataURL(file); }); /* --- LOGIQUE DE SAUVEGARDE ET BACKUP --- */ /** * Enregistre les modifications localement et lance le backup serveur. */ /** * Enregistre les modifications localement et lance le backup serveur immédiat si connecté. */ async function saveSettings() { const nameInput = document.getElementById('inputName'); const loopInput = document.getElementById('inputLoop'); const volumeInput = document.getElementById('inputVolume'); const updatedData = btnData[editingIndex]; updatedData.name = nameInput.value.trim(); updatedData.loop = loopInput.checked; updatedData.volume = parseFloat(volumeInput.value); // 1. Sauvegarde locale (IndexedDB) si nouveau fichier if (tempFileData) { try { await saveSound(`sound-${editingIndex}`, tempFileData); updatedData.hasFile = true; } catch (err) { console.error("Erreur IndexedDB :", err); alert("Erreur lors de la sauvegarde locale."); return; } } // 2. Sauvegarde des métadonnées locales localStorage.setItem('sb_studio_data', JSON.stringify(btnData)); // 3. Réinitialisation du lecteur audio if (players[editingIndex]) { players[editingIndex].pause(); delete players[editingIndex]; } // 4. Fermeture UI tempFileData = null; closeEdit(); init(); // 5. SYNCHRO AUTOMATIQUE CLOUD (Si loggué) const token = localStorage.getItem('auth_token'); if (token) { console.log(`Cloud: Synchronisation automatique du bouton ${editingIndex}...`); syncSingleButtonToCloud(editingIndex, updatedData, token); } } /** * Synchronise un seul bouton vers le cloud (appelé après modification individuelle) */ async function syncSingleButtonToCloud(index, data, token) { try { const formData = new FormData(); formData.append('index', index); formData.append('name', data.name); formData.append('loop', data.loop); formData.append('volume', data.volume); // Récupérer l'audio depuis IndexedDB pour l'envoi const soundData = await getSound(`sound-${index}`); if (soundData) { const response = await fetch(soundData); const blob = await response.blob(); formData.append('audio', blob, `sound-${index}.mp3`); } const res = await fetch('sync_global.php', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData }); if (res.ok) { console.log(`✅ Bouton ${index} synchronisé avec succès.`); } else { console.warn(`⚠️ Échec synchro bouton ${index}: ${res.statusText}`); } } catch (err) { console.error(`❌ Erreur réseau synchro bouton ${index}:`, err); } } async function syncAllToCloud() { const btn = document.getElementById('syncBtn'); if (!btn) return; // Récupération du token depuis le localStorage (posé par callback.php) const token = localStorage.getItem('auth_token'); if (!token) { alert("Action impossible : Vous devez être connecté."); return; } const originalText = btn.innerText; btn.disabled = true; const buttonsToSync = btnData.filter(d => d.hasFile); const total = buttonsToSync.length; try { let successCount = 0; for (const data of buttonsToSync) { btn.innerText = `⏳ SYNC : ${successCount + 1} / ${total}`; const formData = new FormData(); formData.append('index', data.id); formData.append('name', data.name); formData.append('loop', data.loop); formData.append('volume', data.volume); // Récupération du son (DataURL) depuis IndexedDB const soundData = await getSound(`sound-${data.id}`); if (soundData) { // Conversion DataURL -> Blob pour l'envoi PHP const response = await fetch(soundData); const blob = await response.blob(); formData.append('audio', blob, `sound-${data.id}.mp3`); } // Envoi vers le script PHP const res = await fetch('sync_global.php', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData }); if (res.ok) successCount++; } } catch (err) { console.error(err); alert("❌ Erreur lors de la synchronisation."); } finally { btn.innerText = originalText; btn.disabled = false; } } async function exportGlobalBackup() { console.log("Préparation du backup global..."); const backupData = { version: "1.0", timestamp: new Date().toISOString(), settings: btnData, // Le contenu du localStorage sounds: {} // On va remplir ça avec les fichiers IndexedDB }; // On récupère chaque son stocké for (let i = 0; i < btnData.length; i++) { if (btnData[i].hasFile) { const soundData = await getSound(`sound-${i}`); if (soundData) { backupData.sounds[`sound-${i}`] = soundData; } } } // Création du fichier de téléchargement const dataStr = JSON.stringify(backupData); const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr); const exportFileDefaultName = `soundboard_backup_${new Date().toLocaleDateString()}.json`; const linkElement = document.createElement('a'); linkElement.setAttribute('href', dataUri); linkElement.setAttribute('download', exportFileDefaultName); linkElement.click(); } async function importGlobalBackup(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (e) => { try { const backup = JSON.parse(e.target.result); if (!confirm("Attention : l'importation va écraser votre soundboard actuelle. Continuer ?")) return; // 1. Restaurer les réglages (localStorage) btnData = backup.settings; localStorage.setItem('sb_studio_data', JSON.stringify(btnData)); // 2. Restaurer les sons (IndexedDB) for (const [id, data] of Object.entries(backup.sounds)) { await saveSound(id, data); } alert("Restauration complète réussie ! La page va s'actualiser."); window.location.reload(); } catch (err) { console.error(err); alert("Erreur lors de l'importation : fichier invalide."); } }; reader.readAsText(file); } init(); if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js') .then(() => console.log("Service Worker Enregistré")); } requestWakeLock(); /* --- CLOUD & BACKUPS --- */ /** * Synchronise l'intégralité de la soundboard vers le serveur. * Envoie chaque bouton individuellement pour correspondre à la structure du cloud. */ async function syncAllToCloud() { // On la renomme pour correspondre à l'onclick du bouton const btn = document.getElementById('syncBtn'); if (!btn) return; const token = localStorage.getItem('auth_token'); if (!token) { alert("Action impossible : Connectez-vous via l'IDP."); return; } const originalText = btn.innerText; btn.disabled = true; // On ne synchronise que les boutons qui ont un fichier const buttonsToSync = btnData.filter(d => d.hasFile); const total = buttonsToSync.length; try { let successCount = 0; for (const data of buttonsToSync) { btn.innerText = `⏳ SYNC : ${successCount + 1} / ${total}`; const formData = new FormData(); formData.append('index', data.id); formData.append('name', data.name); formData.append('loop', data.loop); formData.append('volume', data.volume); // Récupération du son depuis IndexedDB const soundData = await getSound(`sound-${data.id}`); if (soundData) { // soundData est une DataURL (base64), on la convertit en Blob pour l'envoi const response = await fetch(soundData); const blob = await response.blob(); formData.append('audio', blob, `sound-${data.id}.mp3`); } // ENVOI VERS TON NOUVEAU SCRIPT const res = await fetch('sync_global.php', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData }); if (res.ok) successCount++; } alert(`✅ Cloud mis à jour : ${successCount} boutons sauvegardés.`); } catch (err) { console.error(err); alert("❌ Erreur lors de la synchronisation vers le cloud."); } finally { btn.innerText = originalText; btn.disabled = false; } } /* --- UTILITAIRES --- */ /** * Demande le maintien de l'allumage de l'écran (Wake Lock). * Gère la réactivation automatique lors du retour sur l'onglet. */ async function requestWakeLock() { if (!('wakeLock' in navigator)) return; try { wakeLock = await navigator.wakeLock.request('screen'); // Log pour le debug (optionnel) console.log("Wake Lock actif : l'écran ne s'éteindra pas."); // Si le verrou est libéré (ex: batterie faible), on nettoie la variable wakeLock.addEventListener('release', () => { console.log("Wake Lock libéré."); wakeLock = null; }); } catch (err) { console.error(`Échec du Wake Lock: ${err.message}`); } } /** * Désactive le menu contextuel (clic droit / appui long) sur toute la page. */ window.addEventListener('contextmenu', (e) => { e.preventDefault(); }, false); /** * Ferme la modale d'édition. */ const closeEdit = () => { const overlay = document.getElementById('editOverlay'); if (overlay) overlay.style.display = 'none'; };