add cards & tracks
All checks were successful
Deploy App / build (push) Successful in 1m13s
Deploy App / deploy (push) Successful in 15s

This commit is contained in:
valere
2025-10-02 00:38:54 +02:00
parent 8c1290beae
commit 43b1a11027
11 changed files with 474 additions and 43 deletions

View File

@@ -130,7 +130,7 @@ onMounted(() => {
const down = (ev: PointerEvent) => {
ev.preventDefault()
dragging = true
scene.value?.setPointerCapture(ev.pointerId)
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 }
@@ -158,17 +158,17 @@ onMounted(() => {
const end = (ev: PointerEvent) => {
if (!dragging) return
dragging = false
try { scene.value?.releasePointerCapture(ev.pointerId) } catch { }
try { box.value?.releasePointerCapture(ev.pointerId) } catch { }
if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) {
if (!raf) raf = requestAnimationFrame(tickInertia)
}
}
scene.value?.addEventListener('pointerdown', down)
scene.value?.addEventListener('pointermove', move)
scene.value?.addEventListener('pointerup', end)
scene.value?.addEventListener('pointercancel', end)
scene.value?.addEventListener('pointerleave', end)
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!)

View File

@@ -1,14 +1,17 @@
<template>
<article class="relative">
<main
class="absolute top-0 backdrop-blur-sm z-40 -mt-12 z-10 card w-56 h-80 p-3 bg-opacity-10 bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden">
class="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">
<!-- Cover -->
<figure class="flex-1 overflow-hidden rounded-t-xl">
<figure @click="playerStore.playTrack(props.track)" class="flex-1 overflow-hidden rounded-t-xl cursor-pointer">
<img :src="coverUrl" alt="Pochette de l'album" class="w-full h-full object-cover object-center" />
</figure>
<!-- Body -->
<div class="p-3 text-center bg-white rounded-b-xl">
<div class="label">
{{ props.track.order }}
</div>
<h2 class="text-base text-neutral-800 font-bold truncate">{{ props.track.title }}</h2>
<p class="text-sm text-neutral-500 truncate">
{{ props.track.artist.name }}
@@ -17,11 +20,14 @@
</main>
<footer
class="absolute top-0 ml-32 backdrop-blur-sm -mt-12 z-10 card w-56 h-80 p-3 bg-opacity-10 bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden">
class="ml-32 backdrop-blur-sm -mt-12 z-10 card w-56 h-80 p-3 bg-opacity-10 bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden">
<!-- Back -->
<div class="h-full flex p-16 text-center bg-slate-800 rounded-xl">
<img src="/favicon.svg" />
</div>
<div class="label">
{{ props.track.id }}
</div>
</footer>
</article>
@@ -29,7 +35,19 @@
<script setup lang="ts">
import type { Track } from '~~/types/types'
import { usePlayerStore } from '~/store/player'
const props = defineProps<{ track: Track }>()
const coverUrl = `https://f4.bcbits.com/img/${props.track.artist.coverId}_4.jpg`
</script>
const playerStore = usePlayerStore()
const coverUrl = props.track.coverId.startsWith('http')
? props.track.coverId
: `https://f4.bcbits.com/img/${props.track.coverId}_4.jpg`;
</script>
<style>
.label {
@apply rounded-full size-7 p-2 bg-esyellow leading-3 -mt-6;
font-weight: bold;
text-align: center;
}
</style>

View File

@@ -0,0 +1,38 @@
<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" />
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { usePlayerStore } from '~/store/player'
const playerStore = usePlayerStore()
const audioRef = ref<HTMLAudioElement | null>(null)
onMounted(() => {
if (audioRef.value) playerStore.audio = audioRef.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()
}
}
)
</script>

View File

@@ -12,7 +12,6 @@ import type { BoxState } from '~~/types/types'
import { useRouter } from 'vue-router'
const dataStore = useDataStore()
const router = useRouter()
const boxStates = ref<Record<string, BoxState>>({})
function openCompilation(id: string) {
@@ -20,7 +19,6 @@ function openCompilation(id: string) {
for (const key in boxStates.value) {
boxStates.value[key] = (key === id) ? 'selected' : 'hide'
}
window.history.pushState({}, '', '/compilation/' + id)
}
}
@@ -30,7 +28,6 @@ function closeCompilation(e: KeyboardEvent) {
boxStates.value[key] = 'list'
}
}
window.history.pushState({}, '', '/')
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="mt-8 p-8 w-96">
<div class="mt-8 p-8 w-96 flex flex-wrap">
<MoleculeCard v-for="track in dataStore.getTracksByCompilationId(compilation.id)" :key="track.id" :track="track" />
</div>
</template>
@@ -13,5 +13,4 @@ const props = defineProps<{
}>()
const dataStore = useDataStore()
</script>

View File

@@ -9,6 +9,7 @@
</header>
<main>
<OrganismCompilationList />
<MoleculePlayer />
</main>
</div>
</template>

View File

@@ -1,9 +1,9 @@
<template>
<div class="flex flex-wrap justify-center">
<div class="flex flex-wrap justify-center items-center h-screen">
<div class="bg-page-dark-bg text-white">
<div class="flex flex-col-reverse bg-gradient-to-r from-primary to-primary-dark">
<div class="mt-8 flex flex-wrap justify-center">
<molecule-box :compilation="compilation" />
<!-- <molecule-box :compilation="compilation" /> -->
<div class="devtool absolute right-4 text-white bg-black rounded-2xl px-4 py-2">
<!-- <button @click="currentPosition = boxPositions.side">side</button>
<button @click="currentPosition = boxPositions.front">front</button>

57
app/store/player.ts Normal file
View File

@@ -0,0 +1,57 @@
// ~/store/player.ts
import { defineStore } from 'pinia'
import type { Track } from '~/types/types'
export const usePlayerStore = defineStore('player', {
state: () => ({
currentTrack: null as Track | null,
isPlaying: false,
position: 0,
audio: null as HTMLAudioElement | null,
}),
actions: {
setTrack(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
},
playTrack(track?: Track) {
if (track) this.setTrack(track)
if (!this.currentTrack || !this.audio) return
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()
}
},
setPosition(time: number) {
if (this.audio) this.audio.currentTime = time
this.position = time
},
},
getters: {
getCurrentTrack: (state) => state.currentTrack,
getPlaying: (state) => state.isPlaying,
getCompilationUrlFromTrack: (state) => {
return (track: Track) => `https://files.erudi.fr/evilspins/${track.compilationId}.mp3`
}
},
})