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`
|
|
}
|
|
}
|
|
})
|