diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8fce603
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+data/
diff --git a/public/auth.js b/public/auth.js
index beaffca..dfe1a56 100644
--- a/public/auth.js
+++ b/public/auth.js
@@ -1,20 +1,24 @@
// auth.js - Gestion de la connexion SSO
+
const authConfig = {
- authority: "https://idp.a5l.fr/realms/A5L",
- client_id: "soundboard_a5l", // À enregistrer sur votre IdP
- redirect_uri: window.location.origin + "/callback.html",
- response_type: "code",
- scope: "openid profile email"
+ authority: "https://idp.a5l.fr/realms/A5L",
+ client_id: "soundboard_a5l",
+ // MODIFICATION : pointer vers le fichier PHP
+ redirect_uri: window.location.origin + "/callback.php",
+ response_type: "code",
+ scope: "openid profile email"
};
function login() {
- // Construction de l'URL exacte attendue par Keycloak
+ const state = generateState();
+ localStorage.setItem('auth_state', state); // Optionnel : pour vérification CSRF
+
const authUrl = `${authConfig.authority}/protocol/openid-connect/auth?` +
`client_id=${authConfig.client_id}&` +
`redirect_uri=${encodeURIComponent(authConfig.redirect_uri)}&` +
`response_type=${authConfig.response_type}&` +
`scope=${authConfig.scope}&` +
- `state=${generateState()}`; // Sécurité recommandée
+ `state=${state}`;
window.location.href = authUrl;
}
@@ -30,8 +34,10 @@ function generateState() {
}
function logout() {
- localStorage.removeItem('auth_token');
- window.location.reload();
+ localStorage.removeItem('auth_token');
+ // On supprime le cookie en le faisant expirer
+ document.cookie = "auth_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
+ window.location.href = 'index.html';
}
function checkAuth() {
@@ -41,4 +47,26 @@ function checkAuth() {
return false;
}
return true;
+}
+
+function updateAuthUI() {
+ const token = localStorage.getItem('auth_token');
+ const userNameDisplay = document.getElementById('userNameDisplay');
+
+ if (token && token.includes('.')) {
+ try {
+ // Décodage sécurisé du Base64Url
+ const base64Url = token.split('.')[1];
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
+ const payload = JSON.parse(window.atob(base64));
+
+ const username = payload.preferred_username || "Utilisateur";
+
+ if (userNameDisplay) {
+ userNameDisplay.innerText = username.toUpperCase();
+ }
+ } catch (e) {
+ console.error("Erreur décodage token", e);
+ }
+ }
}
\ No newline at end of file
diff --git a/public/callback.html b/public/callback.html
deleted file mode 100644
index c016916..0000000
--- a/public/callback.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/public/callback.php b/public/callback.php
new file mode 100644
index 0000000..4de5272
--- /dev/null
+++ b/public/callback.php
@@ -0,0 +1,54 @@
+ 'authorization_code',
+ 'code' => $code,
+ 'redirect_uri' => $redirectUri,
+ 'client_id' => $clientId,
+ 'client_secret'=> $clientSecret
+];
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
+$response = curl_exec($ch);
+
+if (!$response) {
+ die("Erreur CURL : " . curl_error($ch));
+}
+
+$data = json_decode($response, true);
+if (isset($data['error'])) {
+ die("Erreur IDP : " . $data['error_description']);
+}
+
+curl_close($ch);
+
+if (isset($data['access_token'])) {
+ $token = $data['access_token'];
+ // On décode pour récupérer le login pour le JS
+ $parts = explode('.', $token);
+ $payload = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $parts[1])), true);
+ $login = $payload['preferred_username'] ?? 'User';
+
+ // COOKIE POUR PHP
+ setcookie("auth_token", $token, time() + 3600, "/", "", true, false);
+
+ // REDIRECTION ET STORAGE POUR JS
+ echo "";
+ exit();
+} else {
+ header("Location: profile.php?error=failed");
+ exit();
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index bd22e9b..f1fec9c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4,7 +4,6 @@
Soundboard A5L
-
@@ -17,18 +16,18 @@
-
1.214
+
+ 1.230
+
-
-
+
+
-
diff --git a/public/profile.php b/public/profile.php
new file mode 100644
index 0000000..7d2f657
--- /dev/null
+++ b/public/profile.php
@@ -0,0 +1,81 @@
+ 'Guest',
+ 'name' => 'Visiteur',
+ 'email' => 'Non renseigné',
+ 'logged' => false
+];
+
+if ($token) {
+ $parts = explode('.', $token);
+ if (count($parts) >= 2) {
+ $payload = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $parts[1])), true);
+ if ($payload) {
+ $userData['login'] = preg_replace('/[^a-zA-Z0-9\-\.\@]/', '', $payload['preferred_username'] ?? ($payload['sub'] ?? ''));
+ $userData['name'] = $payload['name'] ?? 'Utilisateur';
+ $userData['email'] = $payload['email'] ?? 'Non renseigné';
+ $userData['logged'] = true;
+ }
+ }
+}
+?>
+
+
+
+
+ Profil - Cloud A5L
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
?
+
Accès Restreint
+
Authentification requise via A5L Identity Provider.
+
+
+
+
+
+
+
+
+
+
+
Accueil
+
+
+
+
\ No newline at end of file
diff --git a/public/readme.md b/public/readme.md
index 14ad62e..d4e467c 100644
--- a/public/readme.md
+++ b/public/readme.md
@@ -11,15 +11,17 @@
* Progressif Web App : fonctionnement offline
* Utilisation d'IndexedDB pour stocker les son
* Empecher le device de se mettre en veille
+* Authentification A5L
+* Stocker les sons sur le serveur
## Fonctionnalités à venir
* Suppression d'une association
-* Authentification pour stocker les sons sur le serveur
* Authentification pour récuperer les sons depuis le serveur
* 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
+* Ajouter tous les sons d'un dossier
Vous pouvez trouver des sons sur https://boardsounds.com/fr
diff --git a/public/script.js b/public/script.js
index b45a80d..9de15e8 100644
--- a/public/script.js
+++ b/public/script.js
@@ -6,216 +6,264 @@ 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');
-function openDB() {
+/* --- 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) => resolve(e.target.result);
- request.onerror = (e) => reject(e.target.error);
+
+ 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) {
- 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)");
+ const db = await getDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction(STORE_NAME, "readwrite");
+ const store = tx.objectStore(STORE_NAME);
- // 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é");
- });
- }
+ store.put(data, id);
+
+ tx.oncomplete = () => resolve();
+ tx.onerror = (e) => reject(e.target.error);
+ });
} catch (err) {
- console.error(`${err.name}, ${err.message}`);
+ console.error(`Erreur sauvegarde son ${id}:`, err);
}
}
-// Réactiver le verrouillage quand l'app revient au premier plan
-document.addEventListener('visibilitychange', async () => {
- if (wakeLock !== null && document.visibilityState === 'visible') {
- await requestWakeLock();
- }
-});
+/**
+ * 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');
- div.className = `btn ${data.file ? 'active' : ''} ${data.loop ? 'has-loop' : ''}`;
+ // On utilise data.hasFile pour la classe active
+ div.className = `btn ${data.hasFile ? '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);
}
+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');
@@ -258,124 +306,270 @@ async function migrateToIndexedDB() {
}
}
+/* --- MODALE D'ÉDITION --- */
-
-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');
- });
-}
-
+/**
+ * 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];
- 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';
+
+ // 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';
}
-document.getElementById('fileInput').onchange = (e) => {
+/**
+ * Gère la sélection d'un nouveau fichier audio.
+ */
+document.getElementById('fileInput').addEventListener('change', (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);
- }
-};
+ if (!file) return;
-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;
+ // Extraction du nom (sans extension) pour pré-remplir le champ nom
+ const fileNameWithoutExtension = file.name.replace(/\.[^/.]+$/, "");
+ document.getElementById('inputName').value = fileNameWithoutExtension;
- // Sauvegarde du fichier lourd dans IndexedDB si présent
+ 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) {
- await saveSound(`sound-${editingIndex}`, tempFileData);
- btnData[editingIndex].hasFile = true; // Flag pour savoir qu'un son existe
+ try {
+ await saveSound(`sound-${editingIndex}`, tempFileData);
+ updatedData.hasFile = true;
+ } catch (err) {
+ console.error("Erreur IndexedDB :", err);
+ alert("Erreur lors de la sauvegarde locale.");
+ return;
+ }
}
- // Sauvegarde des réglages légers dans localStorage
+ // 2. Sauvegarde des métadonnées locales
localStorage.setItem('sb_studio_data', JSON.stringify(btnData));
- // Nettoyage du lecteur existant
- if (players[editingIndex]) delete players[editingIndex];
+ // 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);
+ }
}
-// 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'; }
+/**
+ * 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) {
@@ -383,4 +577,112 @@ if ('serviceWorker' in navigator) {
.then(() => console.log("Service Worker Enregistré"));
}
-requestWakeLock();
\ No newline at end of file
+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';
+};
\ No newline at end of file
diff --git a/public/sw.js b/public/sw.js
index 4c0bf75..a7c62e8 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -1,13 +1,14 @@
-const CACHE_NAME = 'sb-v1.213'; // Change ce numéro pour forcer une mise à jour globale
+const CACHE_NAME = 'sb-v1.230'; // Change ce numéro pour forcer une mise à jour globale
const ASSETS = [
'./', // Racine
'./index.html',
'./style.css',
'./script.js',
+ './auth.js',
'./manifest.json',
'./icon-192.png',
'./icon-512.png',
- '/favicon.ico'
+ './favicon.ico'
];
// Installation : Mise en cache des ressources critiques
diff --git a/public/sync_global.php b/public/sync_global.php
new file mode 100644
index 0000000..feead71
--- /dev/null
+++ b/public/sync_global.php
@@ -0,0 +1,85 @@
+ 'error', 'message' => 'Non authentifié']));
+}
+
+$token = $matches[1];
+
+// 2. Décodage du Payload du JWT
+$tokenParts = explode('.', $token);
+if (count($tokenParts) < 2) {
+ die(json_encode(['status' => 'error', 'message' => 'Format Token invalide']));
+}
+
+$payloadJson = base64_decode(str_replace(['-', '_'], ['+', '/'], $tokenParts[1]));
+$payload = json_decode($payloadJson, true);
+
+// Nettoyage du login pour le nom du dossier
+$userLogin = preg_replace('/[^a-zA-Z0-9\-\.]/', '', $payload['preferred_username'] ?? $payload['sub'] ?? '');
+
+if (empty($userLogin)) {
+ die(json_encode(['status' => 'error', 'message' => 'Login non trouvé']));
+}
+
+// 3. Définition du chemin (vérifie que ce dossier est accessible en écriture)
+$userDir = $_SERVER['DOCUMENT_ROOT'] . '/../data/' . $userLogin . '/';
+if (!is_dir($userDir)) {
+ mkdir($userDir, 0755, true);
+}
+
+// 4. RÉCUPÉRATION DE LA CONFIG (Envoyée en JSON via FormData dans script.js)
+$configData = isset($_POST['config']) ? json_decode($_POST['config'], true) : null;
+
+if (!$configData) {
+ // Fallback si tu envoies les champs un par un au lieu d'un objet config
+ $index = $_POST['index'] ?? $_POST['id'] ?? null;
+} else {
+ $index = $configData['id'] ?? $configData['index'] ?? null;
+}
+
+if ($index === null) {
+ die(json_encode(['status' => 'error', 'message' => 'Index/ID manquant']));
+}
+
+// 5. Sauvegarde Audio
+$extension = 'mp3'; // par défaut
+if (isset($_FILES['audio'])) {
+ // On essaie de deviner l'extension si non fournie dans le nom
+ $originalName = $_FILES['audio']['name'];
+ $extension = pathinfo($originalName, PATHINFO_EXTENSION);
+
+ // Si le Blob n'a pas d'extension (souvent le cas avec JS), on force selon le mime ou mp3
+ if (empty($extension)) {
+ $finfo = new finfo(FILEINFO_MIME_TYPE);
+ $mime = $finfo->file($_FILES['audio']['tmp_name']);
+ $map = ['audio/wav' => 'wav', 'audio/opus' => 'opus', 'audio/ogg' => 'ogg', 'audio/mpeg' => 'mp3'];
+ $extension = $map[$mime] ?? 'mp3';
+ }
+
+ move_uploaded_file($_FILES['audio']['tmp_name'], $userDir . "sound-$index." . $extension);
+}
+
+// 6. Sauvegarde Config (on fusionne les infos)
+$finalConfig = [
+ 'id' => (int)$index,
+ 'name' => $configData['name'] ?? ($_POST['name'] ?? 'Sans titre'),
+ 'loop' => isset($configData['loop']) ? (bool)$configData['loop'] : ($_POST['loop'] === 'true'),
+ 'volume' => isset($configData['volume']) ? (float)$configData['volume'] : (float)($_POST['volume'] ?? 1),
+ 'extension' => $extension,
+ 'updated_at' => date('Y-m-d H:i:s')
+];
+
+file_put_contents($userDir . "config-$index.json", json_encode($finalConfig, JSON_PRETTY_PRINT));
+
+echo json_encode([
+ 'status' => 'success',
+ 'user' => $userLogin,
+ 'index' => $index,
+ 'file' => "sound-$index.$extension"
+]);
\ No newline at end of file