| import React from "react"; |
| import { useTime } from "../context/time-context"; |
| import { |
| FaPlay, |
| FaPause, |
| FaBackward, |
| FaForward, |
| FaUndoAlt, |
| FaArrowDown, |
| FaArrowUp, |
| } from "react-icons/fa"; |
|
|
| import { debounce } from "@/utils/debounce"; |
|
|
| const PlaybackBar: React.FC = () => { |
| const { duration, isPlaying, setIsPlaying, currentTime, setCurrentTime } = |
| useTime(); |
|
|
| const sliderActiveRef = React.useRef(false); |
| const wasPlayingRef = React.useRef(false); |
| const [sliderValue, setSliderValue] = React.useState(currentTime); |
|
|
| |
| React.useEffect(() => { |
| if (!sliderActiveRef.current) { |
| setSliderValue(currentTime); |
| } |
| }, [currentTime]); |
|
|
| const updateTime = debounce((t: number) => { |
| setCurrentTime(t); |
| }, 200); |
|
|
| const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
| const t = Number(e.target.value); |
| setSliderValue(t); |
| updateTime(t); |
| }; |
|
|
| const handleSliderMouseDown = () => { |
| sliderActiveRef.current = true; |
| wasPlayingRef.current = isPlaying; |
| setIsPlaying(false); |
| }; |
|
|
| const handleSliderMouseUp = () => { |
| sliderActiveRef.current = false; |
| setCurrentTime(sliderValue); |
| if (wasPlayingRef.current) { |
| setIsPlaying(true); |
| } |
| |
| }; |
|
|
| return ( |
| <div className="flex items-center gap-4 w-full max-w-4xl mx-auto sticky bottom-0 bg-slate-900/95 px-4 py-3 rounded-3xl mt-auto"> |
| <button |
| title="Jump backward 5 seconds" |
| onClick={() => setCurrentTime(Math.max(0, currentTime - 5))} |
| className="text-2xl hidden md:block" |
| > |
| <FaBackward size={24} /> |
| </button> |
| <button |
| className={`text-3xl transition-transform ${isPlaying ? "scale-90 opacity-60" : "scale-110"}`} |
| title="Play. Toggle with Space" |
| onClick={() => setIsPlaying(true)} |
| style={{ display: isPlaying ? "none" : "inline-block" }} |
| > |
| <FaPlay size={24} /> |
| </button> |
| <button |
| className={`text-3xl transition-transform ${!isPlaying ? "scale-90 opacity-60" : "scale-110"}`} |
| title="Pause. Toggle with Space" |
| onClick={() => setIsPlaying(false)} |
| style={{ display: !isPlaying ? "none" : "inline-block" }} |
| > |
| <FaPause size={24} /> |
| </button> |
| <button |
| title="Jump forward 5 seconds" |
| onClick={() => setCurrentTime(Math.min(duration, currentTime + 5))} |
| className="text-2xl hidden md:block" |
| > |
| <FaForward size={24} /> |
| </button> |
| <button |
| title="Rewind from start" |
| onClick={() => setCurrentTime(0)} |
| className="text-2xl hidden md:block" |
| > |
| <FaUndoAlt size={24} /> |
| </button> |
| <input |
| type="range" |
| min={0} |
| max={duration} |
| step={0.01} |
| value={sliderValue} |
| onChange={handleSliderChange} |
| onMouseDown={handleSliderMouseDown} |
| onMouseUp={handleSliderMouseUp} |
| onTouchStart={handleSliderMouseDown} |
| onTouchEnd={handleSliderMouseUp} |
| className="flex-1 mx-2 accent-orange-500 focus:outline-none focus:ring-0" |
| aria-label="Seek video" |
| /> |
| <span className="w-16 text-right tabular-nums text-xs text-slate-200 shrink-0"> |
| {Math.floor(sliderValue)} / {Math.floor(duration)} |
| </span> |
| |
| <div className="text-xs text-slate-300 select-none ml-8 flex-col gap-y-0.5 hidden md:flex"> |
| <p> |
| <span className="inline-flex items-center gap-1 font-mono align-middle"> |
| <span className="px-2 py-0.5 rounded border border-slate-400 bg-slate-800 text-slate-200 text-xs shadow-inner"> |
| Space |
| </span> |
| </span>{" "} |
| to pause/unpause |
| </p> |
| <p> |
| <span className="inline-flex items-center gap-1 font-mono align-middle"> |
| <FaArrowUp size={14} />/<FaArrowDown size={14} /> |
| </span>{" "} |
| to previous/next episode |
| </p> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default PlaybackBar; |
|
|