// Local playlist and shared audio controls. Files never leave the browser. export function initMusic({ storage, toast }) { const $ = selector => document.querySelector(selector); const audio = $('#audio'); let tracks = [], selected = -1; let shuffle = storage.get('shuffle') === 'true', shuffleBag = [], playbackHistory = []; $('#immersive-shuffle').setAttribute('aria-pressed', String(shuffle)); $('#immersive-shuffle').onclick = () => { shuffle = !shuffle; shuffleBag = []; playbackHistory = []; storage.set('shuffle', String(shuffle)); $('#immersive-shuffle').setAttribute('aria-pressed', String(shuffle)); toast(shuffle ? 'Lecture aléatoire activée' : 'Lecture aléatoire désactivée'); }; function nextTrack() { if (!shuffle || tracks.length < 2) return selectTrack(selected + 1, true); if (!shuffleBag.length) { shuffleBag = tracks.map((_, index) => index).filter(index => index !== selected); for (let i = shuffleBag.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffleBag[i], shuffleBag[j]] = [shuffleBag[j], shuffleBag[i]]; } } selectTrack(shuffleBag.pop(), true); } function renderPlaylist() { $('#playlist').replaceChildren(); tracks.forEach((track, index) => { const li = document.createElement('li'), button = document.createElement('button'); const number = document.createElement('span'), name = document.createElement('b'); number.textContent = String(index + 1).padStart(2, '0'); name.textContent = track.name; button.append(number, name); button.classList.toggle('selected', index === selected); button.setAttribute('aria-pressed', String(index === selected)); button.onclick = () => selectTrack(index, true); li.append(button); $('#playlist').append(li); }); $('#track-count').textContent = `${tracks.length} TITRE${tracks.length > 1 ? 'S' : ''}`; $('#empty-library').hidden = tracks.length > 0; ['#play', '#previous', '#next'].forEach(id => $(id).disabled = tracks.length === 0); } async function playAudio() { try { await audio.play(); $('#audio-message').textContent = 'Lecture locale · aucun fichier envoyé au serveur.'; } catch { $('#audio-message').textContent = 'Lecture impossible : réessayez ou choisissez un format audio pris en charge.'; } } function selectTrack(index, play = false, remember = true) { if (!tracks.length) return; previousTap = -Infinity; if (remember && selected >= 0 && index !== selected) { playbackHistory.push(selected); if (playbackHistory.length > 256) playbackHistory.shift(); } selected = (index + tracks.length) % tracks.length; shuffleBag = shuffleBag.filter(index => index !== selected); audio.src = tracks[selected].url; $('#track-title').textContent = tracks[selected].name; $('#home-track').textContent = tracks[selected].name; $('#track-subtitle').textContent = `Morceau ${selected + 1} sur ${tracks.length} · Bibliothèque locale`; $('#home-track-sub').textContent = 'Bibliothèque locale'; renderPlaylist(); if (play) playAudio(); } $('#music-files').addEventListener('change', event => { let added = 0; for (const file of event.target.files) { if (!file.type.startsWith('audio/') && !/\.(mp3|flac|ogg|wav|m4a|aac|opus)$/i.test(file.name)) continue; tracks.push({ name: file.name.replace(/\.[^.]+$/, ''), url: URL.createObjectURL(file) }); added++; } shuffleBag = []; renderPlaylist(); if (selected === -1 && tracks.length) selectTrack(0); toast(added ? `${added} morceau${added > 1 ? 'x' : ''} ajouté${added > 1 ? 's' : ''}` : 'Aucun fichier audio reconnu'); event.target.value = ''; }); $('#play').onclick = () => { if (selected === -1) return; if (audio.paused) playAudio(); else audio.pause(); }; let previousTap = -Infinity; $('#previous').onclick = () => { const now = performance.now(), twice = now - previousTap < 600; previousTap = twice ? -Infinity : now; if (!twice) { audio.currentTime = 0; return; } if (shuffle) { shuffleBag = []; selectTrack(playbackHistory.pop() ?? selected, true, false); } else selectTrack(selected - 1, true); }; $('#next').onclick = nextTrack; audio.addEventListener('play', () => $('#play').textContent = 'Pause'); audio.addEventListener('pause', () => $('#play').textContent = 'Lecture'); audio.addEventListener('ended', () => { if (audio.loop) return; if (shuffle || selected + 1 < tracks.length) nextTrack(); else $('#play').textContent = 'Lecture'; }); audio.addEventListener('error', () => $('#audio-message').textContent = 'Ce fichier ne peut pas être lu. Essayez un autre format audio.'); window.addEventListener('pagehide', event => { if (!event.persisted) tracks.forEach(track => URL.revokeObjectURL(track.url)); }); renderPlaylist(); }