11 changed files with 172 additions and 32 deletions
@ -0,0 +1,46 @@ |
|||||||
|
import { useVideoPlayerState } from "../VideoContext"; |
||||||
|
|
||||||
|
function durationExceedsHour(secs: number): boolean { |
||||||
|
return secs > 60 * 60; |
||||||
|
} |
||||||
|
|
||||||
|
function formatSeconds(secs: number, showHours = false): string { |
||||||
|
if (Number.isNaN(secs)) { |
||||||
|
if (showHours) return "0:00:00"; |
||||||
|
return "0:00"; |
||||||
|
} |
||||||
|
|
||||||
|
let time = secs; |
||||||
|
const seconds = Math.floor(time % 60); |
||||||
|
|
||||||
|
time /= 60; |
||||||
|
const minutes = Math.floor(time % 60); |
||||||
|
|
||||||
|
time /= 60; |
||||||
|
const hours = Math.floor(time); |
||||||
|
|
||||||
|
const paddedSecs = seconds.toString().padStart(2, "0"); |
||||||
|
const paddedMins = minutes.toString().padStart(2, "0"); |
||||||
|
|
||||||
|
if (!showHours) return [minutes, paddedSecs].join(":"); |
||||||
|
return [hours, paddedMins, paddedSecs].join(":"); |
||||||
|
} |
||||||
|
|
||||||
|
interface Props { |
||||||
|
className?: string; |
||||||
|
} |
||||||
|
|
||||||
|
export function SkipTime(props: Props) { |
||||||
|
const { videoState } = useVideoPlayerState(); |
||||||
|
const hasHours = durationExceedsHour(videoState.duration); |
||||||
|
const time = formatSeconds(videoState.time, hasHours); |
||||||
|
const duration = formatSeconds(videoState.duration, hasHours); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={props.className}> |
||||||
|
<p className="select-none text-white"> |
||||||
|
{time} / {duration} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,54 @@ |
|||||||
|
import { useEffect, useRef } from "react"; |
||||||
|
import { useVideoPlayerState } from "../VideoContext"; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
children?: React.ReactNode; |
||||||
|
id?: string; |
||||||
|
className?: string; |
||||||
|
} |
||||||
|
|
||||||
|
export function VideoPopout(props: Props) { |
||||||
|
const { videoState } = useVideoPlayerState(); |
||||||
|
const popoutRef = useRef<HTMLDivElement>(null); |
||||||
|
const isOpen = videoState.popout === props.id; |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!isOpen) return; |
||||||
|
const popoutEl = popoutRef.current; |
||||||
|
let hasTriggered = false; |
||||||
|
function windowClick() { |
||||||
|
setTimeout(() => { |
||||||
|
if (hasTriggered) return; |
||||||
|
videoState.closePopout(); |
||||||
|
hasTriggered = false; |
||||||
|
}, 10); |
||||||
|
} |
||||||
|
function popoutClick() { |
||||||
|
hasTriggered = true; |
||||||
|
setTimeout(() => { |
||||||
|
hasTriggered = false; |
||||||
|
}, 100); |
||||||
|
} |
||||||
|
window.addEventListener("click", windowClick); |
||||||
|
popoutEl?.addEventListener("click", popoutClick); |
||||||
|
return () => { |
||||||
|
window.removeEventListener("click", windowClick); |
||||||
|
popoutEl?.removeEventListener("click", popoutClick); |
||||||
|
}; |
||||||
|
}, [isOpen, videoState]); |
||||||
|
|
||||||
|
if (!isOpen) return null; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="is-popout absolute inset-x-0 h-0"> |
||||||
|
<div className="absolute bottom-10 right-0 h-96 w-72 rounded-lg bg-denim-400"> |
||||||
|
<div |
||||||
|
ref={popoutRef} |
||||||
|
className={["h-full w-full", props.className].join(" ")} |
||||||
|
> |
||||||
|
{props.children} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue