yeah
All checks were successful
Deploy App / build (push) Successful in 10s
Deploy App / deploy (push) Successful in 11s

This commit is contained in:
valere
2026-02-02 21:00:28 +01:00
parent e257e076c4
commit b9a3d0184f
98 changed files with 5068 additions and 3713 deletions

171
appOLDD/store/card.ts Normal file
View File

@@ -0,0 +1,171 @@
import { defineStore } from 'pinia'
import type { Track } from '~~/types'
export const useCardStore = defineStore('card', {
state: () => ({
// Stocke les IDs des cartes déjà révélées
revealedCards: new Set<number>(),
// Stocke les pistes dans le panier
bucket: [] as Track[],
// Stocke l'état d'ouverture du panier
isBucketOpen: false
}),
actions: {
// Mettre à jour l'ordre des pistes dans le panier
updateBucketOrder(newOrder: Track[]) {
this.bucket = [...newOrder]
this.saveBucketToLocalStorage()
},
// Marquer une carte comme révélée
revealCard(trackId: number) {
this.revealedCards.add(trackId)
this.saveToLocalStorage()
},
hideCard(trackId: number) {
this.revealedCards.delete(trackId)
this.saveToLocalStorage()
},
flipCard(track: any) {
if (this.isRevealed(track.id)) {
this.hideCard(track.id)
} else {
this.revealCard(track.id)
}
},
// Basculer l'état de révélation de toutes les cartes
revealAllCards(tracks: Track[]) {
tracks.forEach((track) => {
this.revealCard(track.id)
})
this.saveToLocalStorage()
},
hideAllCards(tracks: Track[]) {
tracks.forEach((track) => {
this.hideCard(track.id)
})
this.saveToLocalStorage()
},
// Sauvegarder l'état dans le localStorage
saveToLocalStorage() {
if (typeof window !== 'undefined') {
try {
localStorage.setItem(
'cardStore',
JSON.stringify({
revealedCards: Array.from(this.revealedCards)
})
)
} catch (e) {
console.error('Failed to save card store to localStorage', e)
}
}
},
// Charger l'état depuis le localStorage
loadFromLocalStorage() {
if (typeof window !== 'undefined') {
try {
const saved = localStorage.getItem('cardStore')
if (saved) {
const { revealedCards } = JSON.parse(saved)
if (Array.isArray(revealedCards)) {
this.revealedCards = new Set(
revealedCards.filter((id): id is number => typeof id === 'number')
)
}
}
} catch (e) {
console.error('Failed to load card store from localStorage', e)
}
}
},
// Initialiser le store
initialize() {
this.loadFromLocalStorage()
},
// Gestion du panier
addToBucket(track: Track) {
// Vérifie si la piste n'est pas déjà dans le panier
if (!this.bucket.some((item) => item.id === track.id)) {
this.bucket.push(track)
this.saveBucketToLocalStorage()
}
},
removeFromBucket(trackId: number) {
const index = this.bucket.findIndex((item) => item.id === trackId)
if (index !== -1) {
this.bucket.splice(index, 1)
this.saveBucketToLocalStorage()
}
},
clearBucket() {
this.bucket = []
this.saveBucketToLocalStorage()
},
toggleBucket() {
this.isBucketOpen = !this.isBucketOpen
},
// Sauvegarder le panier dans le localStorage
saveBucketToLocalStorage() {
if (typeof window !== 'undefined') {
try {
localStorage.setItem('cardStoreBucket', JSON.stringify(this.bucket))
} catch (e) {
console.error('Failed to save bucket to localStorage', e)
}
}
},
// Charger le panier depuis le localStorage
loadBucketFromLocalStorage() {
if (typeof window !== 'undefined') {
try {
const saved = localStorage.getItem('cardStoreBucket')
if (saved) {
const bucket = JSON.parse(saved)
if (Array.isArray(bucket)) {
this.bucket = bucket
}
}
} catch (e) {
console.error('Failed to load bucket from localStorage', e)
}
}
},
// Vérifier si une carte est révélée
isCardRevealed(trackId: number): boolean {
return this.revealedCards.has(trackId)
}
},
getters: {
// Getter pour la réactivité dans les templates
isRevealed: (state) => (trackId: number) => {
return state.revealedCards.has(trackId)
},
// Getters pour le panier
bucketCount: (state) => state.bucket.length,
isInBucket: (state) => (trackId: number) => {
return state.bucket.some((track) => track.id === trackId)
},
bucketTotalDuration: (state) => {
return state.bucket.reduce((total, track) => total + ((track as any).duration || 0), 0)
}
}
})

