// ~/store/player.ts import { defineStore } from 'pinia' import type { Track, Box } from '~/../types/types' import { useDataStore } from '~/store/data' import { useCardStore } from '~/store/card' import { useFavoritesStore, FAVORITES_BOX_ID } from '~/store/favorites' export const usePlayerStore = defineStore('player', { state: () => ({ currentTrack: null as Track | null, position: 0, audio: null as HTMLAudioElement | null, progressionLast: 0, isPlaying: false, isLoading: false }), actions: { attachAudio(el: HTMLAudioElement) { this.audio = el // attach listeners if not already attached (idempotent enough for our use) this.audio.addEventListener('play', () => { this.isPlaying = true // Révéler la carte quand la lecture commence if (this.currentTrack) { const cardStore = useCardStore() if (!cardStore.isCardRevealed(this.currentTrack.id)) { requestAnimationFrame(() => { cardStore.revealCard(this.currentTrack!.id) }) } } }) this.audio.addEventListener('playing', () => {}) this.audio.addEventListener('pause', () => { this.isPlaying = false }) this.audio.addEventListener('ended', async () => { const track = this.currentTrack if (!track) return const dataStore = useDataStore() const favoritesStore = useFavoritesStore() // Vérifier si on est dans la playlist des favoris if (track.boxId === FAVORITES_BOX_ID) { const nextTrack = favoritesStore.getNextTrack(track.id) if (nextTrack) { await this.playTrack(nextTrack) return } } // Comportement par défaut pour les playlists standards else if (track.type === 'playlist') { const next = dataStore.getNextPlaylistTrack(track) if (next && next.boxId === track.boxId) { await this.playTrack(next) return } } // Si on arrive ici, c'est qu'il n'y a pas de piste suivante this.currentTrack = null this.isPlaying = false }) }, async playBox(box: Box) { // Si c'est la même box, on toggle simplement la lecture if (this.currentTrack?.boxId === box.id) { this.togglePlay() return } // Sinon, on charge la première piste de la box try { const dataStore = useDataStore() const firstTrack = dataStore.getFirstTrackOfBox(box) if (firstTrack) { await this.playTrack(firstTrack) } } catch (error) { console.error('Error playing box:', error) } }, async playTrack(track: Track) { // Si c'est une piste de la playlist utilisateur, on utilise directement cette piste if (track.boxId === FAVORITES_BOX_ID) { this.currentTrack = track await this.loadAndPlayTrack(track) } else { // Pour les autres types de pistes, on utilise la logique existante this.isCompilationTrack(track) ? await this.playCompilationTrack(track) : await this.playPlaylistTrack(track) } }, async playCompilationTrack(track: Track) { // Si c'est la même piste, on toggle simplement la lecture if (this.currentTrack?.id === track.id) { // Si la lecture est en cours, on met en pause if (this.isPlaying) { this.togglePlay() return } // Si c'est une compilation, on vérifie si on est dans la plage de la piste if (track.type === 'compilation' && track.start !== undefined) { const dataStore = useDataStore() const nextTrack = dataStore.getNextTrack(track) // Si on a une piste suivante et qu'on est dans la plage de la piste courante if (nextTrack?.start && this.position >= track.start && this.position < nextTrack.start) { this.togglePlay() return } // Si c'est la dernière piste de la compilation else if (!nextTrack && this.position >= track.start) { this.togglePlay() return } } } // Sinon, on charge et on lit la piste this.currentTrack = track await this.loadAndPlayTrack(track) }, async playPlaylistTrack(track: Track) { // Toggle simple si c'est la même piste if (this.currentTrack?.id === track.id) { this.togglePlay() return } // Sinon, on charge et on lit la piste this.currentTrack = track await this.loadAndPlayTrack(track) }, async loadTrack(track: Track) { if (!this.audio) return return new Promise((resolve) => { this.currentTrack = track const audio = this.audio as HTMLAudioElement // Si c'est la même source, on ne fait rien if (audio.src === track.url) { resolve() return } // Nouvelle source audio.src = track.url // Une fois que suffisamment de données sont chargées const onCanPlay = () => { audio.removeEventListener('canplay', onCanPlay) resolve() } audio.addEventListener('canplay', onCanPlay) // Timeout de sécurité setTimeout(resolve, 1000) }) }, async loadAndPlayTrack(track: Track) { if (!this.audio) { const newAudio = new Audio() this.attachAudio(newAudio) } const audio = this.audio as HTMLAudioElement try { this.isLoading = true // Mettre en pause audio.pause() // Pour les compilations, on utilise l'URL de la piste avec le point de départ if (track.type === 'compilation' && track.start !== undefined) { audio.src = track.url audio.currentTime = track.start } else { // Pour les playlists, on charge la piste individuelle audio.currentTime = 0 await this.loadTrack(track) } // Attendre que les métadonnées soient chargées await new Promise((resolve) => { const onCanPlay = () => { audio.removeEventListener('canplay', onCanPlay) resolve() } audio.addEventListener('canplay', onCanPlay) // Timeout de sécurité setTimeout(resolve, 1000) }) // Lancer la lecture await audio.play() this.isLoading = false } catch (error) { console.error('Error loading/playing track:', error) this.isLoading = false } }, async togglePlay() { if (!this.audio) return try { if (this.audio.paused) { await this.audio.play() } else { this.audio.pause() } } catch (error) { console.error('Error toggling play state:', error) } }, 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 } // update current track when changing time in compilation const cur = this.currentTrack if (cur && cur.type === 'compilation') { const dataStore = useDataStore() const tracks = dataStore .getTracksByboxId(cur.boxId) .slice() .filter((t) => t.type === 'compilation') .sort((a, b) => (a.start ?? 0) - (b.start ?? 0)) if (tracks.length > 0) { const now = audio.currentTime // find the last track whose start <= now (fallback to first track) let nextTrack = tracks[0] for (const t of tracks) { const s = t.start ?? 0 if (s <= now) { nextTrack = t } else { break } } if (nextTrack && nextTrack.id !== cur.id) { // only update metadata reference; do not reload audio this.currentTrack = nextTrack // Révéler la carte avec une animation fluide const cardStore = useCardStore() if (nextTrack.id && !cardStore.isCardRevealed(nextTrack.id)) { // Utiliser requestAnimationFrame pour une meilleure synchronisation avec le rendu requestAnimationFrame(() => { cardStore.revealCard(nextTrack.id!) }) } } } } } }, getters: { isCurrentBox: (state) => { return (boxId: string) => boxId === state.currentTrack?.boxId }, isPlaylistTrack: () => { return (track: Track) => { return track.type === 'playlist' } }, isCompilationTrack: () => { return (track: Track) => { return track.type === 'compilation' } }, isPaused: (state) => { return state.audio?.paused ?? true }, 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` } } })