diff --git a/front/src/ui/player/controls/index.tsx b/front/src/ui/player/controls/index.tsx index 744ad206..2d3a3d5f 100644 --- a/front/src/ui/player/controls/index.tsx +++ b/front/src/ui/player/controls/index.tsx @@ -150,131 +150,6 @@ export const Controls = ({ ); }; -export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => { - const hover = useAtomValue(hoverAtom); - const setHover = useSetAtom(hoverReasonAtom); - const mouseCallback = useRef(null); - const touch = useRef<{ count: number; timeout?: NodeJS.Timeout }>({ - count: 0, - }); - const playerWidth = useRef(null); - const isTouch = useIsTouch(); - - const show = useCallback(() => { - setHover((x) => ({ ...x, mouseMoved: true })); - if (mouseCallback.current) clearTimeout(mouseCallback.current); - mouseCallback.current = setTimeout(() => { - setHover((x) => ({ ...x, mouseMoved: false })); - }, 2500); - }, [setHover]); - - // On mouse move - useEffect(() => { - if (Platform.OS !== "web") return; - const handler = (e: PointerEvent) => { - if (e.pointerType !== "mouse") return; - show(); - }; - - document.addEventListener("pointermove", handler); - return () => document.removeEventListener("pointermove", handler); - }, [show]); - - // When the controls hide, remove focus so space can be used to play/pause instead of triggering the button - // It also serves to hide the tooltip. - useEffect(() => { - if (Platform.OS !== "web") return; - if (!hover && document.activeElement instanceof HTMLElement) - document.activeElement.blur(); - }, [hover]); - - const { css } = useYoshiki(); - - const duration = useAtomValue(durationAtom); - const setPlay = useSetAtom(playAtom); - const setProgress = useSetAtom(progressAtom); - const setFullscreen = useSetAtom(fullscreenAtom); - - const onPress = (e: { pointerType: string; x: number }) => { - if (Platform.OS === "web" && e.pointerType === "mouse") { - setPlay((x) => !x); - return; - } - if (hover) setHover((x) => ({ ...x, mouseMoved: false })); - else show(); - }; - const onDoublePress = (e: { pointerType: string; x: number }) => { - if (Platform.OS === "web" && e.pointerType === "mouse") { - // Only reset touch count for the web, on mobile you can continue to seek by pressing again. - touch.current.count = 0; - setFullscreen((x) => !x); - return; - } - - show(); - if (!duration || !playerWidth.current) return; - - if (e.x < playerWidth.current * 0.33) { - setProgress((x) => Math.max(x - 10, 0)); - } - if (e.x > playerWidth.current * 0.66) { - setProgress((x) => Math.min(x + 10, duration)); - } - }; - - const onAnyPress = (e: { pointerType: string; x: number }) => { - touch.current.count++; - if (touch.current.count >= 2) { - onDoublePress(e); - clearTimeout(touch.current.timeout); - } else { - onPress(e); - } - - touch.current.timeout = setTimeout(() => { - touch.current.count = 0; - touch.current.timeout = undefined; - }, 400); - }; - - return ( - { - if (e.nativeEvent.pointerType === "mouse") - setHover((x) => ({ ...x, mouseMoved: false })); - }} - onPress={(e) => { - e.preventDefault(); - onAnyPress({ - pointerType: isTouch ? "touch" : "mouse", - x: e.nativeEvent.locationX ?? e.nativeEvent.pageX, - }); - }} - onLayout={(e) => { - playerWidth.current = e.nativeEvent.layout.width; - }} - {...css( - // @ts-expect-error Web only property (cursor: unset) - { - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - position: "absolute", - top: 0, - left: 0, - right: 0, - bottom: 0, - cursor: hover ? "unset" : "none", - }, - props, - )} - > - {children} - - ); -}; - const VideoPoster = ({ poster, alt, diff --git a/front/src/ui/player/controls/touch.tsx b/front/src/ui/player/controls/touch.tsx new file mode 100644 index 00000000..924884e6 --- /dev/null +++ b/front/src/ui/player/controls/touch.tsx @@ -0,0 +1,117 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { + type GestureResponderEvent, + Platform, + Pressable, + type PressableProps, +} from "react-native"; +import type { VideoPlayer } from "react-native-video"; +import { useYoshiki } from "yoshiki/native"; +import { useIsTouch } from "~/primitives"; + +export const TouchControls = ({ + player, + children, + ...props +}: { player: VideoPlayer } & PressableProps) => { + const { css } = useYoshiki(); + const isTouch = useIsTouch(); + + const [shouldShow, setShow] = useState(true); + const hideTimeout = useRef(null); + const show = useCallback((val: boolean = true) => { + setShow(val); + if (hideTimeout.current) clearTimeout(hideTimeout.current); + hideTimeout.current = setTimeout(() => { + hideTimeout.current = null; + setShow(false); + }, 2500); + }, []); + // TODO: handle mouse hover & seek + + // On mouse move + useEffect(() => { + if (Platform.OS !== "web") return; + const handler = (e: PointerEvent) => { + if (e.pointerType !== "mouse") return; + show(); + }; + + document.addEventListener("pointermove", handler); + return () => document.removeEventListener("pointermove", handler); + }, [show]); + + const playerWidth = useRef(null); + + return ( + { + if (isTouch) { + show(!shouldShow); + return; + } + if (player.isPlaying) player.pause(); + else player.play(); + }} + onDoublePress={(e) => { + if (!isTouch) { + player.toggleFullscreen(); + return; + } + + show(); + if (Number.isNaN(player.duration) || !playerWidth.current) return; + + const x = e.nativeEvent.locationX ?? e.nativeEvent.pageX; + if (x < playerWidth.current * 0.33) player.seekBy(-10); + if (x > playerWidth.current * 0.66) player.seekBy(10); + // Do not reset press count, you can continue to seek by pressing again. + return true; + }} + onLayout={(e) => { + playerWidth.current = e.nativeEvent.layout.width; + }} + onPointerLeave={(e) => { + // instantly hide the controls when mouse leaves the view + if (e.nativeEvent.pointerType === "mouse") show(false); + }} + {...css({ cursor: "none" as any }, props)} + > + {shouldShow && children} + + ); +}; + +const DoublePressable = ({ + onPress, + onDoublePress, + ...props +}: { + onDoublePress: (e: GestureResponderEvent) => boolean | undefined; +} & PressableProps) => { + const touch = useRef<{ count: number; timeout?: NodeJS.Timeout }>({ + count: 0, + }); + + return ( + { + e.preventDefault(); + touch.current.count++; + if (touch.current.count >= 2) { + const keepCount = onDoublePress(e); + if (!keepCount) touch.current.count = 0; + clearTimeout(touch.current.timeout); + } else { + onPress?.(e); + } + + touch.current.timeout = setTimeout(() => { + touch.current.count = 0; + touch.current.timeout = undefined; + }, 400); + }} + {...props} + /> + ); +};