compose elt v0.1
This commit is contained in:
@@ -11,10 +11,6 @@
|
|||||||
<deckPlaylist :box="box" class="box-page" v-if="box.type === 'playlist'" @click.stop="" />
|
<deckPlaylist :box="box" class="box-page" v-if="box.type === 'playlist'" @click.stop="" />
|
||||||
</template>
|
</template>
|
||||||
</box>
|
</box>
|
||||||
<nav class="fixed top-0">
|
|
||||||
<a class="button m-4 bg-esyellow" href="/draggable">Draggable</a>
|
|
||||||
<a class="button m-4 bg-esyellow" href="/mix">Mix</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bucket" :class="{ 'drag-over': isDragOver }" @dragenter.prevent="onDragEnter"
|
<div class="bucket" :class="{ 'drag-over': isDragOver }" @dragenter.prevent="onDragEnter"
|
||||||
@dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop">
|
@dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop">
|
||||||
<div class="bucket-content">
|
<div v-if="tracks.length === 0" class="bucket-empty">
|
||||||
<div v-if="cards.length === 0" class="bucket-empty">
|
|
||||||
Drop cards here
|
Drop cards here
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="bucket-cards">
|
<draggable v-else v-model="tracks" item-key="id" class="bucket-cards" @start="onDragStart" @end="onDragEnd"
|
||||||
<Card v-for="(card, index) in cards" :key="index" :track="card" class="bucket-card" isFaceUp />
|
:touch-start-threshold="50">
|
||||||
</div>
|
<template #item="{ element: track }">
|
||||||
</div>
|
<card :track="track" tabindex="0" is-face-up class="bucket-card" @click="flipCard(track)" />
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
import { useDataStore } from '~/store/data'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue?: any[]
|
modelValue?: any[]
|
||||||
|
boxId?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'card-dropped'])
|
const dataStore = useDataStore()
|
||||||
|
|
||||||
const isDragOver = ref(false)
|
const isDragOver = ref(false)
|
||||||
const cards = ref<any[]>(props.modelValue || [])
|
const drag = ref(false)
|
||||||
|
const tracks = ref<any[]>(props.modelValue || [])
|
||||||
|
const touchedCard = ref<any>(null)
|
||||||
|
const touchStartPos = ref<{ x: number, y: number } | null>(null)
|
||||||
|
|
||||||
// Mettre à jour les cartes quand la prop change
|
|
||||||
watch(() => props.modelValue, (newValue) => {
|
watch(() => props.modelValue, (newValue) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
cards.value = [...newValue]
|
tracks.value = [...newValue]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (props.boxId) {
|
||||||
|
onMounted(async () => {
|
||||||
|
await dataStore.loadData()
|
||||||
|
tracks.value = dataStore.getTracksByboxId(props.boxId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion du drag and drop desktop
|
||||||
const onDragEnter = (e: DragEvent) => {
|
const onDragEnter = (e: DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
isDragOver.value = true
|
isDragOver.value = true
|
||||||
@@ -45,76 +58,79 @@ const onDragLeave = () => {
|
|||||||
isDragOver.value = false
|
isDragOver.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onDragStart = () => {
|
||||||
|
drag.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragEnd = () => {
|
||||||
|
drag.value = false
|
||||||
|
isDragOver.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const onDrop = (e: DragEvent) => {
|
const onDrop = (e: DragEvent) => {
|
||||||
isDragOver.value = false
|
isDragOver.value = false
|
||||||
|
|
||||||
// Récupérer les données de la carte depuis l'événement de glisser
|
|
||||||
const cardData = e.dataTransfer?.getData('application/json')
|
const cardData = e.dataTransfer?.getData('application/json')
|
||||||
if (cardData) {
|
if (cardData) {
|
||||||
try {
|
try {
|
||||||
const card = JSON.parse(cardData)
|
const track = JSON.parse(cardData)
|
||||||
cards.value = [...cards.value, card]
|
if (!tracks.value.some(t => t.id === track.id)) {
|
||||||
emit('update:modelValue', cards.value)
|
tracks.value.push(track)
|
||||||
emit('card-dropped', card)
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Invalid card data', e)
|
console.error('Erreur lors du traitement de la carte déposée', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const flipCard = (track: any) => {
|
||||||
|
track.isFaceUp = !track.isFaceUp
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.bucket {
|
.bucket {
|
||||||
width: 120px;
|
min-height: 200px;
|
||||||
min-height: 160px;
|
|
||||||
border: 2px dashed #ccc;
|
border: 2px dashed #ccc;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
position: relative;
|
transition: all 0.3s ease;
|
||||||
overflow: hidden;
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bucket.drag-over {
|
.bucket.drag-over {
|
||||||
border-color: #4f46e5;
|
border-color: #4CAF50;
|
||||||
background-color: #eef2ff;
|
background-color: rgba(76, 175, 80, 0.1);
|
||||||
transform: scale(1.02);
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bucket-content {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bucket-empty {
|
.bucket-empty {
|
||||||
color: #9ca3af;
|
display: flex;
|
||||||
text-align: center;
|
justify-content: center;
|
||||||
padding: 1rem;
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bucket-cards {
|
.bucket-cards {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
gap: 0.5rem;
|
gap: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bucket-card {
|
.bucket-card {
|
||||||
background: white;
|
transition: transform 0.2s;
|
||||||
border-radius: 4px;
|
cursor: move;
|
||||||
padding: 0.5rem;
|
touch-action: none;
|
||||||
font-size: 0.8rem;
|
/* Important pour le touch */
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
}
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
.bucket-card:hover {
|
||||||
text-overflow: ellipsis;
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bucket-card:active {
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -71,22 +71,25 @@ import type { Track } from '~~/types/types'
|
|||||||
import { usePlayerStore } from '~/store/player'
|
import { usePlayerStore } from '~/store/player'
|
||||||
import { useDataStore } from '~/store/data';
|
import { useDataStore } from '~/store/data';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ track: Track; isFaceUp?: boolean }>(), {
|
const props = withDefaults(defineProps<{
|
||||||
isFaceUp: false
|
track?: Track;
|
||||||
|
isFaceUp?: boolean;
|
||||||
|
}>(), {
|
||||||
|
track: () => {
|
||||||
|
const dataStore = useDataStore();
|
||||||
|
return dataStore.getRandomPlaylistTrack() || {} as Track;
|
||||||
|
},
|
||||||
|
isFaceUp: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const playerStore = usePlayerStore()
|
const playerStore = usePlayerStore()
|
||||||
const isManifesto = computed(() => props.track.boxId.startsWith('ES00'))
|
const isManifesto = computed(() => props.track.boxId.startsWith('ES00'))
|
||||||
const isOrder = computed(() => props.track.order && !isManifesto)
|
const isOrder = computed(() => props.track.order && !isManifesto)
|
||||||
const isPlaylistTrack = computed(() => props.track.type === 'playlist')
|
const isPlaylistTrack = computed(() => props.track.type === 'playlist')
|
||||||
const isRedCard = computed(() => props.track.card?.suit === '♥' || props.track.card?.suit === '♦')
|
const isRedCard = computed(() => (props.track.card?.suit === '♥' || props.track.card?.suit === '♦'))
|
||||||
const dataStore = useDataStore()
|
const dataStore = useDataStore()
|
||||||
const cardColor = computed(() => dataStore.getYearColor(props.track.year || 0))
|
const cardColor = computed(() => dataStore.getYearColor(props.track.year || 0))
|
||||||
const coverUrl = computed(() => {
|
const coverUrl = computed(() => props.track.coverId || '/card-dock.svg')
|
||||||
if (!props.track.coverId) return ''
|
|
||||||
return props.track.coverId.startsWith('http')
|
|
||||||
? props.track.coverId
|
|
||||||
: `https://f4.bcbits.com/img/${props.track.coverId}_4.jpg`
|
|
||||||
})
|
|
||||||
|
|
||||||
const dragStart = (event: DragEvent) => {
|
const dragStart = (event: DragEvent) => {
|
||||||
if (event.dataTransfer) {
|
if (event.dataTransfer) {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form
|
|
||||||
class="h-screen flex justify-center items-center flex-col absolute top-0 left-1/2 -translate-x-1/2"
|
|
||||||
>
|
|
||||||
<label for="email" class="block text-xl"> be notify when's evilSpins open : </label>
|
|
||||||
<div>
|
|
||||||
<input id="email" type="email" name="" placeholder="email" >
|
|
||||||
<button>ok</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="platine relative">
|
<div class="platine" :class="{ 'drag-over': isDragOver }" @dragenter.prevent="onDragEnter"
|
||||||
<div class="disc" :style="{ 'background-image': `url(${track?.coverId})` }" ref="discRef" id="disc">
|
@dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop">
|
||||||
|
<div class="disc" ref="discRef" :style="'background-image: url(' + coverUrl + ')'" id="disc">
|
||||||
<div
|
<div
|
||||||
class="bobine bg-slate-800 bg-opacity-50 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full"
|
class="bobine bg-slate-800 bg-opacity-50 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full"
|
||||||
:style="{ height: progressPercentage + '%', width: progressPercentage + '%' }">
|
:style="{ height: progressPercentage + '%', width: progressPercentage + '%' }">
|
||||||
@@ -26,14 +27,19 @@ import Disc from '@/platine-tools/disc'
|
|||||||
import Sampler from '@/platine-tools/sampler'
|
import Sampler from '@/platine-tools/sampler'
|
||||||
import type { Track } from '~~/types/types'
|
import type { Track } from '~~/types/types'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ track: Track | undefined }>(), {})
|
const props = withDefaults(defineProps<{ track?: Track }>(), {})
|
||||||
const discRef = ref<HTMLElement>()
|
const discRef = ref<HTMLElement>()
|
||||||
const currentTurns = ref(0)
|
const currentTurns = ref(0)
|
||||||
const totalTurns = ref(0)
|
const totalTurns = ref(0)
|
||||||
const progressPercentage = ref(0)
|
const progressPercentage = ref(0)
|
||||||
const isPlaying = ref(false)
|
const drag = ref(false)
|
||||||
const isLoadingTrack = ref(false)
|
const isDragOver = ref(false)
|
||||||
const isFirstDrag = ref(true)
|
const isFirstDrag = ref(true)
|
||||||
|
const isLoadingTrack = ref(false)
|
||||||
|
const isPlaying = ref(false)
|
||||||
|
|
||||||
|
const coverUrl = computed(() => props.track?.coverId || '/card-dock.svg')
|
||||||
|
|
||||||
let disc: Disc | null = null
|
let disc: Disc | null = null
|
||||||
let sampler: Sampler | null = null
|
let sampler: Sampler | null = null
|
||||||
|
|
||||||
@@ -79,12 +85,12 @@ const handleRewind = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadTrack = async () => {
|
const loadTrack = async (track: Track) => {
|
||||||
if (!sampler || !props.track) return
|
if (!sampler || !track) return
|
||||||
|
|
||||||
isLoadingTrack.value = true
|
isLoadingTrack.value = true
|
||||||
try {
|
try {
|
||||||
await sampler.loadTrack(props.track.url)
|
await sampler.loadTrack(track.url)
|
||||||
if (disc) {
|
if (disc) {
|
||||||
disc.setDuration(sampler.duration)
|
disc.setDuration(sampler.duration)
|
||||||
updateTurns(disc)
|
updateTurns(disc)
|
||||||
@@ -96,6 +102,44 @@ const loadTrack = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gestion du drag and drop
|
||||||
|
const onDragEnter = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
isDragOver.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragOver = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
isDragOver.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragLeave = () => {
|
||||||
|
isDragOver.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragStart = () => {
|
||||||
|
drag.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragEnd = () => {
|
||||||
|
drag.value = false
|
||||||
|
isDragOver.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDrop = (e: DragEvent) => {
|
||||||
|
isDragOver.value = false
|
||||||
|
const cardData = e.dataTransfer?.getData('application/json')
|
||||||
|
if (cardData) {
|
||||||
|
try {
|
||||||
|
const newTrack = JSON.parse(cardData)
|
||||||
|
if (newTrack && newTrack.url) {
|
||||||
|
loadTrack(newTrack)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du traitement de la carte déposée', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
disc = new Disc(discRef.value!)
|
disc = new Disc(discRef.value!)
|
||||||
sampler = new Sampler()
|
sampler = new Sampler()
|
||||||
@@ -131,8 +175,10 @@ onMounted(async () => {
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => props.track, () => {
|
watch(() => props.track, (newTrack) => {
|
||||||
loadTrack()
|
if (newTrack) {
|
||||||
|
loadTrack(newTrack)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -149,12 +195,15 @@ onUnmounted(() => {
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.platine {
|
.platine {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disc {
|
.disc {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-color: white;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -162,6 +211,11 @@ onUnmounted(() => {
|
|||||||
cursor: grab;
|
cursor: grab;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.dragoOver & {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.disc.is-scratching {
|
.disc.is-scratching {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="deck-playlist">
|
|
||||||
<div class="flex flex-col fixed right-0 top-0 z-50">
|
<div class="flex flex-col fixed right-0 top-0 z-50">
|
||||||
<button @click="closeDatBox" v-if="uiStore.isBoxSelected"
|
<button @click="closeDatBox" v-if="uiStore.isBoxSelected"
|
||||||
class="px-4 py-2 text-black hover:text-black bg-esyellow transition-colors z-50" aria-label="close the box">
|
class="px-4 py-2 text-black hover:text-black bg-esyellow transition-colors z-50" aria-label="close the box">
|
||||||
close
|
close
|
||||||
</button>
|
</button>
|
||||||
<Bucket v-model="bucketCards" @card-dropped="onCardDropped" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="controls flex justify-center z-50 relative">
|
<div class="controls flex justify-center z-50 relative">
|
||||||
<button class="px-4 py-2 text-black hover:text-black bg-esyellow transition-colors" @click="toggleAllCards">
|
<button class="px-4 py-2 text-black hover:text-black bg-esyellow transition-colors" @click="toggleAllCards">
|
||||||
@@ -19,11 +17,14 @@
|
|||||||
<card v-for="(track, i) in filteredTracks" :key="track.id" :track="track" :tabindex="i"
|
<card v-for="(track, i) in filteredTracks" :key="track.id" :track="track" :tabindex="i"
|
||||||
:is-face-up="isCardRevealed(track.id)" />
|
:is-face-up="isCardRevealed(track.id)" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="dock">
|
||||||
|
<Bucket @card-dropped="onCardDropped" />
|
||||||
|
<Platine />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useDataStore } from '~/store/data'
|
import { useDataStore } from '~/store/data'
|
||||||
import { useCardStore } from '~/store/card'
|
import { useCardStore } from '~/store/card'
|
||||||
import { usePlayerStore } from '~/store/player'
|
import { usePlayerStore } from '~/store/player'
|
||||||
@@ -45,7 +46,6 @@ const uiStore = useUiStore()
|
|||||||
const deck = ref()
|
const deck = ref()
|
||||||
const tracks = computed(() => dataStore.getTracksByboxId(props.box.id))
|
const tracks = computed(() => dataStore.getTracksByboxId(props.box.id))
|
||||||
const filteredTracks = ref(tracks.value)
|
const filteredTracks = ref(tracks.value)
|
||||||
const bucketCards = ref([])
|
|
||||||
|
|
||||||
// Variables réactives pour les filtres
|
// Variables réactives pour les filtres
|
||||||
const selectedSuit = ref('')
|
const selectedSuit = ref('')
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full flex flex-col items-center">
|
|
||||||
<slot />
|
<slot />
|
||||||
<searchModal />
|
|
||||||
<loader />
|
|
||||||
<Player />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="w-full flex flex-col items-center">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
27
app/pages/boxes.vue
Normal file
27
app/pages/boxes.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<boxes />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useUiStore } from '~/store/ui'
|
||||||
|
import { useDataStore } from '~/store/data'
|
||||||
|
|
||||||
|
// Configuration du layout
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'default'
|
||||||
|
})
|
||||||
|
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const dataStore = useDataStore()
|
||||||
|
await dataStore.loadData()
|
||||||
|
uiStore.listBoxes()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.logo {
|
||||||
|
filter: drop-shadow(3px 3px 0 rgb(0 0 0 / 0.7));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
class="fixed bottom-4 right-4 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full shadow-lg z-50 transition-colors"
|
|
||||||
title="Ranger les cartes" @click="arrangeCards">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div ref="deck" class="deck">
|
|
||||||
<card v-for="track in tracks" :key="track.id" :track="track"
|
|
||||||
:class="['card', 'id-' + track.id, { dragging: dragging === track }]" :style="{
|
|
||||||
top: track.y + 'px',
|
|
||||||
left: track.x + 'px',
|
|
||||||
zIndex: track.zIndex || 1,
|
|
||||||
transform: `rotate(${track.rotation || 0}deg)`
|
|
||||||
}" :is-face-up="track.isFaceUp" @mousedown="startDrag($event, track)" @click="flipCard(track)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { useDataStore } from '~/store/data'
|
|
||||||
|
|
||||||
const tracks = ref([])
|
|
||||||
const deck = ref(null)
|
|
||||||
let zIndexCounter = 1
|
|
||||||
|
|
||||||
let dragging = null
|
|
||||||
let offset = { x: 0, y: 0 }
|
|
||||||
let velocity = { x: 0, y: 0 }
|
|
||||||
let lastTime = 0
|
|
||||||
|
|
||||||
const dataStore = useDataStore()
|
|
||||||
|
|
||||||
definePageMeta({ layout: 'default' })
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await dataStore.loadData()
|
|
||||||
tracks.value = dataStore.getTracksByboxId('ESPLAYLIST').map((t, i) => ({
|
|
||||||
...t,
|
|
||||||
x: t.x ?? 50 + i * 20,
|
|
||||||
y: t.y ?? 50 + i * 20,
|
|
||||||
zIndex: t.zIndex ?? i + 1,
|
|
||||||
rotation: 0,
|
|
||||||
isFaceUp: t.isFaceUp ?? false
|
|
||||||
}))
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', onDrag)
|
|
||||||
window.addEventListener('mouseup', stopDrag)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
window.removeEventListener('mousemove', onDrag)
|
|
||||||
window.removeEventListener('mouseup', stopDrag)
|
|
||||||
})
|
|
||||||
|
|
||||||
function startDrag(e, track) {
|
|
||||||
dragging = track
|
|
||||||
const rect = deck.value.getBoundingClientRect()
|
|
||||||
offset.x = e.clientX - rect.left - track.x
|
|
||||||
offset.y = e.clientY - rect.top - track.y
|
|
||||||
lastTime = performance.now()
|
|
||||||
velocity = { x: 0, y: 0 }
|
|
||||||
|
|
||||||
zIndexCounter += 1
|
|
||||||
track.zIndex = zIndexCounter
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDrag(e) {
|
|
||||||
if (!dragging) return
|
|
||||||
|
|
||||||
const rect = deck.value.getBoundingClientRect()
|
|
||||||
const newX = e.clientX - rect.left - offset.x
|
|
||||||
const newY = e.clientY - rect.top - offset.y
|
|
||||||
|
|
||||||
const now = performance.now()
|
|
||||||
const dt = now - lastTime
|
|
||||||
velocity.x = (newX - dragging.x) / dt
|
|
||||||
velocity.y = (newY - dragging.y) / dt
|
|
||||||
lastTime = now
|
|
||||||
|
|
||||||
dragging.x = newX
|
|
||||||
dragging.y = newY
|
|
||||||
|
|
||||||
// Rotation dynamique selon la position horizontale
|
|
||||||
const centerX = rect.width / 2
|
|
||||||
const dx = dragging.x - centerX
|
|
||||||
dragging.rotation = dx * 0.05
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopDrag() {
|
|
||||||
if (!dragging) return
|
|
||||||
const track = dragging
|
|
||||||
dragging = null
|
|
||||||
|
|
||||||
// Inertie douce
|
|
||||||
const decay = 0.95
|
|
||||||
function animateInertia() {
|
|
||||||
if (Math.abs(velocity.x) < 0.02 && Math.abs(velocity.y) < 0.02) return
|
|
||||||
track.x += velocity.x * 16
|
|
||||||
track.y += velocity.y * 16
|
|
||||||
velocity.x *= decay
|
|
||||||
velocity.y *= decay
|
|
||||||
requestAnimationFrame(animateInertia)
|
|
||||||
}
|
|
||||||
animateInertia()
|
|
||||||
}
|
|
||||||
|
|
||||||
function flipCard(track) {
|
|
||||||
track.isFaceUp = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function arrangeCards() {
|
|
||||||
const deckRect = deck.value.getBoundingClientRect()
|
|
||||||
const cardWidth = 224
|
|
||||||
const cardHeight = 320
|
|
||||||
const padding = 20
|
|
||||||
const cardsPerRow = Math.max(
|
|
||||||
1,
|
|
||||||
Math.floor((deckRect.width - padding * 2) / (cardWidth + padding))
|
|
||||||
)
|
|
||||||
|
|
||||||
tracks.value.forEach((track, index) => {
|
|
||||||
const row = Math.floor(index / cardsPerRow)
|
|
||||||
const col = index % cardsPerRow
|
|
||||||
|
|
||||||
track.x = padding + col * (cardWidth + padding)
|
|
||||||
track.y = padding + row * (cardHeight / 3)
|
|
||||||
track.zIndex = index + 1
|
|
||||||
track.rotation = 0
|
|
||||||
|
|
||||||
zIndexCounter = Math.max(zIndexCounter, track.zIndex)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.deck {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 80vh;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 1rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
position: absolute;
|
|
||||||
cursor: grab;
|
|
||||||
transition:
|
|
||||||
transform 0.1s ease,
|
|
||||||
box-shadow 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:active {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card.dragging {
|
|
||||||
z-index: 999;
|
|
||||||
cursor: grabbing;
|
|
||||||
transform: scale(1.05) rotate(0deg);
|
|
||||||
/* rotation sera remplacée dynamiquement */
|
|
||||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,27 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<boxes />
|
<card />
|
||||||
|
<platine />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { useUiStore } from '~/store/ui'
|
|
||||||
import { useDataStore } from '~/store/data'
|
|
||||||
|
|
||||||
// Configuration du layout
|
|
||||||
definePageMeta({
|
|
||||||
layout: 'default'
|
|
||||||
})
|
|
||||||
|
|
||||||
const uiStore = useUiStore()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const dataStore = useDataStore()
|
|
||||||
await dataStore.loadData()
|
|
||||||
uiStore.listBoxes()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.logo {
|
|
||||||
filter: drop-shadow(3px 3px 0 rgb(0 0 0 / 0.7));
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
<template>
|
|
||||||
<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">
|
|
||||||
<!-- <box :box="box" /> -->
|
|
||||||
<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>
|
|
||||||
<button @click="currentPosition = boxPositions.back">back</button> -->
|
|
||||||
<div class="w-full block">
|
|
||||||
<input class="w-1/2" type="color" name="color1" id="color1" v-model="box.color1" />
|
|
||||||
<input class="w-1/2" type="color" name="color1" id="color1" v-model="box.color2" />
|
|
||||||
<div
|
|
||||||
class="block w-full h-32"
|
|
||||||
:style="{
|
|
||||||
background: `linear-gradient(to top, ${box.color1}, ${box.color2})`
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
<!-- <label class="block">
|
|
||||||
size: {{ size }}
|
|
||||||
<input v-model.number="size" type="range" step="1" min="1" max="14">
|
|
||||||
</label> -->
|
|
||||||
</div>
|
|
||||||
<!-- <div>
|
|
||||||
<label class="block">
|
|
||||||
X: {{ currentPosition.x }}
|
|
||||||
<input v-model.number="currentPosition.x" type="range" step="1" min="-180" max="180">
|
|
||||||
</label>
|
|
||||||
<label class="block">
|
|
||||||
Y: {{ currentPosition.y }}
|
|
||||||
<input v-model.number="currentPosition.y" type="range" step="1" min="-180" max="180">
|
|
||||||
</label>
|
|
||||||
<label class="block">
|
|
||||||
Z: {{ currentPosition.z }}
|
|
||||||
<input v-model.number="currentPosition.z" type="range" step="1" min="-180" max="180">
|
|
||||||
</label>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<card :track="track" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Box, Track } from '~~/types/types'
|
|
||||||
|
|
||||||
const box = ref<Box>({
|
|
||||||
id: 'ES00A',
|
|
||||||
name: 'zero',
|
|
||||||
duration: 2794,
|
|
||||||
description: 'Zero is for manifesto',
|
|
||||||
color1: '#ffffff',
|
|
||||||
color2: '#48959d',
|
|
||||||
color3: '#00ff00',
|
|
||||||
type: 'compilation'
|
|
||||||
})
|
|
||||||
|
|
||||||
const track = ref<Track>({
|
|
||||||
id: 1,
|
|
||||||
boxId: 'ES00A',
|
|
||||||
title: 'The grinding wheel',
|
|
||||||
artist: {
|
|
||||||
id: 0,
|
|
||||||
name: "L'Efondras",
|
|
||||||
url: '',
|
|
||||||
coverId: '0024705317'
|
|
||||||
},
|
|
||||||
start: 0,
|
|
||||||
url: 'https://arakirecords.bandcamp.com/track/the-grinding-wheel',
|
|
||||||
coverId: 'a3236746052',
|
|
||||||
type: 'compilation'
|
|
||||||
})
|
|
||||||
|
|
||||||
//from-slate-800 to-zinc-900
|
|
||||||
</script>
|
|
||||||
@@ -1,19 +1,9 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { Track } from '~~/types/types'
|
import type { Track } from '~~/types/types'
|
||||||
interface CardPosition {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type CardPositions = Record<string, Record<number, CardPosition>>
|
|
||||||
|
|
||||||
export const useCardStore = defineStore('card', {
|
export const useCardStore = defineStore('card', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
// Stocke les IDs des cartes déjà révélées
|
// Stocke les IDs des cartes déjà révélées
|
||||||
revealedCards: new Set<number>(),
|
revealedCards: new Set<number>()
|
||||||
// Stocke les positions personnalisées des cartes par box
|
|
||||||
// Format: { [boxId]: { [trackId]: { x: number, y: number } } }
|
|
||||||
cardPositions: {} as CardPositions
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -33,20 +23,6 @@ export const useCardStore = defineStore('card', {
|
|||||||
return this.revealedCards.has(trackId)
|
return this.revealedCards.has(trackId)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Définir la position d'une carte dans une box
|
|
||||||
setCardPosition(boxId: string, trackId: number, position: { x: number; y: number }) {
|
|
||||||
if (!this.cardPositions[boxId]) {
|
|
||||||
this.cardPositions[boxId] = {}
|
|
||||||
}
|
|
||||||
this.cardPositions[boxId][trackId] = position
|
|
||||||
this.saveToLocalStorage()
|
|
||||||
},
|
|
||||||
|
|
||||||
// Obtenir la position d'une carte dans une box
|
|
||||||
getCardPosition(boxId: string, trackId: number): { x: number; y: number } | undefined {
|
|
||||||
return this.cardPositions[boxId]?.[trackId]
|
|
||||||
},
|
|
||||||
|
|
||||||
// Basculer l'état de révélation de toutes les cartes
|
// Basculer l'état de révélation de toutes les cartes
|
||||||
revealAllCards(tracks: Track[]) {
|
revealAllCards(tracks: Track[]) {
|
||||||
tracks.forEach((track) => {
|
tracks.forEach((track) => {
|
||||||
@@ -69,8 +45,7 @@ export const useCardStore = defineStore('card', {
|
|||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'cardStore',
|
'cardStore',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
revealedCards: Array.from(this.revealedCards),
|
revealedCards: Array.from(this.revealedCards)
|
||||||
cardPositions: this.cardPositions
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -85,15 +60,12 @@ export const useCardStore = defineStore('card', {
|
|||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem('cardStore')
|
const saved = localStorage.getItem('cardStore')
|
||||||
if (saved) {
|
if (saved) {
|
||||||
const { revealedCards, cardPositions } = JSON.parse(saved)
|
const { revealedCards } = JSON.parse(saved)
|
||||||
if (Array.isArray(revealedCards)) {
|
if (Array.isArray(revealedCards)) {
|
||||||
this.revealedCards = new Set(
|
this.revealedCards = new Set(
|
||||||
revealedCards.filter((id): id is number => typeof id === 'number')
|
revealedCards.filter((id): id is number => typeof id === 'number')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (cardPositions && typeof cardPositions === 'object') {
|
|
||||||
this.cardPositions = cardPositions
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load card store from localStorage', e)
|
console.error('Failed to load card store from localStorage', e)
|
||||||
@@ -111,11 +83,6 @@ export const useCardStore = defineStore('card', {
|
|||||||
// Getter pour la réactivité dans les templates
|
// Getter pour la réactivité dans les templates
|
||||||
isRevealed: (state) => (trackId: number) => {
|
isRevealed: (state) => (trackId: number) => {
|
||||||
return state.revealedCards.has(trackId)
|
return state.revealedCards.has(trackId)
|
||||||
},
|
|
||||||
|
|
||||||
// Obtenir toutes les positions des cartes d'une box
|
|
||||||
getBoxCardPositions: (state) => (boxId: string) => {
|
|
||||||
return state.cardPositions[boxId] || {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
1
public/card-dock.svg
Normal file
1
public/card-dock.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="224" xmlns="http://www.w3.org/2000/svg" height="320" id="screenshot-0fc9bf1a-468d-8062-8007-12c4a2492f68" viewBox="0 0 224 320" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-0fc9bf1a-468d-8062-8007-12c4a2492f68"><defs><clipPath id="frame-clip-0fc9bf1a-468d-8062-8007-12c4a2492f68-render-1" class="frame-clip frame-clip-def"><rect rx="0" ry="0" x="0" y="0" width="224" height="320" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)"/></clipPath></defs><g class="frame-container-wrapper"><g class="frame-container-blur"><g class="frame-container-shadows"><g clip-path="url(#frame-clip-0fc9bf1a-468d-8062-8007-12c4a2492f68-render-1)" fill="none"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c4a2492f68"><rect width="224" height="320" class="frame-background" x="0" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(255, 255, 255); fill-opacity: 1;" ry="0" rx="0" y="0"/></g><g class="frame-children"><g id="shape-0fc9bf1a-468d-8062-8007-12c9d568a6f9"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c9d568a6f9"><rect rx="20" ry="20" x="0" y="0" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" width="224" height="320" style="fill: rgb(0, 0, 0); fill-opacity: 0.15;"/></g></g><g id="shape-0fc9bf1a-468d-8062-8007-12c8693c73a0"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c8693c73a0"><ellipse cx="112" cy="160.00000000000003" rx="144" ry="144" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(177, 178, 181); fill-opacity: 0.2;"/></g></g><g id="shape-0fc9bf1a-468d-8062-8007-12c85a924e31"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c85a924e31"><ellipse cx="112" cy="160.00000000000003" rx="112.5" ry="112.50000000000004" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(177, 178, 181); fill-opacity: 0.2;"/></g></g><g id="shape-0fc9bf1a-468d-8062-8007-12c7cdce5e0a"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c7cdce5e0a"><ellipse cx="112" cy="160" rx="82" ry="82.00000000000003" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(177, 178, 181); fill-opacity: 0.2;"/></g></g><g id="shape-0fc9bf1a-468d-8062-8007-12c820876e0e"><g class="fills" id="fills-0fc9bf1a-468d-8062-8007-12c820876e0e"><ellipse cx="112" cy="160" rx="51" ry="51" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(177, 178, 181); fill-opacity: 0.2;"/></g></g></g></g></g></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
Reference in New Issue
Block a user