frontend/src/component/Viewers/MusicPlayer/MusicPlayer.tsx

210 lines
6.1 KiB
TypeScript

import { IconButton, Tooltip } from "@mui/material";
import { useCallback, useEffect, useRef, useState } from "react";
import { getFileEntityUrl } from "../../../api/api.ts";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
import SessionManager, { UserSettings } from "../../../session";
import { getFileLinkedUri } from "../../../util";
import MusicNote2 from "../../Icons/MusicNote2.tsx";
import MusicNote2Play from "../../Icons/MusicNote2Play.tsx";
import PlayerPopup from "./PlayerPopup.tsx";
export const LoopMode = {
list_repeat: 0,
single_repeat: 1,
shuffle: 2,
};
const MusicPlayer = () => {
const dispatch = useAppDispatch();
const playerState = useAppSelector((state) => state.globalState.musicPlayer);
const audio = useRef<HTMLAudioElement>(null);
const icon = useRef<HTMLButtonElement>(null);
const [playing, setPlaying] = useState(false);
const [volume, setVolume] = useState(0.2);
const [index, setIndex] = useState<number | undefined>(undefined);
const [loading, setLoading] = useState(false);
const [popoverOpen, setPopoverOpen] = useState(false);
const [duration, setDuration] = useState(0);
const [current, setCurrent] = useState(0);
const [loopMode, setLoopMode] = useState(LoopMode.list_repeat);
const [playbackSpeed, setPlaybackSpeed] = useState(1);
const playHistory = useRef<number[]>([]);
useEffect(() => {
if (playerState) {
playHistory.current = [];
setPlaying(true);
setPopoverOpen(true);
const volume = SessionManager.getWithFallback(UserSettings.MusicVolume);
setVolume(volume);
playIndex(playerState.startIndex, volume);
}
audio.current?.addEventListener("timeupdate", timeUpdate);
return () => {
setPlaying(false);
audio.current?.removeEventListener("timeupdate", timeUpdate);
};
}, [playerState]);
const playIndex = useCallback(
async (index: number, latestVolume?: number) => {
if (audio.current && playerState) {
audio.current.pause();
setIndex(index);
try {
const res = await dispatch(
getFileEntityUrl({
uris: [getFileLinkedUri(playerState.files[index])],
entity: playerState.version,
}),
);
audio.current.src = res.urls[0].url;
audio.current.currentTime = 0;
audio.current.play();
audio.current.volume = latestVolume ?? volume;
audio.current.playbackRate = playbackSpeed;
} catch (e) {
console.error(e);
}
}
},
[playerState, volume, playbackSpeed],
);
const loopProceed = useCallback(
(isNext: boolean) => {
if (!playerState) {
return;
}
playHistory.current.push(index ?? 0);
switch (loopMode) {
case LoopMode.list_repeat:
if (isNext) {
playIndex(((index ?? 0) + 1) % playerState?.files.length);
} else {
playIndex(((index ?? 0) - 1 + playerState?.files.length) % playerState?.files.length);
}
break;
case LoopMode.single_repeat:
playIndex(index ?? 0);
break;
case LoopMode.shuffle:
if (isNext) {
const nextIndex = Math.floor(Math.random() * playerState?.files.length);
playIndex(nextIndex);
} else {
playHistory.current.pop();
playIndex(playHistory.current.pop() ?? index ?? 0);
}
break;
}
},
[loopMode, playIndex, playerState, index],
);
const onPlayEnded = useCallback(() => {
loopProceed(true);
}, []);
const timeUpdate = useCallback(() => {
setCurrent(Math.floor(audio.current?.currentTime || 0));
setDuration(Math.floor(audio.current?.duration || 0));
}, []);
const seek = useCallback((time: number) => {
if (audio.current) {
audio.current.currentTime = time;
}
}, []);
const playingTooltip = playerState
? `[${(index ?? 0) + 1}/${playerState.files.length}] ${playerState?.files[index ?? 0]?.name}`
: "";
const onPlayerPopoverClose = useCallback(() => {
setPopoverOpen(false);
}, []);
const onPlayerPopoverOpen = useCallback(() => {
setPopoverOpen(true);
}, []);
const togglePause = useCallback(() => {
if (audio.current) {
if (audio.current.paused) {
audio.current.play();
setPlaying(true);
} else {
audio.current.pause();
setPlaying(false);
}
}
}, []);
const setVolumeLevel = useCallback((volume: number) => {
if (audio.current) {
audio.current.volume = volume;
setVolume(volume);
}
}, []);
const toggleLoopMode = useCallback(() => {
setLoopMode((loopMode) => (loopMode + 1) % 3);
}, []);
const setLoopModeHandler = useCallback((mode: number) => {
setLoopMode(mode);
}, []);
const setPlaybackSpeedHandler = useCallback((speed: number) => {
setPlaybackSpeed(speed);
if (audio.current) {
audio.current.playbackRate = speed;
}
}, []);
return (
<>
<audio
ref={audio}
onPause={() => setPlaying(false)}
onPlay={() => setPlaying(true)}
onEnded={() => loopProceed(true)}
/>
<Tooltip title={playingTooltip} enterDelay={0}>
<IconButton ref={icon} onClick={onPlayerPopoverOpen} size="large">
{playing ? <MusicNote2Play /> : <MusicNote2 />}
</IconButton>
</Tooltip>
{index !== undefined && (
<PlayerPopup
playIndex={playIndex}
loopProceed={loopProceed}
file={playerState?.files[index]}
duration={duration}
current={current}
open={popoverOpen}
setVolumeLevel={setVolumeLevel}
volume={volume}
onSeek={seek}
togglePause={togglePause}
playing={playing}
playlist={playerState?.files}
loopMode={loopMode}
toggleLoopMode={toggleLoopMode}
setLoopMode={setLoopModeHandler}
playbackSpeed={playbackSpeed}
setPlaybackSpeed={setPlaybackSpeedHandler}
anchorEl={icon.current}
onClose={onPlayerPopoverClose}
/>
)}
</>
);
};
export default MusicPlayer;