From 96ffb4b10aed6e545c78d4c1813449c772b3c7c3 Mon Sep 17 00:00:00 2001 From: valere Date: Sat, 4 Oct 2025 00:49:12 +0200 Subject: [PATCH] add working player --- app/components/molecule/box.vue | 114 +++++++++++-------- app/components/molecule/card.vue | 6 +- app/components/molecule/player.vue | 72 ++++++------ app/components/organism/compilationList.vue | 2 - app/components/organism/compilationPage.vue | 5 +- app/pages/index.vue | 2 +- app/store/data.ts | 15 +++ app/store/player.ts | 119 ++++++++++++++------ server/api/tracks.ts | 60 +++++----- 9 files changed, 242 insertions(+), 153 deletions(-) diff --git a/app/components/molecule/box.vue b/app/components/molecule/box.vue index 14f06f6..bcc16ab 100644 --- a/app/components/molecule/box.vue +++ b/app/components/molecule/box.vue @@ -31,6 +31,8 @@ const props = withDefaults( { BoxState: 'list' } ) +const isDraggable = computed(() => !['list', 'hidden'].includes(BoxState.value())) + // --- Réfs --- const scene = ref() const box = ref() @@ -123,61 +125,81 @@ function tickInertia() { } // --- 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(() => { applyColor() applyBoxState() + if (isDraggable) addListeners() +}) - 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) - } - } - - 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!) - }) +onBeforeUnmount(() => { + cancelAnimationFrame(raf!) + removeListeners() }) // --- Watchers --- watch(() => props.BoxState, () => applyBoxState()) watch(() => props.compilation, () => applyColor(), { deep: true }) +watch(() => isDraggable, (enabled) => enabled ? addListeners() : removeListeners()) + diff --git a/app/components/organism/compilationList.vue b/app/components/organism/compilationList.vue index 657207f..266e443 100644 --- a/app/components/organism/compilationList.vue +++ b/app/components/organism/compilationList.vue @@ -9,7 +9,6 @@ \ No newline at end of file + diff --git a/app/pages/index.vue b/app/pages/index.vue index 495ce8f..3cd2ec3 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -18,4 +18,4 @@ .logo { filter: drop-shadow(2px 2px 0 rgb(0 0 0 / 0.8)); } - \ No newline at end of file + diff --git a/app/store/data.ts b/app/store/data.ts index df9a628..35b14b0 100644 --- a/app/store/data.ts +++ b/app/store/data.ts @@ -54,5 +54,20 @@ export const useDataStore = defineStore('data', { getTracksByArtistId: (state) => (artistId: number) => { 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 + } + } }, }) diff --git a/app/store/player.ts b/app/store/player.ts index 10c5574..73a7461 100644 --- a/app/store/player.ts +++ b/app/store/player.ts @@ -1,62 +1,113 @@ // ~/store/player.ts 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', { state: () => ({ currentTrack: null as Track | null, - isPlaying: false, position: 0, audio: null as HTMLAudioElement | null, }), actions: { - setTrack(track: Track) { + async playTrack(track: Track) { this.currentTrack = track - if (!this.audio) this.audio = new Audio(this.getCompilationUrlFromTrack(track)) - else this.audio.src = this.getCompilationUrlFromTrack(track) - // Commencer à start secondes - this.audio.currentTime = track.start || 0 - }, + // toggle si on reclique sur la même + if (this.isPlayingTrack(track)) { + this.togglePlay() + return + } + if (!this.audio) { + this.audio = new Audio() + } - playTrack(track?: Track) { - // load compile if not allready loaded - // play if track is not already played - // else pause + // définir la source (fichier de la compilation entière) + this.audio.src = this.getCompilationUrlFromTrack(track) + this.audio.load() + // attendre que le player soit prêt avant de lire + await new Promise((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 }) + }) - if (track) this.setTrack(track) - if (!this.currentTrack || !this.audio) return + // positionner le début + this.audio.currentTime = track.start ?? 0 - this.audio.play() - this.isPlaying = true - }, - - pauseTrack() { - if (this.audio) this.audio.pause() - this.isPlaying = false - }, - - togglePlay(track?: Track) { - if (track && (!this.currentTrack || track.id !== this.currentTrack.id)) { - this.playTrack(track) - } else { - this.isPlaying ? this.pauseTrack() : this.playTrack() + // lancer la lecture + try { + await this.audio.play() + } catch (err) { + console.error("Impossible de lire la piste :", err) } }, - setPosition(time: number) { - if (this.audio) this.audio.currentTime = time - this.position = time + togglePlay() { + if (!this.audio) return + if (this.audio.paused) { + this.audio.play().catch(err => console.error(err)) + } else { + this.audio.pause() + } }, }, 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, - getPlaying: (state) => state.isPlaying, - getCompilationUrlFromTrack: (state) => { - return (track: Track) => `https://files.erudi.fr/evilspins/${track.compilationId}.mp3` + + getCompilationUrlFromTrack: () => { + 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) + } + } } }, }) diff --git a/server/api/tracks.ts b/server/api/tracks.ts index 206de88..fb31622 100644 --- a/server/api/tracks.ts +++ b/server/api/tracks.ts @@ -18,7 +18,7 @@ export default eventHandler(() => { compilationId: 'ES00A', title: 'Bleach', artist: 1, - start: 393, + start: 392, url: 'https://the-kundalini-genie.bandcamp.com/track/bleach-2', coverId: 'a1714786533', }, @@ -28,7 +28,7 @@ export default eventHandler(() => { compilationId: 'ES00A', title: 'Televised mind', artist: 2, - start: 892, + start: 896, url: 'https://fontainesdc.bandcamp.com/track/televised-mind', coverId: 'a3772806156' }, @@ -38,7 +38,7 @@ export default eventHandler(() => { compilationId: 'ES00A', title: 'In it', artist: 3, - start: 1138, + start: 1139, url: 'https://howlinbananarecords.bandcamp.com/track/in-it', coverId: 'a1720372066', }, @@ -104,11 +104,11 @@ export default eventHandler(() => { }, { id: 10, - order: 0, + order: 11, compilationId: 'ES00A', title: 'Like in the movies', artist: 10, - start: 2559, + start: 2560, url: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies-2', coverId: 'a2203158939', }, @@ -214,11 +214,11 @@ export default eventHandler(() => { }, { id: 21, - order: 0, + order: 11, compilationId: 'ES00B', title: 'Like in the movies', artist: 10, - start: 2185, + start: 2186, url: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies', coverId: 'a3647322740', }, @@ -238,7 +238,7 @@ export default eventHandler(() => { compilationId: 'ES01A', title: 'The Third Wave', artist: 12, - start: 854, + start: 841, url: 'https://firefriend.bandcamp.com/track/the-third-wave', coverId: 'a2803689859', }, @@ -248,7 +248,7 @@ export default eventHandler(() => { compilationId: 'ES01A', title: 'Broadcaster', artist: 13, - start: 0, + start: 1104.5, url: 'https://squiduk.bandcamp.com/track/broadcaster', coverId: 'a3391719769', }, @@ -258,7 +258,7 @@ export default eventHandler(() => { compilationId: 'ES01A', title: 'Mourn', artist: 14, - start: 0, + start: 1441, url: 'https://lysistrata.bandcamp.com/track/mourn-2', coverId: 'a0872900041', }, @@ -268,7 +268,7 @@ export default eventHandler(() => { compilationId: 'ES01A', title: 'Let it Blow', artist: 15, - start: 0, + start: 1844.8, url: 'https://pabloxbroadcastingservices.bandcamp.com/track/let-it-blow', coverId: 'a4000148031', }, @@ -278,7 +278,7 @@ export default eventHandler(() => { compilationId: 'ES01A', title: 'Sunday Mourning', artist: 16, - start: 0, + start: 2091.7, url: 'https://nightbeats.bandcamp.com/track/sunday-mourning', coverId: 'a0031987121', }, @@ -288,7 +288,7 @@ export default eventHandler(() => { compilationId: 'ES01A', title: '3030 Instrumental', artist: 17, - start: 0, + start: 2339.3, url: 'https://delthefunkyhomosapien.bandcamp.com/track/3030', coverId: 'a1948146136', }, @@ -298,7 +298,7 @@ export default eventHandler(() => { compilationId: 'ES01A', title: 'Immortality Break', artist: 18, - start: 0, + start: 2530.5, url: 'https://theaa.bandcamp.com/track/immortality-break', coverId: 'a2749250329', }, @@ -308,7 +308,7 @@ export default eventHandler(() => { compilationId: 'ES01A', title: 'Lazy Bones', artist: 19, - start: 0, + start: 2718, url: 'https://woodenshjips.bandcamp.com/track/lazy-bones', coverId: 'a1884221104', }, @@ -318,17 +318,17 @@ export default eventHandler(() => { compilationId: 'ES01A', title: 'On the Train of Aches', artist: 20, - start: 0, + start: 2948, url: 'https://silasjdirge.bandcamp.com/track/on-the-train-of-aches', coverId: 'a1124177379', }, { id: 32, - order: 0, + order: 11, compilationId: 'ES01A', title: 'Me', artist: 21, - start: 0, + start: 3265, url: 'https://secretcolours.bandcamp.com/track/me', coverId: 'a1497022499', }, @@ -348,7 +348,7 @@ export default eventHandler(() => { compilationId: 'ES01B', title: 'Dreamscapes', artist: 12, - start: 0, + start: 235, url: 'https://littlecloudrecords.bandcamp.com/track/dreamscapes', coverId: 'a3498981203', }, @@ -358,7 +358,7 @@ export default eventHandler(() => { compilationId: 'ES01B', title: 'Crispy Skin', artist: 13, - start: 0, + start: 644.2, url: 'https://squiduk.bandcamp.com/track/crispy-skin-2', coverId: 'a2516727021', }, @@ -368,7 +368,7 @@ export default eventHandler(() => { compilationId: 'ES01B', title: 'The Boy Who Stood Above The Earth', artist: 14, - start: 0, + start: 1018, url: 'https://lysistrata.bandcamp.com/track/the-boy-who-stood-above-the-earth-2', coverId: 'a0350933426', }, @@ -378,7 +378,7 @@ export default eventHandler(() => { compilationId: 'ES01B', title: 'Better Off Alone', artist: 15, - start: 0, + start: 1698, url: 'https://pabloxbroadcastingservices.bandcamp.com/track/better-off-alone', coverId: 'a4000148031', }, @@ -388,7 +388,7 @@ export default eventHandler(() => { compilationId: 'ES01B', title: 'Celebration #1', artist: 16, - start: 0, + start: 2235, url: 'https://nightbeats.bandcamp.com/track/celebration-1', coverId: 'a0031987121', }, @@ -398,7 +398,7 @@ export default eventHandler(() => { compilationId: 'ES01B', title: '3030 Instrumental', artist: 17, - start: 0, + start: 2458.3, url: 'https://delthefunkyhomosapien.bandcamp.com/track/3030', coverId: 'a1948146136', }, @@ -408,7 +408,7 @@ export default eventHandler(() => { compilationId: 'ES01B', title: 'The Emptiness Of Nothingness', artist: 18, - start: 0, + start: 2864.5, url: 'https://theaa.bandcamp.com/track/the-emptiness-of-nothingness', coverId: 'a1053923875', }, @@ -418,7 +418,7 @@ export default eventHandler(() => { compilationId: 'ES01B', title: 'Rising', artist: 19, - start: 0, + start: 3145, url: 'https://woodenshjips.bandcamp.com/track/rising', coverId: 'a1884221104', }, @@ -426,19 +426,19 @@ export default eventHandler(() => { id: 42, order: 10, compilationId: 'ES01B', - title: 'The Last Time / Jealous Woman', + title: 'The Last Time', artist: 22, - start: 0, + start: 3447, 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', }, { id: 43, - order: 0, + order: 11, compilationId: 'ES01B', title: 'Guajira Con Arpa', artist: 23, - start: 0, + start: 3586, url: 'https://elpalmasmusic.bandcamp.com/track/guajira-con-arpa', coverId: 'a3463036407', },