250 lines
6.7 KiB
TypeScript
250 lines
6.7 KiB
TypeScript
/**
|
|
* Store Zustand pour gérer l'état du player
|
|
* FE-TYPE-011: Fully typed store
|
|
*/
|
|
|
|
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
import type { Track, PlayerState, PlayerControls } from '../types';
|
|
|
|
// FE-TYPE-011: Fully typed store interface
|
|
export interface PlayerStore extends PlayerState, PlayerControls {
|
|
setCurrentTime: (time: number) => void;
|
|
setDuration: (duration: number) => void;
|
|
removeFromQueue: (index: number) => void;
|
|
reorderQueue: (from: number, to: number) => void;
|
|
}
|
|
|
|
export const usePlayerStore = create<PlayerStore>()(
|
|
persist(
|
|
(set, get) => ({
|
|
// État initial
|
|
currentTrack: null,
|
|
isPlaying: false,
|
|
currentTime: 0,
|
|
duration: 0,
|
|
volume: 100,
|
|
muted: false,
|
|
queue: [],
|
|
currentIndex: -1,
|
|
repeat: 'off',
|
|
shuffle: false,
|
|
|
|
// Actions de contrôle
|
|
play: (track?: Track) => {
|
|
if (!track) {
|
|
// Si pas de track fourni, reprendre la lecture
|
|
set({ isPlaying: true });
|
|
return;
|
|
}
|
|
|
|
const { queue } = get();
|
|
const trackIndex = queue.findIndex((t) => t.id === track.id);
|
|
|
|
if (trackIndex >= 0) {
|
|
// Track déjà dans la queue
|
|
set({
|
|
currentTrack: track,
|
|
currentIndex: trackIndex,
|
|
isPlaying: true,
|
|
currentTime: 0,
|
|
});
|
|
} else {
|
|
// Nouvelle track, l'ajouter à la queue
|
|
const newQueue = [...queue, track];
|
|
set({
|
|
currentTrack: track,
|
|
currentIndex: newQueue.length - 1,
|
|
queue: newQueue,
|
|
isPlaying: true,
|
|
currentTime: 0,
|
|
});
|
|
}
|
|
},
|
|
|
|
pause: () => {
|
|
set({ isPlaying: false });
|
|
},
|
|
|
|
resume: () => {
|
|
set({ isPlaying: true });
|
|
},
|
|
|
|
stop: () => {
|
|
set({ isPlaying: false, currentTime: 0 });
|
|
},
|
|
|
|
next: () => {
|
|
const { queue, currentIndex, repeat, shuffle } = get();
|
|
|
|
if (queue.length === 0) return;
|
|
|
|
let nextIndex = currentIndex;
|
|
|
|
if (shuffle) {
|
|
// Mode shuffle: choisir une track aléatoire
|
|
nextIndex = Math.floor(Math.random() * queue.length);
|
|
} else if (currentIndex < queue.length - 1) {
|
|
// Track suivante dans la queue
|
|
nextIndex = currentIndex + 1;
|
|
} else if (repeat === 'playlist') {
|
|
// Répéter la playlist: revenir au début
|
|
nextIndex = 0;
|
|
} else {
|
|
// Pas de track suivante
|
|
return;
|
|
}
|
|
|
|
if (nextIndex !== currentIndex && nextIndex < queue.length) {
|
|
set({
|
|
currentIndex: nextIndex,
|
|
currentTrack: queue[nextIndex],
|
|
currentTime: 0,
|
|
isPlaying: true,
|
|
});
|
|
}
|
|
},
|
|
|
|
previous: () => {
|
|
const { queue, currentIndex } = get();
|
|
|
|
if (queue.length === 0 || currentIndex <= 0) return;
|
|
|
|
const prevIndex = currentIndex - 1;
|
|
set({
|
|
currentIndex: prevIndex,
|
|
currentTrack: queue[prevIndex],
|
|
currentTime: 0,
|
|
isPlaying: true,
|
|
});
|
|
},
|
|
|
|
seek: (time: number) => {
|
|
const { duration } = get();
|
|
const clampedTime = Math.max(0, Math.min(time, duration || 0));
|
|
set({ currentTime: clampedTime });
|
|
},
|
|
|
|
setCurrentTime: (time: number) => {
|
|
const { duration } = get();
|
|
const clampedTime = Math.max(0, Math.min(time, duration || 0));
|
|
set({ currentTime: clampedTime });
|
|
},
|
|
|
|
setDuration: (duration: number) => {
|
|
set({ duration: Math.max(0, duration) });
|
|
},
|
|
|
|
setVolume: (volume: number) => {
|
|
const clampedVolume = Math.max(0, Math.min(100, volume));
|
|
set({ volume: clampedVolume });
|
|
},
|
|
|
|
toggleMute: () => {
|
|
set({ muted: !get().muted });
|
|
},
|
|
|
|
toggleShuffle: () => {
|
|
set({ shuffle: !get().shuffle });
|
|
},
|
|
|
|
setRepeat: (mode: 'off' | 'track' | 'playlist') => {
|
|
set({ repeat: mode });
|
|
},
|
|
|
|
// Queue management
|
|
addToQueue: (tracks: Track[]) => {
|
|
const { queue } = get();
|
|
const newQueue = [...queue, ...tracks];
|
|
set({ queue: newQueue });
|
|
},
|
|
|
|
removeFromQueue: (index: number) => {
|
|
const { queue, currentIndex } = get();
|
|
if (index < 0 || index >= queue.length) return;
|
|
|
|
const newQueue = queue.filter((_, i) => i !== index);
|
|
|
|
// Ajuster l'index courant si nécessaire
|
|
let newIndex = currentIndex;
|
|
if (index < currentIndex) {
|
|
newIndex = currentIndex - 1;
|
|
} else if (index === currentIndex) {
|
|
// Si on supprime la track courante, passer à la suivante ou arrêter
|
|
if (newQueue.length > 0) {
|
|
newIndex = Math.min(newIndex, newQueue.length - 1);
|
|
set({
|
|
queue: newQueue,
|
|
currentIndex: newIndex,
|
|
currentTrack: newQueue[newIndex] || null,
|
|
isPlaying: newQueue[newIndex] ? get().isPlaying : false,
|
|
});
|
|
return;
|
|
} else {
|
|
newIndex = -1;
|
|
set({
|
|
queue: newQueue,
|
|
currentIndex: newIndex,
|
|
currentTrack: null,
|
|
isPlaying: false,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
set({ queue: newQueue, currentIndex: newIndex });
|
|
},
|
|
|
|
reorderQueue: (from: number, to: number) => {
|
|
const { queue, currentIndex } = get();
|
|
if (
|
|
from < 0 ||
|
|
from >= queue.length ||
|
|
to < 0 ||
|
|
to >= queue.length ||
|
|
from === to
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const newQueue = [...queue];
|
|
const [removed] = newQueue.splice(from, 1);
|
|
newQueue.splice(to, 0, removed);
|
|
|
|
// Ajuster l'index courant
|
|
let newIndex = currentIndex;
|
|
if (from === currentIndex) {
|
|
newIndex = to;
|
|
} else if (from < currentIndex && to >= currentIndex) {
|
|
newIndex = currentIndex - 1;
|
|
} else if (from > currentIndex && to <= currentIndex) {
|
|
newIndex = currentIndex + 1;
|
|
}
|
|
|
|
set({ queue: newQueue, currentIndex: newIndex });
|
|
},
|
|
|
|
clearQueue: () => {
|
|
set({
|
|
queue: [],
|
|
currentIndex: -1,
|
|
currentTrack: null,
|
|
isPlaying: false,
|
|
currentTime: 0,
|
|
});
|
|
},
|
|
}),
|
|
{
|
|
name: 'player-storage',
|
|
partialize: (state) => ({
|
|
volume: state.volume,
|
|
muted: state.muted,
|
|
repeat: state.repeat,
|
|
shuffle: state.shuffle,
|
|
queue: state.queue,
|
|
currentIndex: state.currentIndex,
|
|
currentTrack: state.currentTrack,
|
|
}),
|
|
},
|
|
),
|
|
);
|