18 changed files with 286 additions and 66 deletions
@ -1,2 +1,3 @@
@@ -1,2 +1,3 @@
|
||||
export * from "./atoms"; |
||||
export * from "./base/Container"; |
||||
export * from "./base/BottomControls"; |
||||
|
@ -0,0 +1,15 @@
@@ -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 @@
@@ -1 +1,2 @@
|
||||
export * from "./pause"; |
||||
export * from "./Pause"; |
||||
export * from "./Fullscreen"; |
||||
|
@ -0,0 +1,18 @@
@@ -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 @@
@@ -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; |
||||
} |
||||
|
||||
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) { |
||||
const propRef = useRef(props.onLoad); |
||||
useEffect(() => { |
||||
propRef.current?.(); |
||||
}, []); |
||||
|
||||
return ( |
||||
<div> |
||||
<BaseContainer> |
||||
<VideoContainer /> |
||||
{props.children} |
||||
</div> |
||||
</BaseContainer> |
||||
); |
||||
} |
||||
|
@ -0,0 +1,18 @@
@@ -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 @@
@@ -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