201 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			201 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| // ~/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<void>((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<void>((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
 | |
|       }
 | |
|       // 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 found = tracks[0]
 | |
|           for (const t of tracks) {
 | |
|             const s = t.start ?? 0
 | |
|             if (s <= now) {
 | |
|               found = t
 | |
|             } else {
 | |
|               break
 | |
|             }
 | |
|           }
 | |
|           if (found && found.id !== cur.id) {
 | |
|             // only update metadata reference; do not reload audio
 | |
|             this.currentTrack = found
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   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`
 | |
|     }
 | |
|   }
 | |
| })
 |