// ~/store/player.ts import { defineStore } from 'pinia' import type { Track, Box } from '~/../types/types' import { useDataStore } from '~/store/data' export const usePlayerStore = defineStore('player', { state: () => ({ currentTrack: null as Track | null, position: 0, audio: null as HTMLAudioElement | null, isPaused: true, progressionLast: 0 }), actions: { attachAudio(el: HTMLAudioElement) { this.audio = el // attach listeners if not already attached (idempotent enough for our use) this.audio.addEventListener('play', () => { this.isPaused = false }) this.audio.addEventListener('playing', () => { this.isPaused = false }) this.audio.addEventListener('pause', () => { this.isPaused = true }) this.audio.addEventListener('ended', () => { this.isPaused = true const track = this.currentTrack if (!track) return const dataStore = useDataStore() if (track.type === 'playlist') { const next = dataStore.getNextPlaylistTrack(track) if (next && next.boxId === track.boxId) { this.playTrack(next) } } else { console.log('ended') this.currentTrack = null } }) }, async playBox(box: Box) { if (this.currentTrack?.boxId === box.id) { this.togglePlay() } else { const dataStore = useDataStore() const first = dataStore.getFirstTrackOfBox(box) if (first) { await this.playTrack(first) } } }, async playTrack(track: Track) { // mettre à jour la piste courante uniquement après avoir géré le toggle if (this.currentTrack && this.currentTrack?.id === track.id) { this.togglePlay() } else { this.currentTrack = track if (!this.audio) { // fallback: create an audio element and attach listeners this.attachAudio(new Audio()) } // Interrompre toute lecture en cours avant de charger une nouvelle source const audio = this.audio as HTMLAudioElement try { audio.pause() } catch (_) {} // on entre en phase de chargement this.isPaused = true audio.src = track.url audio.load() // lancer la lecture (seek si nécessaire une fois les metadata chargées) try { const wantedStart = track.start ?? 0 // Attendre que les metadata soient prêtes pour pouvoir positionner currentTime await new Promise((resolve) => { if (audio.readyState >= 1) return resolve() const onLoaded = () => resolve() audio.addEventListener('loadedmetadata', onLoaded, { once: true }) }) // Appliquer le temps de départ audio.currentTime = wantedStart > 0 ? wantedStart : 0 await new Promise((resolve) => { const onCanPlay = () => { if (wantedStart > 0 && audio.currentTime < wantedStart - 0.05) { audio.currentTime = wantedStart } resolve() } if (audio.readyState >= 3) return resolve() audio.addEventListener('canplay', onCanPlay, { once: true }) }) this.isPaused = false await audio.play() } catch (err: any) { // Ignorer les AbortError (arrivent lorsqu'une nouvelle source est chargée rapidement) if (err && err.name === 'AbortError') return this.isPaused = true console.error('Impossible de lire la piste :', err) } } }, togglePlay() { if (!this.audio) return if (this.audio.paused) { this.isPaused = false this.audio .play() .then(() => { this.isPaused = false }) .catch((err) => console.error(err)) } else { this.audio.pause() this.isPaused = true } }, updateTime() { const audio = this.audio if (!audio) return // update current position this.position = audio.currentTime // compute and cache a stable progression value const duration = audio.duration const progression = (this.position / duration) * 100 if (!isNaN(progression)) { this.progressionLast = progression } // auto advance behavior: playlists use 'ended', compilations use time boundary const track = this.currentTrack if (!track) return const dataStore = useDataStore() const t = audio.currentTime } }, getters: { isCurrentBox: (state) => { return (boxId: string) => boxId === state.currentTrack?.boxId }, isPlaylistTrack: () => { return (track: Track) => { return track.type === 'playlist' } }, getCurrentTrack: (state) => state.currentTrack, getCurrentBox: (state) => { return state.currentTrack ? state.currentTrack.url : null }, getCurrentProgression(state) { if (!state.audio) return 0 const duration = state.audio.duration const progression = (state.position / duration) * 100 return isNaN(progression) ? state.progressionLast : progression }, getCurrentCoverUrl(state) { const id = state.currentTrack?.coverId if (!id) return null return id.startsWith('http') ? id : `https://f4.bcbits.com/img/${id}_4.jpg` } } })