add working player
This commit is contained in:
@@ -31,6 +31,8 @@ const props = withDefaults(
|
||||
{ BoxState: 'list' }
|
||||
)
|
||||
|
||||
const isDraggable = computed(() => !['list', 'hidden'].includes(BoxState.value()))
|
||||
|
||||
// --- Réfs ---
|
||||
const scene = ref<HTMLElement>()
|
||||
const box = ref<HTMLElement>()
|
||||
@@ -123,61 +125,81 @@ function tickInertia() {
|
||||
}
|
||||
|
||||
// --- Pointer events ---
|
||||
let listenersAttached = false
|
||||
|
||||
const down = (ev: PointerEvent) => {
|
||||
ev.preventDefault()
|
||||
dragging = true
|
||||
box.value?.setPointerCapture(ev.pointerId)
|
||||
lastPointer = { x: ev.clientX, y: ev.clientY, time: performance.now() }
|
||||
velocity = { x: 0, y: 0 }
|
||||
if (raf) { cancelAnimationFrame(raf); raf = null }
|
||||
}
|
||||
|
||||
const move = (ev: PointerEvent) => {
|
||||
if (!dragging) return
|
||||
ev.preventDefault()
|
||||
const now = performance.now()
|
||||
const dx = ev.clientX - lastPointer.x
|
||||
const dy = ev.clientY - lastPointer.y
|
||||
const dt = Math.max(1, now - lastPointer.time)
|
||||
|
||||
rotateY.value += dx * sensitivity
|
||||
rotateX.value -= dy * sensitivity
|
||||
rotateX.value = Math.max(-80, Math.min(80, rotateX.value))
|
||||
|
||||
velocity.x = (dx / dt) * 16 * sensitivity
|
||||
velocity.y = (-dy / dt) * 16 * sensitivity
|
||||
|
||||
lastPointer = { x: ev.clientX, y: ev.clientY, time: now }
|
||||
applyTransform(0) // immédiat pendant drag
|
||||
}
|
||||
|
||||
const end = (ev: PointerEvent) => {
|
||||
if (!dragging) return
|
||||
dragging = false
|
||||
try { box.value?.releasePointerCapture(ev.pointerId) } catch { }
|
||||
if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) {
|
||||
if (!raf) raf = requestAnimationFrame(tickInertia)
|
||||
}
|
||||
}
|
||||
|
||||
function addListeners() {
|
||||
if (!box.value || listenersAttached) return
|
||||
box.value.addEventListener('pointerdown', down)
|
||||
box.value.addEventListener('pointermove', move)
|
||||
box.value.addEventListener('pointerup', end)
|
||||
box.value.addEventListener('pointercancel', end)
|
||||
box.value.addEventListener('pointerleave', end)
|
||||
listenersAttached = true
|
||||
}
|
||||
|
||||
function removeListeners() {
|
||||
if (!box.value || !listenersAttached) return
|
||||
box.value.removeEventListener('pointerdown', down)
|
||||
box.value.removeEventListener('pointermove', move)
|
||||
box.value.removeEventListener('pointerup', end)
|
||||
box.value.removeEventListener('pointercancel', end)
|
||||
box.value.removeEventListener('pointerleave', end)
|
||||
listenersAttached = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
applyColor()
|
||||
applyBoxState()
|
||||
if (isDraggable) addListeners()
|
||||
})
|
||||
|
||||
const down = (ev: PointerEvent) => {
|
||||
ev.preventDefault()
|
||||
dragging = true
|
||||
box.value?.setPointerCapture(ev.pointerId)
|
||||
lastPointer = { x: ev.clientX, y: ev.clientY, time: performance.now() }
|
||||
velocity = { x: 0, y: 0 }
|
||||
if (raf) { cancelAnimationFrame(raf); raf = null }
|
||||
}
|
||||
|
||||
const move = (ev: PointerEvent) => {
|
||||
if (!dragging) return
|
||||
ev.preventDefault()
|
||||
const now = performance.now()
|
||||
const dx = ev.clientX - lastPointer.x
|
||||
const dy = ev.clientY - lastPointer.y
|
||||
const dt = Math.max(1, now - lastPointer.time)
|
||||
|
||||
rotateY.value += dx * sensitivity
|
||||
rotateX.value -= dy * sensitivity
|
||||
rotateX.value = Math.max(-80, Math.min(80, rotateX.value))
|
||||
|
||||
velocity.x = (dx / dt) * 16 * sensitivity
|
||||
velocity.y = (-dy / dt) * 16 * sensitivity
|
||||
|
||||
lastPointer = { x: ev.clientX, y: ev.clientY, time: now }
|
||||
applyTransform(0) // immédiat pendant drag
|
||||
}
|
||||
|
||||
const end = (ev: PointerEvent) => {
|
||||
if (!dragging) return
|
||||
dragging = false
|
||||
try { box.value?.releasePointerCapture(ev.pointerId) } catch { }
|
||||
if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) {
|
||||
if (!raf) raf = requestAnimationFrame(tickInertia)
|
||||
}
|
||||
}
|
||||
|
||||
box.value?.addEventListener('pointerdown', down)
|
||||
box.value?.addEventListener('pointermove', move)
|
||||
box.value?.addEventListener('pointerup', end)
|
||||
box.value?.addEventListener('pointercancel', end)
|
||||
box.value?.addEventListener('pointerleave', end)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cancelAnimationFrame(raf!)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
cancelAnimationFrame(raf!)
|
||||
removeListeners()
|
||||
})
|
||||
|
||||
// --- Watchers ---
|
||||
watch(() => props.BoxState, () => applyBoxState())
|
||||
watch(() => props.compilation, () => applyColor(), { deep: true })
|
||||
watch(() => isDraggable, (enabled) => enabled ? addListeners() : removeListeners())
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<article class="flip-card w-56 h-80" :data-flipped="props.isflipped">
|
||||
<article class="flip-card w-56 h-80">
|
||||
<div class="flip-inner">
|
||||
<main
|
||||
class="flip-front backdrop-blur-sm border-2 -mt-12 z-10 card w-56 h-80 p-3 bg-opacity-40 hover:bg-opacity-80 hover:shadow-xl transition-all bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden">
|
||||
@@ -39,7 +39,7 @@
|
||||
import type { Track } from '~~/types/types'
|
||||
import { usePlayerStore } from '~/store/player'
|
||||
|
||||
const props = defineProps<{ track: Track, isflipped: false }>()
|
||||
const props = defineProps<{ track: Track }>()
|
||||
const playerStore = usePlayerStore()
|
||||
const coverUrl = props.track.coverId.startsWith('http')
|
||||
? props.track.coverId
|
||||
@@ -66,7 +66,7 @@ const coverUrl = props.track.coverId.startsWith('http')
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.flip-card[data-flipped=false] .flip-inner {
|
||||
.flipped .flip-inner {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<template>
|
||||
<audio ref="audioRef" class="fixed z-50 bottom-0 left-1/2 -translate-x-1/2 w-1/2"
|
||||
:src="playerStore.currentTrack ? playerStore.getCompilationUrlFromTrack(playerStore.currentTrack) : ''" controls
|
||||
@timeupdate="updatePosition" @ended="onEnded" />
|
||||
<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 class="hidden">
|
||||
{{ Math.round(currentTime) }}
|
||||
{{ Math.round(currentProgression) }}%
|
||||
</p> -->
|
||||
<audio ref="audioRef" class="w-full"
|
||||
:src="playerStore.currentTrack ? playerStore.getCompilationUrlFromTrack(playerStore.currentTrack) : ''"
|
||||
controls />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -10,39 +17,36 @@ import { usePlayerStore } from '~/store/player'
|
||||
|
||||
const playerStore = usePlayerStore()
|
||||
const audioRef = ref<HTMLAudioElement | null>(null)
|
||||
const currentTime = ref(0)
|
||||
const lastValidProgression = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
if (audioRef.value) playerStore.audio = audioRef.value
|
||||
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
|
||||
})
|
||||
|
||||
// Mettre à jour la position
|
||||
function updatePosition() {
|
||||
if (audioRef.value) playerStore.position = audioRef.value.currentTime
|
||||
}
|
||||
|
||||
function onEnded() {
|
||||
playerStore.isPlaying = false
|
||||
}
|
||||
|
||||
// Si la track change, mettre à jour le src et le start
|
||||
watch(
|
||||
() => playerStore.currentTrack,
|
||||
(newTrack) => {
|
||||
if (newTrack && audioRef.value) {
|
||||
audioRef.value.src = newTrack.url
|
||||
audioRef.value.currentTime = newTrack.start || 0
|
||||
if (playerStore.isPlaying) audioRef.value.play()
|
||||
}
|
||||
function updateTime() {
|
||||
if (audioRef.value) {
|
||||
currentTime.value = audioRef.value.currentTime
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (audioRef.value) {
|
||||
playerStore.audio = audioRef.value
|
||||
audioRef.value.addEventListener("timeupdate", updateTime)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (audioRef.value) {
|
||||
audioRef.value.removeEventListener("timeupdate", updateTime)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
audio {
|
||||
transition: all 1s;
|
||||
}
|
||||
|
||||
audio[src=""] {
|
||||
@apply -bottom-1.5 opacity-0
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useDataStore } from '~/store/data'
|
||||
import type { BoxState } from '~~/types/types'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const dataStore = useDataStore()
|
||||
const boxStates = ref<Record<string, BoxState>>({})
|
||||
@@ -30,7 +29,6 @@ function closeCompilation(e: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
const dataStore = await useDataStore()
|
||||
await dataStore.loadData()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div class="mt-8 p-8 w-full flex flex-wrap justify-around">
|
||||
<MoleculeCard v-for="track in dataStore.getTracksByCompilationId(compilation.id)" :key="track.id" :track="track"
|
||||
:isFlipped="true" />
|
||||
<MoleculeCard v-for="track in dataStore.getTracksByCompilationId(compilation.id)" :key="track.id" :track="track" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,4 +13,4 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const dataStore = useDataStore()
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -18,4 +18,4 @@
|
||||
.logo {
|
||||
filter: drop-shadow(2px 2px 0 rgb(0 0 0 / 0.8));
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -54,5 +54,20 @@ export const useDataStore = defineStore('data', {
|
||||
getTracksByArtistId: (state) => (artistId: number) => {
|
||||
return state.tracks.filter(track => track.artist.id === artistId)
|
||||
},
|
||||
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)
|
||||
|
||||
// Trouver l’index de la track courante
|
||||
const index = tracksInCompilation.findIndex(t => t.id === track.id)
|
||||
// Retourner la track suivante ou null si c’est la dernière
|
||||
return index >= 0 && index < tracksInCompilation.length - 1
|
||||
? tracksInCompilation[index + 1]
|
||||
: null
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,62 +1,113 @@
|
||||
// ~/store/player.ts
|
||||
import { defineStore } from 'pinia'
|
||||
import type { Track } from '~/types/types'
|
||||
import type { Track } from '~/../types/types'
|
||||
import { useDataStore } from '~/store/data'
|
||||
|
||||
export const usePlayerStore = defineStore('player', {
|
||||
state: () => ({
|
||||
currentTrack: null as Track | null,
|
||||
isPlaying: false,
|
||||
position: 0,
|
||||
audio: null as HTMLAudioElement | null,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setTrack(track: Track) {
|
||||
async playTrack(track: Track) {
|
||||
this.currentTrack = track
|
||||
if (!this.audio) this.audio = new Audio(this.getCompilationUrlFromTrack(track))
|
||||
else this.audio.src = this.getCompilationUrlFromTrack(track)
|
||||
|
||||
// Commencer à start secondes
|
||||
this.audio.currentTime = track.start || 0
|
||||
},
|
||||
// toggle si on reclique sur la même
|
||||
if (this.isPlayingTrack(track)) {
|
||||
this.togglePlay()
|
||||
return
|
||||
}
|
||||
if (!this.audio) {
|
||||
this.audio = new Audio()
|
||||
}
|
||||
|
||||
playTrack(track?: Track) {
|
||||
// load compile if not allready loaded
|
||||
// play if track is not already played
|
||||
// else pause
|
||||
// définir la source (fichier de la compilation entière)
|
||||
this.audio.src = this.getCompilationUrlFromTrack(track)
|
||||
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 })
|
||||
})
|
||||
|
||||
if (track) this.setTrack(track)
|
||||
if (!this.currentTrack || !this.audio) return
|
||||
// positionner le début
|
||||
this.audio.currentTime = track.start ?? 0
|
||||
|
||||
this.audio.play()
|
||||
this.isPlaying = true
|
||||
},
|
||||
|
||||
pauseTrack() {
|
||||
if (this.audio) this.audio.pause()
|
||||
this.isPlaying = false
|
||||
},
|
||||
|
||||
togglePlay(track?: Track) {
|
||||
if (track && (!this.currentTrack || track.id !== this.currentTrack.id)) {
|
||||
this.playTrack(track)
|
||||
} else {
|
||||
this.isPlaying ? this.pauseTrack() : this.playTrack()
|
||||
// lancer la lecture
|
||||
try {
|
||||
await this.audio.play()
|
||||
} catch (err) {
|
||||
console.error("Impossible de lire la piste :", err)
|
||||
}
|
||||
},
|
||||
|
||||
setPosition(time: number) {
|
||||
if (this.audio) this.audio.currentTime = time
|
||||
this.position = time
|
||||
togglePlay() {
|
||||
if (!this.audio) return
|
||||
if (this.audio.paused) {
|
||||
this.audio.play().catch(err => console.error(err))
|
||||
} else {
|
||||
this.audio.pause()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
getters: {
|
||||
isCurrentCompilation: (state) => {
|
||||
return (compilationId: string) =>
|
||||
compilationId === state.currentTrack?.compilationId
|
||||
},
|
||||
|
||||
isPlayingTrack: (state) => {
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
getCurrentTrack: (state) => state.currentTrack,
|
||||
getPlaying: (state) => state.isPlaying,
|
||||
getCompilationUrlFromTrack: (state) => {
|
||||
return (track: Track) => `https://files.erudi.fr/evilspins/${track.compilationId}.mp3`
|
||||
|
||||
getCompilationUrlFromTrack: () => {
|
||||
return (track: Track) =>
|
||||
`https://files.erudi.fr/evilspins/${track.compilationId}.mp3`
|
||||
},
|
||||
|
||||
getCurrentCompilation: (state) => {
|
||||
return state.currentTrack
|
||||
? state.getCompilationUrlFromTrack(state.currentTrack)
|
||||
: 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user