11 changed files with 302 additions and 120 deletions
@ -1,74 +1,68 @@ |
|||||||
import { useCallback, useEffect, useRef, useState } from "react"; |
import { |
||||||
|
makePercentage, |
||||||
|
makePercentageString, |
||||||
|
useProgressBar, |
||||||
|
} from "@/hooks/useProgressBar"; |
||||||
|
import { useCallback, useRef } from "react"; |
||||||
import { useVideoPlayerState } from "../VideoContext"; |
import { useVideoPlayerState } from "../VideoContext"; |
||||||
|
|
||||||
export function ProgressControl() { |
export function ProgressControl() { |
||||||
const { videoState } = useVideoPlayerState(); |
const { videoState } = useVideoPlayerState(); |
||||||
const ref = useRef<HTMLDivElement>(null); |
const ref = useRef<HTMLDivElement>(null); |
||||||
const [mouseDown, setMouseDown] = useState<boolean>(false); |
|
||||||
const [progress, setProgress] = useState<number>(0); |
|
||||||
|
|
||||||
let watchProgress = `${( |
const commitTime = useCallback( |
||||||
(videoState.time / videoState.duration) * |
(percentage) => { |
||||||
100 |
videoState.setTime(percentage * videoState.duration); |
||||||
).toFixed(2)}%`;
|
}, |
||||||
if (mouseDown) watchProgress = `${progress}%`; |
[videoState] |
||||||
|
); |
||||||
const bufferProgress = `${( |
const { dragging, dragPercentage, dragMouseDown } = useProgressBar( |
||||||
(videoState.buffered / videoState.duration) * |
ref, |
||||||
100 |
commitTime |
||||||
).toFixed(2)}%`;
|
); |
||||||
|
|
||||||
useEffect(() => { |
|
||||||
function mouseMove(ev: MouseEvent) { |
|
||||||
if (!mouseDown || !ref.current) return; |
|
||||||
const rect = ref.current.getBoundingClientRect(); |
|
||||||
const pos = ((ev.pageX - rect.left) / ref.current.offsetWidth) * 100; |
|
||||||
setProgress(pos); |
|
||||||
} |
|
||||||
|
|
||||||
function mouseUp(ev: MouseEvent) { |
|
||||||
if (!mouseDown) return; |
|
||||||
setMouseDown(false); |
|
||||||
document.body.removeAttribute("data-no-select"); |
|
||||||
|
|
||||||
if (!ref.current) return; |
|
||||||
const rect = ref.current.getBoundingClientRect(); |
|
||||||
const pos = (ev.pageX - rect.left) / ref.current.offsetWidth; |
|
||||||
videoState.setTime(pos * videoState.duration); |
|
||||||
} |
|
||||||
|
|
||||||
document.addEventListener("mousemove", mouseMove); |
|
||||||
document.addEventListener("mouseup", mouseUp); |
|
||||||
|
|
||||||
return () => { |
let watchProgress = makePercentageString( |
||||||
document.removeEventListener("mousemove", mouseMove); |
makePercentage((videoState.time / videoState.duration) * 100) |
||||||
document.removeEventListener("mouseup", mouseUp); |
); |
||||||
}; |
if (dragging) |
||||||
}, [mouseDown, videoState]); |
watchProgress = makePercentageString(makePercentage(dragPercentage)); |
||||||
|
|
||||||
const handleMouseDown = useCallback(() => { |
const bufferProgress = makePercentageString( |
||||||
setMouseDown(true); |
makePercentage((videoState.buffered / videoState.duration) * 100) |
||||||
document.body.setAttribute("data-no-select", "true"); |
); |
||||||
}, []); |
|
||||||
|
|
||||||
return ( |
return ( |
||||||
|
<div className="group pointer-events-auto cursor-pointer rounded-full px-2"> |
||||||
<div |
<div |
||||||
ref={ref} |
ref={ref} |
||||||
className="relative m-1 my-4 h-4 w-48 overflow-hidden rounded-full border border-white bg-denim-100" |
className="-my-3 flex h-8 items-center" |
||||||
onMouseDown={handleMouseDown} |
onMouseDown={dragMouseDown} |
||||||
> |
> |
||||||
<div |
<div |
||||||
className="absolute inset-y-0 left-0 bg-denim-700 opacity-50" |
className={`relative h-1 flex-1 rounded-full bg-gray-500 bg-opacity-50 transition-[height] duration-100 group-hover:h-2 ${ |
||||||
|
dragging ? "!h-2" : "" |
||||||
|
}`}
|
||||||
|
> |
||||||
|
<div |
||||||
|
className="absolute inset-y-0 left-0 flex items-center justify-end rounded-full bg-gray-300 bg-opacity-50" |
||||||
style={{ |
style={{ |
||||||
width: bufferProgress, |
width: bufferProgress, |
||||||
}} |
}} |
||||||
/> |
/> |
||||||
<div |
<div |
||||||
className="absolute inset-y-0 left-0 bg-denim-700" |
className="absolute inset-y-0 left-0 flex items-center justify-end rounded-full bg-bink-500" |
||||||
style={{ |
style={{ |
||||||
width: watchProgress, |
width: watchProgress, |
||||||
}} |
}} |
||||||
|
> |
||||||
|
<div |
||||||
|
className={`absolute h-1 w-1 translate-x-1/2 rounded-full bg-white opacity-0 transition-[transform,opacity] group-hover:scale-[400%] group-hover:opacity-100 ${ |
||||||
|
dragging ? "!scale-[400%] !opacity-100" : "" |
||||||
|
}`}
|
||||||
/> |
/> |
||||||
</div> |
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
); |
); |
||||||
} |
} |
||||||
|
@ -1,34 +1,86 @@ |
|||||||
import { useCallback, useRef } from "react"; |
import { Icon, Icons } from "@/components/Icon"; |
||||||
|
import { |
||||||
|
makePercentage, |
||||||
|
makePercentageString, |
||||||
|
useProgressBar, |
||||||
|
} from "@/hooks/useProgressBar"; |
||||||
|
import { useCallback, useRef, useState } from "react"; |
||||||
import { useVideoPlayerState } from "../VideoContext"; |
import { useVideoPlayerState } from "../VideoContext"; |
||||||
|
|
||||||
export function VolumeControl() { |
interface Props { |
||||||
|
className?: string; |
||||||
|
} |
||||||
|
|
||||||
|
// TODO make hoveredOnce false when control bar appears
|
||||||
|
|
||||||
|
export function VolumeControl(props: Props) { |
||||||
const { videoState } = useVideoPlayerState(); |
const { videoState } = useVideoPlayerState(); |
||||||
const ref = useRef<HTMLDivElement>(null); |
const ref = useRef<HTMLDivElement>(null); |
||||||
|
const [storedVolume, setStoredVolume] = useState(1); |
||||||
|
const [hoveredOnce, setHoveredOnce] = useState(false); |
||||||
|
|
||||||
const percentage = `${(videoState.volume * 100).toFixed(2)}%`; |
const commitVolume = useCallback( |
||||||
|
(percentage) => { |
||||||
const handleClick = useCallback( |
videoState.setVolume(percentage); |
||||||
(e: React.MouseEvent<HTMLElement>) => { |
setStoredVolume(percentage); |
||||||
if (!ref.current) return; |
|
||||||
const rect = ref.current.getBoundingClientRect(); |
|
||||||
const pos = (e.pageX - rect.left) / ref.current.offsetWidth; |
|
||||||
videoState.setVolume(pos); |
|
||||||
}, |
}, |
||||||
[videoState, ref] |
[videoState, setStoredVolume] |
||||||
|
); |
||||||
|
const { dragging, dragPercentage, dragMouseDown } = useProgressBar( |
||||||
|
ref, |
||||||
|
commitVolume, |
||||||
|
true |
||||||
); |
); |
||||||
|
|
||||||
|
const handleClick = useCallback(() => { |
||||||
|
if (videoState.volume > 0) { |
||||||
|
videoState.setVolume(0); |
||||||
|
setStoredVolume(videoState.volume); |
||||||
|
} else { |
||||||
|
videoState.setVolume(storedVolume > 0 ? storedVolume : 1); |
||||||
|
} |
||||||
|
}, [videoState, setStoredVolume, storedVolume]); |
||||||
|
|
||||||
|
const handleMouseEnter = useCallback(() => { |
||||||
|
setHoveredOnce(true); |
||||||
|
}, [setHoveredOnce]); |
||||||
|
|
||||||
|
let percentage = makePercentage(videoState.volume * 100); |
||||||
|
if (dragging) percentage = makePercentage(dragPercentage); |
||||||
|
const percentageString = makePercentageString(percentage); |
||||||
|
|
||||||
return ( |
return ( |
||||||
|
<div className={props.className}> |
||||||
|
<div |
||||||
|
className="pointer-events-auto flex cursor-pointer items-center" |
||||||
|
onMouseEnter={handleMouseEnter} |
||||||
|
> |
||||||
|
<div className="px-4 text-2xl text-white" onClick={handleClick}> |
||||||
|
<Icon icon={percentage > 0 ? Icons.VOLUME : Icons.VOLUME_X} /> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
className={`-ml-2 w-0 overflow-hidden transition-[width,opacity] duration-300 ease-in ${ |
||||||
|
hoveredOnce ? "!w-20 opacity-100" : "w-4 opacity-0" |
||||||
|
}`}
|
||||||
|
> |
||||||
<div |
<div |
||||||
ref={ref} |
ref={ref} |
||||||
className="relative m-1 my-4 h-4 w-48 overflow-hidden rounded-full border border-white bg-bink-300" |
className="flex h-10 w-16 items-center px-2" |
||||||
onClick={handleClick} |
onMouseDown={dragMouseDown} |
||||||
> |
> |
||||||
|
<div className="relative h-1 flex-1 rounded-full bg-gray-500 bg-opacity-50"> |
||||||
<div |
<div |
||||||
className="absolute inset-y-0 left-0 bg-bink-700" |
className="absolute inset-y-0 left-0 flex items-center justify-end rounded-full bg-bink-500" |
||||||
style={{ |
style={{ |
||||||
width: percentage, |
width: percentageString, |
||||||
}} |
}} |
||||||
/> |
> |
||||||
|
<div className="absolute h-3 w-3 translate-x-1/2 rounded-full bg-white" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
</div> |
</div> |
||||||
); |
); |
||||||
} |
} |
||||||
|
@ -0,0 +1,26 @@ |
|||||||
|
import { Icon, Icons } from "@/components/Icon"; |
||||||
|
import React from "react"; |
||||||
|
|
||||||
|
export interface VideoPlayerIconButtonProps { |
||||||
|
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void; |
||||||
|
icon: Icons; |
||||||
|
text?: string; |
||||||
|
className?: string; |
||||||
|
} |
||||||
|
|
||||||
|
export function VideoPlayerIconButton(props: VideoPlayerIconButtonProps) { |
||||||
|
return ( |
||||||
|
<div className={props.className}> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
onClick={props.onClick} |
||||||
|
className="group pointer-events-auto p-2 text-white transition-transform duration-100 active:scale-110" |
||||||
|
> |
||||||
|
<div className="flex items-center justify-center rounded-full bg-white bg-opacity-0 p-2 transition-colors duration-100 group-hover:bg-opacity-20"> |
||||||
|
<Icon icon={props.icon} className="text-2xl" /> |
||||||
|
{props.text ? <span className="ml-2">{props.text}</span> : null} |
||||||
|
</div> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
import React, { RefObject, useCallback, useEffect, useState } from "react"; |
||||||
|
|
||||||
|
export function makePercentageString(num: number) { |
||||||
|
return `${num.toFixed(2)}%`; |
||||||
|
} |
||||||
|
|
||||||
|
export function makePercentage(num: number) { |
||||||
|
return Number(Math.max(0, Math.min(num, 100)).toFixed(2)); |
||||||
|
} |
||||||
|
|
||||||
|
export function useProgressBar( |
||||||
|
barRef: RefObject<HTMLElement>, |
||||||
|
commit: (percentage: number) => void, |
||||||
|
commitImmediately = false |
||||||
|
) { |
||||||
|
const [mouseDown, setMouseDown] = useState<boolean>(false); |
||||||
|
const [progress, setProgress] = useState<number>(0); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
function mouseMove(ev: MouseEvent) { |
||||||
|
if (!mouseDown || !barRef.current) return; |
||||||
|
const rect = barRef.current.getBoundingClientRect(); |
||||||
|
const pos = ((ev.pageX - rect.left) / barRef.current.offsetWidth) * 100; |
||||||
|
setProgress(pos); |
||||||
|
if (commitImmediately) commit(pos); |
||||||
|
} |
||||||
|
|
||||||
|
function mouseUp(ev: MouseEvent) { |
||||||
|
if (!mouseDown) return; |
||||||
|
setMouseDown(false); |
||||||
|
document.body.removeAttribute("data-no-select"); |
||||||
|
|
||||||
|
if (!barRef.current) return; |
||||||
|
const rect = barRef.current.getBoundingClientRect(); |
||||||
|
const pos = (ev.pageX - rect.left) / barRef.current.offsetWidth; |
||||||
|
commit(pos); |
||||||
|
} |
||||||
|
|
||||||
|
document.addEventListener("mousemove", mouseMove); |
||||||
|
document.addEventListener("mouseup", mouseUp); |
||||||
|
|
||||||
|
return () => { |
||||||
|
document.removeEventListener("mousemove", mouseMove); |
||||||
|
document.removeEventListener("mouseup", mouseUp); |
||||||
|
}; |
||||||
|
}, [mouseDown, barRef, commit, commitImmediately]); |
||||||
|
|
||||||
|
const dragMouseDown = useCallback( |
||||||
|
(ev: React.MouseEvent<HTMLElement>) => { |
||||||
|
setMouseDown(true); |
||||||
|
document.body.setAttribute("data-no-select", "true"); |
||||||
|
|
||||||
|
if (!barRef.current) return; |
||||||
|
const rect = barRef.current.getBoundingClientRect(); |
||||||
|
const pos = ((ev.pageX - rect.left) / barRef.current.offsetWidth) * 100; |
||||||
|
setProgress(pos); |
||||||
|
}, |
||||||
|
[setProgress, barRef] |
||||||
|
); |
||||||
|
|
||||||
|
return { |
||||||
|
dragging: mouseDown, |
||||||
|
dragPercentage: progress, |
||||||
|
dragMouseDown, |
||||||
|
}; |
||||||
|
} |
Loading…
Reference in new issue