play/pause works
All checks were successful
Deploy App / build (push) Successful in 1m17s
Deploy App / deploy (push) Successful in 15s

This commit is contained in:
valere
2025-10-12 03:49:17 +02:00
parent d21e731bbe
commit 8e4f34dd21
6 changed files with 188 additions and 92 deletions

View File

@@ -4,7 +4,9 @@
:BoxState="boxStates[compilation.id]" @click="() => openCompilation(compilation.id)" :BoxState="boxStates[compilation.id]" @click="() => openCompilation(compilation.id)"
:class="boxStates[compilation.id]" class="text-center"> :class="boxStates[compilation.id]" class="text-center">
<button @click="playerStore.playCompilation(compilation.id)" v-if="boxStates[compilation.id] === 'selected'" <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'" /> <deck :compilation="compilation" class="box-page" v-if="boxStates[compilation.id] === 'selected'" />
</box> </box>
</div> </div>
@@ -30,6 +32,7 @@ function openCompilation(id: string) {
behavior: 'smooth' behavior: 'smooth'
}); });
navigateTo(`/box/${id}`)
} }
} }

View File

@@ -1,50 +1,27 @@
<template> <template>
<div class="fixed left-0 bottom-0 opacity-1 z-50 w-full bg-white transition-all" <div class="fixed left-0 bottom-0 opacity-1 z-50 w-full bg-white transition-all"
:class="{ '-bottom-20 opacity-0': !playerStore.currentTrack }"> :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 /> <audio ref="audioRef" class="w-full" :src="playerStore.currentTrack?.url || ''" controls />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { usePlayerStore } from '~/store/player' import { usePlayerStore } from '~/store/player'
const playerStore = usePlayerStore() const playerStore = usePlayerStore()
const audioRef = ref<HTMLAudioElement | null>(null) 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(() => { onMounted(() => {
if (audioRef.value) { if (audioRef.value) {
playerStore.audio = audioRef.value playerStore.audio = audioRef.value
audioRef.value.addEventListener("timeupdate", updateTime) audioRef.value.addEventListener("timeupdate", playerStore.updateTime)
} }
}) })
onUnmounted(() => { onUnmounted(() => {
if (audioRef.value) { if (audioRef.value) {
audioRef.value.removeEventListener("timeupdate", updateTime) audioRef.value.removeEventListener("timeupdate", playerStore.updateTime)
} }
}) })
</script> </script>

View File

