eS v1
This commit is contained in:
@@ -1,16 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<article class="box box-scene z-10" ref="scene">
|
<article class="box box-scene z-10" ref="scene">
|
||||||
<div class="box-object" ref="box">
|
<div ref="domBox" class="box-object" :class="{ 'is-draggable': isDraggable }">
|
||||||
<div class="face front relative" ref="frontFace">
|
<div class="face front relative" ref="frontFace">
|
||||||
<img v-if="compilation.duration" class="cover absolute" :src="`/${compilation.id}/cover.jpg`" alt="">
|
<img v-if="box.duration" class="cover absolute" :src="`/${box.id}/cover.jpg`" alt="">
|
||||||
<div class="size-full flex justify-center items-center text-7xl" v-else>
|
<div class="size-full flex-col justify-center items-center text-7xl" v-html="box.description" v-else />
|
||||||
{{ compilation.description }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="face back flex flex-col flex-wrap items-start p-4 overflow-hidden" ref="backFace">
|
<div class="face back flex flex-col flex-wrap items-start p-4 overflow-hidden" ref="backFace">
|
||||||
<li class="list-none text-xxs w-1/2 flex flex-row"
|
<li class="list-none text-xxs w-1/2 flex flex-row"
|
||||||
v-for="track in dataStore.getTracksByCompilationId(compilation.id).slice(0, -1)" :key="track.id"
|
v-for="track in dataStore.getTracksByboxId(box.id).slice(0, -1)" :key="track.id" :track="track">
|
||||||
:track="track">
|
|
||||||
<span class="" v-if="isNotManifesto">
|
<span class="" v-if="isNotManifesto">
|
||||||
{{ track.order }}.
|
{{ track.order }}.
|
||||||
</span>
|
</span>
|
||||||
@@ -24,9 +21,9 @@
|
|||||||
<div class="face right" ref="rightFace" />
|
<div class="face right" ref="rightFace" />
|
||||||
<div class="face left" ref="leftFace" />
|
<div class="face left" ref="leftFace" />
|
||||||
<div class="face top" ref="topFace">
|
<div class="face top" ref="topFace">
|
||||||
<template v-if="compilation.duration !== 0">
|
<template v-if="box.duration !== 0">
|
||||||
<img class="logo h-full p-1" src="/logo.svg" alt="">
|
<img class="logo h-full p-1" src="/logo.svg" alt="">
|
||||||
<img class="absolute block h-1/2" style="left:5%;" :src="`/${compilation.id}/title.svg`" alt="">
|
<img class="absolute block h-1/2" style="left:5%;" :src="`/${box.id}/title.svg`" alt="">
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="absolute block h-1/2 right-6">
|
<span class="absolute block h-1/2 right-6">
|
||||||
@@ -34,7 +31,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<img class="logo h-full p-1" src="/favicon.svg" alt="">
|
<img class="logo h-full p-1" src="/favicon.svg" alt="">
|
||||||
<span class="absolute block h-1/2" style="left:5%;">
|
<span class="absolute block h-1/2" style="left:5%;">
|
||||||
{{ compilation.name }}
|
{{ box.name }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,24 +42,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
import { ref, onMounted, onBeforeUnmount, watch, computed } from 'vue'
|
||||||
import type { Compilation, BoxState } from '~~/types/types'
|
import type { Box, BoxState } from '~~/types/types'
|
||||||
import { useDataStore } from '~/store/data'
|
import { useDataStore } from '~/store/data'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = defineProps<{
|
||||||
defineProps<{
|
box: Box
|
||||||
compilation: Compilation
|
}>();
|
||||||
BoxState?: BoxState
|
|
||||||
}>(),
|
const { $isMobile } = useNuxtApp()
|
||||||
{ BoxState: 'list' }
|
|
||||||
)
|
|
||||||
const dataStore = useDataStore()
|
const dataStore = useDataStore()
|
||||||
const isDraggable = computed(() => !['list', 'hidden'].includes(BoxState.value()))
|
const isDraggable = computed(() => !['box-list', 'box-hidden'].includes(props.box.state))
|
||||||
const isNotManifesto = computed(() => !props.compilation.id.startsWith('ES00'))
|
const isNotManifesto = computed(() => !props.box.id.startsWith('ES00'))
|
||||||
|
|
||||||
// --- Réfs ---
|
// --- Réfs ---
|
||||||
const scene = ref<HTMLElement>()
|
const scene = ref<HTMLElement>()
|
||||||
const box = ref<HTMLElement>()
|
const domBox = ref<HTMLElement>()
|
||||||
const frontFace = ref<HTMLElement>()
|
const frontFace = ref<HTMLElement>()
|
||||||
const backFace = ref<HTMLElement>()
|
const backFace = ref<HTMLElement>()
|
||||||
const rightFace = ref<HTMLElement>()
|
const rightFace = ref<HTMLElement>()
|
||||||
@@ -81,36 +77,36 @@ let lastPointer = { x: 0, y: 0, time: 0 }
|
|||||||
let velocity = { x: 0, y: 0 }
|
let velocity = { x: 0, y: 0 }
|
||||||
let raf: number | null = null
|
let raf: number | null = null
|
||||||
|
|
||||||
const sensitivity = 0.3
|
const sensitivity = $isMobile ? 0.5 : 0.15
|
||||||
const friction = 0.95
|
const friction = 0.95
|
||||||
const minVelocity = 0.02
|
const minVelocity = 0.02
|
||||||
const enableInertia = true
|
const enableInertia = true
|
||||||
|
|
||||||
// --- Transformations ---
|
// --- Transformations ---
|
||||||
function applyTransform(duration = 0.5) {
|
function applyTransform(duration = 0.5) {
|
||||||
if (!box.value) return
|
if (!domBox.value) return
|
||||||
rotateX.value = Math.round(rotateX.value)
|
rotateX.value = Math.round(rotateX.value)
|
||||||
rotateY.value = Math.round(rotateY.value)
|
rotateY.value = Math.round(rotateY.value)
|
||||||
rotateZ.value = Math.round(rotateZ.value)
|
rotateZ.value = Math.round(rotateZ.value)
|
||||||
|
|
||||||
box.value.style.transition = `transform ${duration}s ease`
|
domBox.value.style.transition = `transform ${duration}s ease`
|
||||||
box.value.style.transform = `rotateX(${rotateX.value}deg) rotateY(${rotateY.value}deg) rotateZ(${rotateZ.value}deg)`
|
domBox.value.style.transform = `rotateX(${rotateX.value}deg) rotateY(${rotateY.value}deg) rotateZ(${rotateZ.value}deg)`
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Gestion BoxState ---
|
// --- Gestion BoxState ---
|
||||||
function applyBoxState() {
|
function applyBoxState() {
|
||||||
switch (props.BoxState) {
|
switch (props.box.state) {
|
||||||
case 'list':
|
case 'box-list':
|
||||||
rotateX.value = 76
|
rotateX.value = 76
|
||||||
rotateY.value = 0
|
rotateY.value = 0
|
||||||
rotateZ.value = 150
|
rotateZ.value = 150
|
||||||
break
|
break
|
||||||
case 'selected':
|
case 'box-selected':
|
||||||
rotateX.value = -20
|
rotateX.value = -20
|
||||||
rotateY.value = 20
|
rotateY.value = 20
|
||||||
rotateZ.value = 0
|
rotateZ.value = 0
|
||||||
break
|
break
|
||||||
case 'hide':
|
case 'box-hidden':
|
||||||
rotateX.value = 76
|
rotateX.value = 76
|
||||||
rotateY.value = 0
|
rotateY.value = 0
|
||||||
rotateZ.value = 150
|
rotateZ.value = 150
|
||||||
@@ -123,12 +119,12 @@ function applyBoxState() {
|
|||||||
function applyColor() {
|
function applyColor() {
|
||||||
if (!frontFace.value || !backFace.value || !leftFace.value || !topFace.value || !bottomFace.value) return
|
if (!frontFace.value || !backFace.value || !leftFace.value || !topFace.value || !bottomFace.value) return
|
||||||
|
|
||||||
frontFace.value.style.background = props.compilation.color2
|
frontFace.value.style.background = props.box.color2
|
||||||
backFace.value.style.background = `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`
|
backFace.value.style.background = `linear-gradient(to top, ${props.box.color1}, ${props.box.color2})`
|
||||||
leftFace.value.style.background = `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`
|
leftFace.value.style.background = `linear-gradient(to top, ${props.box.color1}, ${props.box.color2})`
|
||||||
rightFace.value.style.background = `linear-gradient(to top, ${props.compilation.color1}, ${props.compilation.color2})`
|
rightFace.value.style.background = `linear-gradient(to top, ${props.box.color1}, ${props.box.color2})`
|
||||||
topFace.value.style.background = `linear-gradient(to top, ${props.compilation.color2}, ${props.compilation.color2})`
|
topFace.value.style.background = `linear-gradient(to top, ${props.box.color2}, ${props.box.color2})`
|
||||||
bottomFace.value.style.background = props.compilation.color1
|
bottomFace.value.style.background = props.box.color1
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Inertie ---
|
// --- Inertie ---
|
||||||
@@ -157,7 +153,7 @@ let listenersAttached = false
|
|||||||
const down = (ev: PointerEvent) => {
|
const down = (ev: PointerEvent) => {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
dragging = true
|
dragging = true
|
||||||
box.value?.setPointerCapture(ev.pointerId)
|
domBox.value?.setPointerCapture(ev.pointerId)
|
||||||
lastPointer = { x: ev.clientX, y: ev.clientY, time: performance.now() }
|
lastPointer = { x: ev.clientX, y: ev.clientY, time: performance.now() }
|
||||||
velocity = { x: 0, y: 0 }
|
velocity = { x: 0, y: 0 }
|
||||||
if (raf) { cancelAnimationFrame(raf); raf = null }
|
if (raf) { cancelAnimationFrame(raf); raf = null }
|
||||||
@@ -185,36 +181,36 @@ const move = (ev: PointerEvent) => {
|
|||||||
const end = (ev: PointerEvent) => {
|
const end = (ev: PointerEvent) => {
|
||||||
if (!dragging) return
|
if (!dragging) return
|
||||||
dragging = false
|
dragging = false
|
||||||
try { box.value?.releasePointerCapture(ev.pointerId) } catch { }
|
try { domBox.value?.releasePointerCapture(ev.pointerId) } catch { }
|
||||||
if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) {
|
if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) {
|
||||||
if (!raf) raf = requestAnimationFrame(tickInertia)
|
if (!raf) raf = requestAnimationFrame(tickInertia)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addListeners() {
|
function addListeners() {
|
||||||
if (!box.value || listenersAttached) return
|
if (!domBox.value || listenersAttached) return
|
||||||
box.value.addEventListener('pointerdown', down)
|
domBox.value.addEventListener('pointerdown', down)
|
||||||
box.value.addEventListener('pointermove', move)
|
domBox.value.addEventListener('pointermove', move)
|
||||||
box.value.addEventListener('pointerup', end)
|
domBox.value.addEventListener('pointerup', end)
|
||||||
box.value.addEventListener('pointercancel', end)
|
domBox.value.addEventListener('pointercancel', end)
|
||||||
box.value.addEventListener('pointerleave', end)
|
domBox.value.addEventListener('pointerleave', end)
|
||||||
listenersAttached = true
|
listenersAttached = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeListeners() {
|
function removeListeners() {
|
||||||
if (!box.value || !listenersAttached) return
|
if (!domBox.value || !listenersAttached) return
|
||||||
box.value.removeEventListener('pointerdown', down)
|
domBox.value.removeEventListener('pointerdown', down)
|
||||||
box.value.removeEventListener('pointermove', move)
|
domBox.value.removeEventListener('pointermove', move)
|
||||||
box.value.removeEventListener('pointerup', end)
|
domBox.value.removeEventListener('pointerup', end)
|
||||||
box.value.removeEventListener('pointercancel', end)
|
domBox.value.removeEventListener('pointercancel', end)
|
||||||
box.value.removeEventListener('pointerleave', end)
|
domBox.value.removeEventListener('pointerleave', end)
|
||||||
listenersAttached = false
|
listenersAttached = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
applyColor()
|
applyColor()
|
||||||
applyBoxState()
|
applyBoxState()
|
||||||
if (isDraggable) addListeners()
|
if (isDraggable.value) addListeners()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -223,9 +219,9 @@ onBeforeUnmount(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// --- Watchers ---
|
// --- Watchers ---
|
||||||
watch(() => props.BoxState, () => applyBoxState())
|
watch(() => props.box.state, () => applyBoxState())
|
||||||
watch(() => props.compilation, () => applyColor(), { deep: true })
|
watch(() => props.box, () => applyColor(), { deep: true })
|
||||||
watch(() => isDraggable, (enabled) => enabled ? addListeners() : removeListeners())
|
watch(isDraggable, (enabled) => (enabled ? addListeners() : removeListeners()))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -235,23 +231,29 @@ watch(() => isDraggable, (enabled) => enabled ? addListeners() : removeListeners
|
|||||||
--height: calc(var(--size) * (100 / 3));
|
--height: calc(var(--size) * (100 / 3));
|
||||||
--width: calc(var(--size) * 50);
|
--width: calc(var(--size) * 50);
|
||||||
--depth: calc(var(--size) * 10);
|
--depth: calc(var(--size) * 10);
|
||||||
transition: all .5s;
|
transition: height .5s ease, opacity .5s ease;
|
||||||
|
|
||||||
&.hide {
|
&.box-list {
|
||||||
height: 0;
|
height: calc(var(--size) * 20);
|
||||||
opacity: 0;
|
@apply hover:scale-105;
|
||||||
z-index: 0;
|
transition: all .5s ease;
|
||||||
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.list {
|
&.box-selected {
|
||||||
@apply hover:scale-105;
|
height: calc(var(--size) * 34);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-scene {
|
&-scene {
|
||||||
height: calc(var(--size) * 20);
|
|
||||||
perspective: 1000px;
|
perspective: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.box-hidden {
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&-object {
|
&-object {
|
||||||
width: var(--width);
|
width: var(--width);
|
||||||
height: var(--height);
|
height: var(--height);
|
||||||
@@ -260,11 +262,11 @@ watch(() => isDraggable, (enabled) => enabled ? addListeners() : removeListeners
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
.list & {
|
.box-list & {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected & {
|
.box-selected & {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,5 +341,31 @@ watch(() => isDraggable, (enabled) => enabled ? addListeners() : removeListeners
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Deck fade in/out purely in CSS */
|
||||||
|
.box-page {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .25s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.box-selected .box-page {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* for tabindex */
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 0;
|
||||||
|
@apply scale-105 outline-none;
|
||||||
|
|
||||||
|
.face {
|
||||||
|
border: 4px solid rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.indice) {
|
||||||
|
@apply text-xl p-2 relative bg-black/50 rounded-full backdrop-blur-md;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
74
app/components/boxes.vue
Normal file
74
app/components/boxes.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col-reverse mt-16">
|
||||||
|
<box v-for="(box, i) in dataStore.boxes.slice()" :key="box.id" :tabindex="dataStore.boxes.length - i" :box="box"
|
||||||
|
@click="onBoxClick(box)" class="text-center" :class="box.state" :id="box.id">
|
||||||
|
<button @click.stop="playSelectedBox(box)" v-if="box.state === 'box-selected'"
|
||||||
|
class="relative z-40 rounded-full size-24 bottom-1/2 text-4xl tex-bold backdrop-blur-sm bg-black/25">
|
||||||
|
{{ !playerStore.isPaused && playerStore.currentTrack?.boxId === box.id ? 'I I' : '▶' }}
|
||||||
|
</button>
|
||||||
|
<deck :box="box" class="box-page" v-if="box.state === 'box-selected'" @click.stop />
|
||||||
|
</box>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import type { Box } from '~~/types/types'
|
||||||
|
import { useDataStore } from '~/store/data'
|
||||||
|
import { usePlayerStore } from '~/store/player'
|
||||||
|
import { useUiStore } from '~/store/ui'
|
||||||
|
|
||||||
|
const dataStore = useDataStore()
|
||||||
|
const playerStore = usePlayerStore()
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
|
||||||
|
function openBox(id: string) {
|
||||||
|
uiStore.selectBox(id)
|
||||||
|
// Scroll to the top smoothly
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBoxClick(b: Box) {
|
||||||
|
if (b.state !== 'box-selected') {
|
||||||
|
openBox(b.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playSelectedBox(b: Box) {
|
||||||
|
playerStore.playBox(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
function KeyboardAction(e: KeyboardEvent) {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Escape':
|
||||||
|
uiStore.closeBox()
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Enter':
|
||||||
|
if (document.activeElement?.id) {
|
||||||
|
openBox(document.activeElement.id)
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const dataStore = await useDataStore()
|
||||||
|
await dataStore.loadData()
|
||||||
|
|
||||||
|
window.addEventListener('keydown', KeyboardAction)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="flex items-center justify-center size-7 absolute top-7 right-7" v-if="isPlaylistTrack">
|
<div class="flex items-center justify-center size-7 absolute top-7 right-7" v-if="isPlaylistTrack">
|
||||||
<div class="suit text-7xl absolute"
|
<div class="suit text-7xl absolute"
|
||||||
:class="[isRedCard ? 'text-red-600' : 'text-slate-800', props.track.card?.suit]">
|
:class="[isRedCard ? 'text-red-600' : 'text-slate-800', props.track.card?.suit]">
|
||||||
{{ props.track.card?.suit }}
|
<img :src="`/${props.track.card?.suit}.svg`" />
|
||||||
</div>
|
</div>
|
||||||
<div class="rank text-white font-bold absolute -mt-1">
|
<div class="rank text-white font-bold absolute -mt-1">
|
||||||
{{ props.track.card?.rank }}
|
{{ props.track.card?.rank }}
|
||||||
@@ -58,9 +58,9 @@ const props = withDefaults(defineProps<{ track: Track; isFaceUp?: boolean }>(),
|
|||||||
isFaceUp: false
|
isFaceUp: false
|
||||||
})
|
})
|
||||||
const playerStore = usePlayerStore()
|
const playerStore = usePlayerStore()
|
||||||
const isManifesto = computed(() => props.track.compilationId.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.compilationId.length === 6)
|
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 coverUrl = props.track.coverId.startsWith('http')
|
const coverUrl = props.track.coverId.startsWith('http')
|
||||||
? props.track.coverId
|
? props.track.coverId
|
||||||
@@ -111,20 +111,11 @@ const coverUrl = props.track.coverId.startsWith('http')
|
|||||||
transform: rotateY(180deg);
|
transform: rotateY(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.♠ {
|
.♠,
|
||||||
@apply -mt-2;
|
.♣,
|
||||||
}
|
.♦,
|
||||||
|
|
||||||
.♣ {
|
|
||||||
@apply text-7xl -mt-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.♦ {
|
|
||||||
@apply -mt-3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.♥ {
|
.♥ {
|
||||||
@apply text-5xl
|
@apply text-5xl size-14;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col-reverse mt-16">
|
|
||||||
<box v-for="compilation in dataStore.getAllCompilations.slice()" :key="compilation.id" :compilation="compilation"
|
|
||||||
: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">
|
|
||||||
{{ !playerStore.isPaused && playerStore.currentTrack?.compilationId === compilation.id ? 'II' : '▶' }}
|
|
||||||
</button>
|
|
||||||
<deck :compilation="compilation" class="box-page" v-if="boxStates[compilation.id] === 'selected'" />
|
|
||||||
</box>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useDataStore } from '~/store/data'
|
|
||||||
import type { BoxState } from '~~/types/types'
|
|
||||||
import { usePlayerStore } from '~/store/player'
|
|
||||||
|
|
||||||
const dataStore = useDataStore()
|
|
||||||
const boxStates = ref<Record<string, BoxState>>({})
|
|
||||||
const playerStore = usePlayerStore()
|
|
||||||
|
|
||||||
function openCompilation(id: string) {
|
|
||||||
if (boxStates.value[id] === 'list') {
|
|
||||||
for (const key in boxStates.value) {
|
|
||||||
boxStates.value[key] = (key === id) ? 'selected' : 'hide'
|
|
||||||
}
|
|
||||||
// Scroll to the top smoothly
|
|
||||||
window.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
|
|
||||||
// navigateTo(`/box/${id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeCompilation(e: KeyboardEvent) {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
for (const key in boxStates.value) {
|
|
||||||
boxStates.value[key] = 'list'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const dataStore = await useDataStore()
|
|
||||||
await dataStore.loadData()
|
|
||||||
|
|
||||||
dataStore.getAllCompilations.forEach(c => {
|
|
||||||
if (!(c.id in boxStates.value)) boxStates.value[c.id] = 'hide'
|
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener('keydown', closeCompilation)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
dataStore.getAllCompilations.forEach(c => {
|
|
||||||
boxStates.value[c.id] = 'list'
|
|
||||||
})
|
|
||||||
}, 333)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -6,21 +6,22 @@
|
|||||||
<button @click="setDisplay('holdem')">holdem</button>
|
<button @click="setDisplay('holdem')">holdem</button>
|
||||||
</div>
|
</div>
|
||||||
<div ref="deck" class="deck flex flex-wrap justify-center gap-4">
|
<div ref="deck" class="deck flex flex-wrap justify-center gap-4">
|
||||||
<card v-for="track in tracks" :key="track.id" :track="track" />
|
<card v-for="(track, i) in tracks" :key="track.id" :track="track" tabindex="i" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
import { useDataStore } from '~/store/data'
|
import { useDataStore } from '~/store/data'
|
||||||
import type { Compilation } from '~~/types/types'
|
import type { Box } from '~~/types/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
compilation: Compilation
|
box: Box
|
||||||
}>()
|
}>()
|
||||||
const dataStore = useDataStore()
|
const dataStore = useDataStore()
|
||||||
const deck = ref()
|
const deck = ref()
|
||||||
const tracks = props.compilation.duration ? dataStore.getTracksByCompilationId(props.compilation.id) : dataStore.getPlaylistTracksByCompilationId(props.compilation.id)
|
const tracks = computed(() => dataStore.getTracksByboxId(props.box.id))
|
||||||
|
|
||||||
function setDisplay(displayMode) {
|
function setDisplay(displayMode) {
|
||||||
deck.value.classList.remove('pile', 'plateau', 'holdem')
|
deck.value.classList.remove('pile', 'plateau', 'holdem')
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fixed left-0 bottom-0 opacity-1 z-50 w-full bg-white transition-all"
|
<div class="fixed left-0 bottom-0 opacity-1 z-50 w-full bg-white transition-all"
|
||||||
:class="{ '-bottom-20 opacity-0': !playerStore.currentTrack }">
|
:class="{ '-bottom-20 opacity-0': !playerStore.currentTrack }">
|
||||||
<audio ref="audioRef" class="w-full" :src="playerStore.currentTrack?.url || ''" controls />
|
<div class="flex items-center gap-3 p-2">
|
||||||
|
<img v-if="playerStore.getCurrentCoverUrl" :src="playerStore.getCurrentCoverUrl as string" alt="Current cover"
|
||||||
|
class="size-16 object-cover object-center rounded" />
|
||||||
|
<audio ref="audioRef" class="flex-1" controls />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -14,7 +18,7 @@ const audioRef = ref<HTMLAudioElement | null>(null)
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (audioRef.value) {
|
if (audioRef.value) {
|
||||||
playerStore.audio = audioRef.value
|
playerStore.attachAudio(audioRef.value)
|
||||||
audioRef.value.addEventListener("timeupdate", playerStore.updateTime)
|
audioRef.value.addEventListener("timeupdate", playerStore.updateTime)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full flex flex-col items-center">
|
<div class="w-full flex flex-col items-center">
|
||||||
|
<div @click="uiStore.closeBox()" class="cursor-pointer">
|
||||||
<logo />
|
<logo />
|
||||||
|
</div>
|
||||||
<main>
|
<main>
|
||||||
<compilations />
|
<boxes />
|
||||||
<player />
|
<player />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useUiStore } from '~/store/ui'
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.logo {
|
.logo {
|
||||||
filter: drop-shadow(2px 2px 0 rgb(0 0 0 / 0.8));
|
filter: drop-shadow(2px 2px 0 rgb(0 0 0 / 0.8));
|
||||||
|
|||||||
@@ -3,22 +3,22 @@
|
|||||||
<div class="bg-page-dark-bg text-white">
|
<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="flex flex-col-reverse bg-gradient-to-r from-primary to-primary-dark">
|
||||||
<div class="mt-8 flex flex-wrap justify-center">
|
<div class="mt-8 flex flex-wrap justify-center">
|
||||||
<!-- <box :compilation="compilation" /> -->
|
<!-- <box :box="box" /> -->
|
||||||
<div class="devtool absolute right-4 text-white bg-black rounded-2xl px-4 py-2">
|
<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.side">side</button>
|
||||||
<button @click="currentPosition = boxPositions.front">front</button>
|
<button @click="currentPosition = boxPositions.front">front</button>
|
||||||
<button @click="currentPosition = boxPositions.back">back</button> -->
|
<button @click="currentPosition = boxPositions.back">back</button> -->
|
||||||
<div class="w-full block">
|
<div class="w-full block">
|
||||||
<input class="w-1/2" type="color" name="color1" id="color1" v-model="compilation.color1">
|
<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="compilation.color2">
|
<input class="w-1/2" type="color" name="color1" id="color1" v-model="box.color2">
|
||||||
<div class="block w-full h-32" :style="{
|
<div class="block w-full h-32" :style="{
|
||||||
background: `linear-gradient(to top, ${compilation.color1}, ${compilation.color2})`
|
background: `linear-gradient(to top, ${box.color1}, ${box.color2})`
|
||||||
}">
|
}">
|
||||||
</div>
|
</div>
|
||||||
<label class="block">
|
<!-- <label class="block">
|
||||||
size: {{ size }}
|
size: {{ size }}
|
||||||
<input v-model.number="size" type="range" step="1" min="1" max="14">
|
<input v-model.number="size" type="range" step="1" min="1" max="14">
|
||||||
</label>
|
</label> -->
|
||||||
</div>
|
</div>
|
||||||
<!-- <div>
|
<!-- <div>
|
||||||
<label class="block">
|
<label class="block">
|
||||||
@@ -43,9 +43,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { BoxPosition, Compilation, Track } from '~~/types/types'
|
import type { Box, Track } from '~~/types/types'
|
||||||
|
|
||||||
const compilation = ref<Compilation>({
|
const box = ref<Box>({
|
||||||
id: 'ES00A',
|
id: 'ES00A',
|
||||||
name: 'zero',
|
name: 'zero',
|
||||||
duration: 2794,
|
duration: 2794,
|
||||||
@@ -53,11 +53,12 @@ const compilation = ref<Compilation>({
|
|||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#48959d',
|
color2: '#48959d',
|
||||||
color3: '#00ff00',
|
color3: '#00ff00',
|
||||||
|
type: 'compilation'
|
||||||
})
|
})
|
||||||
|
|
||||||
const track = ref<Track>({
|
const track = ref<Track>({
|
||||||
id: 1,
|
id: 1,
|
||||||
compilationId: 'ES00A',
|
boxId: 'ES00A',
|
||||||
title: 'The grinding wheel',
|
title: 'The grinding wheel',
|
||||||
artist: {
|
artist: {
|
||||||
id: 0,
|
id: 0,
|
||||||
@@ -68,6 +69,7 @@ const track = ref<Track>({
|
|||||||
start: 0,
|
start: 0,
|
||||||
url: 'https://arakirecords.bandcamp.com/track/the-grinding-wheel',
|
url: 'https://arakirecords.bandcamp.com/track/the-grinding-wheel',
|
||||||
coverId: 'a3236746052',
|
coverId: 'a3236746052',
|
||||||
|
type: 'compilation',
|
||||||
})
|
})
|
||||||
|
|
||||||
//from-slate-800 to-zinc-900
|
//from-slate-800 to-zinc-900
|
||||||
|
|||||||
6
app/plugins/device.client.ts
Normal file
6
app/plugins/device.client.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||||
|
navigator.userAgent
|
||||||
|
)
|
||||||
|
nuxtApp.provide('isMobile', isMobile)
|
||||||
|
})
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { Compilation, Artist, Track } from '~/../types/types'
|
import type { Box, Artist, Track } from '~/../types/types'
|
||||||
|
|
||||||
// stores/data.ts
|
// stores/data.ts
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { useUiStore } from '~/store/ui'
|
||||||
|
|
||||||
export const useDataStore = defineStore('data', {
|
export const useDataStore = defineStore('data', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
compilations: [] as Compilation[], // Store your compilation data here
|
boxes: [] as Box[], // Store your box data here
|
||||||
artists: [] as Artist[], // Store artist data here
|
artists: [] as Artist[], // Store artist data here
|
||||||
tracks: [] as Track[], // Store track data here
|
tracks: [] as Track[], // Store track data here
|
||||||
playlistTracks: [] as Track[], // store playslit tracks
|
|
||||||
isLoaded: false // Remember if data is already loaded
|
isLoaded: false // Remember if data is already loaded
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -16,75 +16,76 @@ export const useDataStore = defineStore('data', {
|
|||||||
async loadData() {
|
async loadData() {
|
||||||
if (this.isLoaded) return
|
if (this.isLoaded) return
|
||||||
|
|
||||||
const { data: compilations } = await useFetch<Compilation[]>('/api/compilations')
|
this.boxes = await $fetch<Box[]>('/api/boxes')
|
||||||
const { data: artists } = await useFetch<Artist[]>('/api/artists')
|
this.artists = await $fetch<Artist[]>('/api/artists')
|
||||||
const { data: rawTracks } = await useFetch<Track[]>('/api/tracks')
|
const compilationTracks = await $fetch<Track[]>('/api/tracks/compilation')
|
||||||
const { data: playlistTracks } = await useFetch<Track[]>('/api/playlists')
|
const playlistTracks = await $fetch<Track[]>('/api/tracks/playlist')
|
||||||
// Stocker les données de base
|
|
||||||
this.compilations = compilations.value ?? []
|
|
||||||
this.artists = artists.value ?? []
|
|
||||||
|
|
||||||
// Mapper les tracks pour remplacer l'artistId par l'objet Artist
|
// Mapper les tracks pour remplacer l'artist avec un objet Artist cohérent
|
||||||
const artistMap = new Map(this.artists.map((a) => [a.id, a]))
|
const artistMap = new Map(this.artists.map((a) => [a.id, a]))
|
||||||
this.tracks = (rawTracks.value ?? []).map((track) => ({
|
const allTracks = [...(compilationTracks ?? []), ...(playlistTracks ?? [])]
|
||||||
...track,
|
|
||||||
artist: artistMap.get(track.artist) ?? {
|
|
||||||
id: track.artist,
|
|
||||||
name: 'Unknown',
|
|
||||||
url: '',
|
|
||||||
coverId: ''
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
this.playlistTracks = (playlistTracks.value ?? []).map((track) => ({
|
this.tracks = allTracks.map((track) => {
|
||||||
...track,
|
const a = track.artist as unknown
|
||||||
artist: artistMap.get(track.artist) ?? {
|
let artistObj: Artist
|
||||||
id: track.artist,
|
if (typeof a === 'number') {
|
||||||
name: track.artist,
|
artistObj = artistMap.get(a) ?? { id: a, name: String(a), url: '', coverId: '' }
|
||||||
url: '',
|
} else if (typeof a === 'string') {
|
||||||
coverId: ''
|
artistObj = { id: 0, name: a, url: '', coverId: '' }
|
||||||
|
} else if (a && typeof a === 'object' && 'id' in (a as any)) {
|
||||||
|
const idVal = (a as any).id as number | undefined
|
||||||
|
artistObj = idVal != null ? artistMap.get(idVal) ?? (a as Artist) : (a as Artist)
|
||||||
|
} else {
|
||||||
|
artistObj = { id: 0, name: '', url: '', coverId: '' }
|
||||||
}
|
}
|
||||||
}))
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
...track,
|
||||||
|
artist: artistObj
|
||||||
|
}
|
||||||
|
})
|
||||||
this.isLoaded = true
|
this.isLoaded = true
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
uiStore.closeBox()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
// Obtenir tous les compilations
|
// Obtenir toutes les boxes
|
||||||
getAllCompilations: (state) => state.compilations,
|
getBoxById: (state) => {
|
||||||
getCompilationById: (state) => {
|
|
||||||
return (id: string) => {
|
return (id: string) => {
|
||||||
return state.compilations.find((compilation) => compilation.id === id)
|
return state.boxes.find((box) => box.id === id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Obtenir toutes les pistes d'une compilation donnée
|
// Obtenir toutes les pistes d'une box donnée
|
||||||
getTracksByCompilationId: (state) => (id: string) => {
|
getTracksByboxId: (state) => (id: string) => {
|
||||||
return state.tracks.filter((track) => track.compilationId === id)
|
return state.tracks.filter((track) => track.boxId === id)
|
||||||
},
|
},
|
||||||
// Filtrer les artistes selon certains critères
|
// Filtrer les artistes selon certains critères
|
||||||
getArtistById: (state) => (id: number) => state.artists.find((artist) => artist.id === id),
|
getArtistById: (state) => (id: number) => state.artists.find((artist) => artist.id === id),
|
||||||
|
|
||||||
// Obtenir toutes les pistes d'un artiste donné
|
// Obtenir toutes les pistes d'un artiste donné
|
||||||
getTracksByArtistId: (state) => (artistId: number) => {
|
getTracksByArtistId: (state) => (artistId: number) => {
|
||||||
return state.tracks.filter((track) => track.artist.id === artistId)
|
return state.tracks.filter(
|
||||||
|
(track) =>
|
||||||
|
typeof track.artist === 'object' &&
|
||||||
|
!!track.artist &&
|
||||||
|
'id' in track.artist &&
|
||||||
|
(track.artist as Artist).id === artistId
|
||||||
|
)
|
||||||
},
|
},
|
||||||
getFirstTrackOfCompilation() {
|
getFirstTrackOfBox() {
|
||||||
return (compilationId: string) => {
|
return (box: Box) => {
|
||||||
const tracks = this.getTracksByCompilationId(compilationId)
|
const tracks = this.getTracksByboxId(box.id)
|
||||||
return tracks.length > 0 ? tracks[0] : null
|
.slice()
|
||||||
}
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||||
},
|
|
||||||
getFirstTrackOfPlaylist() {
|
|
||||||
return (compilationId: string) => {
|
|
||||||
const tracks = this.getPlaylistTracksByCompilationId(compilationId)
|
|
||||||
return tracks.length > 0 ? tracks[0] : null
|
return tracks.length > 0 ? tracks[0] : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getNextPlaylistTrack: (state) => {
|
getNextPlaylistTrack: (state) => {
|
||||||
return (track: Track) => {
|
return (track: Track) => {
|
||||||
const tracksInPlaylist = state.playlistTracks
|
const tracksInPlaylist = state.tracks
|
||||||
.filter((t) => t.compilationId === track.compilationId)
|
.filter((t) => t.boxId === track.boxId)
|
||||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||||
|
|
||||||
const index = tracksInPlaylist.findIndex((t) => t.id === track.id)
|
const index = tracksInPlaylist.findIndex((t) => t.id === track.id)
|
||||||
@@ -95,31 +96,26 @@ export const useDataStore = defineStore('data', {
|
|||||||
},
|
},
|
||||||
getNextTrack: (state) => {
|
getNextTrack: (state) => {
|
||||||
return (track: Track) => {
|
return (track: Track) => {
|
||||||
// Récupérer toutes les tracks de la même compilation et les trier par ordre
|
// Récupérer toutes les tracks de la même box et les trier par ordre
|
||||||
const tracksInCompilation = state.tracks
|
const tracksInBox = state.tracks
|
||||||
.filter((t) => t.compilationId === track.compilationId)
|
.filter((t) => t.boxId === track.boxId)
|
||||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||||
|
|
||||||
// Trouver l’index de la track courante
|
// Trouver l’index de la track courante
|
||||||
const index = tracksInCompilation.findIndex((t) => t.id === track.id)
|
const index = tracksInBox.findIndex((t) => t.id === track.id)
|
||||||
// Retourner la track suivante ou null si c’est la dernière
|
// Retourner la track suivante ou null si c’est la dernière
|
||||||
return index >= 0 && index < tracksInCompilation.length - 1
|
return index >= 0 && index < tracksInBox.length - 1 ? tracksInBox[index + 1] : null
|
||||||
? tracksInCompilation[index + 1]
|
|
||||||
: null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getPrevTrack: (state) => {
|
getPrevTrack: (state) => {
|
||||||
return (track: Track) => {
|
return (track: Track) => {
|
||||||
const tracksInCompilation = state.tracks
|
const tracksInBox = state.tracks
|
||||||
.filter((t) => t.compilationId === track.compilationId)
|
.filter((t) => t.boxId === track.boxId)
|
||||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||||
|
|
||||||
const index = tracksInCompilation.findIndex((t) => t.id === track.id)
|
const index = tracksInBox.findIndex((t) => t.id === track.id)
|
||||||
return index > 0 ? tracksInCompilation[index - 1] : null
|
return index > 0 ? tracksInBox[index - 1] : null
|
||||||
}
|
}
|
||||||
},
|
|
||||||
getPlaylistTracksByCompilationId: (state) => (id: string) => {
|
|
||||||
return state.playlistTracks.filter((track) => track.compilationId === id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// ~/store/player.ts
|
// ~/store/player.ts
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { Track } from '~/../types/types'
|
import type { Track, Box } from '~/../types/types'
|
||||||
import { useDataStore } from '~/store/data'
|
import { useDataStore } from '~/store/data'
|
||||||
|
|
||||||
export const usePlayerStore = defineStore('player', {
|
export const usePlayerStore = defineStore('player', {
|
||||||
@@ -30,22 +30,23 @@ export const usePlayerStore = defineStore('player', {
|
|||||||
const track = this.currentTrack
|
const track = this.currentTrack
|
||||||
if (!track) return
|
if (!track) return
|
||||||
const dataStore = useDataStore()
|
const dataStore = useDataStore()
|
||||||
if (track.compilationId.length === 6) {
|
if (track.type === 'playlist') {
|
||||||
const next = dataStore.getNextPlaylistTrack(track)
|
const next = dataStore.getNextPlaylistTrack(track)
|
||||||
if (next && next.compilationId === track.compilationId) {
|
if (next && next.boxId === track.boxId) {
|
||||||
this.playTrack(next)
|
this.playTrack(next)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('ended')
|
console.log('ended')
|
||||||
|
this.currentTrack = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async playCompilation(compilationId: string) {
|
async playBox(box: Box) {
|
||||||
if (this.currentTrack?.compilationId === compilationId) {
|
if (this.currentTrack?.boxId === box.id) {
|
||||||
this.togglePlay()
|
this.togglePlay()
|
||||||
} else {
|
} else {
|
||||||
const dataStore = useDataStore()
|
const dataStore = useDataStore()
|
||||||
const first = (compilationId.length === 6) ? dataStore.getFirstTrackOfPlaylist(compilationId) : dataStore.getFirstTrackOfCompilation(compilationId)
|
const first = dataStore.getFirstTrackOfBox(box)
|
||||||
if (first) {
|
if (first) {
|
||||||
await this.playTrack(first)
|
await this.playTrack(first)
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,7 @@ export const usePlayerStore = defineStore('player', {
|
|||||||
},
|
},
|
||||||
async playTrack(track: Track) {
|
async playTrack(track: Track) {
|
||||||
// mettre à jour la piste courante uniquement après avoir géré le toggle
|
// mettre à jour la piste courante uniquement après avoir géré le toggle
|
||||||
if (this.currentTrack?.id === track.id) {
|
if (this.currentTrack && this.currentTrack?.id === track.id) {
|
||||||
this.togglePlay()
|
this.togglePlay()
|
||||||
} else {
|
} else {
|
||||||
this.currentTrack = track
|
this.currentTrack = track
|
||||||
@@ -83,10 +84,18 @@ export const usePlayerStore = defineStore('player', {
|
|||||||
const onLoaded = () => resolve()
|
const onLoaded = () => resolve()
|
||||||
audio.addEventListener('loadedmetadata', onLoaded, { once: true })
|
audio.addEventListener('loadedmetadata', onLoaded, { once: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Appliquer le temps de départ
|
// Appliquer le temps de départ
|
||||||
audio.currentTime = wantedStart > 0 ? wantedStart : 0
|
audio.currentTime = wantedStart > 0 ? wantedStart : 0
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const onCanPlay = () => {
|
||||||
|
if (wantedStart > 0 && audio.currentTime < wantedStart - 0.05) {
|
||||||
|
audio.currentTime = wantedStart
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
if (audio.readyState >= 3) return resolve()
|
||||||
|
audio.addEventListener('canplay', onCanPlay, { once: true })
|
||||||
|
})
|
||||||
this.isPaused = false
|
this.isPaused = false
|
||||||
await audio.play()
|
await audio.play()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -133,61 +142,37 @@ export const usePlayerStore = defineStore('player', {
|
|||||||
if (!track) return
|
if (!track) return
|
||||||
const dataStore = useDataStore()
|
const dataStore = useDataStore()
|
||||||
const t = audio.currentTime
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
isCurrentCompilation: (state) => {
|
isCurrentBox: (state) => {
|
||||||
return (compilationId: string) => compilationId === state.currentTrack?.compilationId
|
return (boxId: string) => boxId === state.currentTrack?.boxId
|
||||||
},
|
},
|
||||||
|
|
||||||
isPlaylistTrack: () => {
|
isPlaylistTrack: () => {
|
||||||
return (track: Track) => {
|
return (track: Track) => {
|
||||||
return track.compilationId.length === 6
|
return track.type === 'playlist'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getCurrentTrack: (state) => state.currentTrack,
|
getCurrentTrack: (state) => state.currentTrack,
|
||||||
|
|
||||||
getCurrentCompilation: (state) => {
|
getCurrentBox: (state) => {
|
||||||
return state.currentTrack ? state.currentTrack.url : null
|
return state.currentTrack ? state.currentTrack.url : null
|
||||||
},
|
},
|
||||||
|
|
||||||
currentProgression(state) {
|
getCurrentProgression(state) {
|
||||||
if (!state.audio) return 0
|
if (!state.audio) return 0
|
||||||
const duration = state.audio.duration
|
const duration = state.audio.duration
|
||||||
const progression = (state.position / duration) * 100
|
const progression = (state.position / duration) * 100
|
||||||
return isNaN(progression) ? state.progressionLast : progression
|
return isNaN(progression) ? state.progressionLast : progression
|
||||||
|
},
|
||||||
|
|
||||||
|
getCurrentCoverUrl(state) {
|
||||||
|
const id = state.currentTrack?.coverId
|
||||||
|
if (!id) return null
|
||||||
|
return id.startsWith('http') ? id : `https://f4.bcbits.com/img/${id}_4.jpg`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
46
app/store/ui.ts
Normal file
46
app/store/ui.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { useDataStore } from '~/store/data'
|
||||||
|
import type { Box } from '~/../types/types'
|
||||||
|
|
||||||
|
export const useUiStore = defineStore('ui', {
|
||||||
|
state: () => ({
|
||||||
|
// UI-only state can live here later
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
selectBox(id: string) {
|
||||||
|
const dataStore = useDataStore()
|
||||||
|
dataStore.boxes.forEach((box) => {
|
||||||
|
box.state = box.id === id ? 'box-selected' : 'box-hidden'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
closeBox() {
|
||||||
|
const selectedBox = this.getSelectedBox
|
||||||
|
const dataStore = useDataStore()
|
||||||
|
dataStore.boxes.forEach((box) => {
|
||||||
|
box.state = 'box-list'
|
||||||
|
})
|
||||||
|
// Scroll back to the unselected box in the list
|
||||||
|
if (selectedBox) this.scrollToBox(selectedBox)
|
||||||
|
},
|
||||||
|
|
||||||
|
scrollToBox(box: Box) {
|
||||||
|
if (box) {
|
||||||
|
const boxElement = document.getElementById(box.id)
|
||||||
|
if (boxElement) {
|
||||||
|
setTimeout(() => {
|
||||||
|
boxElement.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}, 333)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
getSelectedBox: () => {
|
||||||
|
const dataStore = useDataStore()
|
||||||
|
return (dataStore.boxes as Box[]).find((box) => box.state === 'box-selected') || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
59
public/♠.svg
Normal file
59
public/♠.svg
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="180mm"
|
||||||
|
height="180mm"
|
||||||
|
viewBox="0 0 180 180"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="spades.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="0.1984375"
|
||||||
|
inkscape:cx="2980.7874"
|
||||||
|
inkscape:cy="798.74016"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1132"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="180"
|
||||||
|
height="180"
|
||||||
|
id="page1"
|
||||||
|
margin="0 0.625 0 0"
|
||||||
|
bleed="0" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="m 83.298209,123.60739 a 0.11377083,0.11377083 0 0 0 -0.17463,-0.12171 c -4.318,3.08239 -8.95085,5.57477 -14.05202,7.07495 -2.81252,0.82815 -6.18596,0.79111 -9.03817,0.69057 -14.65791,-0.52388 -27.23356,-10.09121 -30.90598,-24.43427 -4.73075,-18.473218 3.94494,-34.184176 16.9889,-46.704259 7.05644,-6.773334 14.67644,-12.961938 22.20383,-19.198167 5.41073,-4.484688 10.49338,-9.339794 17.15559,-16.123714 0.87048,-0.88635 2.00289,-1.86796 3.27818,-2.12725 q 3.07711,-0.62971 5.503343,1.79123 c 4.92654,4.91067 9.165158,9.24983 15.062728,14.353651 9.05404,7.836958 16.97302,13.95677 25.05604,21.666729 q 6.31825,6.027208 11.00137,13.350875 c 5.88698,9.207499 8.01688,19.698228 6.13834,30.487945 -2.36273,13.55989 -12.94871,23.97919 -26.33927,26.3816 q -4.953,0.88636 -10.35315,0.48684 c -6.477,-0.4789 -12.38779,-4.10369 -17.692688,-7.54857 q -0.13494,-0.0873 -0.20373,-0.1217 a 0.1031875,0.1031875 0 0 0 -0.14287,0.1217 c 3.780888,12.18142 7.791978,22.91821 14.030848,33.53594 q 0.15346,0.26194 -0.15081,0.26987 -0.56885,0.0132 -1.28323,-0.0873 c -4.2836,-0.61383 -8.636,-1.11389 -12.964578,-1.27529 q -8.702153,-0.32543 -17.399003,0.2249 -1.79917,0.11377 -8.79475,1.15094 -0.27781,0.0397 -0.84667,-0.037 -0.14552,-0.0185 -0.0926,-0.15611 0.254,-0.67469 0.61648,-1.27 3.5216,-5.78908 6.19918,-12.01473 4.29155,-9.98273 7.19932,-20.36762 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:0.264583" />
|
||||||
|
<g
|
||||||
|
id="g5"
|
||||||
|
transform="matrix(0.26458333,0,0,0.26458333,527.61409,-106.44214)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
58
public/♣.svg
Normal file
58
public/♣.svg
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="180mm"
|
||||||
|
height="180mm"
|
||||||
|
viewBox="0 0 180 180"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="clubs.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="0.1984375"
|
||||||
|
inkscape:cx="2980.7874"
|
||||||
|
inkscape:cy="798.74016"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1132"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="180"
|
||||||
|
height="180"
|
||||||
|
id="page1"
|
||||||
|
margin="0 0.625 0 0"
|
||||||
|
bleed="0" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
id="path3"
|
||||||
|
style="fill:#000000;stroke-width:0.264583"
|
||||||
|
d="m 90.000022,16.86819 a 32.819258,32.819258 0 0 0 -32.819176,32.819182 32.819258,32.819258 0 0 0 12.24214,25.567427 c 0.45986,0.67849 0.93396,1.34539 1.41956,1.99937 a 32.819258,32.819258 0 0 0 -16.14785,-4.24729 32.819258,32.819258 0 0 0 -32.81919,32.819191 32.819258,32.819258 0 0 0 32.81919,32.81919 32.819258,32.819258 0 0 0 31.463186,-23.48229 c 0.13863,-0.0901 0.27759,-0.17961 0.41703,-0.26871 a 0.06614583,0.06614583 0 0 1 0.10025,0.0713 c -1.82915,8.07332 -4.03647,16.0141 -6.62233,23.82283 -2.8284,8.54604 -6.180456,16.69282 -10.834476,24.02178 a 0.11641667,0.11641667 0 0 0 0.0713,0.17468 c 0.61736,0.14288 1.10161,0.18153 1.45262,0.11626 4.75192,-0.86077 9.865556,-1.33004 15.340666,-1.40766 8.12006,-0.11377 15.345448,0.0267 23.145368,1.4025 0.34043,0.06 0.79935,0.0246 1.37614,-0.10594 0.0952,-0.0212 0.11812,-0.074 0.0687,-0.15864 -4.21217,-7.23019 -7.6376,-14.81736 -10.27638,-22.76192 -2.416528,-7.27075 -4.641798,-15.08437 -6.675568,-23.43991 -0.11994,-0.49389 -0.17573,-0.9406 -0.16691,-1.341 a 0.12435417,0.12170833 16 0 1 0.1881,-0.10336 c 0.0396,0.0238 0.0789,0.0479 0.11834,0.0718 4.05187,13.52893 16.596578,23.38824 31.444088,23.38824 18.12557,0 32.81967,-14.69362 32.81967,-32.81919 0,-18.125581 -14.6941,-32.819191 -32.81967,-32.819191 -5.84735,0 -11.33713,1.52926 -16.09205,4.20956 0.51661,-0.6831 1.01243,-1.36973 1.48622,-2.06034 A 32.819258,32.819258 0 0 0 122.81917,49.687332 32.819258,32.819258 0 0 0 89.999982,16.86815 Z" />
|
||||||
|
<g
|
||||||
|
id="g5"
|
||||||
|
transform="matrix(0.26458333,0,0,0.26458333,527.61409,-106.44214)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
59
public/♥.svg
Normal file
59
public/♥.svg
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="180mm"
|
||||||
|
height="180mm"
|
||||||
|
viewBox="0 0 180 180"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="hearts.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="0.1984375"
|
||||||
|
inkscape:cx="2980.7874"
|
||||||
|
inkscape:cy="798.74016"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1132"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="180"
|
||||||
|
height="180"
|
||||||
|
id="page1"
|
||||||
|
margin="0 0.625 0 0"
|
||||||
|
bleed="0" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
fill="#ff0000"
|
||||||
|
d="m 90.078668,46.355621 q 1.66952,-1.568979 3.34698,-3.127375 7.781392,-7.228416 17.774712,-10.871729 c 1.62983,-0.595311 3.87879,-0.997478 5.77849,-1.113894 q 5.4954,-0.336021 10.70504,0.502708 c 13.57313,2.185457 24.52159,12.00679 28.21781,25.225373 4.10634,14.681729 -0.11377,29.458708 -9.41916,41.301458 q -4.50586,5.733518 -8.43756,9.477368 c -13.77686,13.10746 -29.337,24.08501 -42.410062,37.95446 -1.97909,2.09814 -4.07988,3.78619 -7.10936,2.96598 -1.91293,-0.51859 -3.4872,-2.17488 -4.89479,-3.67771 -8.852957,-9.45091 -19.282827,-17.72707 -29.262907,-26.16462 -12.95136,-10.94846 -25.90536,-22.314958 -30.46148,-39.441436 -2.35744,-8.863542 -1.70656,-19.187583 2.05581,-27.585458 4.45558,-9.956271 13.47258,-17.118541 24.05856,-19.645312 2.14577,-0.513291 4.91596,-0.928686 7.34748,-1.00277 4.51379,-0.132291 8.46138,0.06085 12.63121,1.664228 q 9.239247,3.550708 16.494117,10.371667 1.68275,1.582208 3.36815,3.167062 0.10848,0.100542 0.21696,0 z"
|
||||||
|
id="path2"
|
||||||
|
style="stroke-width:0.264583" />
|
||||||
|
<g
|
||||||
|
id="g5"
|
||||||
|
transform="matrix(0.26458333,0,0,0.26458333,527.61409,-106.44214)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
59
public/♦.svg
Normal file
59
public/♦.svg
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="180mm"
|
||||||
|
height="180mm"
|
||||||
|
viewBox="0 0 180 180"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="diamonds.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="0.1984375"
|
||||||
|
inkscape:cx="2980.7874"
|
||||||
|
inkscape:cy="798.74016"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1132"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="180"
|
||||||
|
height="180"
|
||||||
|
id="page1"
|
||||||
|
margin="0 0.625 0 0"
|
||||||
|
bleed="0" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
fill="#0000ff"
|
||||||
|
d="m 92.443692,23.012778 c 0.48419,0.17727 0.91546,0.37836 1.29646,0.762 q 1.95527,1.97909 2.92364,3.15648 22.412858,27.278537 46.466128,57.380187 c 1.05304,1.31762 2.37066,2.667 2.75166,4.18835 0.76465,3.03477 -0.59795,4.47411 -2.65641,7.05115 q -20.58988,25.791585 -41.48402,51.339755 -3.653898,4.46881 -7.482418,8.7921 c -2.02406,2.28336 -6.21242,2.42359 -8.28146,0.20902 q -2.45004,-2.61937 -3.86821,-4.34445 -23.88393,-29.09094 -47.3366,-58.531135 -0.11906,-0.15346 -0.27252,-0.43656 c -1.10596,-2.05581 -0.57944,-4.68048 0.7964,-6.45583 q 1.57162,-2.02936 3.17764,-4.01903 c 15.55221,-19.26165 31.14146,-38.502157 46.92385,-57.575967 1.7489,-2.11138 4.52703,-2.44475 7.04586,-1.51607 z"
|
||||||
|
id="path4"
|
||||||
|
style="fill:#ff0000;stroke-width:0.264583" />
|
||||||
|
<g
|
||||||
|
id="g5"
|
||||||
|
transform="matrix(0.26458333,0,0,0.26458333,527.61409,-106.44214)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -1,126 +1,140 @@
|
|||||||
import { eventHandler } from 'h3'
|
import { eventHandler } from 'h3'
|
||||||
|
|
||||||
export default eventHandler(() => {
|
export default eventHandler(() => {
|
||||||
return [
|
const boxes = [
|
||||||
{
|
{
|
||||||
id: 'ES2012',
|
id: 'ES2012',
|
||||||
|
type: 'playlist',
|
||||||
name: '2012',
|
name: '2012',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
description: '🐉💧',
|
description: '🐉<i class="indice">💧</i>',
|
||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#32021F',
|
color2: '#32021F',
|
||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES2013',
|
id: 'ES2013',
|
||||||
|
type: 'playlist',
|
||||||
name: '2013',
|
name: '2013',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
description: '🐍💧',
|
description: '🐍<i class="indice">💧</i>',
|
||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#32021F',
|
color2: '#32021F',
|
||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES2015',
|
id: 'ES2015',
|
||||||
|
type: 'playlist',
|
||||||
name: '2015',
|
name: '2015',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
description: '🐐🌳',
|
description: '🐐<i class="indice">🌳</i>',
|
||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#32021F',
|
color2: '#32021F',
|
||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES2016',
|
id: 'ES2016',
|
||||||
|
type: 'playlist',
|
||||||
name: '2016',
|
name: '2016',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
description: '🐒🔥',
|
description: '🐒<i class="indice">🔥</i>',
|
||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#32021F',
|
color2: '#32021F',
|
||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES2017',
|
id: 'ES2017',
|
||||||
|
type: 'playlist',
|
||||||
name: '2017',
|
name: '2017',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
description: '🐓🔥',
|
description: '🐓<i class="indice">🔥</i>',
|
||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#32021F',
|
color2: '#32021F',
|
||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES2018',
|
id: 'ES2018',
|
||||||
|
type: 'playlist',
|
||||||
name: '2018',
|
name: '2018',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
description: '🐕🌱',
|
description: '🐕<i class="indice">🌱</i>',
|
||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#32021F',
|
color2: '#32021F',
|
||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES2019',
|
id: 'ES2019',
|
||||||
|
type: 'playlist',
|
||||||
name: '2019',
|
name: '2019',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
description: '🐖🌱',
|
description: '🐖<i class="indice">🌱</i>',
|
||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#32021F',
|
color2: '#32021F',
|
||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES2020',
|
id: 'ES2020',
|
||||||
|
type: 'playlist',
|
||||||
name: '2020',
|
name: '2020',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
description: '🐀🪙',
|
description: '🐀<i class="indice">🪙</i>',
|
||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#32021F',
|
color2: '#32021F',
|
||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES2021',
|
id: 'ES2021',
|
||||||
|
type: 'playlist',
|
||||||
name: '2021',
|
name: '2021',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
description: '🐃🪙',
|
description: '🐃<i class="indice">🪙</i>',
|
||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#32021F',
|
color2: '#32021F',
|
||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES2022',
|
id: 'ES2022',
|
||||||
|
type: 'playlist',
|
||||||
name: '2022',
|
name: '2022',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
description: '🐅💧',
|
description: '🐅<i class="indice">💧</i>',
|
||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#32021F',
|
color2: '#32021F',
|
||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES2023',
|
id: 'ES2023',
|
||||||
|
type: 'playlist',
|
||||||
name: '2023',
|
name: '2023',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
description: '🐇💧',
|
description: '🐇<i class="indice">💧</i>',
|
||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#32021F',
|
color2: '#32021F',
|
||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES2024',
|
id: 'ES2024',
|
||||||
|
type: 'playlist',
|
||||||
name: '2024',
|
name: '2024',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
description: '🐉🌳',
|
description: '🐉<i class="indice">🌳</i>',
|
||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#32021F',
|
color2: '#32021F',
|
||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES2025',
|
id: 'ES2025',
|
||||||
|
type: 'playlist',
|
||||||
name: '2025',
|
name: '2025',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
description: '🐍🌳',
|
description: '🐍<i class="indice">🌳</i>',
|
||||||
color1: '#ffffff',
|
color1: '#ffffff',
|
||||||
color2: '#32021F',
|
color2: '#32021F',
|
||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES00A',
|
id: 'ES00A',
|
||||||
|
type: 'compilation',
|
||||||
name: 'manifeste',
|
name: 'manifeste',
|
||||||
duration: 2794,
|
duration: 2794,
|
||||||
description: 'Zero is for manifesto',
|
description: 'Zero is for manifesto',
|
||||||
@@ -130,6 +144,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES00B',
|
id: 'ES00B',
|
||||||
|
type: 'compilation',
|
||||||
name: 'manifeste B',
|
name: 'manifeste B',
|
||||||
duration: 2470,
|
duration: 2470,
|
||||||
description: 'Even Zero has a b-side',
|
description: 'Even Zero has a b-side',
|
||||||
@@ -139,6 +154,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES01A',
|
id: 'ES01A',
|
||||||
|
type: 'compilation',
|
||||||
name: '...',
|
name: '...',
|
||||||
duration: 3487,
|
duration: 3487,
|
||||||
description: '...',
|
description: '...',
|
||||||
@@ -148,6 +164,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ES01B',
|
id: 'ES01B',
|
||||||
|
type: 'compilation',
|
||||||
name: '... B',
|
name: '... B',
|
||||||
duration: 3773,
|
duration: 3773,
|
||||||
description: '...',
|
description: '...',
|
||||||
@@ -156,4 +173,5 @@ export default eventHandler(() => {
|
|||||||
color3: '#00ff00'
|
color3: '#00ff00'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
return boxes.map((b) => ({ ...b, state: 'box-hidden' }))
|
||||||
})
|
})
|
||||||
@@ -4,7 +4,7 @@ export default eventHandler(() => {
|
|||||||
const tracks = [
|
const tracks = [
|
||||||
{
|
{
|
||||||
order: 1,
|
order: 1,
|
||||||
compilationId: 'ES00A',
|
boxId: 'ES00A',
|
||||||
title: 'The grinding wheel',
|
title: 'The grinding wheel',
|
||||||
artist: 0,
|
artist: 0,
|
||||||
start: 0,
|
start: 0,
|
||||||
@@ -13,7 +13,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 2,
|
order: 2,
|
||||||
compilationId: 'ES00A',
|
boxId: 'ES00A',
|
||||||
title: 'Bleach',
|
title: 'Bleach',
|
||||||
artist: 1,
|
artist: 1,
|
||||||
start: 392,
|
start: 392,
|
||||||
@@ -22,7 +22,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 3,
|
order: 3,
|
||||||
compilationId: 'ES00A',
|
boxId: 'ES00A',
|
||||||
title: 'Televised mind',
|
title: 'Televised mind',
|
||||||
artist: 2,
|
artist: 2,
|
||||||
start: 896,
|
start: 896,
|
||||||
@@ -31,7 +31,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 4,
|
order: 4,
|
||||||
compilationId: 'ES00A',
|
boxId: 'ES00A',
|
||||||
title: 'In it',
|
title: 'In it',
|
||||||
artist: 3,
|
artist: 3,
|
||||||
start: 1139,
|
start: 1139,
|
||||||
@@ -40,7 +40,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 5,
|
order: 5,
|
||||||
compilationId: 'ES00A',
|
boxId: 'ES00A',
|
||||||
title: 'Bad michel',
|
title: 'Bad michel',
|
||||||
artist: 4,
|
artist: 4,
|
||||||
start: 1245,
|
start: 1245,
|
||||||
@@ -49,7 +49,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 6,
|
order: 6,
|
||||||
compilationId: 'ES00A',
|
boxId: 'ES00A',
|
||||||
title: 'Overall',
|
title: 'Overall',
|
||||||
artist: 5,
|
artist: 5,
|
||||||
start: 1394,
|
start: 1394,
|
||||||
@@ -58,7 +58,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 7,
|
order: 7,
|
||||||
compilationId: 'ES00A',
|
boxId: 'ES00A',
|
||||||
title: 'Blowup',
|
title: 'Blowup',
|
||||||
artist: 6,
|
artist: 6,
|
||||||
start: 1674,
|
start: 1674,
|
||||||
@@ -67,7 +67,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 8,
|
order: 8,
|
||||||
compilationId: 'ES00A',
|
boxId: 'ES00A',
|
||||||
title: 'Guitar jet',
|
title: 'Guitar jet',
|
||||||
artist: 7,
|
artist: 7,
|
||||||
start: 1880,
|
start: 1880,
|
||||||
@@ -76,7 +76,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 9,
|
order: 9,
|
||||||
compilationId: 'ES00A',
|
boxId: 'ES00A',
|
||||||
title: 'Intercontinental radio waves',
|
title: 'Intercontinental radio waves',
|
||||||
artist: 8,
|
artist: 8,
|
||||||
start: 2024,
|
start: 2024,
|
||||||
@@ -85,7 +85,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 10,
|
order: 10,
|
||||||
compilationId: 'ES00A',
|
boxId: 'ES00A',
|
||||||
title: 'Here comes the sun',
|
title: 'Here comes the sun',
|
||||||
artist: 9,
|
artist: 9,
|
||||||
start: 2211,
|
start: 2211,
|
||||||
@@ -94,7 +94,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 11,
|
order: 11,
|
||||||
compilationId: 'ES00A',
|
boxId: 'ES00A',
|
||||||
title: 'Like in the movies',
|
title: 'Like in the movies',
|
||||||
artist: 10,
|
artist: 10,
|
||||||
start: 2560,
|
start: 2560,
|
||||||
@@ -103,7 +103,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 1,
|
order: 1,
|
||||||
compilationId: 'ES00B',
|
boxId: 'ES00B',
|
||||||
title: "Ce que révèle l'éclipse",
|
title: "Ce que révèle l'éclipse",
|
||||||
artist: 0,
|
artist: 0,
|
||||||
start: 0,
|
start: 0,
|
||||||
@@ -112,7 +112,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 2,
|
order: 2,
|
||||||
compilationId: 'ES00B',
|
boxId: 'ES00B',
|
||||||
title: "Bleedin' Gums Mushrool",
|
title: "Bleedin' Gums Mushrool",
|
||||||
artist: 1,
|
artist: 1,
|
||||||
start: 263,
|
start: 263,
|
||||||
@@ -121,7 +121,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 3,
|
order: 3,
|
||||||
compilationId: 'ES00B',
|
boxId: 'ES00B',
|
||||||
title: 'A lucid dream',
|
title: 'A lucid dream',
|
||||||
artist: 2,
|
artist: 2,
|
||||||
start: 554,
|
start: 554,
|
||||||
@@ -130,7 +130,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 4,
|
order: 4,
|
||||||
compilationId: 'ES00B',
|
boxId: 'ES00B',
|
||||||
title: 'Lights off',
|
title: 'Lights off',
|
||||||
artist: 3,
|
artist: 3,
|
||||||
start: 781,
|
start: 781,
|
||||||
@@ -139,7 +139,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 5,
|
order: 5,
|
||||||
compilationId: 'ES00B',
|
boxId: 'ES00B',
|
||||||
title: "I'm sentimental",
|
title: "I'm sentimental",
|
||||||
artist: 4,
|
artist: 4,
|
||||||
start: 969,
|
start: 969,
|
||||||
@@ -148,7 +148,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 6,
|
order: 6,
|
||||||
compilationId: 'ES00B',
|
boxId: 'ES00B',
|
||||||
title: 'Thrill or trip',
|
title: 'Thrill or trip',
|
||||||
artist: 5,
|
artist: 5,
|
||||||
start: 1128,
|
start: 1128,
|
||||||
@@ -157,7 +157,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 7,
|
order: 7,
|
||||||
compilationId: 'ES00B',
|
boxId: 'ES00B',
|
||||||
title: 'Redhead',
|
title: 'Redhead',
|
||||||
artist: 6,
|
artist: 6,
|
||||||
start: 1303,
|
start: 1303,
|
||||||
@@ -166,7 +166,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 8,
|
order: 8,
|
||||||
compilationId: 'ES00B',
|
boxId: 'ES00B',
|
||||||
title: 'Supersonic twist',
|
title: 'Supersonic twist',
|
||||||
artist: 7,
|
artist: 7,
|
||||||
start: 1584,
|
start: 1584,
|
||||||
@@ -175,7 +175,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 9,
|
order: 9,
|
||||||
compilationId: 'ES00B',
|
boxId: 'ES00B',
|
||||||
title: 'Flowers',
|
title: 'Flowers',
|
||||||
artist: 8,
|
artist: 8,
|
||||||
start: 1749,
|
start: 1749,
|
||||||
@@ -184,7 +184,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 10,
|
order: 10,
|
||||||
compilationId: 'ES00B',
|
boxId: 'ES00B',
|
||||||
title: 'The shade',
|
title: 'The shade',
|
||||||
artist: 9,
|
artist: 9,
|
||||||
start: 1924,
|
start: 1924,
|
||||||
@@ -193,7 +193,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 11,
|
order: 11,
|
||||||
compilationId: 'ES00B',
|
boxId: 'ES00B',
|
||||||
title: 'Like in the movies',
|
title: 'Like in the movies',
|
||||||
artist: 10,
|
artist: 10,
|
||||||
start: 2186,
|
start: 2186,
|
||||||
@@ -202,7 +202,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 1,
|
order: 1,
|
||||||
compilationId: 'ES01A',
|
boxId: 'ES01A',
|
||||||
title: 'He Walked In',
|
title: 'He Walked In',
|
||||||
artist: 11,
|
artist: 11,
|
||||||
start: 0,
|
start: 0,
|
||||||
@@ -211,7 +211,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 2,
|
order: 2,
|
||||||
compilationId: 'ES01A',
|
boxId: 'ES01A',
|
||||||
title: 'The Third Wave',
|
title: 'The Third Wave',
|
||||||
artist: 12,
|
artist: 12,
|
||||||
start: 841,
|
start: 841,
|
||||||
@@ -220,7 +220,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 3,
|
order: 3,
|
||||||
compilationId: 'ES01A',
|
boxId: 'ES01A',
|
||||||
title: 'Broadcaster',
|
title: 'Broadcaster',
|
||||||
artist: 13,
|
artist: 13,
|
||||||
start: 1104.5,
|
start: 1104.5,
|
||||||
@@ -229,7 +229,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 4,
|
order: 4,
|
||||||
compilationId: 'ES01A',
|
boxId: 'ES01A',
|
||||||
title: 'Mourn',
|
title: 'Mourn',
|
||||||
artist: 14,
|
artist: 14,
|
||||||
start: 1441,
|
start: 1441,
|
||||||
@@ -238,7 +238,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 5,
|
order: 5,
|
||||||
compilationId: 'ES01A',
|
boxId: 'ES01A',
|
||||||
title: 'Let it Blow',
|
title: 'Let it Blow',
|
||||||
artist: 15,
|
artist: 15,
|
||||||
start: 1844.8,
|
start: 1844.8,
|
||||||
@@ -247,7 +247,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 6,
|
order: 6,
|
||||||
compilationId: 'ES01A',
|
boxId: 'ES01A',
|
||||||
title: 'Sunday Mourning',
|
title: 'Sunday Mourning',
|
||||||
artist: 16,
|
artist: 16,
|
||||||
start: 2091.7,
|
start: 2091.7,
|
||||||
@@ -256,7 +256,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 7,
|
order: 7,
|
||||||
compilationId: 'ES01A',
|
boxId: 'ES01A',
|
||||||
title: '3030 Instrumental',
|
title: '3030 Instrumental',
|
||||||
artist: 17,
|
artist: 17,
|
||||||
start: 2339.3,
|
start: 2339.3,
|
||||||
@@ -265,7 +265,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 8,
|
order: 8,
|
||||||
compilationId: 'ES01A',
|
boxId: 'ES01A',
|
||||||
title: 'Immortality Break',
|
title: 'Immortality Break',
|
||||||
artist: 18,
|
artist: 18,
|
||||||
start: 2530.5,
|
start: 2530.5,
|
||||||
@@ -274,7 +274,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 9,
|
order: 9,
|
||||||
compilationId: 'ES01A',
|
boxId: 'ES01A',
|
||||||
title: 'Lazy Bones',
|
title: 'Lazy Bones',
|
||||||
artist: 19,
|
artist: 19,
|
||||||
start: 2718,
|
start: 2718,
|
||||||
@@ -283,7 +283,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 10,
|
order: 10,
|
||||||
compilationId: 'ES01A',
|
boxId: 'ES01A',
|
||||||
title: 'On the Train of Aches',
|
title: 'On the Train of Aches',
|
||||||
artist: 20,
|
artist: 20,
|
||||||
start: 2948,
|
start: 2948,
|
||||||
@@ -292,7 +292,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 11,
|
order: 11,
|
||||||
compilationId: 'ES01A',
|
boxId: 'ES01A',
|
||||||
title: 'Me',
|
title: 'Me',
|
||||||
artist: 21,
|
artist: 21,
|
||||||
start: 3265,
|
start: 3265,
|
||||||
@@ -301,7 +301,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 1,
|
order: 1,
|
||||||
compilationId: 'ES01B',
|
boxId: 'ES01B',
|
||||||
title: 'Lady Hawke Blues',
|
title: 'Lady Hawke Blues',
|
||||||
artist: 11,
|
artist: 11,
|
||||||
start: 0,
|
start: 0,
|
||||||
@@ -310,7 +310,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 2,
|
order: 2,
|
||||||
compilationId: 'ES01B',
|
boxId: 'ES01B',
|
||||||
title: 'Dreamscapes',
|
title: 'Dreamscapes',
|
||||||
artist: 12,
|
artist: 12,
|
||||||
start: 235,
|
start: 235,
|
||||||
@@ -319,7 +319,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 3,
|
order: 3,
|
||||||
compilationId: 'ES01B',
|
boxId: 'ES01B',
|
||||||
title: 'Crispy Skin',
|
title: 'Crispy Skin',
|
||||||
artist: 13,
|
artist: 13,
|
||||||
start: 644.2,
|
start: 644.2,
|
||||||
@@ -328,7 +328,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 4,
|
order: 4,
|
||||||
compilationId: 'ES01B',
|
boxId: 'ES01B',
|
||||||
title: 'The Boy Who Stood Above The Earth',
|
title: 'The Boy Who Stood Above The Earth',
|
||||||
artist: 14,
|
artist: 14,
|
||||||
start: 1018,
|
start: 1018,
|
||||||
@@ -337,7 +337,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 5,
|
order: 5,
|
||||||
compilationId: 'ES01B',
|
boxId: 'ES01B',
|
||||||
title: 'Better Off Alone',
|
title: 'Better Off Alone',
|
||||||
artist: 15,
|
artist: 15,
|
||||||
start: 1698,
|
start: 1698,
|
||||||
@@ -346,7 +346,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 6,
|
order: 6,
|
||||||
compilationId: 'ES01B',
|
boxId: 'ES01B',
|
||||||
title: 'Celebration #1',
|
title: 'Celebration #1',
|
||||||
artist: 16,
|
artist: 16,
|
||||||
start: 2235,
|
start: 2235,
|
||||||
@@ -355,7 +355,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 7,
|
order: 7,
|
||||||
compilationId: 'ES01B',
|
boxId: 'ES01B',
|
||||||
title: '3030 Instrumental',
|
title: '3030 Instrumental',
|
||||||
artist: 17,
|
artist: 17,
|
||||||
start: 2458.3,
|
start: 2458.3,
|
||||||
@@ -364,7 +364,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 8,
|
order: 8,
|
||||||
compilationId: 'ES01B',
|
boxId: 'ES01B',
|
||||||
title: 'The Emptiness Of Nothingness',
|
title: 'The Emptiness Of Nothingness',
|
||||||
artist: 18,
|
artist: 18,
|
||||||
start: 2864.5,
|
start: 2864.5,
|
||||||
@@ -373,7 +373,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 9,
|
order: 9,
|
||||||
compilationId: 'ES01B',
|
boxId: 'ES01B',
|
||||||
title: 'Rising',
|
title: 'Rising',
|
||||||
artist: 19,
|
artist: 19,
|
||||||
start: 3145,
|
start: 3145,
|
||||||
@@ -382,7 +382,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 10,
|
order: 10,
|
||||||
compilationId: 'ES01B',
|
boxId: 'ES01B',
|
||||||
title: 'The Last Time',
|
title: 'The Last Time',
|
||||||
artist: 22,
|
artist: 22,
|
||||||
start: 3447,
|
start: 3447,
|
||||||
@@ -392,7 +392,7 @@ export default eventHandler(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 11,
|
order: 11,
|
||||||
compilationId: 'ES01B',
|
boxId: 'ES01B',
|
||||||
title: 'Guajira Con Arpa',
|
title: 'Guajira Con Arpa',
|
||||||
artist: 23,
|
artist: 23,
|
||||||
start: 3586,
|
start: 3586,
|
||||||
@@ -404,6 +404,8 @@ export default eventHandler(() => {
|
|||||||
return tracks.map((track, index) => ({
|
return tracks.map((track, index) => ({
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
...track,
|
...track,
|
||||||
url: `https://files.erudi.fr/evilspins/${track.compilationId}.mp3`
|
url: `https://files.erudi.fr/evilspins/${track.boxId}.mp3`,
|
||||||
|
coverId: `https://f4.bcbits.com/img/${track.coverId}_4.jpg`,
|
||||||
|
type: 'compilation',
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { eventHandler } from 'h3'
|
import { eventHandler } from 'h3'
|
||||||
import { getCardFromDate } from '../../utils/cards'
|
import { getCardFromDate } from '../../../utils/cards'
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
export default eventHandler(async (event) => {
|
||||||
const basePath = path.join(process.cwd(), '/mnt/media/files/music')
|
const basePath = path.join(process.cwd(), '/mnt/media/files/music')
|
||||||
@@ -47,17 +47,21 @@ export default eventHandler(async (event) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: Number(`${year}${index + 1}`),
|
id: Number(`${year}${index + 1}`),
|
||||||
compilationId: `ES${year}`,
|
boxId: `ES${year}`,
|
||||||
date,
|
date,
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
artist: artist.trim(),
|
artist: artist.trim(),
|
||||||
url,
|
url,
|
||||||
coverId,
|
coverId,
|
||||||
card
|
card,
|
||||||
|
order: 0,
|
||||||
|
type: 'playlist',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
tracks.sort((a, b) => b.date.getTime() - a.date.getTime())
|
tracks.sort((a, b) => b.date.getTime() - a.date.getTime())
|
||||||
|
// assign a stable order after sort (1..N)
|
||||||
|
tracks.forEach((t, i) => (t.order = i + 1))
|
||||||
allTracks.push(...tracks)
|
allTracks.push(...tracks)
|
||||||
|
|
||||||
return allTracks
|
return allTracks
|
||||||
80
spade.svg
Normal file
80
spade.svg
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="180mm"
|
||||||
|
height="180mm"
|
||||||
|
viewBox="0 0 180 180"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="spade.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="0.396875"
|
||||||
|
inkscape:cx="797.48031"
|
||||||
|
inkscape:cy="452.28346"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1132"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="180"
|
||||||
|
height="180"
|
||||||
|
id="page1"
|
||||||
|
margin="0 0.625 0 0"
|
||||||
|
bleed="0" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
fill="#ff0000"
|
||||||
|
d="m 90.078671,46.355618 q 1.66952,-1.568979 3.34698,-3.127375 7.781389,-7.228416 17.774709,-10.871729 c 1.62983,-0.595311 3.87879,-0.997478 5.77849,-1.113894 q 5.4954,-0.336021 10.70504,0.502708 c 13.57313,2.185457 24.52159,12.00679 28.21781,25.225373 4.10634,14.681729 -0.11377,29.458708 -9.41916,41.301458 q -4.50586,5.733521 -8.43756,9.477371 c -13.77686,13.10746 -29.337,24.08501 -42.410059,37.95447 -1.97909,2.09814 -4.07988,3.78619 -7.10936,2.96598 -1.91293,-0.51859 -3.487205,-2.17488 -4.894788,-3.67771 C 74.777814,135.54135 64.347939,127.26519 54.367856,118.82764 41.416502,107.87918 28.462502,96.512679 23.906377,79.386201 21.54894,70.522659 22.199815,60.198618 25.96219,51.800743 30.417773,41.844472 39.434773,34.682202 50.020752,32.155431 c 2.145771,-0.513291 4.915958,-0.928686 7.347479,-1.00277 4.513792,-0.132291 8.461375,0.06085 12.631208,1.664228 q 9.23925,3.550708 16.494122,10.371667 1.68275,1.582208 3.36815,3.167062 0.10848,0.100542 0.21696,0 z"
|
||||||
|
id="path2"
|
||||||
|
style="stroke-width:0.264583" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="m 470.83582,48.369648 a 0.11377083,0.11377083 0 0 0 -0.17463,-0.121709 c -4.318,3.082396 -8.95085,5.574771 -14.05202,7.074959 -2.81252,0.828146 -6.18596,0.791104 -9.03817,0.690562 -14.65791,-0.523875 -27.23356,-10.091208 -30.90598,-24.43427 -4.73075,-18.473208 3.94494,-34.1841663 16.9889,-46.704249 7.05644,-6.773334 14.67644,-12.961938 22.20383,-19.198167 5.41073,-4.484688 10.49338,-9.339792 17.15559,-16.123708 0.87048,-0.886354 2.00289,-1.867959 3.27818,-2.12725 q 3.07711,-0.629709 5.50334,1.791229 c 4.92654,4.910666 9.16516,9.249833 15.06273,14.353646 9.05404,7.836958 16.97302,13.95677 25.05604,21.666729 q 6.31825,6.027208 11.00137,13.3508746 c 5.88698,9.2074997 8.01688,19.6982284 6.13834,30.4879364 -2.36273,13.559896 -12.94871,23.979188 -26.33927,26.381604 q -4.953,0.886354 -10.35315,0.486834 c -6.477,-0.478896 -12.38779,-4.103688 -17.69269,-7.548563 q -0.13494,-0.08731 -0.20373,-0.121708 a 0.1031875,0.1031875 0 0 0 -0.14287,0.121708 c 3.78089,12.181417 7.79198,22.918208 14.03085,33.535937 q 0.15346,0.261938 -0.15081,0.269875 -0.56885,0.01323 -1.28323,-0.08731 c -4.2836,-0.613834 -8.636,-1.113896 -12.96458,-1.275292 q -8.70215,-0.325437 -17.399,0.224896 -1.79917,0.113771 -8.79475,1.150937 -0.27781,0.03969 -0.84667,-0.03704 -0.14552,-0.01852 -0.0926,-0.156104 0.254,-0.674688 0.61648,-1.27 3.5216,-5.789084 6.19918,-12.014729 4.29155,-9.98273 7.19932,-20.367625 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:0.264583" />
|
||||||
|
<path
|
||||||
|
id="path3"
|
||||||
|
style="fill:#000000;stroke-width:0.264583"
|
||||||
|
d="m 689.88977,93.581748 a 32.819258,32.819258 0 0 0 -32.81918,32.819182 32.819258,32.819258 0 0 0 12.24214,25.56743 c 0.45986,0.67849 0.93396,1.34539 1.41956,1.99937 a 32.819258,32.819258 0 0 0 -16.14785,-4.24729 32.819258,32.819258 0 0 0 -32.81919,32.81918 32.819258,32.819258 0 0 0 32.81919,32.81919 32.819258,32.819258 0 0 0 31.46319,-23.48229 c 0.13863,-0.0901 0.27759,-0.17961 0.41703,-0.26871 a 0.06614583,0.06614583 0 0 1 0.10025,0.0713 c -1.82915,8.07332 -4.03647,16.0141 -6.62233,23.82283 -2.8284,8.54604 -6.18046,16.69282 -10.83448,24.02178 a 0.11641667,0.11641667 0 0 0 0.0713,0.17468 c 0.61736,0.14288 1.10161,0.18153 1.45262,0.11626 4.75192,-0.86077 9.86556,-1.33004 15.34067,-1.40766 8.12006,-0.11377 15.34544,0.0267 23.14536,1.4025 0.34043,0.06 0.79935,0.0246 1.37614,-0.10594 0.0952,-0.0212 0.11812,-0.074 0.0687,-0.15864 -4.21217,-7.23019 -7.6376,-14.81736 -10.27638,-22.76192 -2.41652,-7.27075 -4.64179,-15.08437 -6.67556,-23.43991 -0.11994,-0.49389 -0.17573,-0.9406 -0.16691,-1.341 a 0.12435417,0.12170833 16 0 1 0.1881,-0.10336 c 0.0396,0.0238 0.0789,0.0479 0.11834,0.0718 4.05187,13.52893 16.59657,23.38824 31.44408,23.38824 18.12557,0 32.81968,-14.69362 32.81968,-32.81919 0,-18.12557 -14.69411,-32.81918 -32.81968,-32.81918 -5.84735,0 -11.33713,1.52926 -16.09205,4.20956 0.51661,-0.6831 1.01243,-1.36973 1.48622,-2.06034 a 32.819258,32.819258 0 0 0 12.12018,-25.46873 32.819258,32.819258 0 0 0 -32.81918,-32.819182 z" />
|
||||||
|
<path
|
||||||
|
fill="#0000ff"
|
||||||
|
d="m 349.32373,119.55231 c 0.48419,0.17727 0.91546,0.37836 1.29646,0.762 q 1.95527,1.97909 2.92364,3.15648 22.41286,27.27854 46.46613,57.38019 c 1.05304,1.31762 2.37066,2.667 2.75166,4.18835 0.76465,3.03477 -0.59795,4.47411 -2.65641,7.05115 q -20.58988,25.79158 -41.48402,51.33975 -3.6539,4.46881 -7.48242,8.7921 c -2.02406,2.28336 -6.21242,2.42359 -8.28146,0.20902 q -2.45004,-2.61937 -3.86821,-4.34445 -23.88393,-29.09094 -47.3366,-58.53113 -0.11906,-0.15346 -0.27252,-0.43656 c -1.10596,-2.05581 -0.57944,-4.68048 0.7964,-6.45583 q 1.57162,-2.02936 3.17764,-4.01903 c 15.55221,-19.26166 31.14146,-38.50216 46.92385,-57.57597 1.7489,-2.11138 4.52703,-2.44475 7.04586,-1.51607 z"
|
||||||
|
id="path4"
|
||||||
|
style="fill:#ff0000;stroke-width:0.264583" />
|
||||||
|
<g
|
||||||
|
id="g5"
|
||||||
|
transform="matrix(0.26458333,0,0,0.26458333,273.92232,-63.930064)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#808080;stroke-width:0.7;stroke-linecap:square;stroke-miterlimit:2.3;paint-order:markers stroke fill"
|
||||||
|
id="rect1"
|
||||||
|
width="179.375"
|
||||||
|
height="180"
|
||||||
|
x="0"
|
||||||
|
y="0" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.4 KiB |
@@ -1,6 +1,7 @@
|
|||||||
// types.ts
|
// types.ts
|
||||||
export interface Compilation {
|
export interface Box {
|
||||||
id: string
|
id: string
|
||||||
|
type: 'playlist' | 'compilation'
|
||||||
name: string
|
name: string
|
||||||
duration: number
|
duration: number
|
||||||
tracks?: Track[]
|
tracks?: Track[]
|
||||||
@@ -8,6 +9,7 @@ export interface Compilation {
|
|||||||
color2: string
|
color2: string
|
||||||
color1: string
|
color1: string
|
||||||
color3: string
|
color3: string
|
||||||
|
state: BoxState
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Artist {
|
export interface Artist {
|
||||||
@@ -19,15 +21,16 @@ export interface Artist {
|
|||||||
export interface Track {
|
export interface Track {
|
||||||
id: number
|
id: number
|
||||||
order?: number
|
order?: number
|
||||||
compilationId: string
|
boxId: string
|
||||||
title: string
|
title: string
|
||||||
artist?: Artist | { id?: Artist }
|
artist?: Artist | number | string
|
||||||
start?: number
|
start?: number
|
||||||
url: string
|
url: string
|
||||||
coverId?: string
|
coverId?: string
|
||||||
date?: Date
|
date?: Date
|
||||||
card?: { suit: CardSuit; value: CardRank }
|
card?: { suit: CardSuit; rank: CardRank }
|
||||||
link?: string
|
link?: string
|
||||||
|
type: 'playlist' | 'compilation'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Playlist {
|
export interface Playlist {
|
||||||
@@ -38,6 +41,6 @@ export interface Playlist {
|
|||||||
filename: string
|
filename: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BoxState = 'hide' | 'list' | 'selected'
|
export type BoxState = 'box-hidden' | 'box-list' | 'box-selected'
|
||||||
export type CardSuit = '♠' | '♣' | '♦' | '♥'
|
export type CardSuit = '♠' | '♣' | '♦' | '♥'
|
||||||
export type CardRank = 'A' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | 'J' | 'Q' | 'K'
|
export type CardRank = 'A' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | 'J' | 'Q' | 'K'
|
||||||
|
|||||||
Reference in New Issue
Block a user