diff --git a/app/Http/Controllers/SongController.php b/app/Http/Controllers/SongController.php index 11e786c..dcfca2c 100644 --- a/app/Http/Controllers/SongController.php +++ b/app/Http/Controllers/SongController.php @@ -72,11 +72,26 @@ class SongController extends Controller return response()->noContent(200); } /** - * @return void - * @param mixed $id + * @param int $id */ - public function stream($id):void + public function stream($id) { + $song = Song::find($id); + // Stream specified song + $this->authorize('view', $song); + + if ($song && Storage::exists($song->path)) { + $file = Storage::path($song->path); + return response()->stream(function () use ($file) { + $stream = fopen($file, 'rb'); + fpassthru($stream); + fclose($stream); + }, 200, [ + 'Content-Type' => 'audio/mpeg', + 'Content-Disposition' => 'inline; filename="' . $file . '"', + 'Accept-Ranges' => 'bytes', + ]); + } } } diff --git a/resources/js/Components/Reproductor.tsx b/resources/js/Components/Reproductor.tsx index e6652f7..b9f1461 100644 --- a/resources/js/Components/Reproductor.tsx +++ b/resources/js/Components/Reproductor.tsx @@ -1,12 +1,32 @@ -import { useEffect, useState } from 'react'; +import { Cancion } from '@/types/types'; +import { useEffect, useRef, useState } from 'react'; import PauseIcon from './Icons/PauseIcon'; import PlayIcon from './Icons/PlayIcon'; -export default function Reproductor({ titulo }) { +export default function Reproductor({ + song = { + id: 0, + title: 'Sin título', + artist: 'Artista desconocido', + path: '', + cover: null, + }, + setSong, + playlist = [], + setplaylist, +}: { + song: Cancion; + setSong: (arg0: Cancion) => void; + playlist: Cancion[]; + setplaylist: (arg0: Cancion[]) => void; +}) { const [isPlaying, setIsPlaying] = useState(false); const [progress, setProgress] = useState(0); const [timeLeft, setTimeLeft] = useState('0:00'); + const [isPlaylistVisible, setIsPlaylistVisible] = useState(false); + const audioRef = useRef(null); + useEffect(() => { const handleKeyPress = (event: KeyboardEvent) => { if (event.code === 'Space') { @@ -21,27 +41,163 @@ export default function Reproductor({ titulo }) { }; }, [isPlaying]); - return ( -
-
-
-
-
+ useEffect(() => { + function handleClickOutside(event: MouseEvent) { + const playlistElement = document.querySelector('div[tabIndex="0"]'); + const toggleButton = event.target as HTMLElement; -
- - {titulo} - {timeLeft} + if ( + playlistElement && + !playlistElement.contains(event.target as Node) && + !toggleButton.closest('button') + ) { + setIsPlaylistVisible(false); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + useEffect(() => { + if (!audioRef.current) return; + if (isPlaying) { + audioRef.current.play(); + } else { + audioRef.current.pause(); + } + }, [isPlaying]); + + const playNextSong = () => { + const currentIndex = playlist.findIndex((s) => s.id === song.id); + if (currentIndex > -1 && currentIndex < playlist.length - 1) { + setSong(playlist[currentIndex + 1]); + } + }; + + useEffect(() => { + if (song.id === 0) { + setIsPlaying(false); + return; + } + + setIsPlaying(true); + + if (!audioRef.current) { + audioRef.current = new Audio(); + } + const audio = audioRef.current; + audio.src = `/canciones/stream/${song.id}`; + audio.load(); + + if (timeLeft === '0:00' && song.id !== 0) { + setIsPlaying(false); + setTimeLeft('0:00'); + setProgress(0); + song = { + id: 0, + title: 'Sin título', + artist: 'Artista desconocido', + path: '', + cover: null, + }; + } + + setIsPlaying(true); + + audio.addEventListener('timeupdate', () => { + const duration = audio.duration; + const currentTime = audio.currentTime; + const progress = (currentTime / duration) * 100; + const timeLeft = Math.floor(duration - currentTime); + const minutes = Math.floor(timeLeft / 60); + const seconds = Math.floor(timeLeft % 60); + + setProgress(progress); + setTimeLeft(`${minutes}:${seconds.toString().padStart(2, '0')}`); + }); + + audio.addEventListener('ended', () => { + playNextSong(); + }); + + if (isPlaying) { + audio.play(); + } + + return () => { + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.src = ''; + } + }; + }, [song.id]); + + const handleSelectSong = (selectedSong: Cancion) => { + if (selectedSong == null) return; + setSong(selectedSong); + + setIsPlaying(true); + }; + + return ( + <> + {isPlaylistVisible && ( +
+ )} +
+
+
+
+ {playlist.length > 0 && ( +
setIsPlaylistVisible(false)} + tabIndex={0} + > + {playlist.map((song) => ( +
handleSelectSong(song)} + > + {song.title} +
+ ))} +
+ )} +
+ +
+
+ + +
+ {song.title} + {timeLeft} +
-
+ ); } diff --git a/resources/js/Pages/Gallery.tsx b/resources/js/Pages/Gallery.tsx index 8dc9d05..9b99a54 100644 --- a/resources/js/Pages/Gallery.tsx +++ b/resources/js/Pages/Gallery.tsx @@ -10,7 +10,17 @@ import { useState } from 'react'; export default function Gallery({ songs }: { songs: Cancion[] }) { const [progress, setProgress] = useState(0); const [isPlaying, setIsPlaying] = useState(false); - const [cancionSeleccionada, setCancion] = useState({ title: '' }); + const [queue, setQueue] = useState([]); + const [currentSong, setCurrentSong] = useState(); + + const addToQueue = (song: Cancion) => { + if (queue.length === 0) { + setQueue([song]); + } else { + setQueue([...queue, song]); + } + }; + return (
- + setCurrentSong(song)}> - + addToQueue(song)}>
@@ -52,9 +62,13 @@ export default function Gallery({ songs }: { songs: Cancion[] }) { ))}
- +
- c ); } diff --git a/resources/js/types/types.d.ts b/resources/js/types/types.d.ts index 3eb1f04..0488d10 100644 --- a/resources/js/types/types.d.ts +++ b/resources/js/types/types.d.ts @@ -1,5 +1,4 @@ export type Cancion = { - name: ReactNode; id: number; title: string; artist: string | null;