18 changed files with 286 additions and 66 deletions
@ -1,2 +1,3 @@ |
|||||||
export * from "./atoms"; |
export * from "./atoms"; |
||||||
export * from "./base/Container"; |
export * from "./base/Container"; |
||||||
|
export * from "./base/BottomControls"; |
||||||
|
@ -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 ( |
||||||
|
<VideoPlayerButton |
||||||
|
onClick={() => display?.toggleFullscreen()} |
||||||
|
icon={isFullscreen ? Icons.COMPRESS : Icons.EXPAND} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
@ -1 +1,2 @@ |
|||||||
export * from "./pause"; |
export * from "./Pause"; |
||||||
|
export * from "./Fullscreen"; |
||||||
|
@ -0,0 +1,18 @@ |
|||||||
|
import { Transition } from "@/components/Transition"; |
||||||
|
|
||||||
|
export function BottomControls(props: { |
||||||
|
show: boolean; |
||||||
|
children: React.ReactNode; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<div className="w-full absolute bottom-0 flex flex-col pt-32 bg-gradient-to-t from-black to-transparent [margin-bottom:env(safe-area-inset-bottom)]"> |
||||||
|
<Transition |
||||||
|
animation="slide-up" |
||||||
|
show={props.show} |
||||||
|
className="pointer-events-auto px-4 pb-2 flex justify-end" |
||||||
|
> |
||||||
|
{props.children} |
||||||
|
</Transition> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -1,16 +1,90 @@ |
|||||||
import { ReactNode } from "react"; |
import { ReactNode, RefObject, useEffect, useRef } from "react"; |
||||||
|
|
||||||
import { VideoContainer } from "@/components/player/internals/VideoContainer"; |
import { VideoContainer } from "@/components/player/internals/VideoContainer"; |
||||||
|
import { PlayerHoverState } from "@/stores/player/slices/interface"; |
||||||
|
import { usePlayerStore } from "@/stores/player/store"; |
||||||
|
|
||||||
export interface PlayerProps { |
export interface PlayerProps { |
||||||
children?: ReactNode; |
children?: ReactNode; |
||||||
|
onLoad?: () => void; |
||||||
|
} |
||||||
|
|
||||||
|
function useHovering(containerEl: RefObject<HTMLDivElement>) { |
||||||
|
const timeoutRef = useRef<ReturnType<typeof setTimeout> | 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<HTMLDivElement | null>(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 ( |
||||||
|
<div className="relative overflow-hidden h-screen" ref={containerEl}> |
||||||
|
{props.children} |
||||||
|
</div> |
||||||
|
); |
||||||
} |
} |
||||||
|
|
||||||
export function Container(props: PlayerProps) { |
export function Container(props: PlayerProps) { |
||||||
|
const propRef = useRef(props.onLoad); |
||||||
|
useEffect(() => { |
||||||
|
propRef.current?.(); |
||||||
|
}, []); |
||||||
|
|
||||||
return ( |
return ( |
||||||
<div> |
<BaseContainer> |
||||||
<VideoContainer /> |
<VideoContainer /> |
||||||
{props.children} |
{props.children} |
||||||
</div> |
</BaseContainer> |
||||||
); |
); |
||||||
} |
} |
||||||
|
@ -0,0 +1,18 @@ |
|||||||
|
import { Icon, Icons } from "@/components/Icon"; |
||||||
|
|
||||||
|
export function VideoPlayerButton(props: { |
||||||
|
children?: React.ReactNode; |
||||||
|
onClick: () => void; |
||||||
|
icon?: Icons; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
onClick={props.onClick} |
||||||
|
className="p-2 rounded-full hover:bg-video-buttonBackground hover:bg-opacity-75 transition-transform duration-100 active:scale-110 active:bg-opacity-100 active:text-white" |
||||||
|
> |
||||||
|
{props.icon && <Icon className="text-2xl" icon={props.icon} />} |
||||||
|
{props.children} |
||||||
|
</button> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
import { DisplayInterface } from "@/components/player/display/displayInterface"; |
||||||
|
import { MakeSlice } from "@/stores/player/slices/types"; |
||||||
|
|
||||||
|
export interface DisplaySlice { |
||||||
|
display: DisplayInterface | null; |
||||||
|
setDisplay(display: DisplayInterface): void; |
||||||
|
} |
||||||
|
|
||||||
|
export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({ |
||||||
|
display: null, |
||||||
|
setDisplay(newDisplay: DisplayInterface) { |
||||||
|
const display = get().display; |
||||||
|
if (display) display.destroy(); |
||||||
|
|
||||||
|
// make display events update the state
|
||||||
|
newDisplay.on("pause", () => |
||||||
|
set((s) => { |
||||||
|
s.mediaPlaying.isPaused = true; |
||||||
|
s.mediaPlaying.isPlaying = false; |
||||||
|
}) |
||||||
|
); |
||||||
|
newDisplay.on("play", () => |
||||||
|
set((s) => { |
||||||
|
s.mediaPlaying.isPaused = false; |
||||||
|
s.mediaPlaying.isPlaying = true; |
||||||
|
}) |
||||||
|
); |
||||||
|
newDisplay.on("fullscreen", (isFullscreen) => |
||||||
|
set((s) => { |
||||||
|
s.interface.isFullscreen = isFullscreen; |
||||||
|
}) |
||||||
|
); |
||||||
|
|
||||||
|
set((s) => { |
||||||
|
s.display = newDisplay; |
||||||
|
}); |
||||||
|
}, |
||||||
|
}); |
Loading…
Reference in new issue