ahora reproduce canciones

This commit is contained in:
2025-03-31 16:50:45 -03:00
parent b8a4cdaa6a
commit 74e0463393
4 changed files with 214 additions and 30 deletions

View File

@@ -72,11 +72,26 @@ class SongController extends Controller
return response()->noContent(200); return response()->noContent(200);
} }
/** /**
* @return void * @param int $id
* @param mixed $id
*/ */
public function stream($id):void public function stream($id)
{ {
$song = Song::find($id);
// Stream specified song // 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',
]);
}
} }
} }

View File

@@ -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 PauseIcon from './Icons/PauseIcon';
import PlayIcon from './Icons/PlayIcon'; 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 [isPlaying, setIsPlaying] = useState(false);
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [timeLeft, setTimeLeft] = useState('0:00'); const [timeLeft, setTimeLeft] = useState('0:00');
const [isPlaylistVisible, setIsPlaylistVisible] = useState(false);
const audioRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => { useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => { const handleKeyPress = (event: KeyboardEvent) => {
if (event.code === 'Space') { if (event.code === 'Space') {
@@ -21,27 +41,163 @@ export default function Reproductor({ titulo }) {
}; };
}, [isPlaying]); }, [isPlaying]);
return ( useEffect(() => {
<div className="fixed bottom-0 left-0 right-0 bg-gray-800 p-4"> function handleClickOutside(event: MouseEvent) {
<div className="flex flex-col items-center justify-center"> const playlistElement = document.querySelector('div[tabIndex="0"]');
<div className="mb-4 h-2 w-full rounded-full bg-gray-600"> const toggleButton = event.target as HTMLElement;
<div
className="h-2 rounded-full bg-blue-500"
style={{ width: `${progress}%` }}
></div>
</div>
<div className="flex w-full items-center justify-between"> if (
<button playlistElement &&
className="mx-4 text-white" !playlistElement.contains(event.target as Node) &&
onClick={() => setIsPlaying(!isPlaying)} !toggleButton.closest('button')
> ) {
{!isPlaying ? <PlayIcon /> : <PauseIcon />} setIsPlaylistVisible(false);
</button> }
<span className="text-white">{titulo}</span> }
<span className="text-white">{timeLeft}</span>
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 && (
<div className="fixed inset-0 z-40 bg-black bg-opacity-50" />
)}
<div className="fixed bottom-0 left-0 right-0 z-50 rounded-t-xl bg-gray-800 p-4">
<div className="flex flex-col items-center justify-center">
<div className="mb-4 h-2 w-full rounded-full bg-gray-600">
<div
className="h-2 rounded-full bg-green-500"
style={{ width: `${progress}%` }}
></div>
{playlist.length > 0 && (
<div
className={`fixed bottom-0 left-0 right-0 transform rounded-t-xl bg-gray-700 transition-transform duration-300 ease-in-out ${
!isPlaylistVisible
? 'translate-y-full bg-opacity-30'
: 'translate-y-0'
} max-h-[50%] overflow-y-auto p-2`}
onBlur={() => setIsPlaylistVisible(false)}
tabIndex={0}
>
{playlist.map((song) => (
<div
key={song.id}
className="cursor-pointer p-2 text-white hover:bg-gray-600"
onClick={() => handleSelectSong(song)}
>
{song.title}
</div>
))}
</div>
)}
</div>
<div className="flex w-full items-center justify-between">
<div>
<button
className="mx-4 text-white"
onClick={() => setIsPlaying(!isPlaying)}
disabled={song.id === 0}
>
{!isPlaying ? <PlayIcon /> : <PauseIcon />}
</button>
<button
className="mx-4 text-white"
onClick={() => setIsPlaylistVisible(!isPlaylistVisible)}
>
{isPlaylistVisible ? '▼' : '▲'}
</button>
</div>
<span className="text-white">{song.title}</span>
<span className="w-16 text-right text-white">{timeLeft}</span>
</div>
</div> </div>
</div> </div>
</div> </>
); );
} }

View File

@@ -10,7 +10,17 @@ import { useState } from 'react';
export default function Gallery({ songs }: { songs: Cancion[] }) { export default function Gallery({ songs }: { songs: Cancion[] }) {
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [cancionSeleccionada, setCancion] = useState({ title: '' }); const [queue, setQueue] = useState<Cancion[]>([]);
const [currentSong, setCurrentSong] = useState<Cancion>();
const addToQueue = (song: Cancion) => {
if (queue.length === 0) {
setQueue([song]);
} else {
setQueue([...queue, song]);
}
};
return ( return (
<Authenticated <Authenticated
header={ header={
@@ -40,10 +50,10 @@ export default function Gallery({ songs }: { songs: Cancion[] }) {
{song.artist} {song.artist}
</p> </p>
<div className="mt-1 flex gap-1"> <div className="mt-1 flex gap-1">
<PrimaryButton> <PrimaryButton onClick={() => setCurrentSong(song)}>
<AddIcon /> <AddIcon />
</PrimaryButton> </PrimaryButton>
<SecondaryButton> <SecondaryButton onClick={() => addToQueue(song)}>
<AddStackIcon /> <AddStackIcon />
</SecondaryButton> </SecondaryButton>
</div> </div>
@@ -52,9 +62,13 @@ export default function Gallery({ songs }: { songs: Cancion[] }) {
))} ))}
</div> </div>
</div> </div>
<Reproductor /> <Reproductor
song={currentSong}
setSong={setCurrentSong}
playlist={queue}
setplaylist={setQueue}
/>
</div> </div>
c
</Authenticated> </Authenticated>
); );
} }

View File

@@ -1,5 +1,4 @@
export type Cancion = { export type Cancion = {
name: ReactNode;
id: number; id: number;
title: string; title: string;
artist: string | null; artist: string | null;