play/pause works
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
:BoxState="boxStates[compilation.id]" @click="() => openCompilation(compilation.id)"
|
||||
:class="boxStates[compilation.id]" class="text-center">
|
||||
<button @click="playerStore.playCompilation(compilation.id)" v-if="boxStates[compilation.id] === 'selected'"
|
||||
class="relative z-40 rounded-full size-24 bottom-1/2 text-2xl">▶</button>
|
||||
class="relative z-40 rounded-full size-24 bottom-1/2 text-2xl">
|
||||
{{ !playerStore.isPaused && playerStore.currentTrack?.compilationId === compilation.id ? 'II' : '▶' }}
|
||||
</button>
|
||||
<deck :compilation="compilation" class="box-page" v-if="boxStates[compilation.id] === 'selected'" />
|
||||
</box>
|
||||
</div>
|
||||
@@ -30,6 +32,7 @@ function openCompilation(id: string) {
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
navigateTo(`/box/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +1,27 @@
|
||||
<template>
|
||||
<div class="fixed left-0 bottom-0 opacity-1 z-50 w-full bg-white transition-all"
|
||||
:class="{ '-bottom-20 opacity-0': !playerStore.currentTrack }">
|
||||
<!-- <p>
|
||||
{{ Math.round(currentTime) }}
|
||||
{{ Math.round(currentProgression) }}%
|
||||
</p> -->
|
||||
<audio ref="audioRef" class="w-full" :src="playerStore.currentTrack?.url || ''" controls />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { usePlayerStore } from '~/store/player'
|
||||
|
||||
const playerStore = usePlayerStore()
|
||||
const audioRef = ref<HTMLAudioElement | null>(null)
|
||||
const currentTime = ref(0)
|
||||
const lastValidProgression = ref(0)
|
||||
|
||||
const currentProgression = computed(() => {
|
||||
if (!audioRef.value) return 0
|
||||
const progression = (currentTime.value / audioRef.value.duration) * 100
|
||||
|
||||
if (!isNaN(progression)) {
|
||||
lastValidProgression.value = progression
|
||||
}
|
||||
|
||||
return lastValidProgression.value
|
||||
})
|
||||
|
||||
function updateTime() {
|
||||
if (audioRef.value) {
|
||||
currentTime.value = audioRef.value.currentTime
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (audioRef.value) {
|
||||
playerStore.audio = audioRef.value
|
||||
audioRef.value.addEventListener("timeupdate", updateTime)
|
||||
audioRef.value.addEventListener("timeupdate", playerStore.updateTime)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (audioRef.value) {
|
||||
audioRef.value.removeEventListener("timeupdate", updateTime)
|
||||
audioRef.value.removeEventListener("timeupdate", playerStore.updateTime)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<div class="w-full flex flex-col items-center">
|
||||
<logo />
|
||||
<main>
|
||||
<newsletter />
|
||||
<compilations />
|
||||
<player />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.logo {
|
||||
filter: drop-shadow(2px 2px 0 rgb(0 0 0 / 0.8));
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<div class="w-full flex flex-col items-center">
|
||||
<logo />
|
||||
<main>
|
||||
<compilations />
|
||||
<player />
|
||||
<newsletter />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.logo {
|
||||
filter: drop-shadow(2px 2px 0 rgb(0 0 0 / 0.8));
|
||||
@@ -75,12 +75,30 @@ export const useDataStore = defineStore('data', {
|
||||
return tracks.length > 0 ? tracks[0] : null
|
||||
}
|
||||
},
|
||||
getFirstTrackOfPlaylist() {
|
||||
return (compilationId: string) => {
|
||||
const tracks = this.getPlaylistTracksByCompilationId(compilationId)
|
||||
return tracks.length > 0 ? tracks[0] : null
|
||||
}
|
||||
},
|
||||
getNextPlaylistTrack: (state) => {
|
||||
return (track: Track) => {
|
||||
const tracksInPlaylist = state.playlistTracks
|
||||
.filter((t) => t.compilationId === track.compilationId)
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
|
||||
const index = tracksInPlaylist.findIndex((t) => t.id === track.id)
|
||||
return index >= 0 && index < tracksInPlaylist.length - 1
|
||||
? tracksInPlaylist[index + 1]
|
||||
: null
|
||||
}
|
||||
},
|
||||
getNextTrack: (state) => {
|
||||
return (track: Track) => {
|
||||
// Récupérer toutes les tracks de la même compilation et les trier par ordre
|
||||
const tracksInCompilation = state.tracks
|
||||
.filter((t) => t.compilationId === track.compilationId)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
|
||||
// Trouver l’index de la track courante
|
||||
const index = tracksInCompilation.findIndex((t) => t.id === track.id)
|
||||
@@ -90,6 +108,16 @@ export const useDataStore = defineStore('data', {
|
||||
: null
|
||||
}
|
||||
},
|
||||
getPrevTrack: (state) => {
|
||||
return (track: Track) => {
|
||||
const tracksInCompilation = state.tracks
|
||||
.filter((t) => t.compilationId === track.compilationId)
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
|
||||
const index = tracksInCompilation.findIndex((t) => t.id === track.id)
|
||||
return index > 0 ? tracksInCompilation[index - 1] : null
|
||||
}
|
||||
},
|
||||
getPlaylistTracksByCompilationId: (state) => (id: string) => {
|
||||
return state.playlistTracks.filter((track) => track.compilationId === id)
|
||||
}
|
||||
|
||||
@@ -7,57 +7,161 @@ export const usePlayerStore = defineStore('player', {
|
||||
state: () => ({
|
||||
currentTrack: null as Track | null,
|
||||
position: 0,
|
||||
audio: null as HTMLAudioElement | null
|
||||
audio: null as HTMLAudioElement | null,
|
||||
isPaused: true,
|
||||
progressionLast: 0
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async playTrack(track: Track) {
|
||||
const oldTrack = this.currentTrack
|
||||
this.currentTrack = track
|
||||
|
||||
// toggle si on reclique sur la même
|
||||
if (this.isPlayingTrack(track)) {
|
||||
this.togglePlay()
|
||||
return
|
||||
}
|
||||
if (!this.audio) {
|
||||
this.audio = new Audio()
|
||||
}
|
||||
|
||||
// définir la source (fichier de la compilation entière)
|
||||
this.audio.load()
|
||||
|
||||
// attendre que le player soit prêt avant de lire
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onCanPlay = () => {
|
||||
this.audio!.removeEventListener('canplay', onCanPlay)
|
||||
resolve()
|
||||
}
|
||||
const onError = (e: Event) => {
|
||||
this.audio!.removeEventListener('error', onError)
|
||||
reject(e)
|
||||
}
|
||||
this.audio!.addEventListener('canplay', onCanPlay, { once: true })
|
||||
this.audio!.addEventListener('error', onError, { once: true })
|
||||
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.compilationId.length === 6) {
|
||||
const next = dataStore.getNextPlaylistTrack(track)
|
||||
if (next && next.compilationId === track.compilationId) {
|
||||
this.playTrack(next)
|
||||
}
|
||||
} else {
|
||||
console.log('ended')
|
||||
}
|
||||
})
|
||||
},
|
||||
async playCompilation(compilationId: string) {
|
||||
if (this.currentTrack?.compilationId === compilationId) {
|
||||
this.togglePlay()
|
||||
} else {
|
||||
const dataStore = useDataStore()
|
||||
const first = (compilationId.length === 6) ? dataStore.getFirstTrackOfPlaylist(compilationId) : dataStore.getFirstTrackOfCompilation(compilationId)
|
||||
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?.id === track.id) {
|
||||
this.togglePlay()
|
||||
} else {
|
||||
this.currentTrack = track
|
||||
if (!this.audio) {
|
||||
// fallback: create an audio element and attach listeners
|
||||
this.attachAudio(new Audio())
|
||||
}
|
||||
|
||||
// positionner le début
|
||||
this.audio.currentTime = track.start ?? 0
|
||||
// Interrompre toute lecture en cours avant de charger une nouvelle source
|
||||
const audio = this.audio as HTMLAudioElement
|
||||
try {
|
||||
audio.pause()
|
||||
} catch (_) {}
|
||||
|
||||
// lancer la lecture
|
||||
try {
|
||||
await this.audio.play()
|
||||
} catch (err) {
|
||||
console.error('Impossible de lire la piste :', err)
|
||||
// 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
|
||||
|
||||
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.audio.play().catch((err) => console.error(err))
|
||||
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
|
||||
|
||||
// For playlists (id length 6), do NOT forward auto-advance here; rely on 'ended'
|
||||
if (track.compilationId.length === 6) {
|
||||
const from = track.start ?? 0
|
||||
const prevTrack = dataStore.getPrevTrack(track)
|
||||
if (t < from + 0.05) {
|
||||
if (prevTrack && prevTrack.compilationId === track.compilationId) {
|
||||
this.currentTrack = prevTrack
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// For compilations, forward auto-advance at segment boundary
|
||||
const nextTrack = dataStore.getNextTrack(track)
|
||||
const to = (track.order === 0) ? Math.round(this.audio?.duration ?? 0) : nextTrack?.start
|
||||
if (!to || isNaN(to)) return
|
||||
if (t >= to - 0.05) {
|
||||
if (nextTrack && nextTrack.compilationId === track.compilationId) {
|
||||
this.currentTrack = nextTrack
|
||||
}
|
||||
} else {
|
||||
const from = track.start ?? 0
|
||||
const prevTrack = dataStore.getPrevTrack(track)
|
||||
if (t < from + 0.05) {
|
||||
if (prevTrack && prevTrack.compilationId === track.compilationId) {
|
||||
this.currentTrack = prevTrack
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -67,18 +171,9 @@ export const usePlayerStore = defineStore('player', {
|
||||
return (compilationId: string) => compilationId === state.currentTrack?.compilationId
|
||||
},
|
||||
|
||||
isPlayingTrack: (state) => {
|
||||
isPlaylistTrack: () => {
|
||||
return (track: Track) => {
|
||||
if (!state.audio || !state.currentTrack) return false
|
||||
|
||||
const currentTime = state.audio.currentTime
|
||||
if (!currentTime || isNaN(currentTime)) return false
|
||||
|
||||
const from = track.start ?? 0
|
||||
const to = state.getTrackStop(track)
|
||||
if (!to || isNaN(to)) return false
|
||||
|
||||
return currentTime >= from && currentTime < to
|
||||
return track.compilationId.length === 6
|
||||
}
|
||||
},
|
||||
|
||||
@@ -88,18 +183,11 @@ export const usePlayerStore = defineStore('player', {
|
||||
return state.currentTrack ? state.currentTrack.url : null
|
||||
},
|
||||
|
||||
getTrackStop: (state) => {
|
||||
return (track: Track) => {
|
||||
if (!state.audio) return 0
|
||||
|
||||
if (track.order === 0) {
|
||||
return Math.round(state.audio.duration)
|
||||
} else {
|
||||
const dataStore = useDataStore()
|
||||
const nextTrack = dataStore.getNextTrack(track)
|
||||
return nextTrack ? nextTrack.start : Math.round(state.audio.duration)
|
||||
}
|
||||
}
|
||||
currentProgression(state) {
|
||||
if (!state.audio) return 0
|
||||
const duration = state.audio.duration
|
||||
const progression = (state.position / duration) * 100
|
||||
return isNaN(progression) ? state.progressionLast : progression
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user