@@ -2,11 +2,11 @@
<div class="w-full flex flex-col items-center"> <div class="w-full flex flex-col items-center">
<logo /> <logo />
<main> <main>
<newsletter /> <compilations />
<player />
</main> </main>
</div> </div>
</template> </template>
<style> <style>
.logo { .logo {
filter: drop-shadow(2px 2px 0 rgb(0 0 0 / 0.8)); filter: drop-shadow(2px 2px 0 rgb(0 0 0 / 0.8));

View File

@@ -2,11 +2,11 @@
<div class="w-full flex flex-col items-center"> <div class="w-full flex flex-col items-center">
<logo /> <logo />
<main> <main>
<compilations /> <newsletter />
<player />
</main> </main>
</div> </div>
</template> </template>
<style> <style>
.logo { .logo {
filter: drop-shadow(2px 2px 0 rgb(0 0 0 / 0.8)); filter: drop-shadow(2px 2px 0 rgb(0 0 0 / 0.8));

View File

@@ -75,12 +75,30 @@ export const useDataStore = defineStore('data', {
return tracks.length > 0 ? tracks[0] : null 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) => { getNextTrack: (state) => {
return (track: Track) => { return (track: Track) => {
// Récupérer toutes les tracks de la même compilation et les trier par ordre // Récupérer toutes les tracks de la même compilation et les trier par ordre
const tracksInCompilation = state.tracks const tracksInCompilation = state.tracks
.filter((t) => t.compilationId === track.compilationId) .filter((t) => t.compilationId === track.compilationId)
.sort((a, b) => a.order - b.order) .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
// Trouver lindex de la track courante // Trouver lindex de la track courante
const index = tracksInCompilation.findIndex((t) => t.id === track.id) const index = tracksInCompilation.findIndex((t) => t.id === track.id)
@@ -90,6 +108,16 @@ export const useDataStore = defineStore('data', {
: null : 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) => { getPlaylistTracksByCompilationId: (state) => (id: string) => {
return state.playlistTracks.filter((track) => track.compilationId === id) return state.playlistTracks.filter((track) => track.compilationId === id)
} }

View File

@@ -7,57 +7,161 @@ export const usePlayerStore = defineStore('player', {
state: () => ({ state: () => ({
currentTrack: null as Track | null, currentTrack: null as Track | null,
position: 0, position: 0,
audio: null as HTMLAudioElement | null audio: null as HTMLAudioElement | null,
isPaused: true,
progressionLast: 0
}), }),
actions: { actions: {
async playTrack(track: Track) { attachAudio(el: HTMLAudioElement) {
const oldTrack = this.currentTrack this.audio = el
this.currentTrack = track // attach listeners if not already attached (idempotent enough for our use)
this.audio.addEventListener('play', () => {
// toggle si on reclique sur la même this.isPaused = false
if (this.isPlayingTrack(track)) { })
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() this.togglePlay()
return } 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) { if (!this.audio) {
this.audio = new Audio() // fallback: create an audio element and attach listeners
this.attachAudio(new Audio())
} }
// définir la source (fichier de la compilation entière) // Interrompre toute lecture en cours avant de charger une nouvelle source
this.audio.load() const audio = this.audio as HTMLAudioElement
try {
audio.pause()
} catch (_) {}
// attendre que le player soit prêt avant de lire // on entre en phase de chargement
await new Promise<void>((resolve, reject) => { this.isPaused = true
const onCanPlay = () => { audio.src = track.url
this.audio!.removeEventListener('canplay', onCanPlay) audio.load()
resolve()
} // lancer la lecture (seek si nécessaire une fois les metadata chargées)
const onError = (e: Event) => { try {
this.audio!.removeEventListener('error', onError) const wantedStart = track.start ?? 0
reject(e)
} // Attendre que les metadata soient prêtes pour pouvoir positionner currentTime
this.audio!.addEventListener('canplay', onCanPlay, { once: true }) await new Promise<void>((resolve) => {
this.audio!.addEventListener('error', onError, { once: true }) if (audio.readyState >= 1) return resolve()
const onLoaded = () => resolve()
audio.addEventListener('loadedmetadata', onLoaded, { once: true })
}) })
// positionner le début // Appliquer le temps de départ
this.audio.currentTime = track.start ?? 0 audio.currentTime = wantedStart > 0 ? wantedStart : 0
// lancer la lecture this.isPaused = false
try { await audio.play()
await this.audio.play() } catch (err: any) {
} catch (err) { // 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) console.error('Impossible de lire la piste :', err)
} }
}
}, },
togglePlay() { togglePlay() {
if (!this.audio) return if (!this.audio) return
if (this.audio.paused) { 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 { } else {
this.audio.pause() 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 return (compilationId: string) => compilationId === state.currentTrack?.compilationId
}, },
isPlayingTrack: (state) => { isPlaylistTrack: () => {
return (track: Track) => { return (track: Track) => {
if (!state.audio || !state.currentTrack) return false return track.compilationId.length === 6
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
} }
}, },
@@ -88,18 +183,11 @@ export const usePlayerStore = defineStore('player', {
return state.currentTrack ? state.currentTrack.url : null return state.currentTrack ? state.currentTrack.url : null
}, },
getTrackStop: (state) => { currentProgression(state) {
return (track: Track) => {
if (!state.audio) return 0 if (!state.audio) return 0
const duration = state.audio.duration
if (track.order === 0) { const progression = (state.position / duration) * 100
return Math.round(state.audio.duration) return isNaN(progression) ? state.progressionLast : progression
} else {
const dataStore = useDataStore()
const nextTrack = dataStore.getNextTrack(track)
return nextTrack ? nextTrack.start : Math.round(state.audio.duration)
}
}
} }
} }
}) })