karusic/front2/src/components/AudioPlayer.tsx

359 lines
9.7 KiB
TypeScript

import { SyntheticEvent, useEffect, useRef, useState } from 'react';
import {
Box,
Button,
Flex,
IconButton,
Slider,
SliderFilledTrack,
SliderThumb,
SliderTrack,
Text,
position,
} from '@chakra-ui/react';
import {
MdCheck,
MdFastForward,
MdFastRewind,
MdGraphicEq,
MdLooksOne,
MdNavigateBefore,
MdNavigateNext,
MdOutlinePlayArrow,
MdPause,
MdPlayArrow,
MdRepeat,
MdRepeatOne,
MdStop,
MdTrendingFlat,
} from 'react-icons/md';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useSpecificAlbum } from '@/service/Album';
import { useSpecificArtists } from '@/service/Artist';
import { useSpecificGender } from '@/service/Gender';
import { useSpecificTrack } from '@/service/Track';
import { DataUrlAccess } from '@/utils/data-url-access';
import { useThemeMode } from '@/utils/theme-tools';
import { isNullOrUndefined } from '@/utils/validator';
export enum PlayMode {
PLAY_ONE,
PLAY_ALL,
PLAY_ONE_LOOP,
PLAY_ALL_LOOP,
}
const playModeIcon = {
[PlayMode.PLAY_ONE]: <MdLooksOne size="30px" />,
[PlayMode.PLAY_ALL]: <MdTrendingFlat size="30px" />,
[PlayMode.PLAY_ONE_LOOP]: <MdRepeatOne size="30px" />,
[PlayMode.PLAY_ALL_LOOP]: <MdRepeat size="30px" />,
};
export type AudioPlayerProps = {};
const formatTime = (time) => {
if (time && !isNaN(time)) {
const minutes = Math.floor(time / 60);
const formatMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
const seconds = Math.floor(time % 60);
const formatSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
return `${formatMinutes}:${formatSeconds}`;
}
return '00:00';
};
export const AudioPlayer = ({}: AudioPlayerProps) => {
const { mode } = useThemeMode();
const { playTrackList, trackOffset, previous, next, first } =
useActivePlaylistService();
const audioRef = useRef<HTMLAudioElement>(null);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [timeProgress, setTimeProgress] = useState<number>(0);
const [playingMode, setPlayingMode] = useState<PlayMode>(PlayMode.PLAY_ALL);
const [duration, setDuration] = useState<number>(0);
const { dataTrack } = useSpecificTrack(
trackOffset !== undefined ? playTrackList[trackOffset] : undefined
);
const { dataAlbum } = useSpecificAlbum(dataTrack?.albumId);
const { dataGender } = useSpecificGender(dataTrack?.genderId);
const { dataArtists } = useSpecificArtists(dataTrack?.artists);
const [mediaSource, setMediaSource] = useState<string>('');
useEffect(() => {
setMediaSource(
dataTrack && dataTrack?.dataId
? DataUrlAccess.getUrl(dataTrack?.dataId)
: ''
);
}, [dataTrack, setMediaSource]);
const backColor = mode('back.100', 'back.800');
const configButton = {
borderRadius: 'full',
backgroundColor: '#00000000',
_hover: {
boxShadow: 'outline-over',
bgColor: 'brand.500',
},
};
useEffect(() => {
if (!audioRef || !audioRef.current) {
return;
}
if (isPlaying) {
audioRef.current.play();
} else {
audioRef.current.pause();
}
}, [isPlaying, audioRef]);
const onAudioEnded = () => {
if (playTrackList.length === 0 || isNullOrUndefined(trackOffset)) {
return;
}
if (playingMode === PlayMode.PLAY_ALL_LOOP) {
if (playTrackList.length == trackOffset + 1) {
first();
} else {
next();
}
} else if (playingMode === PlayMode.PLAY_ALL) {
next();
} else if (playingMode === PlayMode.PLAY_ONE_LOOP) {
onSeek(0);
onPlay();
}
};
const onSeek = (newValue) => {
console.log(`onSeek: ${newValue}`);
if (!audioRef || !audioRef.current) {
return;
}
audioRef.current.currentTime = newValue;
};
const onPlay = () => {
if (!audioRef || !audioRef.current) {
return;
}
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
};
const onStop = () => {
if (!audioRef || !audioRef.current) {
return;
}
if (audioRef.current.currentTime == 0 && audioRef.current.paused) {
// TODO remove curent playing value
} else {
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
};
const onNavigatePrevious = () => {
previous();
};
const onFastRewind = () => {
if (!audioRef || !audioRef.current) {
return;
}
audioRef.current.currentTime -= 10;
};
const onFastForward = () => {
if (!audioRef || !audioRef.current) {
return;
}
audioRef.current.currentTime += 10;
};
const onNavigateNext = () => {
next();
};
const onTypePlay = () => {
setPlayingMode((value: PlayMode) => {
if (value === PlayMode.PLAY_ONE) {
return PlayMode.PLAY_ALL;
} else if (value === PlayMode.PLAY_ALL) {
return PlayMode.PLAY_ONE_LOOP;
} else if (value === PlayMode.PLAY_ONE_LOOP) {
return PlayMode.PLAY_ALL_LOOP;
} else {
return PlayMode.PLAY_ONE;
}
});
};
/**
* Call when meta-data is updated
*/
function onChangeMetadata(): void {
const seconds = audioRef.current?.duration;
if (seconds !== undefined) {
setDuration(seconds);
}
}
const onTimeUpdate = () => {
if (!audioRef || !audioRef.current) {
return;
}
console.log(`onTimeUpdate ${audioRef.current.currentTime}`);
setTimeProgress(audioRef.current.currentTime);
};
const onDurationChange = (event) => {};
const onChangeStateToPlay = () => {
setIsPlaying(true);
};
const onChangeStateToPause = () => {
setIsPlaying(false);
};
return (
<>
{!isNullOrUndefined(trackOffset) && (
<Flex
position="absolute"
height="150px"
minHeight="150px"
paddingY="5px"
paddingX="10px"
marginX="15px"
bottom={0}
left={0}
right={0}
zIndex={1000}
borderWidth="1px"
borderColor="brand.900"
bgColor={backColor}
borderTopRadius="10px"
direction="column"
>
<Text
align="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={1}
>
{dataTrack?.name ?? '???'}
</Text>
<Text
align="left"
fontSize="16px"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={1}
>
{dataArtists.map((data) => data.name).join(', ')} /{' '}
{dataAlbum && dataAlbum?.name}
{dataGender && ` / ${dataGender.name}`}
</Text>
<Box width="full" paddingX="15px">
<Slider
aria-label="slider-ex-4"
defaultValue={0}
value={timeProgress}
min={0}
max={duration}
step={0.1}
onChange={onSeek}
focusThumbOnChange={false}
>
<SliderTrack bg="gray.200" height="10px" borderRadius="full">
<SliderFilledTrack bg="brand.600" />
</SliderTrack>
<SliderThumb boxSize={6}>
<Box color="brand.600" as={MdGraphicEq} />
</SliderThumb>
</Slider>
</Box>
<Flex>
<Text
align="left"
fontSize="16px"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={1}
>
{formatTime(timeProgress)}
</Text>
<Text align="left" fontSize="16px" userSelect="none">
{formatTime(duration)}
</Text>
</Flex>
<Flex gap="5px">
<IconButton
{...configButton}
aria-label={'Play'}
icon={
isPlaying ? (
<MdPause size="30px" />
) : (
<MdPlayArrow size="30px" />
)
}
onClick={onPlay}
/>
<IconButton
{...configButton}
aria-label={'Stop'}
icon={<MdStop size="30px" />}
onClick={onStop}
/>
<IconButton
{...configButton}
aria-label={'Previous track'}
icon={<MdNavigateBefore size="30px" />}
onClick={onNavigatePrevious}
marginLeft="auto"
/>
<IconButton
{...configButton}
aria-label={'jump 15sec in past'}
icon={<MdFastRewind size="30px" />}
onClick={onFastRewind}
/>
<IconButton
{...configButton}
aria-label={'jump 15sec in future'}
icon={<MdFastForward size="30px" />}
onClick={onFastForward}
/>
<IconButton
{...configButton}
aria-label={'Next track'}
icon={<MdNavigateNext size="30px" />}
marginRight="auto"
onClick={onNavigateNext}
/>
<IconButton
{...configButton}
aria-label={'continue to the end'}
icon={playModeIcon[playingMode]}
onClick={onTypePlay}
/>
</Flex>
</Flex>
)}
<audio
src={mediaSource}
ref={audioRef}
//preload={true}
onPlay={onChangeStateToPlay}
onPause={onChangeStateToPause}
onTimeUpdate={onTimeUpdate}
onDurationChange={onDurationChange}
onLoadedMetadata={onChangeMetadata}
autoPlay={true}
onEnded={onAudioEnded}
/>
</>
);
};