Compare commits
	
		
			2 Commits
		
	
	
		
			43b1a11027
			...
			96ffb4b10a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 96ffb4b10a | ||
|  | fef1a8c234 | 
| @@ -31,6 +31,8 @@ const props = withDefaults( | |||||||
|   { BoxState: 'list' } |   { BoxState: 'list' } | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | const isDraggable = computed(() => !['list', 'hidden'].includes(BoxState.value())) | ||||||
|  |  | ||||||
| // --- Réfs --- | // --- Réfs --- | ||||||
| const scene = ref<HTMLElement>() | const scene = ref<HTMLElement>() | ||||||
| const box = ref<HTMLElement>() | const box = ref<HTMLElement>() | ||||||
| @@ -123,61 +125,81 @@ function tickInertia() { | |||||||
| } | } | ||||||
|  |  | ||||||
| // --- Pointer events --- | // --- Pointer events --- | ||||||
|  | let listenersAttached = false | ||||||
|  |  | ||||||
|  | const down = (ev: PointerEvent) => { | ||||||
|  |   ev.preventDefault() | ||||||
|  |   dragging = true | ||||||
|  |   box.value?.setPointerCapture(ev.pointerId) | ||||||
|  |   lastPointer = { x: ev.clientX, y: ev.clientY, time: performance.now() } | ||||||
|  |   velocity = { x: 0, y: 0 } | ||||||
|  |   if (raf) { cancelAnimationFrame(raf); raf = null } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const move = (ev: PointerEvent) => { | ||||||
|  |   if (!dragging) return | ||||||
|  |   ev.preventDefault() | ||||||
|  |   const now = performance.now() | ||||||
|  |   const dx = ev.clientX - lastPointer.x | ||||||
|  |   const dy = ev.clientY - lastPointer.y | ||||||
|  |   const dt = Math.max(1, now - lastPointer.time) | ||||||
|  |  | ||||||
|  |   rotateY.value += dx * sensitivity | ||||||
|  |   rotateX.value -= dy * sensitivity | ||||||
|  |   rotateX.value = Math.max(-80, Math.min(80, rotateX.value)) | ||||||
|  |  | ||||||
|  |   velocity.x = (dx / dt) * 16 * sensitivity | ||||||
|  |   velocity.y = (-dy / dt) * 16 * sensitivity | ||||||
|  |  | ||||||
|  |   lastPointer = { x: ev.clientX, y: ev.clientY, time: now } | ||||||
|  |   applyTransform(0) // immédiat pendant drag | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const end = (ev: PointerEvent) => { | ||||||
|  |   if (!dragging) return | ||||||
|  |   dragging = false | ||||||
|  |   try { box.value?.releasePointerCapture(ev.pointerId) } catch { } | ||||||
|  |   if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) { | ||||||
|  |     if (!raf) raf = requestAnimationFrame(tickInertia) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function addListeners() { | ||||||
|  |   if (!box.value || listenersAttached) return | ||||||
|  |   box.value.addEventListener('pointerdown', down) | ||||||
|  |   box.value.addEventListener('pointermove', move) | ||||||
|  |   box.value.addEventListener('pointerup', end) | ||||||
|  |   box.value.addEventListener('pointercancel', end) | ||||||
|  |   box.value.addEventListener('pointerleave', end) | ||||||
|  |   listenersAttached = true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function removeListeners() { | ||||||
|  |   if (!box.value || !listenersAttached) return | ||||||
|  |   box.value.removeEventListener('pointerdown', down) | ||||||
|  |   box.value.removeEventListener('pointermove', move) | ||||||
|  |   box.value.removeEventListener('pointerup', end) | ||||||
|  |   box.value.removeEventListener('pointercancel', end) | ||||||
|  |   box.value.removeEventListener('pointerleave', end) | ||||||
|  |   listenersAttached = false | ||||||
|  | } | ||||||
|  |  | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
|   applyColor() |   applyColor() | ||||||
|   applyBoxState() |   applyBoxState() | ||||||
|  |   if (isDraggable) addListeners() | ||||||
|  | }) | ||||||
|  |  | ||||||
|   const down = (ev: PointerEvent) => { | onBeforeUnmount(() => { | ||||||
|     ev.preventDefault() |   cancelAnimationFrame(raf!) | ||||||
|     dragging = true |   removeListeners() | ||||||
|     box.value?.setPointerCapture(ev.pointerId) |  | ||||||
|     lastPointer = { x: ev.clientX, y: ev.clientY, time: performance.now() } |  | ||||||
|     velocity = { x: 0, y: 0 } |  | ||||||
|     if (raf) { cancelAnimationFrame(raf); raf = null } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const move = (ev: PointerEvent) => { |  | ||||||
|     if (!dragging) return |  | ||||||
|     ev.preventDefault() |  | ||||||
|     const now = performance.now() |  | ||||||
|     const dx = ev.clientX - lastPointer.x |  | ||||||
|     const dy = ev.clientY - lastPointer.y |  | ||||||
|     const dt = Math.max(1, now - lastPointer.time) |  | ||||||
|  |  | ||||||
|     rotateY.value += dx * sensitivity |  | ||||||
|     rotateX.value -= dy * sensitivity |  | ||||||
|     rotateX.value = Math.max(-80, Math.min(80, rotateX.value)) |  | ||||||
|  |  | ||||||
|     velocity.x = (dx / dt) * 16 * sensitivity |  | ||||||
|     velocity.y = (-dy / dt) * 16 * sensitivity |  | ||||||
|  |  | ||||||
|     lastPointer = { x: ev.clientX, y: ev.clientY, time: now } |  | ||||||
|     applyTransform(0) // immédiat pendant drag |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const end = (ev: PointerEvent) => { |  | ||||||
|     if (!dragging) return |  | ||||||
|     dragging = false |  | ||||||
|     try { box.value?.releasePointerCapture(ev.pointerId) } catch { } |  | ||||||
|     if (enableInertia && (Math.abs(velocity.x) > minVelocity || Math.abs(velocity.y) > minVelocity)) { |  | ||||||
|       if (!raf) raf = requestAnimationFrame(tickInertia) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   box.value?.addEventListener('pointerdown', down) |  | ||||||
|   box.value?.addEventListener('pointermove', move) |  | ||||||
|   box.value?.addEventListener('pointerup', end) |  | ||||||
|   box.value?.addEventListener('pointercancel', end) |  | ||||||
|   box.value?.addEventListener('pointerleave', end) |  | ||||||
|  |  | ||||||
|   onBeforeUnmount(() => { |  | ||||||
|     cancelAnimationFrame(raf!) |  | ||||||
|   }) |  | ||||||
| }) | }) | ||||||
|  |  | ||||||
| // --- Watchers --- | // --- Watchers --- | ||||||
| watch(() => props.BoxState, () => applyBoxState()) | watch(() => props.BoxState, () => applyBoxState()) | ||||||
| watch(() => props.compilation, () => applyColor(), { deep: true }) | watch(() => props.compilation, () => applyColor(), { deep: true }) | ||||||
|  | watch(() => isDraggable, (enabled) => enabled ? addListeners() : removeListeners()) | ||||||
|  |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @@ -200,7 +222,6 @@ watch(() => props.compilation, () => applyColor(), { deep: true }) | |||||||
|  |  | ||||||
|   &-scene { |   &-scene { | ||||||
|     height: calc(var(--size) * 20); |     height: calc(var(--size) * 20); | ||||||
|     width: var(--width); |  | ||||||
|     perspective: 1000px; |     perspective: 1000px; | ||||||
|     touch-action: none; |     touch-action: none; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,34 +1,36 @@ | |||||||
| <template> | <template> | ||||||
|   <article class="relative"> |   <article class="flip-card w-56 h-80"> | ||||||
|     <main |     <div class="flip-inner"> | ||||||
|       class="backdrop-blur-sm border-2 -mt-12 z-10 card w-56 h-80 p-3 bg-opacity-40 hover:bg-opacity-80 hover:shadow-xl transition-all bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden"> |       <main | ||||||
|       <!-- Cover --> |         class="flip-front backdrop-blur-sm border-2 -mt-12 z-10 card w-56 h-80 p-3 bg-opacity-40 hover:bg-opacity-80 hover:shadow-xl transition-all bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden"> | ||||||
|       <figure @click="playerStore.playTrack(props.track)" class="flex-1 overflow-hidden rounded-t-xl cursor-pointer"> |         <!-- Cover --> | ||||||
|         <img :src="coverUrl" alt="Pochette de l'album" class="w-full h-full object-cover object-center" /> |         <figure @click="playerStore.playTrack(props.track)" class="flex-1 overflow-hidden rounded-t-xl cursor-pointer"> | ||||||
|       </figure> |           <img :src="coverUrl" alt="Pochette de l'album" class="w-full h-full object-cover object-center" /> | ||||||
|  |         </figure> | ||||||
|  |  | ||||||
|       <!-- Body --> |         <!-- Body --> | ||||||
|       <div class="p-3 text-center bg-white rounded-b-xl"> |         <div class="p-3 text-center bg-white rounded-b-xl"> | ||||||
|         <div class="label"> |           <div class="label"> | ||||||
|           {{ props.track.order }} |             {{ props.track.order }} | ||||||
|  |           </div> | ||||||
|  |           <h2 class="text-base text-neutral-800 font-bold truncate">{{ props.track.title }}</h2> | ||||||
|  |           <p class="text-sm text-neutral-500 truncate"> | ||||||
|  |             {{ props.track.artist.name }} | ||||||
|  |           </p> | ||||||
|         </div> |         </div> | ||||||
|         <h2 class="text-base text-neutral-800 font-bold truncate">{{ props.track.title }}</h2> |       </main> | ||||||
|         <p class="text-sm text-neutral-500 truncate"> |  | ||||||
|           {{ props.track.artist.name }} |  | ||||||
|         </p> |  | ||||||
|       </div> |  | ||||||
|     </main> |  | ||||||
|  |  | ||||||
|     <footer |       <footer | ||||||
|       class="ml-32 backdrop-blur-sm -mt-12 z-10 card w-56 h-80 p-3 bg-opacity-10 bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden"> |         class="flip-back backdrop-blur-sm -mt-12 z-10 card w-56 h-80 p-3 bg-opacity-10 bg-white rounded-2xl shadow-lg flex flex-col overflow-hidden"> | ||||||
|       <!-- Back --> |         <!-- Back --> | ||||||
|       <div class="h-full flex p-16 text-center bg-slate-800 rounded-xl"> |         <div class="h-full flex p-16 text-center bg-slate-800 rounded-xl"> | ||||||
|         <img src="/favicon.svg" /> |           <img src="/favicon.svg" /> | ||||||
|       </div> |           <div class="label label--id"> | ||||||
|       <div class="label"> |             {{ props.track.order }} | ||||||
|         {{ props.track.id }} |           </div> | ||||||
|       </div> |         </div> | ||||||
|     </footer> |       </footer> | ||||||
|  |     </div> | ||||||
|   </article> |   </article> | ||||||
|  |  | ||||||
| </template> | </template> | ||||||
| @@ -50,4 +52,38 @@ const coverUrl = props.track.coverId.startsWith('http') | |||||||
|   font-weight: bold; |   font-weight: bold; | ||||||
|   text-align: center; |   text-align: center; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /* Flip effect */ | ||||||
|  | .flip-card { | ||||||
|  |   perspective: 1000px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .flip-inner { | ||||||
|  |   position: relative; | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  |   transition: transform 0.6s; | ||||||
|  |   transform-style: preserve-3d; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .flipped .flip-inner { | ||||||
|  |   transform: rotateY(180deg); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .flip-front, | ||||||
|  | .flip-back { | ||||||
|  |   position: absolute; | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  |   backface-visibility: hidden; | ||||||
|  |   will-change: transform; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .flip-front { | ||||||
|  |   transform: rotateY(0deg); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .flip-back { | ||||||
|  |   transform: rotateY(180deg); | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
| @@ -1,7 +1,14 @@ | |||||||
| <template> | <template> | ||||||
|   <audio ref="audioRef" class="fixed z-50 bottom-0 left-1/2 -translate-x-1/2 w-1/2" |   <div class="fixed left-0 bottom-0 opacity-1 z-50 w-full bg-white transition-all" | ||||||
|     :src="playerStore.currentTrack ? playerStore.getCompilationUrlFromTrack(playerStore.currentTrack) : ''" controls |     :class="{ '-bottom-20 opacity-0': !playerStore.currentTrack }"> | ||||||
|     @timeupdate="updatePosition" @ended="onEnded" /> |     <!-- <p class="hidden"> | ||||||
|  |       {{ Math.round(currentTime) }} | ||||||
|  |       {{ Math.round(currentProgression) }}% | ||||||
|  |     </p> --> | ||||||
|  |     <audio ref="audioRef" class="w-full" | ||||||
|  |       :src="playerStore.currentTrack ? playerStore.getCompilationUrlFromTrack(playerStore.currentTrack) : ''" | ||||||
|  |       controls /> | ||||||
|  |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| @@ -10,29 +17,36 @@ import { usePlayerStore } from '~/store/player' | |||||||
|  |  | ||||||
| const playerStore = usePlayerStore() | const playerStore = usePlayerStore() | ||||||
| const audioRef = ref<HTMLAudioElement | null>(null) | const audioRef = ref<HTMLAudioElement | null>(null) | ||||||
|  | const currentTime = ref(0) | ||||||
|  | const lastValidProgression = ref(0) | ||||||
|  |  | ||||||
| onMounted(() => { | const currentProgression = computed(() => { | ||||||
|   if (audioRef.value) playerStore.audio = audioRef.value |   if (!audioRef.value) return 0 | ||||||
|  |   const progression = (currentTime.value / audioRef.value.duration) * 100 | ||||||
|  |  | ||||||
|  |   if (!isNaN(progression)) { | ||||||
|  |     lastValidProgression.value = progression | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return lastValidProgression.value | ||||||
| }) | }) | ||||||
|  |  | ||||||
| // Mettre à jour la position | function updateTime() { | ||||||
| function updatePosition() { |   if (audioRef.value) { | ||||||
|   if (audioRef.value) playerStore.position = audioRef.value.currentTime |     currentTime.value = audioRef.value.currentTime | ||||||
| } |  | ||||||
|  |  | ||||||
| function onEnded() { |  | ||||||
|   playerStore.isPlaying = false |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Si la track change, mettre à jour le src et le start |  | ||||||
| watch( |  | ||||||
|   () => playerStore.currentTrack, |  | ||||||
|   (newTrack) => { |  | ||||||
|     if (newTrack && audioRef.value) { |  | ||||||
|       audioRef.value.src = newTrack.url |  | ||||||
|       audioRef.value.currentTime = newTrack.start || 0 |  | ||||||
|       if (playerStore.isPlaying) audioRef.value.play() |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| ) | } | ||||||
|  |  | ||||||
|  | onMounted(() => { | ||||||
|  |   if (audioRef.value) { | ||||||
|  |     playerStore.audio = audioRef.value | ||||||
|  |     audioRef.value.addEventListener("timeupdate", updateTime) | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | onUnmounted(() => { | ||||||
|  |   if (audioRef.value) { | ||||||
|  |     audioRef.value.removeEventListener("timeupdate", updateTime) | ||||||
|  |   } | ||||||
|  | }) | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -9,7 +9,6 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useDataStore } from '~/store/data' | import { useDataStore } from '~/store/data' | ||||||
| import type { BoxState } from '~~/types/types' | import type { BoxState } from '~~/types/types' | ||||||
| import { useRouter } from 'vue-router' |  | ||||||
|  |  | ||||||
| const dataStore = useDataStore() | const dataStore = useDataStore() | ||||||
| const boxStates = ref<Record<string, BoxState>>({}) | const boxStates = ref<Record<string, BoxState>>({}) | ||||||
| @@ -30,7 +29,6 @@ function closeCompilation(e: KeyboardEvent) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| onMounted(async () => { | onMounted(async () => { | ||||||
|   const dataStore = await useDataStore() |   const dataStore = await useDataStore() | ||||||
|   await dataStore.loadData() |   await dataStore.loadData() | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="mt-8 p-8 w-96 flex flex-wrap"> |   <div class="mt-8 p-8 w-full flex flex-wrap justify-around"> | ||||||
|     <MoleculeCard v-for="track in dataStore.getTracksByCompilationId(compilation.id)" :key="track.id" :track="track" /> |     <MoleculeCard v-for="track in dataStore.getTracksByCompilationId(compilation.id)" :key="track.id" :track="track" /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|   | |||||||
| @@ -54,5 +54,20 @@ export const useDataStore = defineStore('data', { | |||||||
|     getTracksByArtistId: (state) => (artistId: number) => { |     getTracksByArtistId: (state) => (artistId: number) => { | ||||||
|       return state.tracks.filter(track => track.artist.id === artistId) |       return state.tracks.filter(track => track.artist.id === artistId) | ||||||
|     }, |     }, | ||||||
|  |     getNextTrack: (state) => { | ||||||
|  |       return (track: Track) => { | ||||||
|  |         // Récupérer toutes les tracks de la même compilation et les trier par ordre | ||||||
|  |         const tracksInCompilation = state.tracks | ||||||
|  |           .filter(t => t.compilationId === track.compilationId) | ||||||
|  |           .sort((a, b) => a.order - b.order) | ||||||
|  |  | ||||||
|  |         // Trouver l’index de la track courante | ||||||
|  |         const index = tracksInCompilation.findIndex(t => t.id === track.id) | ||||||
|  |         // Retourner la track suivante ou null si c’est la dernière | ||||||
|  |         return index >= 0 && index < tracksInCompilation.length - 1 | ||||||
|  |           ? tracksInCompilation[index + 1] | ||||||
|  |           : null | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   }, |   }, | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -1,57 +1,113 @@ | |||||||
| // ~/store/player.ts | // ~/store/player.ts | ||||||
| import { defineStore } from 'pinia' | import { defineStore } from 'pinia' | ||||||
| import type { Track } from '~/types/types' | import type { Track } from '~/../types/types' | ||||||
|  | import { useDataStore } from '~/store/data' | ||||||
|  |  | ||||||
| export const usePlayerStore = defineStore('player', { | export const usePlayerStore = defineStore('player', { | ||||||
|   state: () => ({ |   state: () => ({ | ||||||
|     currentTrack: null as Track | null, |     currentTrack: null as Track | null, | ||||||
|     isPlaying: false, |  | ||||||
|     position: 0, |     position: 0, | ||||||
|     audio: null as HTMLAudioElement | null, |     audio: null as HTMLAudioElement | null, | ||||||
|   }), |   }), | ||||||
|  |  | ||||||
|   actions: { |   actions: { | ||||||
|     setTrack(track: Track) { |     async playTrack(track: Track) { | ||||||
|       this.currentTrack = track |       this.currentTrack = track | ||||||
|       if (!this.audio) this.audio = new Audio(this.getCompilationUrlFromTrack(track)) |  | ||||||
|       else this.audio.src = this.getCompilationUrlFromTrack(track) |  | ||||||
|  |  | ||||||
|       // Commencer à start secondes |       // toggle si on reclique sur la même | ||||||
|       this.audio.currentTime = track.start || 0 |       if (this.isPlayingTrack(track)) { | ||||||
|     }, |         this.togglePlay() | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |       if (!this.audio) { | ||||||
|  |         this.audio = new Audio() | ||||||
|  |       } | ||||||
|  |  | ||||||
|     playTrack(track?: Track) { |       // définir la source (fichier de la compilation entière) | ||||||
|       if (track) this.setTrack(track) |       this.audio.src = this.getCompilationUrlFromTrack(track) | ||||||
|       if (!this.currentTrack || !this.audio) return |       this.audio.load() | ||||||
|  |  | ||||||
|       this.audio.play() |       // attendre que le player soit prêt avant de lire | ||||||
|       this.isPlaying = true |       await new Promise<void>((resolve, reject) => { | ||||||
|     }, |         const onCanPlay = () => { | ||||||
|  |           this.audio!.removeEventListener("canplay", onCanPlay) | ||||||
|  |           resolve() | ||||||
|  |         } | ||||||
|  |         const onError = (e: Event) => { | ||||||
|  |           this.audio!.removeEventListener("error", onError) | ||||||
|  |           reject(e) | ||||||
|  |         } | ||||||
|  |         this.audio!.addEventListener("canplay", onCanPlay, { once: true }) | ||||||
|  |         this.audio!.addEventListener("error", onError, { once: true }) | ||||||
|  |       }) | ||||||
|  |  | ||||||
|     pauseTrack() { |       // positionner le début | ||||||
|       if (this.audio) this.audio.pause() |       this.audio.currentTime = track.start ?? 0 | ||||||
|       this.isPlaying = false |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     togglePlay(track?: Track) { |       // lancer la lecture | ||||||
|       if (track && (!this.currentTrack || track.id !== this.currentTrack.id)) { |       try { | ||||||
|         this.playTrack(track) |         await this.audio.play() | ||||||
|       } else { |       } catch (err) { | ||||||
|         this.isPlaying ? this.pauseTrack() : this.playTrack() |         console.error("Impossible de lire la piste :", err) | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     setPosition(time: number) { |     togglePlay() { | ||||||
|       if (this.audio) this.audio.currentTime = time |       if (!this.audio) return | ||||||
|       this.position = time |       if (this.audio.paused) { | ||||||
|  |         this.audio.play().catch(err => console.error(err)) | ||||||
|  |       } else { | ||||||
|  |         this.audio.pause() | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   getters: { |   getters: { | ||||||
|  |     isCurrentCompilation: (state) => { | ||||||
|  |       return (compilationId: string) => | ||||||
|  |         compilationId === state.currentTrack?.compilationId | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     isPlayingTrack: (state) => { | ||||||
|  |       return (track: Track) => { | ||||||
|  |         if (!state.audio || !state.currentTrack) return false | ||||||
|  |  | ||||||
|  |         const currentTime = state.audio.currentTime | ||||||
|  |         if (!currentTime || isNaN(currentTime)) return false | ||||||
|  |  | ||||||
|  |         const from = track.start ?? 0 | ||||||
|  |         const to = state.getTrackStop(track) | ||||||
|  |         if (!to || isNaN(to)) return false | ||||||
|  |  | ||||||
|  |         return currentTime >= from && currentTime < to | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|     getCurrentTrack: (state) => state.currentTrack, |     getCurrentTrack: (state) => state.currentTrack, | ||||||
|     getPlaying: (state) => state.isPlaying, |  | ||||||
|     getCompilationUrlFromTrack: (state) => { |     getCompilationUrlFromTrack: () => { | ||||||
|       return (track: Track) => `https://files.erudi.fr/evilspins/${track.compilationId}.mp3` |       return (track: Track) => | ||||||
|  |         `https://files.erudi.fr/evilspins/${track.compilationId}.mp3` | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     getCurrentCompilation: (state) => { | ||||||
|  |       return state.currentTrack | ||||||
|  |         ? state.getCompilationUrlFromTrack(state.currentTrack) | ||||||
|  |         : null | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     getTrackStop: (state) => { | ||||||
|  |       return (track: Track) => { | ||||||
|  |         if (!state.audio) return 0 | ||||||
|  |  | ||||||
|  |         if (track.order === 0) { | ||||||
|  |           return Math.round(state.audio.duration) | ||||||
|  |         } else { | ||||||
|  |           const dataStore = useDataStore() | ||||||
|  |           const nextTrack = dataStore.getNextTrack(track) | ||||||
|  |           return nextTrack ? nextTrack.start : Math.round(state.audio.duration) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES00A', |       compilationId: 'ES00A', | ||||||
|       title: 'Bleach', |       title: 'Bleach', | ||||||
|       artist: 1, |       artist: 1, | ||||||
|       start: 393, |       start: 392, | ||||||
|       url: 'https://the-kundalini-genie.bandcamp.com/track/bleach-2', |       url: 'https://the-kundalini-genie.bandcamp.com/track/bleach-2', | ||||||
|       coverId: 'a1714786533', |       coverId: 'a1714786533', | ||||||
|     }, |     }, | ||||||
| @@ -28,7 +28,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES00A', |       compilationId: 'ES00A', | ||||||
|       title: 'Televised mind', |       title: 'Televised mind', | ||||||
|       artist: 2, |       artist: 2, | ||||||
|       start: 892, |       start: 896, | ||||||
|       url: 'https://fontainesdc.bandcamp.com/track/televised-mind', |       url: 'https://fontainesdc.bandcamp.com/track/televised-mind', | ||||||
|       coverId: 'a3772806156' |       coverId: 'a3772806156' | ||||||
|     }, |     }, | ||||||
| @@ -38,7 +38,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES00A', |       compilationId: 'ES00A', | ||||||
|       title: 'In it', |       title: 'In it', | ||||||
|       artist: 3, |       artist: 3, | ||||||
|       start: 1138, |       start: 1139, | ||||||
|       url: 'https://howlinbananarecords.bandcamp.com/track/in-it', |       url: 'https://howlinbananarecords.bandcamp.com/track/in-it', | ||||||
|       coverId: 'a1720372066', |       coverId: 'a1720372066', | ||||||
|     }, |     }, | ||||||
| @@ -104,11 +104,11 @@ export default eventHandler(() => { | |||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       id: 10, |       id: 10, | ||||||
|       order: 0, |       order: 11, | ||||||
|       compilationId: 'ES00A', |       compilationId: 'ES00A', | ||||||
|       title: 'Like in the movies', |       title: 'Like in the movies', | ||||||
|       artist: 10, |       artist: 10, | ||||||
|       start: 2559, |       start: 2560, | ||||||
|       url: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies-2', |       url: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies-2', | ||||||
|       coverId: 'a2203158939', |       coverId: 'a2203158939', | ||||||
|     }, |     }, | ||||||
| @@ -214,11 +214,11 @@ export default eventHandler(() => { | |||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       id: 21, |       id: 21, | ||||||
|       order: 0, |       order: 11, | ||||||
|       compilationId: 'ES00B', |       compilationId: 'ES00B', | ||||||
|       title: 'Like in the movies', |       title: 'Like in the movies', | ||||||
|       artist: 10, |       artist: 10, | ||||||
|       start: 2185, |       start: 2186, | ||||||
|       url: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies', |       url: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies', | ||||||
|       coverId: 'a3647322740', |       coverId: 'a3647322740', | ||||||
|     }, |     }, | ||||||
| @@ -238,7 +238,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01A', |       compilationId: 'ES01A', | ||||||
|       title: 'The Third Wave', |       title: 'The Third Wave', | ||||||
|       artist: 12, |       artist: 12, | ||||||
|       start: 854, |       start: 841, | ||||||
|       url: 'https://firefriend.bandcamp.com/track/the-third-wave', |       url: 'https://firefriend.bandcamp.com/track/the-third-wave', | ||||||
|       coverId: 'a2803689859', |       coverId: 'a2803689859', | ||||||
|     }, |     }, | ||||||
| @@ -248,7 +248,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01A', |       compilationId: 'ES01A', | ||||||
|       title: 'Broadcaster', |       title: 'Broadcaster', | ||||||
|       artist: 13, |       artist: 13, | ||||||
|       start: 0, |       start: 1104.5, | ||||||
|       url: 'https://squiduk.bandcamp.com/track/broadcaster', |       url: 'https://squiduk.bandcamp.com/track/broadcaster', | ||||||
|       coverId: 'a3391719769', |       coverId: 'a3391719769', | ||||||
|     }, |     }, | ||||||
| @@ -258,7 +258,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01A', |       compilationId: 'ES01A', | ||||||
|       title: 'Mourn', |       title: 'Mourn', | ||||||
|       artist: 14, |       artist: 14, | ||||||
|       start: 0, |       start: 1441, | ||||||
|       url: 'https://lysistrata.bandcamp.com/track/mourn-2', |       url: 'https://lysistrata.bandcamp.com/track/mourn-2', | ||||||
|       coverId: 'a0872900041', |       coverId: 'a0872900041', | ||||||
|     }, |     }, | ||||||
| @@ -268,7 +268,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01A', |       compilationId: 'ES01A', | ||||||
|       title: 'Let it Blow', |       title: 'Let it Blow', | ||||||
|       artist: 15, |       artist: 15, | ||||||
|       start: 0, |       start: 1844.8, | ||||||
|       url: 'https://pabloxbroadcastingservices.bandcamp.com/track/let-it-blow', |       url: 'https://pabloxbroadcastingservices.bandcamp.com/track/let-it-blow', | ||||||
|       coverId: 'a4000148031', |       coverId: 'a4000148031', | ||||||
|     }, |     }, | ||||||
| @@ -278,7 +278,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01A', |       compilationId: 'ES01A', | ||||||
|       title: 'Sunday Mourning', |       title: 'Sunday Mourning', | ||||||
|       artist: 16, |       artist: 16, | ||||||
|       start: 0, |       start: 2091.7, | ||||||
|       url: 'https://nightbeats.bandcamp.com/track/sunday-mourning', |       url: 'https://nightbeats.bandcamp.com/track/sunday-mourning', | ||||||
|       coverId: 'a0031987121', |       coverId: 'a0031987121', | ||||||
|     }, |     }, | ||||||
| @@ -288,7 +288,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01A', |       compilationId: 'ES01A', | ||||||
|       title: '3030 Instrumental', |       title: '3030 Instrumental', | ||||||
|       artist: 17, |       artist: 17, | ||||||
|       start: 0, |       start: 2339.3, | ||||||
|       url: 'https://delthefunkyhomosapien.bandcamp.com/track/3030', |       url: 'https://delthefunkyhomosapien.bandcamp.com/track/3030', | ||||||
|       coverId: 'a1948146136', |       coverId: 'a1948146136', | ||||||
|     }, |     }, | ||||||
| @@ -298,7 +298,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01A', |       compilationId: 'ES01A', | ||||||
|       title: 'Immortality Break', |       title: 'Immortality Break', | ||||||
|       artist: 18, |       artist: 18, | ||||||
|       start: 0, |       start: 2530.5, | ||||||
|       url: 'https://theaa.bandcamp.com/track/immortality-break', |       url: 'https://theaa.bandcamp.com/track/immortality-break', | ||||||
|       coverId: 'a2749250329', |       coverId: 'a2749250329', | ||||||
|     }, |     }, | ||||||
| @@ -308,7 +308,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01A', |       compilationId: 'ES01A', | ||||||
|       title: 'Lazy Bones', |       title: 'Lazy Bones', | ||||||
|       artist: 19, |       artist: 19, | ||||||
|       start: 0, |       start: 2718, | ||||||
|       url: 'https://woodenshjips.bandcamp.com/track/lazy-bones', |       url: 'https://woodenshjips.bandcamp.com/track/lazy-bones', | ||||||
|       coverId: 'a1884221104', |       coverId: 'a1884221104', | ||||||
|     }, |     }, | ||||||
| @@ -318,17 +318,17 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01A', |       compilationId: 'ES01A', | ||||||
|       title: 'On the Train of Aches', |       title: 'On the Train of Aches', | ||||||
|       artist: 20, |       artist: 20, | ||||||
|       start: 0, |       start: 2948, | ||||||
|       url: 'https://silasjdirge.bandcamp.com/track/on-the-train-of-aches', |       url: 'https://silasjdirge.bandcamp.com/track/on-the-train-of-aches', | ||||||
|       coverId: 'a1124177379', |       coverId: 'a1124177379', | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       id: 32, |       id: 32, | ||||||
|       order: 0, |       order: 11, | ||||||
|       compilationId: 'ES01A', |       compilationId: 'ES01A', | ||||||
|       title: 'Me', |       title: 'Me', | ||||||
|       artist: 21, |       artist: 21, | ||||||
|       start: 0, |       start: 3265, | ||||||
|       url: 'https://secretcolours.bandcamp.com/track/me', |       url: 'https://secretcolours.bandcamp.com/track/me', | ||||||
|       coverId: 'a1497022499', |       coverId: 'a1497022499', | ||||||
|     }, |     }, | ||||||
| @@ -348,7 +348,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01B', |       compilationId: 'ES01B', | ||||||
|       title: 'Dreamscapes', |       title: 'Dreamscapes', | ||||||
|       artist: 12, |       artist: 12, | ||||||
|       start: 0, |       start: 235, | ||||||
|       url: 'https://littlecloudrecords.bandcamp.com/track/dreamscapes', |       url: 'https://littlecloudrecords.bandcamp.com/track/dreamscapes', | ||||||
|       coverId: 'a3498981203', |       coverId: 'a3498981203', | ||||||
|     }, |     }, | ||||||
| @@ -358,7 +358,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01B', |       compilationId: 'ES01B', | ||||||
|       title: 'Crispy Skin', |       title: 'Crispy Skin', | ||||||
|       artist: 13, |       artist: 13, | ||||||
|       start: 0, |       start: 644.2, | ||||||
|       url: 'https://squiduk.bandcamp.com/track/crispy-skin-2', |       url: 'https://squiduk.bandcamp.com/track/crispy-skin-2', | ||||||
|       coverId: 'a2516727021', |       coverId: 'a2516727021', | ||||||
|     }, |     }, | ||||||
| @@ -368,7 +368,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01B', |       compilationId: 'ES01B', | ||||||
|       title: 'The Boy Who Stood Above The Earth', |       title: 'The Boy Who Stood Above The Earth', | ||||||
|       artist: 14, |       artist: 14, | ||||||
|       start: 0, |       start: 1018, | ||||||
|       url: 'https://lysistrata.bandcamp.com/track/the-boy-who-stood-above-the-earth-2', |       url: 'https://lysistrata.bandcamp.com/track/the-boy-who-stood-above-the-earth-2', | ||||||
|       coverId: 'a0350933426', |       coverId: 'a0350933426', | ||||||
|     }, |     }, | ||||||
| @@ -378,7 +378,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01B', |       compilationId: 'ES01B', | ||||||
|       title: 'Better Off Alone', |       title: 'Better Off Alone', | ||||||
|       artist: 15, |       artist: 15, | ||||||
|       start: 0, |       start: 1698, | ||||||
|       url: 'https://pabloxbroadcastingservices.bandcamp.com/track/better-off-alone', |       url: 'https://pabloxbroadcastingservices.bandcamp.com/track/better-off-alone', | ||||||
|       coverId: 'a4000148031', |       coverId: 'a4000148031', | ||||||
|     }, |     }, | ||||||
| @@ -388,7 +388,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01B', |       compilationId: 'ES01B', | ||||||
|       title: 'Celebration #1', |       title: 'Celebration #1', | ||||||
|       artist: 16, |       artist: 16, | ||||||
|       start: 0, |       start: 2235, | ||||||
|       url: 'https://nightbeats.bandcamp.com/track/celebration-1', |       url: 'https://nightbeats.bandcamp.com/track/celebration-1', | ||||||
|       coverId: 'a0031987121', |       coverId: 'a0031987121', | ||||||
|     }, |     }, | ||||||
| @@ -398,7 +398,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01B', |       compilationId: 'ES01B', | ||||||
|       title: '3030 Instrumental', |       title: '3030 Instrumental', | ||||||
|       artist: 17, |       artist: 17, | ||||||
|       start: 0, |       start: 2458.3, | ||||||
|       url: 'https://delthefunkyhomosapien.bandcamp.com/track/3030', |       url: 'https://delthefunkyhomosapien.bandcamp.com/track/3030', | ||||||
|       coverId: 'a1948146136', |       coverId: 'a1948146136', | ||||||
|     }, |     }, | ||||||
| @@ -408,7 +408,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01B', |       compilationId: 'ES01B', | ||||||
|       title: 'The Emptiness Of Nothingness', |       title: 'The Emptiness Of Nothingness', | ||||||
|       artist: 18, |       artist: 18, | ||||||
|       start: 0, |       start: 2864.5, | ||||||
|       url: 'https://theaa.bandcamp.com/track/the-emptiness-of-nothingness', |       url: 'https://theaa.bandcamp.com/track/the-emptiness-of-nothingness', | ||||||
|       coverId: 'a1053923875', |       coverId: 'a1053923875', | ||||||
|     }, |     }, | ||||||
| @@ -418,7 +418,7 @@ export default eventHandler(() => { | |||||||
|       compilationId: 'ES01B', |       compilationId: 'ES01B', | ||||||
|       title: 'Rising', |       title: 'Rising', | ||||||
|       artist: 19, |       artist: 19, | ||||||
|       start: 0, |       start: 3145, | ||||||
|       url: 'https://woodenshjips.bandcamp.com/track/rising', |       url: 'https://woodenshjips.bandcamp.com/track/rising', | ||||||
|       coverId: 'a1884221104', |       coverId: 'a1884221104', | ||||||
|     }, |     }, | ||||||
| @@ -426,19 +426,19 @@ export default eventHandler(() => { | |||||||
|       id: 42, |       id: 42, | ||||||
|       order: 10, |       order: 10, | ||||||
|       compilationId: 'ES01B', |       compilationId: 'ES01B', | ||||||
|       title: 'The Last Time / Jealous Woman', |       title: 'The Last Time', | ||||||
|       artist: 22, |       artist: 22, | ||||||
|       start: 0, |       start: 3447, | ||||||
|       url: 'https://www.discogs.com/release/12110815-Larry-McNeil-And-The-Blue-Knights-Jealous-Woman', |       url: 'https://www.discogs.com/release/12110815-Larry-McNeil-And-The-Blue-Knights-Jealous-Woman', | ||||||
|       coverId: 'https://i.discogs.com/Yr05_neEXwzPwKlDeV7dimmTG34atkAMgpxbMBhHBkI/rs:fit/g:sm/q:90/h:600/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTEyMTEw/ODE1LTE1Mjg1NjU1/NzQtMjcyOC5qcGVn.jpeg', |       coverId: 'https://i.discogs.com/Yr05_neEXwzPwKlDeV7dimmTG34atkAMgpxbMBhHBkI/rs:fit/g:sm/q:90/h:600/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTEyMTEw/ODE1LTE1Mjg1NjU1/NzQtMjcyOC5qcGVn.jpeg', | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       id: 43, |       id: 43, | ||||||
|       order: 0, |       order: 11, | ||||||
|       compilationId: 'ES01B', |       compilationId: 'ES01B', | ||||||
|       title: 'Guajira Con Arpa', |       title: 'Guajira Con Arpa', | ||||||
|       artist: 23, |       artist: 23, | ||||||
|       start: 0, |       start: 3586, | ||||||
|       url: 'https://elpalmasmusic.bandcamp.com/track/guajira-con-arpa', |       url: 'https://elpalmasmusic.bandcamp.com/track/guajira-con-arpa', | ||||||
|       coverId: 'a3463036407', |       coverId: 'a3463036407', | ||||||
|     }, |     }, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user