188
appOLDD/store/data.ts Normal file
View File

@@ -0,0 +1,188 @@
import type { Box, Artist, Track } from '~/../types/types'
import { defineStore } from 'pinia'
export const useDataStore = defineStore('data', {
state: () => ({
boxes: [] as Box[], // Store your box data here
artists: [] as Artist[], // Store artist data here
tracks: [] as Track[], // Store track data here
isLoaded: false, // Remember if data is already loaded
isLoading: true
}),
actions: {
async loadData() {
if (this.isLoaded) return
this.isLoading = true
try {
this.boxes = await $fetch<Box[]>('/api/boxes')
this.artists = await $fetch<Artist[]>('/api/artists')
const compilationTracks = await $fetch<Track[]>('/api/tracks/compilation')
const playlistTracks = await $fetch<Track[]>('/api/tracks/playlist')
// 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 allTracks = [
...(Array.isArray(compilationTracks) ? compilationTracks : []),
...(Array.isArray(playlistTracks) ? playlistTracks : [])
]
this.tracks = allTracks.map((track) => {
const a = track.artist as unknown
let artistObj: Artist
if (typeof a === 'number') {
artistObj = artistMap.get(a) ?? { id: a, name: String(a), url: '', coverId: '' }
} else if (typeof a === 'string') {
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
} finally {
this.isLoading = false
}
},
setActiveSideByBoxId(boxId: string, side: 'A' | 'B') {
const box = this.boxes.find((box) => box.id === boxId.replace(/[AB]$/, ''))
if (box) {
box.activeSide = side
}
},
getRandomPlaylistTrack() {
if (this.tracks.length === 0) return null
const randomIndex = Math.floor(Math.random() * this.tracks.length)
return this.tracks[randomIndex]
}
},
getters: {
// Obtenir toutes les boxes
getBoxById: (state) => {
return (id: string) => {
return state.boxes.find((box) => box.id === id)
}
},
getTrackById: (state) => {
return (id: string) => {
return state.tracks.find((track) => track.id === id)
}
},
getTracksByboxId: (state) => (id: string, side?: 'A' | 'B') => {
const box = state.boxes.find((box) => box.id === id)
if (box?.type !== 'compilation' || !side) {
return state.tracks.filter((track) => track.boxId === id)
}
return state.tracks.filter((track) => track.boxId === id && track.side === side)
},
getActiveSideByBoxId: (state) => (id: string) => {
const box = state.boxes.find((box) => box.id === id)
return box?.activeSide
},
// Filtrer les artistes selon certains critères
getArtistById: (state) => (id: number) => state.artists.find((artist) => artist.id === id),
// Obtenir toutes les pistes d'un artiste donné
getTracksByArtistId: (state) => (artistId: number) => {
return state.tracks.filter(
(track) =>
typeof track.artist === 'object' &&
!!track.artist &&
'id' in track.artist &&
(track.artist as Artist).id === artistId
)
},
getFirstTrackOfBox() {
return (box: Box) => {
const tracks = this.getTracksByboxId(box.id, box.activeSide)
.slice()
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
return tracks.length > 0 ? tracks[0] : null
}
},
getNextPlaylistTrack: (state) => {
return (track: Track) => {
const tracksInPlaylist = state.tracks
.filter((t) => t.boxId === track.boxId)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
const index = tracksInPlaylist.findIndex((t) => t.id === track.id)
return index >= 0 && index < tracksInPlaylist.length - 1
? tracksInPlaylist[index + 1]
: null
}
},
getNextTrack: (state) => {
return (track: Track) => {
// Récupérer toutes les tracks de la même box et les trier par ordre
const tracksInBox = state.tracks
.filter((t) => t.boxId === track.boxId && t.side === track.side)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
// Trouver lindex de la track courante
const index = tracksInBox.findIndex((t) => t.id === track.id)
// Retourner la track suivante ou null si cest la dernière
return index >= 0 && index < tracksInBox.length - 1 ? tracksInBox[index + 1] : null
}
},
getPrevTrack: (state) => {
return (track: Track) => {
const tracksInBox = state.tracks
.filter((t) => t.boxId === track.boxId && t.side === track.side)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
const index = tracksInBox.findIndex((t) => t.id === track.id)
return index > 0 ? tracksInBox[index - 1] : null
}
},
getYearColor: () => (year: number) => {
// Palette élargie avec des différences plus marquées
const colorMap: Record<number, string> = {
// Années récentes - teintes froides et claires
2025: '#3a4a6c', // bleu-gris clair
2024: '#1e3a7a', // bleu vif
2023: '#1a4d5c', // bleu-vert émeraude
2022: '#3a4a6a', // bleu-gris moyen
// Années 2020-2021 - transition
2021: '#3a2e6a', // bleu-violet
2020: '#2a467a', // bleu-gris chaud
// Années 2010-2019 - teintes moyennes
2019: '#2a2a7a', // bleu nuit profond
2018: '#1e2a8a', // bleu roi
2017: '#1a5a6a', // bleu canard vif
2016: '#1a5a4a', // vert bleuté
2015: '#1a3a7a', // bleu marine
2014: '#4a1e7a', // violet profond
2013: '#1a5a4a', // vert émeraude
2012: '#1e3a9a', // bleu ciel profond
// Années 2000-2011 - teintes chaudes et foncées
2011: '#1e293b', // slate-800 de base
2010: '#2a467a', // bleu-gris chaud
2009: '#3a4a6a', // bleu-gris moyen
2008: '#1a3a8a', // bleu nuit clair
2007: '#5a2a4a', // bordeaux
2006: '#5a1e6a', // violet profond
2005: '#3a1a7a', // bleu-violet foncé
2004: '#2a1a5a', // bleu nuit profond
2003: '#3a3a5a', // bleu-gris foncé
2002: '#1a5a4a', // vert foncé
2001: '#5a3a2a', // marron chaud
2000: '#3a3a5a' // bleu-gris foncé
}
return colorMap[year] || '#1e293b' // slate-800 par défaut
}
}
})

169
appOLDD/store/platine.ts Normal file
View File

@@ -0,0 +1,169 @@
import { defineStore } from 'pinia'
import type { Track } from '~~/types'
import Disc from '~/platine-tools/disc'
import Sampler from '~/platine-tools/sampler'
import { useCardStore } from '~/store/card'
export const usePlatineStore = defineStore('platine', () => {
// State
const currentTrack = ref<Track | null>(null)
const isPlaying = ref(false)
const isLoadingTrack = ref(false)
const isFirstDrag = ref(true)
const progressPercentage = ref(0)
const currentTurns = ref(0)
const totalTurns = ref(0)
const isMuted = ref(false)
// Refs pour les instances
const disc = ref<Disc | null>(null)
const sampler = ref<Sampler | null>(null)
const discRef = ref<HTMLElement>()
// Actions
const initPlatine = (element: HTMLElement) => {
discRef.value = element
disc.value = new Disc(element)
sampler.value = new Sampler()
// Configurer les callbacks du disque
if (disc.value) {
disc.value.callbacks.onStop = () => {
sampler.value?.pause()
}
disc.value.callbacks.onDragStart = () => {
if (isFirstDrag.value) {
isFirstDrag.value = false
togglePlay()
if (sampler.value && disc.value) {
sampler.value.play(disc.value.secondsPlayed)
disc.value.powerOn()
}
}
}
disc.value.callbacks.onDragEnded = () => {
if (!isPlaying.value) return
sampler.value?.play(disc.value?.secondsPlayed || 0)
}
disc.value.callbacks.onLoop = ({ playbackSpeed, isReversed, secondsPlayed }) => {
sampler.value?.updateSpeed(playbackSpeed, isReversed, secondsPlayed)
updateTurns()
}
}
}
const updateTurns = () => {
if (!disc.value) return
currentTurns.value = disc.value.secondsPlayed * 0.75
totalTurns.value = (disc.value as any)._duration * 0.75
progressPercentage.value = Math.min(
100,
(disc.value.secondsPlayed / (disc.value as any)._duration) * 100
)
}
const loadTrack = async (track: Track) => {
const cardStore = useCardStore()
if (!sampler.value || !track) return
currentTrack.value = track
isLoadingTrack.value = true
try {
await sampler.value.loadTrack(track.filePath)
if (disc.value) {
disc.value.setDuration(sampler.value.duration)
updateTurns()
play()
}
} finally {
isLoadingTrack.value = false
cardStore.revealCard(track.id)
}
}
const play = (position = 0) => {
if (!disc.value || !sampler.value || !currentTrack.value) return
isPlaying.value = true
sampler.value.play(position)
disc.value.powerOn()
}
const pause = () => {
if (!disc.value || !sampler.value) return
isPlaying.value = false
sampler.value.pause()
disc.value.powerOff()
}
const togglePlay = () => {
if (isPlaying.value) {
pause()
} else {
play()
}
}
const toggleMute = () => {
if (!sampler.value) return
isMuted.value = !isMuted.value
if (isMuted.value) {
sampler.value.mute()
} else {
sampler.value.unmute()
}
}
const seek = (position: number) => {
if (!disc.value) return
disc.value.secondsPlayed = position
if (sampler.value) {
sampler.value.play(position)
}
}
// Nettoyage
const cleanup = () => {
if (disc.value) {
disc.value.stop()
disc.value.powerOff()
}
if (sampler.value) {
sampler.value.pause()
}
}
return {
// State
currentTrack,
isPlaying,
isLoadingTrack,
progressPercentage,
currentTurns,
totalTurns,
isMuted,
// Getters
coverUrl: computed(() => currentTrack.value?.coverId || '/card-dock.svg'),
// Actions
initPlatine,
loadTrack,
play,
pause,
togglePlay,
toggleMute,
seek,
cleanup
}
})
export default usePlatineStore

279
appOLDD/store/player.ts Normal file
View File

@@ -0,0 +1,279 @@
// ~/store/player.ts
import { defineStore } from 'pinia'
import type { Track, Box } from '~/../types/types'
import { useDataStore } from '~/store/data'
import { useCardStore } from '~/store/card'
import { usePlatineStore } from '~/store/platine'
export const usePlayerStore = defineStore('player', {
state: () => ({
currentTrack: null as Track | null,
position: 0,
progressionLast: 0,
isLoading: false,
history: [] as string[]
}),
actions: {
attachAudio() {
const platineStore = usePlatineStore()
// Écouter les changements de piste dans le platineStore
watch(
() => platineStore.currentTrack,
(newTrack) => {
if (newTrack) {
this.currentTrack = newTrack
// Révéler la carte quand la lecture commence
const cardStore = useCardStore()
if (!cardStore.isCardRevealed(newTrack.id)) {
requestAnimationFrame(() => {
cardStore.revealCard(newTrack.id)
})
}
} else {
this.currentTrack = null
}
}
)
// Écouter les changements d'état de lecture
watch(
() => platineStore.isPlaying,
(isPlaying) => {
if (isPlaying) {
// Gérer la logique de lecture suivante quand la lecture se termine
if (platineStore.currentTrack?.type === 'playlist') {
const dataStore = useDataStore()
const nextTrack = dataStore.getNextPlaylistTrack(platineStore.currentTrack)
if (nextTrack) {
platineStore.loadTrack(nextTrack)
platineStore.play()
}
}
}
}
)
},
async playBox(box: Box) {
const platineStore = usePlatineStore()
// Si c'est la même box, on toggle simplement la lecture
if (this.currentTrack?.boxId === box.id && this.currentTrack?.side === box.activeSide) {
platineStore.togglePlay()
return
}
// Sinon, on charge la première piste de la box
try {
const dataStore = useDataStore()
const firstTrack = dataStore.getFirstTrackOfBox(box)
if (firstTrack) {
this.currentTrack = firstTrack
await platineStore.loadTrack(firstTrack)
await platineStore.play()
}
} catch (error) {
console.error('Error playing box:', error)
}
},
async playTrack(track: Track) {
const platineStore = usePlatineStore()
// Si c'est la même piste, on toggle simplement la lecture
if (this.currentTrack?.id === track.id) {
platineStore.togglePlay()
return
}
// Sinon, on charge et on lit la piste
this.currentTrack = track
await platineStore.loadTrack(track)
platineStore.play()
},
async playCompilationTrack(track: Track) {
const platineStore = usePlatineStore()
// Si c'est la même piste, on toggle simplement la lecture
if (this.currentTrack?.id === track.id) {
platineStore.togglePlay()
return
}
// Pour les compilations, on charge la piste avec le point de départ
this.currentTrack = track
await platineStore.loadTrack(track)
// Si c'est une compilation, on définit la position de départ
if (track.type === 'compilation' && track.start !== undefined) {
platineStore.seek(track.start)
}
platineStore.play()
},
async playPlaylistTrack(track: Track) {
const platineStore = usePlatineStore()
// Toggle simple si c'est la même piste
if (this.currentTrack?.id === track.id) {
platineStore.togglePlay()
return
}
// Sinon, on charge et on lit la piste
this.currentTrack = track
await platineStore.loadTrack(track)
platineStore.play()
},
async loadTrack(track: Track) {
const platineStore = usePlatineStore()
await platineStore.loadTrack(track)
},
async loadAndPlayTrack(track: Track) {
const platineStore = usePlatineStore()
try {
this.isLoading = true
// Charger la piste
await platineStore.loadTrack(track)
// Pour les compilations, on définit la position de départ
if (track.type === 'compilation' && track.start !== undefined) {
platineStore.seek(track.start)
}
// Lancer la lecture
await platineStore.play()
this.history.push(track.id.toString()) // S'assurer que l'ID est une chaîne
this.isLoading = false
} catch (error) {
console.error('Error loading/playing track:', error)
this.isLoading = false
}
},
togglePlay() {
const platineStore = usePlatineStore()
platineStore.togglePlay()
},
updateTime() {
const platineStore = usePlatineStore()
// Mettre à jour la position actuelle
if (platineStore.currentTrack) {
this.position = platineStore.currentTurns / 0.75 // Convertir les tours en secondes
// Calculer et mettre en cache la progression
const duration = platineStore.totalTurns / 0.75 // Durée totale en secondes
const progression = (this.position / duration) * 100
if (!isNaN(progression) && isFinite(progression)) {
this.progressionLast = progression
}
}
},
// update current track when changing time in compilation
async updateCurrentTrack() {
const platineStore = usePlatineStore()
const currentTrack = this.currentTrack
if (currentTrack && currentTrack.type === 'compilation') {
const dataStore = useDataStore()
const tracks = dataStore
.getTracksByboxId(currentTrack.boxId, currentTrack.side)
.slice()
.filter((t) => t.type === 'compilation')
.sort((a, b) => (a.start ?? 0) - (b.start ?? 0))
if (tracks.length > 0) {
const now = platineStore.currentTurns / 0.75
// find the last track whose start <= now (fallback to first track)
let nextTrack = tracks[0]
for (const t of tracks) {
const s = t.start ?? 0
if (s <= now) {
nextTrack = t
} else {
break
}
}
if (nextTrack && nextTrack.id !== currentTrack.id) {
// only update metadata reference; do not reload audio
this.currentTrack = nextTrack
// Révéler la carte avec une animation fluide
const cardStore = useCardStore()
if (nextTrack.id && !cardStore.isCardRevealed(nextTrack.id)) {
// Utiliser requestAnimationFrame pour une meilleure synchronisation avec le rendu
requestAnimationFrame(() => {
cardStore.revealCard(nextTrack.id)
})
}
}
}
}
}
},
getters: {
isCurrentBox: (state) => {
return (box: Box) => {
if (box.type === 'compilation') {
return box.id === state.currentTrack?.boxId && box.activeSide === state.currentTrack?.side
} else {
return box.id === state.currentTrack?.boxId
}
}
},
isCurrentSide: (state) => {
return (side: string) => side === state.currentTrack?.side
},
isPlaylistTrack: () => {
return (track: Track) => {
return track.type === 'playlist'
}
},
isCompilationTrack: () => {
return (track: Track) => {
return track.type === 'compilation'
}
},
isPaused() {
const platineStore = usePlatineStore()
return !platineStore.isPlaying
},
getCurrentTrack: (state) => state.currentTrack,
getCurrentBox: (state) => {
return state.currentTrack ? state.currentTrack.boxId : null
},
getCurrentProgression() {
const platineStore = usePlatineStore()
if (!platineStore.currentTrack) return 0
// Calculer la progression en fonction des tours actuels et totaux
if (platineStore.totalTurns > 0) {
return (platineStore.currentTurns / platineStore.totalTurns) * 100
}
return 0
},
getCurrentCoverUrl(state) {
const id = state.currentTrack?.coverId
if (!id) return null
return id.startsWith('http') ? id : `https://f4.bcbits.com/img/${id}_4.jpg`
}
}
})

86
appOLDD/store/ui.ts Normal file
View File

@@ -0,0 +1,86 @@
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
showSearch: false,
searchQuery: '',
showCardSharer: false
}),
actions: {
openSearch() {
this.showSearch = true
// reset query on open to avoid stale state
this.searchQuery = ''
},
closeSearch() {
this.showSearch = false
this.searchQuery = ''
},
setSearchQuery(q: string) {
this.searchQuery = q
},
listBoxes() {
const dataStore = useDataStore()
dataStore.boxes.forEach((box) => {
box.state = 'box-list'
})
},
selectBox(id: string) {
const dataStore = useDataStore()
dataStore.boxes.forEach((box) => {
id = id.replace(/[AB]$/, '')
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'
})
},
openCardSharer() {
this.showCardSharer = true
},
scrollToBox(box: Box) {
if (box) {
const boxElement = document.getElementById(box.id)
if (boxElement) {
setTimeout(() => {
// Récupérer la position de l'élément
const elementRect = boxElement.getBoundingClientRect()
// Calculer la position de défilement (une boîte plus haut)
const offsetPosition = elementRect.top + window.pageYOffset - elementRect.height * 1.5
// Faire défiler à la nouvelle position
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
})
}, 333)
}
}
}
},
getters: {
isBoxSelected: () => {
const dataStore = useDataStore()
return dataStore.boxes.some((box) => box.state === 'box-selected')
},
getSelectedBox: () => {
const dataStore = useDataStore()
return (dataStore.boxes as Box[]).find((box) => box.state === 'box-selected') || null
}
}
})

11
appOLDD/store/user.ts Normal file
View File

@@ -0,0 +1,11 @@
import type { Compilation, Artist, Track } from '~/../types/types'
// stores/user.ts
import { defineStore } from 'pinia'
export const useDataStore = defineStore('data', {
state: () => ({
badge: [] // un badge par user achievement pour enrichir le déchifrage de l'app (afichage des nom des titres/artiste, collection de carte déjà joué (et du coups possibilité de les rejouer dans son deck))
// evilSpins est un jeux mais pas vraiment pokemon (un morceau = un pokemon) mais aussi un jeux d'aventure / exploration ou plus on progresse plus on peu voir de chose
})
})