180 lines
6.3 KiB
TypeScript
180 lines
6.3 KiB
TypeScript
import { BottomNavigation, BottomNavigationAction, Box, Button, IconButton, Theme, Typography, useTheme } from "@mui/material";
|
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
import useQueue from "../hooks/useQueue";
|
|
import { getSongURL, Song } from "../utils/MusicServer";
|
|
import { Pause, PlayArrow, Repeat, RepeatOn, RepeatOne, RepeatOneOn, SkipNext, SkipPrevious } from "@mui/icons-material";
|
|
import durationToStr from "../utils/duration";
|
|
import { MotionBox } from "./MotionComponents";
|
|
import { Variants } from "framer-motion";
|
|
|
|
const variants = (theme: Theme): Variants => (
|
|
{
|
|
initial: {
|
|
bottom: theme.spacing(-12)
|
|
},
|
|
final: {
|
|
bottom: theme.spacing(0)
|
|
}
|
|
}
|
|
);
|
|
|
|
export default function Player() {
|
|
const theme = useTheme();
|
|
const queue = useQueue();
|
|
const audioRef = useRef<HTMLAudioElement>(null);
|
|
const [song, setSong] = useState<Song | undefined>();
|
|
const [paused, setPaused] = useState(audioRef.current?.paused || false);
|
|
const [canPlayNext, setCanPlayNext] = useState(false);
|
|
const [canPlayPrev, setCanPlayPrev] = useState(false);
|
|
const [repeatOne, setRepeatOne] = useState(false);
|
|
const [currentTime, setCurrentTime] = useState(0);
|
|
const songURL = useMemo(
|
|
() => getSongURL(song),
|
|
[song, queue]
|
|
);
|
|
|
|
const togglePlaying = () => {
|
|
const audio = audioRef.current;
|
|
if (!audio) return;
|
|
if (audio.paused) audio.play();
|
|
else audio.pause();
|
|
|
|
setPaused(audio.paused);
|
|
};
|
|
|
|
const handlePrev = () => {
|
|
queue?.prev();
|
|
};
|
|
|
|
const handleNext = () => {
|
|
if (repeatOne) {
|
|
const audio = audioRef.current;
|
|
if (audio) {
|
|
audio.currentTime = 0;
|
|
audio.play();
|
|
}
|
|
return;
|
|
}
|
|
queue?.next();
|
|
};
|
|
|
|
const handleRepeat = () => {
|
|
if (!queue) return console.error('No queue bruh');
|
|
console.log(`handleRepeat`, queue.repeat, repeatOne);
|
|
if (!queue.repeat && !repeatOne) {
|
|
queue.setRepeat(true);
|
|
} else if (queue.repeat && !repeatOne) {
|
|
queue.setRepeat(false);
|
|
setRepeatOne(true);
|
|
} else if (repeatOne) {
|
|
setRepeatOne(false);
|
|
}
|
|
};
|
|
|
|
const updateCurrentPlaying = () => {
|
|
const audio = audioRef.current;
|
|
if (!audio) return;
|
|
setCurrentTime(audio.currentTime);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const intervalId = setInterval(() => {
|
|
updateCurrentPlaying();
|
|
}, 500);
|
|
|
|
return () => {
|
|
clearInterval(intervalId);
|
|
};
|
|
}, [audioRef]);
|
|
|
|
useEffect(() => {
|
|
setSong(queue?.getCurrent());
|
|
console.log(`Song updated. ${songURL}`, song);
|
|
console.log(`Queue updated.`, queue);
|
|
}, [queue!.i]);
|
|
|
|
useEffect(() => {
|
|
setSong(queue?.getCurrent());
|
|
console.log(`Song updated. ${songURL}`, song);
|
|
}, [queue!.songs]);
|
|
|
|
useEffect(() => {
|
|
setCanPlayNext(queue?.canPlayNext() || false);
|
|
setCanPlayPrev(queue?.canPlayPrev() || false);
|
|
}, [queue!.songs, queue!.i, queue!.repeat]);
|
|
|
|
return song && (
|
|
<MotionBox
|
|
display='flex'
|
|
position='absolute'
|
|
height={theme.spacing(12)}
|
|
width='100%'
|
|
maxWidth='100%'
|
|
bgcolor='background.paper'
|
|
borderColor='divider'
|
|
borderTop={0.2}
|
|
variants={variants(theme)}
|
|
initial='initial'
|
|
animate='final'
|
|
>
|
|
<Box width='100%' height={theme.spacing(6)} p={2}>
|
|
{song && songURL && (
|
|
<audio onEnded={handleNext} autoPlay src={songURL} ref={audioRef} controls={false} />
|
|
)}
|
|
<Box display='flex' flexGrow={1} gap={2}>
|
|
<img src={song?.thumbnails[0].url} width={64} />
|
|
<Box display='flex' flexDirection='column' flexGrow={1}>
|
|
<Typography variant='h6' component='div'>
|
|
{song?.name}
|
|
</Typography>
|
|
<Box display='flex' gap={1}>
|
|
<Typography color='textSecondary'>
|
|
{song?.artist.name}
|
|
</Typography>
|
|
{song?.album.name !== song?.name && (
|
|
<Typography color='textDisabled'>
|
|
{song?.album.name}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
<Typography
|
|
color='textDisabled'
|
|
component='div'
|
|
variant='button'
|
|
alignSelf='center'
|
|
>
|
|
{currentTime && durationToStr(currentTime)}
|
|
/
|
|
{currentTime && song && durationToStr(song.duration)}
|
|
</Typography>
|
|
<Box display='flex' height='min-content' alignSelf='center' gap={2} flexShrink={0}>
|
|
<IconButton
|
|
color={(queue!.repeat || repeatOne) ? 'success' : 'inherit'}
|
|
onClick={handleRepeat}
|
|
>
|
|
{queue!.repeat && (
|
|
<Repeat />
|
|
)}
|
|
{repeatOne && (
|
|
<RepeatOne />
|
|
)}
|
|
{!queue!.repeat && !repeatOne && (
|
|
<Repeat />
|
|
)}
|
|
</IconButton>
|
|
<IconButton disabled={!canPlayPrev} onClick={handlePrev}>
|
|
<SkipPrevious />
|
|
</IconButton>
|
|
<IconButton onClick={togglePlaying}>
|
|
{paused ? <PlayArrow /> : <Pause />}
|
|
</IconButton>
|
|
<IconButton disabled={!canPlayNext} onClick={handleNext}>
|
|
<SkipNext />
|
|
</IconButton>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</MotionBox>
|
|
);
|
|
} |