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

View File

@@ -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>

View File

@@ -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));

View File

@@ -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));

View File

@@ -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 lindex 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)
}

View File

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