ahora reproduce canciones
This commit is contained in:
@@ -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',
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
const playlistElement = document.querySelector('div[tabIndex="0"]');
|
||||||
|
const toggleButton = event.target as HTMLElement;
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="fixed bottom-0 left-0 right-0 bg-gray-800 p-4">
|
<>
|
||||||
|
{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="flex flex-col items-center justify-center">
|
||||||
<div className="mb-4 h-2 w-full rounded-full bg-gray-600">
|
<div className="mb-4 h-2 w-full rounded-full bg-gray-600">
|
||||||
<div
|
<div
|
||||||
className="h-2 rounded-full bg-blue-500"
|
className="h-2 rounded-full bg-green-500"
|
||||||
style={{ width: `${progress}%` }}
|
style={{ width: `${progress}%` }}
|
||||||
></div>
|
></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>
|
||||||
|
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<div>
|
||||||
<button
|
<button
|
||||||
className="mx-4 text-white"
|
className="mx-4 text-white"
|
||||||
onClick={() => setIsPlaying(!isPlaying)}
|
onClick={() => setIsPlaying(!isPlaying)}
|
||||||
|
disabled={song.id === 0}
|
||||||
>
|
>
|
||||||
{!isPlaying ? <PlayIcon /> : <PauseIcon />}
|
{!isPlaying ? <PlayIcon /> : <PauseIcon />}
|
||||||
</button>
|
</button>
|
||||||
<span className="text-white">{titulo}</span>
|
<button
|
||||||
<span className="text-white">{timeLeft}</span>
|
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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
1
resources/js/types/types.d.ts
vendored
1
resources/js/types/types.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user