diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx index 0931ffa1..71552757 100644 --- a/src/components/player/Player.tsx +++ b/src/components/player/Player.tsx @@ -1,2 +1,3 @@ export * from "./atoms"; export * from "./base/Container"; +export * from "./base/BottomControls"; diff --git a/src/components/player/atoms/Fullscreen.tsx b/src/components/player/atoms/Fullscreen.tsx new file mode 100644 index 00000000..6ae6a884 --- /dev/null +++ b/src/components/player/atoms/Fullscreen.tsx @@ -0,0 +1,15 @@ +import { Icons } from "@/components/Icon"; +import { VideoPlayerButton } from "@/components/player/internals/Button"; +import { usePlayerStore } from "@/stores/player/store"; + +export function Fullscreen() { + const { isFullscreen } = usePlayerStore((s) => s.interface); + const display = usePlayerStore((s) => s.display); + + return ( + display?.toggleFullscreen()} + icon={isFullscreen ? Icons.COMPRESS : Icons.EXPAND} + /> + ); +} diff --git a/src/components/player/atoms/pause.tsx b/src/components/player/atoms/Pause.tsx similarity index 58% rename from src/components/player/atoms/pause.tsx rename to src/components/player/atoms/Pause.tsx index 752fdcc4..beb68386 100644 --- a/src/components/player/atoms/pause.tsx +++ b/src/components/player/atoms/Pause.tsx @@ -1,3 +1,5 @@ +import { Icons } from "@/components/Icon"; +import { VideoPlayerButton } from "@/components/player/internals/Button"; import { usePlayerStore } from "@/stores/player/store"; export function Pause() { @@ -10,8 +12,9 @@ export function Pause() { }; return ( - + ); } diff --git a/src/components/player/atoms/index.ts b/src/components/player/atoms/index.ts index 69b43b9b..5fedb0ef 100644 --- a/src/components/player/atoms/index.ts +++ b/src/components/player/atoms/index.ts @@ -1 +1,2 @@ -export * from "./pause"; +export * from "./Pause"; +export * from "./Fullscreen"; diff --git a/src/components/player/base/BottomControls.tsx b/src/components/player/base/BottomControls.tsx new file mode 100644 index 00000000..21af9c5d --- /dev/null +++ b/src/components/player/base/BottomControls.tsx @@ -0,0 +1,18 @@ +import { Transition } from "@/components/Transition"; + +export function BottomControls(props: { + show: boolean; + children: React.ReactNode; +}) { + return ( +
+ + {props.children} + +
+ ); +} diff --git a/src/components/player/base/Container.tsx b/src/components/player/base/Container.tsx index d8e3cb93..d00612f2 100644 --- a/src/components/player/base/Container.tsx +++ b/src/components/player/base/Container.tsx @@ -1,16 +1,90 @@ -import { ReactNode } from "react"; +import { ReactNode, RefObject, useEffect, useRef } from "react"; import { VideoContainer } from "@/components/player/internals/VideoContainer"; +import { PlayerHoverState } from "@/stores/player/slices/interface"; +import { usePlayerStore } from "@/stores/player/store"; export interface PlayerProps { children?: ReactNode; + onLoad?: () => void; } -export function Container(props: PlayerProps) { +function useHovering(containerEl: RefObject) { + const timeoutRef = useRef | null>(null); + const updateInterfaceHovering = usePlayerStore( + (s) => s.updateInterfaceHovering + ); + const hovering = usePlayerStore((s) => s.interface.hovering); + + useEffect(() => { + if (!containerEl.current) return; + const el = containerEl.current; + + function pointerMove(e: PointerEvent) { + if (e.pointerType !== "mouse") return; + updateInterfaceHovering(PlayerHoverState.MOUSE_HOVER); + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => { + updateInterfaceHovering(PlayerHoverState.NOT_HOVERING); + timeoutRef.current = null; + }, 3000); + } + + function pointerLeave(e: PointerEvent) { + if (e.pointerType !== "mouse") return; + updateInterfaceHovering(PlayerHoverState.NOT_HOVERING); + if (timeoutRef.current) clearTimeout(timeoutRef.current); + } + + function pointerUp(e: PointerEvent) { + if (e.pointerType === "mouse") return; + if (timeoutRef.current) clearTimeout(timeoutRef.current); + if (hovering !== PlayerHoverState.MOBILE_TAPPED) + updateInterfaceHovering(PlayerHoverState.MOBILE_TAPPED); + else updateInterfaceHovering(PlayerHoverState.NOT_HOVERING); + } + + el.addEventListener("pointermove", pointerMove); + el.addEventListener("pointerleave", pointerLeave); + el.addEventListener("pointerup", pointerUp); + + return () => { + el.removeEventListener("pointermove", pointerMove); + el.removeEventListener("pointerleave", pointerLeave); + el.removeEventListener("pointerup", pointerUp); + }; + }, [containerEl, hovering, updateInterfaceHovering]); +} + +function BaseContainer(props: { children?: ReactNode }) { + const containerEl = useRef(null); + const display = usePlayerStore((s) => s.display); + useHovering(containerEl); + + // report container element to display interface + useEffect(() => { + if (display && containerEl.current) { + display.processContainerElement(containerEl.current); + } + }, [display, containerEl]); + return ( -
- +
{props.children}
); } + +export function Container(props: PlayerProps) { + const propRef = useRef(props.onLoad); + useEffect(() => { + propRef.current?.(); + }, []); + + return ( + + + {props.children} + + ); +} diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts index 63737b76..8acdf1d1 100644 --- a/src/components/player/display/base.ts +++ b/src/components/player/display/base.ts @@ -1,14 +1,23 @@ +import fscreen from "fscreen"; + import { DisplayInterface, DisplayInterfaceEvents, } from "@/components/player/display/displayInterface"; import { Source } from "@/components/player/hooks/usePlayer"; +import { + canFullscreen, + canFullscreenAnyElement, + canWebkitFullscreen, +} from "@/utils/detectFeatures"; import { makeEmitter } from "@/utils/events"; export function makeVideoElementDisplayInterface(): DisplayInterface { const { emit, on, off } = makeEmitter(); let source: Source | null = null; let videoElement: HTMLVideoElement | null = null; + let containerElement: HTMLElement | null = null; + let isFullscreen = false; function setSource() { if (!videoElement || !source) return; @@ -17,13 +26,19 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { videoElement.addEventListener("pause", () => emit("pause", undefined)); } + function fullscreenChange() { + isFullscreen = + !!document.fullscreenElement || // other browsers + !!(document as any).webkitFullscreenElement; // safari + } + fscreen.addEventListener("fullscreenchange", fullscreenChange); + return { on, off, - - // no need to destroy anything - destroy: () => {}, - + destroy: () => { + fscreen.removeEventListener("fullscreenchange", fullscreenChange); + }, load(newSource) { source = newSource; setSource(); @@ -33,13 +48,36 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { videoElement = video; setSource(); }, + processContainerElement(container) { + containerElement = container; + }, pause() { videoElement?.pause(); }, - play() { videoElement?.play(); }, + toggleFullscreen() { + if (isFullscreen) { + isFullscreen = false; + emit("fullscreen", isFullscreen); + if (!fscreen.fullscreenElement) return; + fscreen.exitFullscreen(); + return; + } + + // enter fullscreen + isFullscreen = true; + emit("fullscreen", isFullscreen); + if (!canFullscreen() || fscreen.fullscreenElement) return; + if (canFullscreenAnyElement()) { + if (containerElement) fscreen.requestFullscreen(containerElement); + return; + } + if (canWebkitFullscreen()) { + if (videoElement) (videoElement as any).webkitEnterFullscreen(); + } + }, }; } diff --git a/src/components/player/display/displayInterface.ts b/src/components/player/display/displayInterface.ts index 4dff00d9..60e75a4d 100644 --- a/src/components/player/display/displayInterface.ts +++ b/src/components/player/display/displayInterface.ts @@ -4,6 +4,7 @@ import { Listener } from "@/utils/events"; export type DisplayInterfaceEvents = { play: void; pause: void; + fullscreen: boolean; }; export interface DisplayInterface extends Listener { @@ -11,5 +12,7 @@ export interface DisplayInterface extends Listener { pause(): void; load(source: Source): void; processVideoElement(video: HTMLVideoElement): void; + processContainerElement(container: HTMLElement): void; + toggleFullscreen(): void; destroy(): void; } diff --git a/src/components/player/hooks/usePlayer.ts b/src/components/player/hooks/usePlayer.ts index 9b5301fa..add6b988 100644 --- a/src/components/player/hooks/usePlayer.ts +++ b/src/components/player/hooks/usePlayer.ts @@ -18,5 +18,8 @@ export function usePlayer() { display?.load(source); setStatus(playerStatus.PLAYING); }, + setScrapeStatus() { + setStatus(playerStatus.SCRAPING); + }, }; } diff --git a/src/components/player/internals/Button.tsx b/src/components/player/internals/Button.tsx new file mode 100644 index 00000000..420ff70b --- /dev/null +++ b/src/components/player/internals/Button.tsx @@ -0,0 +1,18 @@ +import { Icon, Icons } from "@/components/Icon"; + +export function VideoPlayerButton(props: { + children?: React.ReactNode; + onClick: () => void; + icon?: Icons; +}) { + return ( + + ); +} diff --git a/src/components/player/internals/VideoContainer.tsx b/src/components/player/internals/VideoContainer.tsx index 59679181..04c0331e 100644 --- a/src/components/player/internals/VideoContainer.tsx +++ b/src/components/player/internals/VideoContainer.tsx @@ -34,7 +34,7 @@ function VideoElement() { } }, [display, videoEl]); - return