evilSpins v1
All checks were successful
Deploy App / build (push) Successful in 43s
Deploy App / deploy (push) Successful in 41s

This commit is contained in:
valere
2025-11-04 22:41:41 +01:00
parent deb15b3ea1
commit 34d22b3b17
49 changed files with 5791 additions and 2447 deletions

View File

@@ -2,14 +2,17 @@
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,
isPaused: true,
progressionLast: 0
progressionLast: 0,
isPlaying: false,
isLoading: false
}),
actions: {
@@ -17,109 +20,209 @@ export const usePlayerStore = defineStore('player', {
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.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.isPaused = true
this.isPlaying = false
})
this.audio.addEventListener('ended', () => {
this.isPaused = true
this.audio.addEventListener('ended', async () => {
const track = this.currentTrack
if (!track) return
const dataStore = useDataStore()
if (track.type === 'playlist') {
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) {
this.playTrack(next)
await this.playTrack(next)
return
}
} else {
console.log('ended')
this.currentTrack = null
}
// 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()
} else {
return
}
// Sinon, on charge la première piste de la box
try {
const dataStore = useDataStore()
const first = dataStore.getFirstTrackOfBox(box)
if (first) {
await this.playTrack(first)
const firstTrack = dataStore.getFirstTrackOfBox(box)
if (firstTrack) {
await this.playTrack(firstTrack)
}
} catch (error) {
console.error('Error playing box:', error)
}
},
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 {
// Si c'est une piste de la playlist utilisateur, on utilise directement cette piste
if (track.boxId === FAVORITES_BOX_ID) {
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)
}
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)
}
},
togglePlay() {
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
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
return new Promise<void>((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<void>((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)
}
},
@@ -149,18 +252,27 @@ export const usePlayerStore = defineStore('player', {
if (tracks.length > 0) {
const now = audio.currentTime
// find the last track whose start <= now (fallback to first track)
let found = tracks[0]
let nextTrack = tracks[0]
for (const t of tracks) {
const s = t.start ?? 0
if (s <= now) {
found = t
nextTrack = t
} else {
break
}
}
if (found && found.id !== cur.id) {
if (nextTrack && nextTrack.id !== cur.id) {
// only update metadata reference; do not reload audio
this.currentTrack = found
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!)
})
}
}
}
}
@@ -178,6 +290,16 @@ export const usePlayerStore = defineStore('player', {
}
},
isCompilationTrack: () => {
return (track: Track) => {
return track.type === 'compilation'
}
},
isPaused: (state) => {
return state.audio?.paused ?? true
},
getCurrentTrack: (state) => state.currentTrack,
getCurrentBox: (state) => {