diff options
Diffstat (limited to 'src/components/YTPlayer/index.tsx')
| -rw-r--r-- | src/components/YTPlayer/index.tsx | 149 |
1 files changed, 122 insertions, 27 deletions
diff --git a/src/components/YTPlayer/index.tsx b/src/components/YTPlayer/index.tsx index 1aac9ac..9910351 100644 --- a/src/components/YTPlayer/index.tsx +++ b/src/components/YTPlayer/index.tsx @@ -9,81 +9,160 @@ interface Props { currentTry: number; } +const DEFAULT_VOLUME = 0.7; + +const loadVolume = () => { + try { + const storedVolume = localStorage.getItem("playerVolume"); + if (storedVolume === null) return DEFAULT_VOLUME; + + const parsedVolume = Number(storedVolume); + if (!Number.isFinite(parsedVolume)) return DEFAULT_VOLUME; + + return Math.max(0, Math.min(1, parsedVolume)); + } catch { + return DEFAULT_VOLUME; + } +}; + export function Player({ id, currentTry }: Props) { const opts = { width: "0", height: "0", }; - // react-youtube doesn't export types for this // eslint-disable-next-line @typescript-eslint/no-explicit-any const playerRef = React.useRef<any>(null); + const MIN_REMAINING_SEC = 17; const currentPlayTime = playTimes[currentTry]; + const totalPlayTime = playTimes[playTimes.length - 1]; + const progressDurationSec = totalPlayTime / 1000; - const [play, setPlay] = React.useState<boolean>(false); - - const [currentTime, setCurrentTime] = React.useState<number>(0); + const [play, setPlay] = React.useState(false); + const [startTime, setStartTime] = React.useState(0); + const [currentTime, setCurrentTime] = React.useState(0); + const [isReady, setIsReady] = React.useState(false); + const [volume, setVolume] = React.useState(loadVolume); - const [isReady, setIsReady] = React.useState<boolean>(false); + const progressValue = Math.min( + progressDurationSec, + Math.max(0, currentTime - startTime), + ); React.useEffect(() => { - setInterval(() => { + const interval = setInterval(() => { playerRef.current?.internalPlayer - .getCurrentTime() - .then((time: number) => { - setCurrentTime(time); - }); + ?.getCurrentTime() + .then((time: number) => setCurrentTime(time)); }, 250); + + return () => clearInterval(interval); }, []); React.useEffect(() => { - if (play) { - if (currentTime * 1000 >= currentPlayTime) { - playerRef.current?.internalPlayer.pauseVideo(); - playerRef.current?.internalPlayer.seekTo(0); - setPlay(false); - } + if (!play) return; + + const elapsedMs = Math.max(0, (currentTime - startTime) * 1000); + + if (elapsedMs >= currentPlayTime) { + playerRef.current?.internalPlayer.pauseVideo(); + playerRef.current?.internalPlayer.seekTo(startTime, true); + setPlay(false); } - }, [play, currentTime]); + }, [play, currentTime, startTime, currentPlayTime]); - // don't call play video each time currentTime changes const startPlayback = React.useCallback(() => { - playerRef.current?.internalPlayer.playVideo(); + const player = playerRef.current?.internalPlayer; + if (!player) return; + + player.seekTo(startTime, true); + player.playVideo(); setPlay(true); - }, []); + }, [startTime]); + + const updateVolume = React.useCallback( + (event: React.ChangeEvent<HTMLInputElement>) => { + setVolume(Number(event.target.value)); + }, + [], + ); + + const setReady = React.useCallback(async () => { - const setReady = React.useCallback(() => { + const player = playerRef.current?.internalPlayer; + if (!player) return; + + const duration = await player.getDuration(); + + const maxStart = Math.max(0, duration - MIN_REMAINING_SEC); + const randomStart = Math.random() * maxStart; + + setStartTime(randomStart); + player.setVolume(volume * 100); + player.seekTo(randomStart, true); setIsReady(true); - }, []); + }, [volume]); + + React.useEffect(() => { + if (!isReady) return; + + const player = playerRef.current?.internalPlayer; + if (!player) return; + + player.getDuration().then((duration: number) => { + const maxStart = Math.max(0, duration - MIN_REMAINING_SEC); + const randomStart = Math.random() * maxStart; + + setStartTime(randomStart); + setPlay(false); + setCurrentTime(0); + + player.seekTo(randomStart, true); + }); + }, [id, isReady]); + + React.useEffect(() => { + if (!isReady) return; + + playerRef.current?.internalPlayer?.setVolume(volume * 100); + + try { + localStorage.setItem("playerVolume", String(volume)); + } catch { + } + }, [isReady, volume]); return ( <> <YouTube opts={opts} videoId={id} onReady={setReady} ref={playerRef} /> + {isReady ? ( <> <Styled.ProgressBackground> - {currentTime !== 0 && <Styled.Progress value={currentTime} />} + <Styled.Progress value={progressValue} /> + {playTimes.map((playTime) => ( <Styled.Separator - style={{ left: `${(playTime / 16000) * 100}%` }} key={playTime} + style={{ left: `${(playTime / totalPlayTime) * 100}%` }} /> ))} </Styled.ProgressBackground> + <Styled.TimeStamps> <Styled.TimeStamp>1s</Styled.TimeStamp> <Styled.TimeStamp>16s</Styled.TimeStamp> </Styled.TimeStamps> - {!play && ( + + {!play ? ( <IoPlay style={{ cursor: "pointer" }} size={36} color="var(--cl-green-6)" onClick={startPlayback} /> - )} - {play && ( + ) : ( <IoPause style={{ cursor: "pointer" }} size={36} @@ -91,6 +170,22 @@ export function Player({ id, currentTry }: Props) { onClick={startPlayback} /> )} + + <Styled.VolumeControl> + <Styled.VolumeLabel htmlFor="youtube-player-volume"> + Volume {Math.round(volume * 100)}% + </Styled.VolumeLabel> + <Styled.VolumeSlider + id="youtube-player-volume" + type="range" + min="0" + max="1" + step="0.01" + value={volume} + onChange={updateVolume} + aria-label="Volume" + /> + </Styled.VolumeControl> </> ) : ( <p>Loading player...</p> |
