sql server + platine v2
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<article :role="props.role" :class="[
|
||||
'card cursor-pointer',
|
||||
isFaceUp ? 'face-up' : 'face-down'
|
||||
isFaceUp ? 'face-up' : 'face-down',
|
||||
showPlayButtonFaceUp ? 'show-play-button-face-up' : ''
|
||||
]" :tabindex="props.tabindex" :aria-disabled="false" @click="$emit('click', $event)"
|
||||
@keydown.enter="$emit('click', $event)" @keydown.space.prevent="$emit('click', $event)">
|
||||
<div class="flip-inner" ref="cardElement">
|
||||
@@ -21,9 +22,9 @@
|
||||
|
||||
<!-- Cover -->
|
||||
<figure class="pochette flex-1 flex justify-center items-center overflow-hidden rounded-xl cursor-pointer">
|
||||
<playButton :objectToPlay="card" />
|
||||
<img v-if="isFaceUp" :src="props.card.url_image" alt="Pochette de l'album" loading="lazy"
|
||||
class="w-full h-full object-cover object-center" />
|
||||
<playButton />
|
||||
<img :src="props.card.url_image" alt="Pochette de l'album" :loading="props.imageLoadingType"
|
||||
@load="$emit('image-loaded', $event)" class="w-full h-full object-cover object-center" />
|
||||
</figure>
|
||||
|
||||
<!-- Body -->
|
||||
@@ -43,7 +44,7 @@
|
||||
class="face-down backdrop-blur-sm z-10 card w-56 h-80 p-3 bg-opacity-10 bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden select-none">
|
||||
<figure class="h-full flex text-center rounded-xl justify-center items-center"
|
||||
:style="{ backgroundColor: cardColor }">
|
||||
<playButton :objectToPlay="card" />
|
||||
<playButton />
|
||||
<img src="/face-down.svg" />
|
||||
</figure>
|
||||
</footer>
|
||||
@@ -54,17 +55,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { Card } from '~~/types/types'
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
const emit = defineEmits(['click', 'image-loaded'])
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
card: Card;
|
||||
isFaceUp?: boolean;
|
||||
role?: string;
|
||||
tabindex?: string | number;
|
||||
imageLoadingType?: 'lazy' | 'eager';
|
||||
showPlayButtonFaceUp?: boolean;
|
||||
}>(), {
|
||||
isFaceUp: false,
|
||||
role: 'button',
|
||||
tabindex: '0'
|
||||
tabindex: '0',
|
||||
imageLoadingType: 'eager',
|
||||
showPlayButtonFaceUp: false
|
||||
})
|
||||
|
||||
import { getYearColor } from '~/utils/colors'
|
||||
@@ -72,6 +77,11 @@ import { getYearColor } from '~/utils/colors'
|
||||
const cardColor = computed(() => getYearColor(props.card.year || 0))
|
||||
const isRedCard = computed(() => (props.card?.suit === '♥' || props.card?.suit === '♦'))
|
||||
|
||||
/* loading states of the card */
|
||||
const isApiLoaded = ref(false)
|
||||
const isImageLoaded = ref(false)
|
||||
const isTrackLoaded = ref(false)
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -163,9 +173,12 @@ const isRedCard = computed(() => (props.card?.suit === '♥' || props.card?.suit
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.face-up:hover {
|
||||
.play-button {
|
||||
opacity: 1;
|
||||
.face-up {
|
||||
|
||||
&:hover {
|
||||
.play-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.flip-inner {
|
||||
@@ -177,6 +190,20 @@ const isRedCard = computed(() => (props.card?.suit === '♥' || props.card?.suit
|
||||
@apply absolute bottom-1/2 top-28 opacity-0 hover:opacity-100;
|
||||
}
|
||||
|
||||
&.show-play-button {
|
||||
.play-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.show-play-button-face-up {
|
||||
.face-up {
|
||||
.play-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pochette:active,
|
||||
.face-down:active {
|
||||
.play-button {
|
||||
|
||||
357
app/components/Platine.vue
Normal file
357
app/components/Platine.vue
Normal file
@@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<div class="platine" :class="{ 'loading': isLoadingCard, 'mounted': isMounted }" ref="platine">
|
||||
<div class="disc" ref="discRef" id="disc">
|
||||
<div class="bobine" :style="{
|
||||
height: progressPercentage + '%',
|
||||
width: progressPercentage + '%'
|
||||
}"></div>
|
||||
<div class="power-button" @mousedown.stop @click.stop="handlePowerButtonClick">
|
||||
<img class="power-logo" src="/favicon.svg">
|
||||
<div class="power-loading" v-if="isLoadingCard">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="turn-point" v-if="!isLoadingCard">
|
||||
</div>
|
||||
</div>
|
||||
<div class="debug">
|
||||
<b>progressPercentage</b>: <br>
|
||||
</br>{{ Math.round(progressPercentage) }}%
|
||||
<br>
|
||||
<b>Disc</b>: <br>
|
||||
</br>{{ Math.round(disc?.secondsPlayed || 0) }}sec
|
||||
<br>
|
||||
</br>{{ Math.round(sampler?.duration || 0) }}sec
|
||||
<br>
|
||||
<!-- <pre>{{ sampler }}</pre> -->
|
||||
isPlaying:
|
||||
</br>{{ isPlaying }}
|
||||
<br>
|
||||
<b>isFirstDrag</b>: <br>
|
||||
</br>{{ isFirstDrag }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Card } from '~~/types/types'
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
|
||||
import Disc from '~/utils/platine/disc'
|
||||
import Sampler from '~/utils/platine/sampler'
|
||||
|
||||
const props = defineProps<{ card?: Card }>()
|
||||
|
||||
// State
|
||||
const isLoadingCard = ref(false)
|
||||
const isFirstDrag = ref(true)
|
||||
const progressPercentage = ref(0)
|
||||
const currentTurns = ref(0)
|
||||
const totalTurns = ref(0)// Refs pour les instances
|
||||
|
||||
// Refs pour les instances
|
||||
const disc = ref<Disc | null>(null)
|
||||
const sampler = ref<Sampler | null>(null)
|
||||
const discRef = ref<HTMLElement>()
|
||||
const platine = ref<HTMLElement>()
|
||||
const isMounted = ref(false)
|
||||
|
||||
const isPlaying = computed(() => Math.abs(Math.round(sampler.value?.currentSpeed || 0)) !== 0)
|
||||
|
||||
// Actions
|
||||
const initPlatine = (element: HTMLElement) => {
|
||||
// console.log('[INIT] Platine')
|
||||
discRef.value = element
|
||||
disc.value = new Disc(element)
|
||||
sampler.value = new Sampler()
|
||||
|
||||
// Configurer les callbacks du disque
|
||||
if (disc.value) {
|
||||
disc.value.callbacks.onStop = () => {
|
||||
// console.log('[DISC] On Stop')
|
||||
sampler.value?.pause()
|
||||
}
|
||||
|
||||
disc.value.callbacks.onDragStart = () => {
|
||||
// console.log('[DISC] On Drag Start')
|
||||
if (isFirstDrag.value) {
|
||||
isFirstDrag.value = false
|
||||
// togglePlay()
|
||||
if (sampler.value && disc.value) {
|
||||
sampler.value.play(disc.value.secondsPlayed)
|
||||
disc.value.powerOn()
|
||||
// console.log('[DISC] Power ON')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disc.value.callbacks.onDragEnded = () => {
|
||||
// console.log('[DISC] On Drag END')
|
||||
sampler.value?.play(disc.value?.secondsPlayed || 0)
|
||||
}
|
||||
|
||||
disc.value.callbacks.onLoop = ({ playbackSpeed, isReversed, secondsPlayed }) => {
|
||||
// Ne mettre à jour que si nécessaire et s'assurer que la position est valide
|
||||
if (Math.abs((sampler.value?.currentSpeed || 0) - playbackSpeed) > 0.01) {
|
||||
const safeSecondsPlayed = Math.max(0, secondsPlayed)
|
||||
sampler.value?.updateSpeed(playbackSpeed, isReversed, safeSecondsPlayed)
|
||||
}
|
||||
updateTurns()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateTurns = () => {
|
||||
if (!disc.value) return
|
||||
|
||||
const newTurns = disc.value.secondsPlayed * 0.75
|
||||
const newTotalTurns = (disc.value as any)._duration * 0.75
|
||||
|
||||
// Calcul du pourcentage de progression pour l'affichage visuel (22% à 100%)
|
||||
const minPercentage = 22
|
||||
const progressRatio = disc.value.secondsPlayed / (disc.value as any)._duration
|
||||
const newProgressPercentage = minPercentage + (progressRatio * (100 - minPercentage))
|
||||
|
||||
// Ne mettre à jour que si les valeurs ont changé de manière significative
|
||||
if (
|
||||
Math.abs(currentTurns.value - newTurns) > 0.01 ||
|
||||
Math.abs(totalTurns.value - newTotalTurns) > 0.01 ||
|
||||
Math.abs(progressPercentage.value - newProgressPercentage) > 0.1
|
||||
) {
|
||||
currentTurns.value = newTurns
|
||||
totalTurns.value = newTotalTurns
|
||||
progressPercentage.value = newProgressPercentage
|
||||
}
|
||||
}
|
||||
|
||||
const loadCard = async (card: Card) => {
|
||||
// console.log('[LOAD CARD]', card)
|
||||
if (!sampler.value || !card) return
|
||||
|
||||
isLoadingCard.value = true
|
||||
// console.log(disc.value)
|
||||
try {
|
||||
await sampler.value.loadTrack(card.url_audio)
|
||||
if (disc.value) {
|
||||
// console.log('[DISC] Set Duration', sampler.value.duration)
|
||||
disc.value.setDuration(sampler.value.duration)
|
||||
updateTurns()
|
||||
}
|
||||
} finally {
|
||||
isLoadingCard.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const play = (position = 0) => {
|
||||
// console.log('[PLAY]')
|
||||
if (!disc.value || !sampler.value || !props.card) return
|
||||
|
||||
sampler.value.play(position)
|
||||
disc.value.powerOn()
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
// console.log('[PAUSE]')
|
||||
if (!disc.value || !sampler.value) return
|
||||
|
||||
sampler.value.pause()
|
||||
disc.value.powerOff()
|
||||
}
|
||||
|
||||
const togglePlay = () => {
|
||||
if (isPlaying.value) {
|
||||
pause()
|
||||
} else {
|
||||
// Reprendre la lecture à la position actuelle
|
||||
const currentPosition = disc.value?.secondsPlayed || 0
|
||||
play(currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
// Nettoyage
|
||||
const cleanup = () => {
|
||||
// console.log('[CLEANUP] Platine')
|
||||
if (disc.value) {
|
||||
disc.value.stop()
|
||||
disc.value.powerOff()
|
||||
}
|
||||
if (sampler.value) {
|
||||
sampler.value.pause()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePowerButtonClick = (e: MouseEvent) => {
|
||||
e.stopPropagation() // Empêcher la propagation de l'événement
|
||||
e.preventDefault() // Empêcher tout comportement par défaut
|
||||
togglePlay()
|
||||
return false // Empêcher tout autre comportement
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === 'Space' || e.key === ' ' || e.keyCode === 32) {
|
||||
e.preventDefault() // Empêcher le défilement de la page
|
||||
togglePlay()
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisation du lecteur
|
||||
onMounted(() => {
|
||||
isMounted.value = true
|
||||
if (discRef.value) {
|
||||
initPlatine(discRef.value)
|
||||
loadCard(props.card!)
|
||||
}
|
||||
|
||||
// Ajouter l'écouteur d'événement clavier
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
// Nettoyage de l'écouteur d'événement
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
isMounted.value = false
|
||||
cleanup()
|
||||
})
|
||||
|
||||
// Surveillance des changements de piste
|
||||
watch(() => props.card, (newCard) => {
|
||||
// console.log('[WATCH] Card', newCard)
|
||||
if (newCard) {
|
||||
loadCard(newCard)
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.platine {
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.disc {
|
||||
pointer-events: auto;
|
||||
position: fixed;
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
cursor: grab;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.4);
|
||||
|
||||
.loading & {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.turn-point {
|
||||
@apply absolute top-1/2 right-8 size-1/12 rounded-full bg-esyellow;
|
||||
}
|
||||
|
||||
.disc.is-scratching {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.power-button {
|
||||
border-radius: 999px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 45%;
|
||||
aspect-ratio: 1/1;
|
||||
// background: no-repeat url(/favicon.svg) center center;
|
||||
background-size: 30%;
|
||||
border-radius: 50%;
|
||||
cursor: pointer !important;
|
||||
|
||||
.power-logo {
|
||||
@apply size-1/2 bg-black rounded-full p-5;
|
||||
}
|
||||
|
||||
.power-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.disc-middle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgb(26, 26, 26);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
background: rgb(69, 69, 69);
|
||||
font-size: 0.75rem;
|
||||
padding: 0.4rem;
|
||||
color: #fff;
|
||||
line-height: 1.3;
|
||||
cursor: pointer;
|
||||
will-change: box-shadow;
|
||||
transition:
|
||||
box-shadow 0.2s ease-out,
|
||||
transform 0.05s ease-in;
|
||||
}
|
||||
|
||||
.power.is-active {
|
||||
transform: translate(1px, 2px);
|
||||
color: red;
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.bobine {
|
||||
@apply bg-slate-900 bg-opacity-50 backdrop-blur absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full;
|
||||
}
|
||||
|
||||
.debug {
|
||||
@apply fixed top-4 right-4 bg-slate-200 rounded-md p-2;
|
||||
}
|
||||
</style>
|
||||
@@ -2,11 +2,11 @@
|
||||
<button tabindex="-1"
|
||||
class="play-button pointer-events-none rounded-full size-24 flex items-center justify-center text-esyellow backdrop-blur-sm bg-black/25 transition-all duration-200 ease-in-out transform active:scale-90 scale-110 text-4xl font-bold"
|
||||
:class="{ loading: isLoading }" :disabled="isLoading">
|
||||
<template v-if="isLoading">
|
||||
<template v-if="props.isLoading">
|
||||
<img src="/loader.svg" alt="Chargement" class="size-16" />
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ isPlaying ? 'I I' : '▶' }}
|
||||
{{ props.isPlaying ? 'I I' : '▶' }}
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user